дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Goal configurations saved from the Maven tool window kept the literal mvn / mvn.cmd, which could not start when Maven lives in a configured home or a version manager's shims. Saved configurations with a bare mvn launcher now take the configured Maven; custom paths and the wrapper are left alone. Co-authored-by: a-rookie-of-C-language <[email protected]>
941 рядки
41 KiB
Rust
941 рядки
41 KiB
Rust
use crate::apply::{
|
|
apply_to_run_configs, configure_server, jdtls_jdk, maven_command, maven_options, project_jdk, python_interpreter,
|
|
terminal_activation, tsdk,
|
|
};
|
|
use crate::python::parse_pip_list;
|
|
use crate::{bin_dir, exe, venv_of};
|
|
use ide_core::settings::Settings;
|
|
use ide_language::LanguageServerConfig;
|
|
use ide_run::RunConfig;
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
fn fake_venv(root: &Path, name: &str) -> std::path::PathBuf {
|
|
let venv = root.join(name);
|
|
std::fs::create_dir_all(venv.join(bin_dir())).unwrap();
|
|
std::fs::write(venv.join("pyvenv.cfg"), "home = /usr/bin\nversion = 3.12.1\n").unwrap();
|
|
let py = venv.join(bin_dir()).join(exe("python"));
|
|
std::fs::write(&py, "").unwrap();
|
|
std::fs::write(venv.join(bin_dir()).join("activate"), "").unwrap();
|
|
py
|
|
}
|
|
|
|
fn server(name: &str) -> LanguageServerConfig {
|
|
LanguageServerConfig {
|
|
name: name.into(),
|
|
command: name.into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
root_markers: vec![],
|
|
require_root_marker: false,
|
|
initialization_options: None,
|
|
settings: None,
|
|
install_hint: None,
|
|
connect: None,
|
|
fallback: false,
|
|
max_java: None,
|
|
launch_error: None,
|
|
}
|
|
}
|
|
|
|
fn run(kind: &str, command: &str, args: &[&str]) -> RunConfig {
|
|
RunConfig {
|
|
id: format!("{kind}:x"),
|
|
name: "x".into(),
|
|
kind: kind.into(),
|
|
command: command.into(),
|
|
args: args.iter().map(|s| (*s).to_owned()).collect(),
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: true,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn project_venv_is_the_default_interpreter() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let s = Settings::default();
|
|
assert_eq!(python_interpreter(tmp.path(), &s), None);
|
|
let py = fake_venv(tmp.path(), ".venv");
|
|
assert_eq!(python_interpreter(tmp.path(), &s), Some(py.clone()));
|
|
assert_eq!(venv_of(&py), Some(tmp.path().join(".venv")));
|
|
let mut s = Settings::default();
|
|
s.languages.python.interpreter = "/opt/py/bin/python3".into();
|
|
assert_eq!(python_interpreter(tmp.path(), &s).unwrap(), Path::new("/opt/py/bin/python3"));
|
|
}
|
|
|
|
#[test]
|
|
fn pyright_gets_python_path_and_venv_env() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let py = fake_venv(tmp.path(), ".venv");
|
|
let mut cfg = server("basedpyright");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &Settings::default());
|
|
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
|
assert_eq!(opts["python"]["pythonPath"], py.to_string_lossy().as_ref());
|
|
assert_eq!(cfg.env["VIRTUAL_ENV"], tmp.path().join(".venv").to_string_lossy());
|
|
assert!(cfg.env["PATH"].starts_with(&*tmp.path().join(".venv").join(bin_dir()).to_string_lossy()));
|
|
}
|
|
|
|
#[test]
|
|
fn jdtls_vm_args() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.java.jdtls_vm_args = " -Xmx2g -javaagent:/opt/lombok.jar ".into();
|
|
let mut cfg = server("jdtls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(cfg.args, ["--jvm-arg=-Xmx2g", "--jvm-arg=-javaagent:/opt/lombok.jar"]);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_analyzer_check_command_and_toolchain() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.rust.check_command = "clippy".into();
|
|
s.languages.rust.stdlib = "/src/rust/library".into();
|
|
s.languages.rust.rustfmt_args = "--edition 2024".into();
|
|
let mut cfg = server("rust-analyzer");
|
|
cfg.initialization_options =
|
|
Some(toml::Value::try_from(serde_json::json!({ "cargo": { "features": "all" } })).unwrap());
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap();
|
|
assert_eq!(opts["check"]["command"], "clippy");
|
|
assert_eq!(opts["cargo"]["features"], "all", "existing options are kept");
|
|
assert_eq!(opts["rustfmt"]["extraArgs"], serde_json::json!(["--edition", "2024"]));
|
|
assert_eq!(cfg.env["RUST_SRC_PATH"], "/src/rust/library");
|
|
// Unchanged settings produce an identical config (no needless restarts).
|
|
let mut again = server("rust-analyzer");
|
|
again.initialization_options =
|
|
Some(toml::Value::try_from(serde_json::json!({ "cargo": { "features": "all" } })).unwrap());
|
|
configure_server(&mut again, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(again, cfg);
|
|
}
|
|
|
|
#[test]
|
|
fn typescript_workspace_version() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let lib = tmp.path().join("node_modules/typescript/lib");
|
|
std::fs::create_dir_all(&lib).unwrap();
|
|
std::fs::write(lib.join("tsserver.js"), "").unwrap();
|
|
let mut s = Settings::default();
|
|
assert_eq!(tsdk(tmp.path(), &s), None);
|
|
s.languages.typescript.version = "workspace".into();
|
|
assert_eq!(tsdk(tmp.path(), &s), Some(lib.clone()));
|
|
let mut cfg = server("vtsls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
|
assert_eq!(opts["typescript"]["tsdk"], lib.to_string_lossy().as_ref());
|
|
}
|
|
|
|
#[test]
|
|
fn run_configs_use_selected_interpreter_and_package_manager() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.python.interpreter = "/opt/py/bin/python3".into();
|
|
s.languages.node.package_manager = "pnpm".into();
|
|
let mut configs = vec![
|
|
run("python", "uv", &["run", "python", "-m", "app"]),
|
|
run("pytest", ".venv/bin/pytest", &[]),
|
|
run("pytest", "uv", &["run", "pytest", "-x"]),
|
|
run("npm", "npm", &["run", "dev"]),
|
|
run("shell", "python3", &["x.py"]),
|
|
];
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
assert_eq!(configs[0].command, "/opt/py/bin/python3");
|
|
assert_eq!(configs[0].args, ["-m", "app"]);
|
|
assert_eq!(configs[1].command, "/opt/py/bin/python3");
|
|
assert_eq!(configs[1].args, ["-m", "pytest"]);
|
|
assert_eq!(configs[2].args, ["-m", "pytest", "-x"]);
|
|
assert_eq!(configs[3].command, "pnpm");
|
|
assert_eq!(configs[4].command, "python3", "shell configs are left alone");
|
|
}
|
|
|
|
fn fake_jdk(dir: &Path) {
|
|
std::fs::create_dir_all(dir.join("bin")).unwrap();
|
|
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
|
|
std::fs::write(dir.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn project_jdk_and_maven_settings() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let jdk = tmp.path().join("jdk17");
|
|
fake_jdk(&jdk);
|
|
let maven = tmp.path().join("maven");
|
|
std::fs::create_dir_all(maven.join("bin")).unwrap();
|
|
std::fs::write(maven.join("bin/mvn"), "").unwrap();
|
|
|
|
let mut s = Settings::default();
|
|
assert_eq!(project_jdk(tmp.path(), &s), None);
|
|
assert_eq!(maven_command(tmp.path(), &s), None);
|
|
assert!(maven_options(tmp.path(), &s).is_empty());
|
|
|
|
// Project-relative paths, a JDK's `bin/java`, a Maven home.
|
|
s.languages.java.jdk = "jdk17/bin/java".into();
|
|
s.languages.java.maven.home = maven.to_string_lossy().into_owned();
|
|
s.languages.java.maven.user_settings = "/m2/settings.xml".into();
|
|
s.languages.java.maven.local_repository = "/m2/repo".into();
|
|
assert_eq!(project_jdk(tmp.path(), &s).map(|j| j.major), Some(17));
|
|
assert_eq!(maven_command(tmp.path(), &s), Some(maven.join("bin/mvn")));
|
|
assert_eq!(maven_options(tmp.path(), &s), ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo"]);
|
|
|
|
let mut configs = vec![run("maven", "./mvnw", &["package"]), run("gradle", "./gradlew", &["build"])];
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
let home = project_jdk(tmp.path(), &s).unwrap().home.to_string_lossy().into_owned();
|
|
assert_eq!(configs[0].command, maven.join("bin/mvn").to_string_lossy());
|
|
assert_eq!(configs[0].args, ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo", "package"]);
|
|
assert_eq!(configs[0].env["JAVA_HOME"], home);
|
|
assert_eq!(configs[1].env["JAVA_HOME"], home);
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
assert_eq!(configs[0].args.len(), 4, "applied once");
|
|
|
|
s.languages.java.jdk = "missing".into();
|
|
assert_eq!(project_jdk(tmp.path(), &s), None);
|
|
}
|
|
|
|
#[test]
|
|
fn saved_maven_goal_configs_take_the_configured_launcher() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let maven = tmp.path().join("maven");
|
|
std::fs::create_dir_all(maven.join("bin")).unwrap();
|
|
std::fs::write(maven.join("bin/mvn"), "").unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.java.maven.home = maven.to_string_lossy().into_owned();
|
|
s.languages.java.maven.user_settings = "/m2/settings.xml".into();
|
|
|
|
// A goal configuration saved from the tool window (`mvn -s … clean`):
|
|
// the bare launcher resolves to the configured Maven home (#328) and its
|
|
// own args are left alone.
|
|
let mut saved = run("maven", "mvn", &["-s", "/m2/settings.xml", "clean"]);
|
|
saved.detected = false;
|
|
// A custom path / the wrapper stay as configured.
|
|
let mut custom = run("maven", "/opt/other/bin/mvn", &["package"]);
|
|
custom.detected = false;
|
|
let mut wrapper = run("maven", "./mvnw", &["package"]);
|
|
wrapper.detected = false;
|
|
let mut configs = vec![saved, custom, wrapper];
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
assert_eq!(configs[0].command, maven.join("bin/mvn").to_string_lossy());
|
|
assert_eq!(configs[0].args, ["-s", "/m2/settings.xml", "clean"], "saved args are kept");
|
|
assert_eq!(configs[1].command, "/opt/other/bin/mvn");
|
|
assert_eq!(configs[2].command, "./mvnw");
|
|
}
|
|
|
|
#[test]
|
|
fn language_server_settings_disable_add_and_override() {
|
|
use crate::apply::{language_servers, server_launch_overrides, server_protocol_overrides, with_custom_servers};
|
|
use ide_core::settings::LanguageServerSettings;
|
|
use serde_json::json;
|
|
|
|
let mut s = Settings::default();
|
|
s.language_servers.insert("pylsp".into(), LanguageServerSettings { enabled: false, ..Default::default() });
|
|
s.language_servers.insert(
|
|
"ruff".into(),
|
|
LanguageServerSettings {
|
|
command: "ruff".into(),
|
|
args: Some(vec!["server".into()]),
|
|
languages: vec!["python".into()],
|
|
root_markers: vec!["pyproject.toml".into()],
|
|
..Default::default()
|
|
},
|
|
);
|
|
let defined = [server("basedpyright"), server("pylsp")];
|
|
let names = |v: Vec<LanguageServerConfig>| v.into_iter().map(|c| c.name).collect::<Vec<_>>();
|
|
assert_eq!(names(language_servers("python", &defined, &s)), ["basedpyright", "ruff"]);
|
|
assert_eq!(names(language_servers("rust", &[server("rust-analyzer")], &s)), ["rust-analyzer"]);
|
|
|
|
// Listed with the tools even while disabled.
|
|
let mut langs = ide_language::LanguageRegistry::load(None).unwrap().all();
|
|
with_custom_servers(&mut langs, &s);
|
|
let python = langs.iter().find(|l| l.id.as_str() == "python").unwrap();
|
|
let mut ruff = python.language_servers.iter().find(|c| c.name == "ruff").unwrap().clone();
|
|
assert_eq!(ruff.root_markers, ["pyproject.toml"]);
|
|
server_launch_overrides(&mut ruff, &s);
|
|
assert_eq!((ruff.command.as_str(), ruff.args.as_slice()), ("ruff", &["server".to_string()][..]));
|
|
|
|
s.language_servers.insert(
|
|
"rust-analyzer".into(),
|
|
LanguageServerSettings {
|
|
env: BTreeMap::from([("RA_LOG".into(), "info".into())]),
|
|
initialization_options: Some(json!({ "check": { "command": "clippy" } })),
|
|
settings: Some(json!({ "rust-analyzer": { "cargo": { "features": "all" } } })),
|
|
..Default::default()
|
|
},
|
|
);
|
|
let mut ra = server("rust-analyzer");
|
|
ra.args = vec!["--stdio".into()];
|
|
ra.initialization_options = Some(toml::Value::Table(toml::toml! { checkOnSave = false }));
|
|
server_launch_overrides(&mut ra, &s);
|
|
assert_eq!(ra.args, ["--stdio"], "no args setting keeps the definition's");
|
|
server_protocol_overrides(&mut ra, &s);
|
|
assert_eq!(ra.env["RA_LOG"], "info");
|
|
let init = serde_json::to_value(ra.initialization_options.unwrap()).unwrap();
|
|
assert_eq!(init, json!({ "checkOnSave": false, "check": { "command": "clippy" } }));
|
|
assert_eq!(ra.settings, Some(json!({ "rust-analyzer": { "cargo": { "features": "all" } } })));
|
|
}
|
|
|
|
#[test]
|
|
fn jdtls_jdk_setting() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
fake_jdk(&tmp.path().join("jdk17"));
|
|
let mut s = Settings::default();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s), Ok(None));
|
|
// Separate from the project JDK, which run configurations keep using.
|
|
s.languages.java.jdk = "jdk17".into();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s), Ok(None));
|
|
s.languages.java.jdtls_jdk = "jdk17/bin".into();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s).unwrap().map(|j| j.major), Some(17));
|
|
// A chosen path that is not a JDK is an error, not a silent fallback.
|
|
s.languages.java.jdtls_jdk = "missing".into();
|
|
assert!(jdtls_jdk(tmp.path(), &s).unwrap_err().contains("is not a JDK home"));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_activation_for_project_venv() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/bin/zsh")), None);
|
|
fake_venv(tmp.path(), ".venv");
|
|
let line = terminal_activation(tmp.path(), &s, Some("/bin/zsh")).unwrap();
|
|
assert!(line.starts_with("source '") && line.ends_with("/activate'"), "{line}");
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/usr/bin/fish")), None, "no activate.fish");
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("docker")), None, "not a shell");
|
|
s.languages.python.activate_virtualenv = false;
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/bin/zsh")), None);
|
|
}
|
|
|
|
#[test]
|
|
fn pip_list_output_is_parsed() {
|
|
let out = "$ python -m pip list\n[{\"name\": \"requests\", \"version\": \"2.32.0\"}]\n";
|
|
let pkgs = parse_pip_list(out).unwrap();
|
|
assert_eq!(pkgs[0].name, "requests");
|
|
assert_eq!(pkgs[0].version, "2.32.0");
|
|
}
|
|
|
|
#[test]
|
|
fn excluded_folders_become_anchored_globs() {
|
|
let mut s = Settings::default();
|
|
s.project_structure.excluded = vec!["build/".into(), " ".into()];
|
|
let globs = s.exclude_globs();
|
|
assert!(globs.contains(&"/build".to_owned()));
|
|
assert_eq!(globs.len(), s.file_excludes().len() + 1);
|
|
}
|
|
|
|
/// Prints what this machine has installed.
|
|
#[test]
|
|
#[ignore]
|
|
fn detect_on_this_machine() {
|
|
for kind in [
|
|
crate::SdkKind::Python,
|
|
crate::SdkKind::Node,
|
|
crate::SdkKind::PackageManager,
|
|
crate::SdkKind::Rust,
|
|
crate::SdkKind::Go,
|
|
] {
|
|
let t = std::time::Instant::now();
|
|
let found = crate::detect(kind, Some(Path::new(env!("CARGO_MANIFEST_DIR"))));
|
|
eprintln!("{kind:?} ({} ms)", t.elapsed().as_millis());
|
|
for sdk in found {
|
|
eprintln!(" {} {:?} [{}] {}", sdk.name, sdk.version, sdk.source, sdk.path.display());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn custom_tool_path_replaces_the_command() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.tools.insert("elixir-ls".into(), ide_core::settings::ToolSettings { path: "tools/ls/language_server.sh".into() });
|
|
s.tools.insert("zls".into(), ide_core::settings::ToolSettings { path: " ".into() });
|
|
let mut cfg = server("elixir-ls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(Path::new(&cfg.command), tmp.path().join("tools/ls/language_server.sh"));
|
|
// A blank path keeps auto-detection.
|
|
let mut cfg = server("zls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(cfg.command, "zls");
|
|
}
|
|
|
|
#[test]
|
|
fn the_godot_setting_applies_to_server_debugger_and_run_configurations() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.tools.insert("godot".into(), ide_core::settings::ToolSettings { path: "~/Godot/godot".into() });
|
|
let expected = crate::home_dir().unwrap().join("Godot/godot");
|
|
assert_eq!(crate::apply::tool_command(tmp.path(), &s, "godot"), Some(expected.clone()));
|
|
let mut cfg = LanguageServerConfig { command: "godot".into(), ..server("godot") };
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(Path::new(&cfg.command), expected);
|
|
let mut run = RunConfig {
|
|
id: "godot:project".into(),
|
|
name: "Godot Project".into(),
|
|
kind: "godot".into(),
|
|
command: "godot".into(),
|
|
args: vec!["--path".into(), ".".into()],
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: true,
|
|
is_test: false,
|
|
debug_adapter: Some("godot".into()),
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
};
|
|
apply_to_run_configs(std::slice::from_mut(&mut run), tmp.path(), &s);
|
|
assert_eq!(Path::new(&run.command), expected);
|
|
// Other commands keep their name without a setting.
|
|
assert_eq!(crate::apply::tool_command(tmp.path(), &s, "zls"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn python_library_paths_are_parsed_and_deduplicated() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let stdlib = tmp.path().join("lib/python3.12");
|
|
let site = stdlib.join("site-packages");
|
|
std::fs::create_dir_all(&site).unwrap();
|
|
let json = serde_json::to_string(&[Some(&stdlib), Some(&stdlib), Some(&site), None]).unwrap();
|
|
let paths = crate::libraries::parse_python_paths(&format!("warning: something\n{json}"));
|
|
assert_eq!(paths.len(), 3);
|
|
let roots = crate::libraries::library_roots([paths, vec![tmp.path().join("gone")]].concat());
|
|
assert_eq!(roots.iter().map(|r| r.name.as_str()).collect::<Vec<_>>(), ["python3.12", "site-packages"]);
|
|
assert!(crate::libraries::parse_python_paths("not json").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_libraries_without_an_interpreter() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
assert!(crate::libraries::project_libraries(tmp.path(), &Settings::default(), &[]).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn node_libraries_list_direct_dependencies_and_core_typings() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
std::fs::write(
|
|
root.join("package.json"),
|
|
r#"{"dependencies":{"react":"^18","@scope/ui":"1"},"devDependencies":{"vitest":"2","react":"^18"}}"#,
|
|
)
|
|
.unwrap();
|
|
for dir in ["react", "@scope/ui", "@types/node", "transitive"] {
|
|
std::fs::create_dir_all(root.join("node_modules").join(dir)).unwrap();
|
|
}
|
|
let groups = crate::libraries::node_libraries(root, None);
|
|
let summary: Vec<(&str, &str, Vec<&str>)> = groups
|
|
.iter()
|
|
.map(|g| (g.name.as_str(), g.kind.as_str(), g.roots.iter().map(|r| r.name.as_str()).collect()))
|
|
.collect();
|
|
assert_eq!(
|
|
summary,
|
|
[
|
|
("Node.js Core", "nodeCore", vec!["@types/node"]),
|
|
("node_modules", "nodeModules", vec!["@scope/ui", "react"])
|
|
]
|
|
);
|
|
assert_eq!(crate::libraries::package_dependencies("{"), Vec::<String>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_metadata_lists_registry_packages_only() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let serde = tmp.path().join("registry/src/index/serde-1.0.200");
|
|
let json = serde_json::json!({ "packages": [
|
|
{ "name": "app", "version": "0.1.0", "source": null, "manifest_path": tmp.path().join("Cargo.toml") },
|
|
{ "name": "serde", "version": "1.0.200", "source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
"manifest_path": serde.join("Cargo.toml") },
|
|
]});
|
|
assert_eq!(crate::libraries::parse_cargo_metadata(&json.to_string()), [("serde 1.0.200".to_owned(), serde)]);
|
|
assert!(crate::libraries::parse_cargo_metadata("error: no Cargo.toml").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn go_modules_skip_the_main_module_and_missing_downloads() {
|
|
let out = r#"{"Path":"example.com/app","Main":true,"Dir":"/p"}
|
|
{"Path":"golang.org/x/text","Version":"v0.14.0","Dir":"/gopath/pkg/mod/golang.org/x/[email protected]"}
|
|
{"Path":"example.com/missing","Version":"v1.0.0","Error":{"Err":"module lookup disabled"}}"#;
|
|
assert_eq!(
|
|
crate::libraries::parse_go_modules(out),
|
|
[(
|
|
"golang.org/x/[email protected]".to_owned(),
|
|
std::path::PathBuf::from("/gopath/pkg/mod/golang.org/x/[email protected]")
|
|
)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn jdk_library_is_the_jdk_home() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path().join("temurin-21.jdk/Contents/Home");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
assert_eq!(crate::libraries::jdk_name(&home).as_deref(), Some("temurin-21"));
|
|
assert_eq!(crate::libraries::jdk_name(&tmp.path().join("jdk-17")).as_deref(), Some("jdk-17"));
|
|
let group = crate::libraries::jdk_library(&crate::java::Jdk { major: 21, home: home.clone() }).unwrap();
|
|
assert_eq!((group.name.as_str(), group.kind.as_str()), ("temurin-21", "jdk"));
|
|
assert_eq!(group.roots[0].path, ide_core::IpcPath::from(home.clone()));
|
|
assert_eq!(group.roots.len(), 1, "no sources archive");
|
|
|
|
std::fs::create_dir_all(home.join("lib")).unwrap();
|
|
std::fs::write(home.join("lib/src.zip"), "").unwrap();
|
|
let group = crate::libraries::jdk_library(&crate::java::Jdk { major: 21, home: home.clone() }).unwrap();
|
|
assert_eq!(group.roots[1].name, "src.zip");
|
|
assert_eq!(group.roots[1].path, ide_core::IpcPath::from(ide_vfs::archive_root(&home.join("lib/src.zip"))));
|
|
}
|
|
|
|
#[test]
|
|
fn build_dependencies_resolve_to_local_jars() {
|
|
use crate::libraries::{BuildDependency, dependency_libraries};
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (m2, gradle) = (tmp.path().join("m2"), tmp.path().join("gradle"));
|
|
let touch = |p: std::path::PathBuf| {
|
|
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
std::fs::write(&p, "").unwrap();
|
|
p
|
|
};
|
|
touch(m2.join("com/google/guava/guava/33.0/guava-33.0.jar"));
|
|
let guava_src = touch(m2.join("com/google/guava/guava/33.0/guava-33.0-sources.jar"));
|
|
let junit = touch(m2.join("junit/junit/4.13/junit-4.13.jar"));
|
|
let files = gradle.join("caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.9");
|
|
touch(files.join("a1b2/slf4j-api-2.0.9.jar"));
|
|
let slf4j_src = touch(files.join("c3d4/slf4j-api-2.0.9-sources.jar"));
|
|
let dep = |gradle: bool, g: &str, a: &str, v: &str| BuildDependency {
|
|
gradle,
|
|
group: g.into(),
|
|
artifact: a.into(),
|
|
version: v.into(),
|
|
};
|
|
let deps = [
|
|
dep(false, "junit", "junit", "4.13"),
|
|
dep(false, "com.google.guava", "guava", "33.0"),
|
|
dep(false, "com.google.guava", "guava", "33.0"),
|
|
dep(true, "org.slf4j", "slf4j-api", "2.0.9"),
|
|
dep(false, "missing", "nope", "1.0"),
|
|
dep(false, "..", "evil", "1"),
|
|
];
|
|
let groups = dependency_libraries(&deps, Some(&m2), Some(&gradle));
|
|
let shown: Vec<(&str, &str, &str)> =
|
|
groups.iter().map(|g| (g.name.as_str(), g.kind.as_str(), g.roots[0].name.as_str())).collect();
|
|
assert_eq!(
|
|
shown,
|
|
[
|
|
("Maven: com.google.guava:guava:33.0", "maven", "guava-33.0-sources.jar"),
|
|
("Maven: junit:junit:4.13", "maven", "junit-4.13.jar"),
|
|
("Gradle: org.slf4j:slf4j-api:2.0.9", "gradle", "slf4j-api-2.0.9-sources.jar"),
|
|
]
|
|
);
|
|
assert_eq!(groups[0].roots[0].path, ide_core::IpcPath::from(ide_vfs::archive_root(&guava_src)));
|
|
assert_eq!(groups[1].roots[0].path, ide_core::IpcPath::from(ide_vfs::archive_root(&junit)));
|
|
assert_eq!(groups[2].roots[0].path, ide_core::IpcPath::from(ide_vfs::archive_root(&slf4j_src)));
|
|
// A Gradle build also finds jars installed in the Maven local repository (`mavenLocal()`).
|
|
let groups = dependency_libraries(&[dep(true, "junit", "junit", "4.13")], Some(&m2), Some(&gradle));
|
|
assert_eq!(groups[0].name, "Gradle: junit:junit:4.13");
|
|
}
|
|
|
|
#[test]
|
|
fn library_detection_is_cached_until_a_manifest_changes() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let manifest = tmp.path().join("package.json");
|
|
std::fs::write(&manifest, "{}").unwrap();
|
|
let runs = std::cell::Cell::new(0);
|
|
let detect = || {
|
|
runs.set(runs.get() + 1);
|
|
Vec::new()
|
|
};
|
|
let files = [manifest.clone()];
|
|
crate::libraries::cached(tmp.path(), "test", "a".into(), &files, detect);
|
|
crate::libraries::cached(tmp.path(), "test", "a".into(), &files, detect);
|
|
assert_eq!(runs.get(), 1);
|
|
std::fs::write(&manifest, r#"{"dependencies":{}}"#).unwrap();
|
|
crate::libraries::cached(tmp.path(), "test", "a".into(), &files, detect);
|
|
assert_eq!(runs.get(), 2);
|
|
crate::libraries::cached(tmp.path(), "test", "b".into(), &files, detect);
|
|
assert_eq!(runs.get(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn no_ecosystem_libraries_without_manifests() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
std::fs::create_dir_all(tmp.path().join("node_modules/react")).unwrap();
|
|
assert!(crate::libraries::project_libraries(tmp.path(), &Settings::default(), &[]).is_empty());
|
|
}
|
|
|
|
/// Tools dir with vue-language-server and the given TypeScript files, plus a
|
|
/// project; returns (tools, project).
|
|
fn vue_setup(tmp: &Path, tools_ts: &str, project_ts: &str) -> (std::path::PathBuf, std::path::PathBuf) {
|
|
let tools = tmp.join("tools/node_modules");
|
|
let project = tmp.join("app");
|
|
for (p, content) in [
|
|
(tools.join(".bin/vue-language-server"), ""),
|
|
(tools.join("@vue/language-server/package.json"), "{}"),
|
|
(tools.join("typescript/lib").join(tools_ts), ""),
|
|
(project.join("node_modules/typescript/lib").join(project_ts), ""),
|
|
(project.join("node_modules/typescript/lib/tsserver.js"), ""),
|
|
] {
|
|
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
std::fs::write(p, content).unwrap();
|
|
}
|
|
(tools, project)
|
|
}
|
|
|
|
#[test]
|
|
fn vue_language_server_gets_a_typescript_with_the_language_service_api() {
|
|
// Issue #25: `typescript@latest` (7.x) in the tools dir has no `ts.server`.
|
|
let s = Settings::default();
|
|
let cases = [
|
|
// (tools TypeScript, project TypeScript, expected --tsdk = project lib, launch error)
|
|
("typescript.js", "typescript.js", false, false), // own TypeScript 5/6 works
|
|
("tsc.js", "typescript.js", true, false), // own TS 7: the project's
|
|
("tsc.js", "tsc.js", false, true), // nothing usable: clear error
|
|
];
|
|
for (tools_ts, project_ts, uses_project, error) in cases {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (tools, project) = vue_setup(tmp.path(), tools_ts, project_ts);
|
|
let mut cfg = server("vue-language-server");
|
|
cfg.command = tools.join(".bin/vue-language-server").to_string_lossy().into_owned();
|
|
configure_server(&mut cfg, &project, &project, &s);
|
|
let tsdk = cfg.args.iter().find_map(|a| a.strip_prefix("--tsdk=")).map(std::path::PathBuf::from);
|
|
let project_lib = project.join("node_modules/typescript/lib");
|
|
assert_eq!(tsdk, uses_project.then_some(project_lib), "{tools_ts} / {project_ts}");
|
|
assert_eq!(cfg.launch_error.is_some(), error, "{tools_ts} / {project_ts}");
|
|
}
|
|
|
|
// A selected version wins over the server's own.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (tools, project) = vue_setup(tmp.path(), "typescript.js", "typescript.js");
|
|
let mut s = Settings::default();
|
|
s.languages.typescript.version = "workspace".into();
|
|
let mut cfg = server("vue-language-server");
|
|
cfg.command = tools.join(".bin/vue-language-server").to_string_lossy().into_owned();
|
|
configure_server(&mut cfg, &project, &project, &s);
|
|
let lib = project.join("node_modules/typescript/lib");
|
|
assert_eq!(cfg.args, [format!("--tsdk={}", lib.to_string_lossy())]);
|
|
}
|
|
|
|
/// An npm shim for `name` in `dir`: `name.cmd` on Windows, an executable elsewhere.
|
|
fn npm_shim(dir: &Path, name: &str) -> std::path::PathBuf {
|
|
std::fs::create_dir_all(dir).unwrap();
|
|
let shim = dir.join(if cfg!(windows) { format!("{name}.cmd") } else { name.to_owned() });
|
|
std::fs::write(&shim, "").unwrap();
|
|
#[cfg(unix)]
|
|
std::fs::set_permissions(&shim, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
|
|
shim
|
|
}
|
|
|
|
#[test]
|
|
fn angular_language_server_probes_its_own_package_then_the_project() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let tools = tmp.path().join("tools/node_modules");
|
|
let project = tmp.path().join("app");
|
|
let ngserver = npm_shim(&tools.join(".bin"), "ngserver");
|
|
for (p, content) in [
|
|
(tools.join("@angular/language-server/package.json"), "{}"),
|
|
(project.join("node_modules/@angular/core/package.json"), r#"{"version":"17.3.0"}"#),
|
|
(project.join("node_modules/typescript/lib/tsserver.js"), ""),
|
|
] {
|
|
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
std::fs::write(p, content).unwrap();
|
|
}
|
|
let configured = |command: &str, s: &Settings| {
|
|
let mut cfg = server("angular-language-server");
|
|
cfg.command = command.into();
|
|
cfg.args = vec!["--stdio".into()];
|
|
configure_server(&mut cfg, &project, &project, s);
|
|
cfg
|
|
};
|
|
let probes = format!("{},{}", tools.join("@angular/language-server").to_string_lossy(), project.to_string_lossy());
|
|
let cfg = configured(&ngserver.to_string_lossy(), &Settings::default());
|
|
assert_eq!(
|
|
cfg.args,
|
|
[
|
|
"--stdio",
|
|
"--ngProbeLocations",
|
|
&probes,
|
|
"--tsProbeLocations",
|
|
&probes,
|
|
"--angularCoreVersion",
|
|
"17.3.0",
|
|
"--disableLetSyntax"
|
|
]
|
|
);
|
|
assert_eq!(cfg.launch_error, None);
|
|
|
|
// The project's TypeScript when selected.
|
|
let mut s = Settings::default();
|
|
s.languages.typescript.version = "workspace".into();
|
|
let args = configured(&ngserver.to_string_lossy(), &s).args;
|
|
let tsdk = args.iter().position(|a| a == "--tsdk").map(|i| args[i + 1].clone());
|
|
assert_eq!(tsdk, Some(project.join("node_modules/typescript/lib").to_string_lossy().into_owned()));
|
|
|
|
// Found on the selected Node.js's PATH like the launcher finds it, and
|
|
// launched from there.
|
|
let mut s = Settings::default();
|
|
s.languages.node.interpreter = tools.join(".bin/node").to_string_lossy().into_owned();
|
|
let cfg = configured("ngserver", &s);
|
|
assert_eq!(dunce::canonicalize(&cfg.command).unwrap(), dunce::canonicalize(&ngserver).unwrap());
|
|
assert!(cfg.args.contains(&probes));
|
|
|
|
// Without its package the server can't start; not installed, it asks to be.
|
|
let lone = npm_shim(&tmp.path().join("bin"), "ngserver");
|
|
let cfg = configured(&lone.to_string_lossy(), &Settings::default());
|
|
assert!(cfg.launch_error.is_some_and(|m| m.contains("@angular/language-server")));
|
|
assert_eq!(cfg.args, ["--stdio"]);
|
|
let cfg = configured(&tmp.path().join("nowhere/ngserver").to_string_lossy(), &Settings::default());
|
|
assert_eq!((cfg.launch_error, cfg.args), (None, vec!["--stdio".to_owned()]));
|
|
}
|
|
|
|
#[test]
|
|
fn angular_language_server_is_found_behind_version_manager_shims() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let project = tmp.path().join("app");
|
|
std::fs::create_dir_all(&project).unwrap();
|
|
let install = |node_modules: &Path| {
|
|
let package = node_modules.join("@angular/language-server");
|
|
std::fs::create_dir_all(&package).unwrap();
|
|
std::fs::write(package.join("package.json"), "{}").unwrap();
|
|
package
|
|
};
|
|
let probes = |command: &Path, s: &Settings, env: &[(&str, &str)]| {
|
|
let mut cfg = server("angular-language-server");
|
|
cfg.command = command.to_string_lossy().into_owned();
|
|
cfg.env.extend(env.iter().map(|(k, v)| (k.to_string(), v.to_string())));
|
|
configure_server(&mut cfg, &project, &project, s);
|
|
cfg.args.iter().position(|a| a == "--ngProbeLocations").map(|i| cfg.args[i + 1].clone())
|
|
};
|
|
let with_project = |package: &Path| Some(format!("{},{}", package.to_string_lossy(), project.to_string_lossy()));
|
|
|
|
// pnpm: the shim script runs the package from pnpm's global directory.
|
|
let pnpm = tmp.path().join("pnpm");
|
|
let package = install(&pnpm.join("global/v11/abc/node_modules"));
|
|
let shim = npm_shim(&pnpm.join("bin"), "ngserver");
|
|
let script = "exec node \"$basedir/../global/v11/abc/node_modules/@angular/language-server/bin/ngserver\" \"$@\"\n";
|
|
std::fs::write(&shim, format!("#!/bin/sh\n{script}")).unwrap();
|
|
assert_eq!(probes(&shim, &Settings::default(), &[]), with_project(&dunce::canonicalize(&package).unwrap()));
|
|
|
|
// Volta: the package's image, next to the shim's directory. Joined as the lookup
|
|
// joins it: the probe locations compare as text, and Windows keeps a `/` as given.
|
|
let volta = tmp.path().join("volta");
|
|
let image = volta.join("tools").join("image").join("packages").join("@angular/language-server");
|
|
let package = install(&image.join("lib").join("node_modules"));
|
|
let shim = npm_shim(&volta.join("bin"), "ngserver");
|
|
assert_eq!(probes(&shim, &Settings::default(), &[]), with_project(&package));
|
|
|
|
// asdf, mise: the shim names only the command; the manager's npm shim knows
|
|
// where global packages are, run as the server is: in the project (whose
|
|
// `.tool-versions` picks the Node), with the login shell's and its own variables.
|
|
let root_of = |package: &Path| package.parent().unwrap().parent().unwrap().display().to_string();
|
|
let package = install(&tmp.path().join("asdf/installs/nodejs/24/lib/node_modules"));
|
|
std::fs::write(project.join(".tool-versions"), "nodejs 24\n").unwrap();
|
|
// The only writer in this test binary.
|
|
ide_core::process::set_shell_env(vec![("OPENIDEA_TEST_SHELL".into(), "1".into())]);
|
|
let shims = tmp.path().join(".asdf/shims");
|
|
// The manager's npm runs on the first Node on PATH (`#!/usr/bin/env node`).
|
|
std::fs::write(
|
|
npm_shim(&shims, "npm"),
|
|
if cfg!(windows) {
|
|
"@if exist .tool-versions if \"%OPENIDEA_TEST_SHELL%\"==\"1\" call node\r\n"
|
|
} else {
|
|
"#!/bin/sh\n[ -f .tool-versions ] && [ \"$OPENIDEA_TEST_SHELL\" = 1 ] && exec node\n"
|
|
},
|
|
)
|
|
.unwrap();
|
|
std::fs::write(
|
|
npm_shim(&shims, "node"),
|
|
if cfg!(windows) {
|
|
"@echo %OPENIDEA_TEST_NPM_ROOT%\r\n"
|
|
} else {
|
|
"#!/bin/sh\necho \"$OPENIDEA_TEST_NPM_ROOT\"\n"
|
|
},
|
|
)
|
|
.unwrap();
|
|
// Another installation's npm and Node, first on PATH.
|
|
let node = tmp.path().join("node/bin");
|
|
let other = root_of(&install(&tmp.path().join("node/lib/node_modules")));
|
|
for name in ["npm", "node"] {
|
|
std::fs::write(
|
|
npm_shim(&node, name),
|
|
if cfg!(windows) { format!("@echo {other}\r\n") } else { format!("#!/bin/sh\necho '{other}'\n") },
|
|
)
|
|
.unwrap();
|
|
}
|
|
let mut s = Settings::default();
|
|
s.languages.node.interpreter = node.join("node").to_string_lossy().into_owned();
|
|
let root = root_of(&package);
|
|
let env = [("OPENIDEA_TEST_NPM_ROOT", root.as_str())];
|
|
assert_eq!(probes(&npm_shim(&shims, "ngserver"), &s, &env), with_project(&package));
|
|
// Not without the manager's own npm, nor for an executable elsewhere: the npm
|
|
// on PATH may be another installation's.
|
|
assert_eq!(probes(&npm_shim(&tmp.path().join("mise/shims"), "ngserver"), &s, &env), None);
|
|
assert_eq!(probes(&npm_shim(&tmp.path().join("bin"), "ngserver"), &s, &env), None);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn query_output_is_read_within_the_deadline() {
|
|
// Keep process startup outside a tight timing budget even under workspace load.
|
|
let timeout = std::time::Duration::from_secs(60);
|
|
let env = [("PATH", "/usr/bin:/bin")];
|
|
let started = std::time::Instant::now();
|
|
let out = crate::exec::query_in(Path::new("/bin/sh"), &["-c", "printf /global/root"], None, &env, timeout);
|
|
assert_eq!(out.as_deref(), Some("/global/root"));
|
|
// EOF must return promptly, without waiting for the generous deadline.
|
|
assert!(started.elapsed() < std::time::Duration::from_secs(30));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn query_drains_both_pipes_before_the_child_exits() {
|
|
let out = crate::exec::query_in(
|
|
Path::new("/bin/sh"),
|
|
&["-c", "head -c 262144 /dev/zero; head -c 262144 /dev/zero >&2"],
|
|
None,
|
|
&[("PATH", "/usr/bin:/bin")],
|
|
std::time::Duration::from_secs(60),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(out.len(), 262144);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn query_is_bounded_when_descendants_keep_pipes_open() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let pid = tmp.path().join("pid");
|
|
// The descendant outlives the query; clean it up even if an assertion fails.
|
|
struct Descendant(std::path::PathBuf);
|
|
impl Drop for Descendant {
|
|
fn drop(&mut self) {
|
|
if let Ok(pid) = std::fs::read_to_string(&self.0) {
|
|
let _ = std::process::Command::new("/bin/kill").arg(pid.trim()).status();
|
|
}
|
|
}
|
|
}
|
|
let _descendant = Descendant(pid.clone());
|
|
let started = std::time::Instant::now();
|
|
let out = crate::exec::query_in(
|
|
Path::new("/bin/sh"),
|
|
&["-c", "sleep 120 & echo $! > \"$1\"; printf /global/root", "wrapper", pid.to_str().unwrap()],
|
|
None,
|
|
&[("PATH", "/usr/bin:/bin")],
|
|
std::time::Duration::from_secs(30),
|
|
);
|
|
assert_eq!(out.as_deref(), Some("/global/root"));
|
|
// The descendant keeps both pipes open for longer than the query deadline.
|
|
assert!(started.elapsed() < std::time::Duration::from_secs(90));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn query_times_out_when_the_child_does_not_exit() {
|
|
// exec avoids leaving a descendant behind when the timed-out child is killed.
|
|
let out = crate::exec::query_in(
|
|
Path::new("/bin/sh"),
|
|
&["-c", "printf /global/root; exec sleep 120"],
|
|
None,
|
|
&[("PATH", "/usr/bin:/bin")],
|
|
std::time::Duration::from_millis(100),
|
|
);
|
|
assert_eq!(out, None);
|
|
}
|
|
|
|
#[test]
|
|
fn max_java_picks_an_older_jdk() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let jdk = |major: u32| {
|
|
let home = dir.path().join(format!("jdk-{major}"));
|
|
std::fs::create_dir_all(home.join("bin")).unwrap();
|
|
std::fs::write(home.join("release"), format!("JAVA_VERSION=\"{major}.0.1\"\n")).unwrap();
|
|
std::fs::write(home.join("bin").join(exe("java")), "").unwrap();
|
|
crate::java::jdk_at(&home).unwrap()
|
|
};
|
|
let (j17, j21, j25) = (jdk(17), jdk(21), jdk(25));
|
|
let jdks = [j25.clone(), j21.clone(), j17];
|
|
let mut cfg = LanguageServerConfig { max_java: Some(21), ..server("kotlin-language-server") };
|
|
cfg.env.insert("JAVA_HOME".into(), j25.home.to_string_lossy().into_owned());
|
|
crate::java::cap_java_home(&mut cfg, &jdks);
|
|
assert_eq!(cfg.env["JAVA_HOME"], j21.home.to_string_lossy());
|
|
|
|
// A JAVA_HOME that fits, or no limit: left alone.
|
|
crate::java::cap_java_home(&mut cfg, &[]);
|
|
assert_eq!(cfg.env["JAVA_HOME"], j21.home.to_string_lossy());
|
|
let mut free = server("other");
|
|
free.env.insert("JAVA_HOME".into(), j25.home.to_string_lossy().into_owned());
|
|
crate::java::cap_java_home(&mut free, &jdks);
|
|
assert_eq!(free.env["JAVA_HOME"], j25.home.to_string_lossy());
|
|
}
|
|
|
|
#[test]
|
|
fn gradle_settings_precedence() {
|
|
use crate::apply::{gradle_command, gradle_env};
|
|
use ide_core::settings::GradleDistribution;
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
let plain = if cfg!(windows) { "gradle.bat" } else { "gradle" };
|
|
let wrapper = root.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" });
|
|
let mut settings = Settings::default();
|
|
assert_eq!(gradle_command(root, root, &settings), std::path::PathBuf::from(plain));
|
|
std::fs::write(&wrapper, "").unwrap();
|
|
assert_eq!(gradle_command(root, root, &settings), wrapper);
|
|
let fallback = std::collections::BTreeMap::from([("JAVA_HOME".into(), "project-jdk".into())]);
|
|
assert_eq!(gradle_env(root, &settings, fallback.clone()), fallback);
|
|
settings.languages.java.gradle.jvm = "gradle-jdk".into();
|
|
settings.languages.java.gradle.user_home = "gradle-cache".into();
|
|
settings.languages.java.gradle.home = "local-gradle".into();
|
|
let env = gradle_env(root, &settings, fallback);
|
|
assert_eq!(env["JAVA_HOME"], root.join("gradle-jdk").to_string_lossy());
|
|
assert_eq!(env["GRADLE_USER_HOME"], root.join("gradle-cache").to_string_lossy());
|
|
// A stored local home is inactive in wrapper mode.
|
|
assert_eq!(gradle_command(root, root, &settings), wrapper);
|
|
for local in [false, true] {
|
|
settings.languages.java.gradle.distribution =
|
|
if local { GradleDistribution::Local } else { GradleDistribution::Wrapper };
|
|
let expected = if local { root.join("local-gradle").join("bin").join(plain) } else { plain.into() };
|
|
for explicit_java in [false, true] {
|
|
for is_test in [false, true] {
|
|
let mut configs =
|
|
[run("gradle", wrapper.to_str().unwrap(), &["test"]), run("gradle", plain, &["test"])];
|
|
for c in &mut configs {
|
|
c.is_test = is_test;
|
|
if explicit_java {
|
|
c.env.insert("JAVA_HOME".into(), "explicit-jdk".into());
|
|
}
|
|
}
|
|
apply_to_run_configs(&mut configs, root, &settings);
|
|
assert_eq!(configs[0].command, wrapper.to_string_lossy());
|
|
assert_eq!(configs[1].command, expected.to_string_lossy());
|
|
for c in &configs {
|
|
assert_eq!(
|
|
c.env["JAVA_HOME"],
|
|
if explicit_java {
|
|
"explicit-jdk".into()
|
|
} else {
|
|
root.join("gradle-jdk").to_string_lossy().into_owned()
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(gradle_command(root, root, &settings), if local { expected } else { wrapper.clone() });
|
|
}
|
|
}
|