mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(plugins): Windows: local marketplace paths, localhost connections, tests
- A marketplace index given as a local Windows path (`C:\market\index.json`) resolves its packages next to it; only URLs are joined with `/`. - HTTP requests to `localhost` try IPv4 loopback first, then IPv6: local servers such as Ollama usually listen on IPv4, and on Windows a refused `::1` cost about two seconds per request. - Tests that assumed Unix: an absolute program path is the platform's, a manifest rewritten by a test may have CRLF line ends, the precompiled-cache test replaces the cache file only after the host that maps it is gone, and the MyBatis test lets its first call build the index before asserting. - The icon theme test's time limits guard against work growing faster than the icon count rather than measure the runner.
This commit is contained in:
1 parent
20960c1d96
commit
20281e9b66
4 files changed
+70
-20
No files matched your search
@@ -230,11 +230,13 @@ mod tests {
|
||||
"net:crates.io",
|
||||
"net:*.github.com",
|
||||
"process:cargo",
|
||||
"process:/usr/bin/git",
|
||||
"fs.read:/etc",
|
||||
] {
|
||||
assert_eq!(s.parse::<Capability>().unwrap().to_string(), s);
|
||||
}
|
||||
// An absolute program path is the platform's.
|
||||
let git = if cfg!(windows) { r"process:C:\Program Files\Git\bin\git.exe" } else { "process:/usr/bin/git" };
|
||||
assert_eq!(git.parse::<Capability>().unwrap().to_string(), git);
|
||||
assert_eq!("project-read".parse::<Capability>().unwrap(), Capability::ProjectRead);
|
||||
assert!("net:".parse::<Capability>().is_err());
|
||||
assert!("net:a b".parse::<Capability>().is_err());
|
||||
|
||||
@@ -56,13 +56,20 @@ pub fn read_url(url: &str, max: usize) -> Result<Vec<u8>, String> {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// `href` relative to the index at `base`.
|
||||
/// `href` relative to the index at `base`: a URL, or a local path (with the
|
||||
/// platform's separators, e.g. `C:\\market\\index.json`).
|
||||
pub fn resolve(base: &str, href: &str) -> String {
|
||||
if href.contains("://") || href.starts_with('/') {
|
||||
if href.contains("://") || href.starts_with('/') || Path::new(href).is_absolute() {
|
||||
return href.to_owned();
|
||||
}
|
||||
match base.rfind('/') {
|
||||
Some(i) => format!("{}/{href}", &base[..i]),
|
||||
if base.contains("://") {
|
||||
return match base.rfind('/') {
|
||||
Some(i) => format!("{}/{href}", &base[..i]),
|
||||
None => href.to_owned(),
|
||||
};
|
||||
}
|
||||
match Path::new(base).parent() {
|
||||
Some(dir) => dir.join(href).to_string_lossy().into_owned(),
|
||||
None => href.to_owned(),
|
||||
}
|
||||
}
|
||||
@@ -296,7 +303,12 @@ mod tests {
|
||||
#[test]
|
||||
fn downloads_resolve_against_the_index() {
|
||||
assert_eq!(resolve("https://x.dev/market/index.json", "p/a.oiplugin"), "https://x.dev/market/p/a.oiplugin");
|
||||
assert_eq!(resolve("/srv/market/index.json", "a.oiplugin"), "/srv/market/a.oiplugin");
|
||||
assert_eq!(
|
||||
Path::new(&resolve("/srv/market/index.json", "a.oiplugin")),
|
||||
Path::new("/srv/market").join("a.oiplugin")
|
||||
);
|
||||
#[cfg(windows)]
|
||||
assert_eq!(resolve(r"C:\market\index.json", "a.oiplugin"), r"C:\market\a.oiplugin");
|
||||
assert_eq!(resolve("https://x.dev/index.json", "https://cdn.dev/a"), "https://cdn.dev/a");
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,9 @@ fn hello_02(text: &str, required: &str) -> String {
|
||||
|
||||
/// `hello` with a 0.1 manifest (`openidea-api`, `permissions`, no activation).
|
||||
fn hello_01(text: &str) -> String {
|
||||
text.replace("engines.openidea = \"^1.0\"", "openidea-api = \"0.1\"")
|
||||
// A Windows checkout may have CRLF line ends.
|
||||
text.replace("\r\n", "\n")
|
||||
.replace("engines.openidea = \"^1.0\"", "openidea-api = \"0.1\"")
|
||||
.replace("activation = [\"on-command:*\", \"on-view:greetings\"]\n", "")
|
||||
.replace(
|
||||
"[capabilities]\nrequired = [\"editor.read\", \"editor.write\"]",
|
||||
@@ -1164,19 +1166,19 @@ fn precompiled_components_make_cold_activation_cheap() {
|
||||
std::fs::write(cache.join("0000-stale.cwasm"), b"junk").unwrap();
|
||||
again.warm_component_cache();
|
||||
assert_eq!(cached(), 1);
|
||||
// A corrupt entry is discarded and recompiled. Replaced, not overwritten:
|
||||
// the running component maps the file it was loaded from.
|
||||
// A corrupt entry is discarded and recompiled. The host that loaded it goes
|
||||
// first: its component maps the file (which Windows won't let anyone replace).
|
||||
drop(again);
|
||||
for e in std::fs::read_dir(&cache).unwrap().flatten() {
|
||||
let junk = e.path().with_extension("junk");
|
||||
std::fs::write(&junk, b"junk").unwrap();
|
||||
std::fs::rename(&junk, e.path()).unwrap();
|
||||
std::fs::write(e.path(), b"junk").unwrap();
|
||||
}
|
||||
again.reload("hello").unwrap();
|
||||
let third = PluginHost::new(f.plugins_dir.clone(), Arc::new(|_: Option<&str>, _| {}), Limits::default()).unwrap();
|
||||
third.load_all();
|
||||
assert!(
|
||||
again.run_command("hello", "word-count", None, Invocation::default()).is_err(),
|
||||
third.run_command("hello", "word-count", None, Invocation::default()).is_err(),
|
||||
"no active file, but it runs"
|
||||
);
|
||||
assert_eq!(again.info_of("hello").unwrap().status, PluginStatus::Active);
|
||||
assert_eq!(third.info_of("hello").unwrap().status, PluginStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1342,9 +1344,12 @@ fn thousands_of_icons_install_and_reload_within_budget() {
|
||||
let reload = started.elapsed();
|
||||
assert_eq!(again.assets("acme.many", &icon_theme_svg_prefix("many")).unwrap().len(), ICONS);
|
||||
eprintln!("{ICONS} icons: install {install:?}, load at start-up {reload:?} (from the contribution index)");
|
||||
// Generous for debug builds; release is several times faster.
|
||||
assert!(install < Duration::from_secs(5), "install took {install:?}");
|
||||
assert!(reload < Duration::from_secs(3), "start-up load took {reload:?}");
|
||||
// Guards against work that grows faster than the icon count, not a
|
||||
// benchmark: a debug build takes ~1.6 s / ~0.35 s on a laptop; shared CI
|
||||
// runners running the suite in parallel take several times that (Windows
|
||||
// ~11 s to install).
|
||||
assert!(install < Duration::from_secs(30), "install took {install:?}");
|
||||
assert!(reload < Duration::from_secs(10), "start-up load took {reload:?}");
|
||||
|
||||
// Start-up reads the validated declaration from the index, not the SVGs:
|
||||
// a tampered icon file is not even looked at...
|
||||
@@ -1814,6 +1819,9 @@ fn official_mybatis_navigates_between_interface_and_xml() {
|
||||
text: text.into(),
|
||||
};
|
||||
let jdoc = doc(&java_path, "java", java);
|
||||
// The first call indexes the XML files; on a slow machine that can run past
|
||||
// the edit-path budget (the answer is dropped, the index kept).
|
||||
let _ = f.host.provide_line_markers(&jdoc);
|
||||
let markers = f.host.provide_line_markers(&jdoc);
|
||||
assert_eq!(markers.len(), 1, "{markers:?}");
|
||||
let target = markers[0].1.target.clone().unwrap();
|
||||
@@ -1831,6 +1839,7 @@ fn official_mybatis_navigates_between_interface_and_xml() {
|
||||
);
|
||||
|
||||
// Warm (index cached): well inside the edit-path budget.
|
||||
let slow = f.host.info_of("openidea.mybatis").unwrap().stats.slow_calls;
|
||||
let mut times = Vec::new();
|
||||
for _ in 0..20 {
|
||||
let started = Instant::now();
|
||||
@@ -1840,7 +1849,7 @@ fn official_mybatis_navigates_between_interface_and_xml() {
|
||||
times.sort();
|
||||
eprintln!("mybatis line markers p95: {:?}", times[18]);
|
||||
assert!(times[18] < Duration::from_millis(50), "{times:?}");
|
||||
assert_eq!(f.host.info_of("openidea.mybatis").unwrap().stats.slow_calls, 0);
|
||||
assert_eq!(f.host.info_of("openidea.mybatis").unwrap().stats.slow_calls, slow);
|
||||
}
|
||||
|
||||
/// CSV Editor: a custom editor renders the document as a table (the
|
||||
|
||||
@@ -35,8 +35,16 @@ pub fn build(proxy: &ProxySettings) -> IdeResult<reqwest::Client> {
|
||||
.with_platform_verifier()
|
||||
.map_err(|e| fail(e.to_string()))?
|
||||
.with_no_client_auth();
|
||||
// `localhost` is tried on IPv4 first: local servers (Ollama, dev servers)
|
||||
// usually listen there, and on Windows a refused `::1` costs ~2 s before
|
||||
// the system resolver's order falls back. Ports come from the URL.
|
||||
let loopback = [
|
||||
std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)),
|
||||
std::net::SocketAddr::from((std::net::Ipv6Addr::LOCALHOST, 0)),
|
||||
];
|
||||
let builder = reqwest::Client::builder()
|
||||
.tls_backend_preconfigured(tls)
|
||||
.resolve_to_addrs("localhost", &loopback)
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.user_agent(concat!("OpenIDEA-HttpClient/", env!("CARGO_PKG_VERSION")));
|
||||
with_proxy(builder, proxy).build().map_err(|e| fail(e.to_string()))
|
||||
@@ -113,7 +121,11 @@ mod tests {
|
||||
|
||||
/// A one-shot HTTP server on loopback: answers `200` and hands back the request head.
|
||||
fn server() -> (u16, std::thread::JoinHandle<String>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
server_on("127.0.0.1:0")
|
||||
}
|
||||
|
||||
fn server_on(addr: &str) -> (u16, std::thread::JoinHandle<String>) {
|
||||
let listener = TcpListener::bind(addr).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
@@ -141,6 +153,21 @@ mod tests {
|
||||
check_connection(proxy, url, &CancellationToken::new()).await
|
||||
}
|
||||
|
||||
/// `localhost` reaches a server on either loopback address.
|
||||
#[tokio::test]
|
||||
async fn localhost_reaches_ipv4_and_ipv6_servers() {
|
||||
let direct = ProxySettings::default();
|
||||
let (port, v4) = server();
|
||||
assert_eq!(get(&direct, &format!("http://localhost:{port}/")).await.unwrap(), "200 OK");
|
||||
v4.join().unwrap();
|
||||
// Hosts without IPv6 loopback skip this half.
|
||||
if TcpListener::bind("[::1]:0").is_ok() {
|
||||
let (port, v6) = server_on("[::1]:0");
|
||||
assert_eq!(get(&direct, &format!("http://localhost:{port}/")).await.unwrap(), "200 OK");
|
||||
v6.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn builds_for_every_mode() {
|
||||
for mode in [ProxyMode::None, ProxyMode::Auto, ProxyMode::Manual] {
|
||||
|
||||
Reference in new issue
Block a user