Файли
openidea/crates/ide-sdk/src/tests.rs
T
l0ng-ai 1e00ea5ee2 feat: Third pass aligning core experience with IntelliJ IDEA
Project & files
- Project view modes (Project Files / Open Files / All Changed Files /
  Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go,
  Maven/Gradle jars; read-only jar/zip browsing
- ⌘N New popup with type search and more file kinds; New Project, Rename
  Project, Reload All from Disk, Export/Import Settings, Power Save Mode
- File type associations, extra content roots and exclude patterns (indexed
  and watched without reopening the project)

Editor & navigation
- Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline,
  history; Find in Files: Module scope, custom pattern scopes, multiline
- Search Everywhere filters, Assign Shortcut, settings results; named bookmark
  lists, file/folder bookmarks; Structure filters and Show Inherited; Recent
  Locations timestamps and breadcrumbs; Code Vision author
- Completion options, editor tab placement/layout, inline completion (LSP 3.18)
- Spell checking (Typo inspection, ide-spell) and Inspections settings with
  custom profiles; Code Style schemes, formatter markers, Do not format

Run, VCS, services, database
- Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker,
  Compound), Modify options, Before launch, env dialog, Current File,
  Emulate terminal (PTY)
- Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the
  graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees
- Generic Services host (Docker connections/registries, run sessions); TODO
  scope/changes tabs with changelists; Coverage report import
- Data sources: wire-compatible vendors, URL import, SSL certs, Options,
  Schemas and Advanced tabs, color and comment

Workbench
- Run Anything, Customize Main Toolbar, named tool window layouts, menu
  completeness, Code Cleanup, macros, "Don't Show Again", editor banners
- Chinese translations for the new UI

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
2026-10-25 21:24:00 +08:00

464 рядки
19 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![],
initialization_options: None,
install_hint: None,
fallback: false,
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 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 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());
}