Файли
l0ng-ai 1e8551ad0b fix(cpp): run CMake targets and rename through file aliases once (#539)
C/C++ files in a CMake project ran as standalone compiler commands, so multi-source programs failed to link. Single-file debugging selected CodeLLDB although the language uses lldb-dap. File aliases such as /tmp and /private/tmp could apply one rename edit twice. CMake command names were highlighted as variable definitions.

- Add CMake Configure, Build and CTest configurations, and read the File API codemodel after configuring to discover executable targets and their sources. Context runs select the owning target and build it first. Configuring only runs before the first build, since a configured build directory regenerates by itself.
- Debug C/C++ files and CMake targets with lldb-dap, and map CMake's recorded source root to its canonical path so breakpoints resolve.
- Coalesce file aliases and duplicate edits before preview and application, deduplicate references, and avoid reopening an open document through an alias.
- Replace the CMake syntax mode with command, argument keyword, predefined variable, boolean, number, string, comment and expansion highlighting.
2026-10-31 14:28:27 +08:00

640 рядки
27 KiB
Rust

use ide_run::{RunConfig, detect};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn project(files: &[(&str, &str)]) -> TempDir {
let dir = tempfile::tempdir().unwrap();
for (path, content) in files {
let p = dir.path().join(path);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, content).unwrap();
}
dir
}
fn find<'a>(configs: &'a [RunConfig], id: &str) -> &'a RunConfig {
configs
.iter()
.find(|c| c.id == id)
.unwrap_or_else(|| panic!("missing {id}; have {:?}", configs.iter().map(|c| &c.id).collect::<Vec<_>>()))
}
fn ids(configs: &[RunConfig]) -> Vec<&str> {
configs.iter().map(|c| c.id.as_str()).collect()
}
fn cmdline(c: &RunConfig) -> String {
std::iter::once(c.command.as_str()).chain(c.args.iter().map(String::as_str)).collect::<Vec<_>>().join(" ")
}
#[test]
fn empty_project_detects_nothing() {
let dir = tempfile::tempdir().unwrap();
assert!(detect(dir.path()).is_empty());
assert!(detect(Path::new("/definitely/not/here")).is_empty());
}
#[test]
fn npm_scripts_with_lockfile_package_manager() {
let dir = project(&[
("package.json", r#"{"scripts":{"dev":"vite","test":"vitest","build":"vite build","test:e2e":"playwright"}}"#),
("pnpm-lock.yaml", ""),
]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["npm:build", "npm:dev", "npm:test", "npm:test:e2e"]);
let dev = find(&c, "npm:dev");
assert_eq!(cmdline(dev), "pnpm run dev");
assert!(dev.detected && !dev.is_test);
assert_eq!(dev.kind, "npm");
assert!(find(&c, "npm:test").is_test);
assert!(find(&c, "npm:test:e2e").is_test);
}
#[test]
fn package_manager_selection() {
for (lock, pm) in [("yarn.lock", "yarn"), ("bun.lockb", "bun"), ("bun.lock", "bun"), ("package-lock.json", "npm")] {
let dir = project(&[("package.json", r#"{"scripts":{"start":"node ."}}"#), (lock, "")]);
assert_eq!(find(&detect(dir.path()), "npm:start").command, pm, "{lock}");
}
let dir = project(&[("package.json", r#"{"packageManager":"[email protected]","scripts":{"start":"x"}}"#)]);
assert_eq!(find(&detect(dir.path()), "npm:start").command, "yarn");
}
#[test]
fn npm_workspace_packages() {
let dir = project(&[
("package.json", r#"{"workspaces":["packages/*"],"scripts":{"lint":"eslint"}}"#),
("packages/web/package.json", r#"{"name":"@acme/web","scripts":{"dev":"vite"}}"#),
("packages/api/package.json", r#"{"scripts":{"test":"jest"}}"#),
("packages/notapkg/readme.md", ""),
]);
let c = detect(dir.path());
let web = find(&c, "npm:@acme/web:dev");
assert_eq!(web.cwd.as_deref(), Some(Path::new("packages/web")));
assert_eq!(cmdline(web), "npm run dev");
let api = find(&c, "npm:packages/api:test");
assert!(api.is_test);
let dir = project(&[
("package.json", "{}"),
("pnpm-lock.yaml", ""),
("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - \"libs/core\"\n"),
("apps/site/package.json", r#"{"name":"site","scripts":{"build":"astro build"}}"#),
("libs/core/package.json", r#"{"name":"core","scripts":{"test":"vitest"}}"#),
]);
let c = detect(dir.path());
assert_eq!(cmdline(find(&c, "npm:site:build")), "pnpm run build");
assert!(find(&c, "npm:core:test").is_test);
}
#[test]
fn cargo_single_package() {
let dir =
project(&[("Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"), ("src/main.rs", "fn main() {}")]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["cargo:run:app", "cargo:test", "cargo:build"]);
let run = find(&c, "cargo:run:app");
assert_eq!(cmdline(run), "cargo run");
assert_eq!(run.debug_adapter.as_deref(), Some("codelldb"));
assert!(find(&c, "cargo:test").is_test);
assert_eq!(cmdline(find(&c, "cargo:test")), "cargo test");
}
#[test]
fn cargo_multiple_bins() {
let dir = project(&[
(
"Cargo.toml",
"[package]\nname = \"tool\"\nversion = \"0.1.0\"\n\n[[bin]]\nname = \"special\"\npath = \"tools/special.rs\"\n",
),
("src/main.rs", ""),
("src/bin/extra.rs", ""),
("src/bin/multi/main.rs", ""),
]);
let c = detect(dir.path());
assert_eq!(cmdline(find(&c, "cargo:run:tool")), "cargo run --bin tool");
assert_eq!(cmdline(find(&c, "cargo:run:special")), "cargo run --bin special");
assert_eq!(cmdline(find(&c, "cargo:run:extra")), "cargo run --bin extra");
assert_eq!(cmdline(find(&c, "cargo:run:multi")), "cargo run --bin multi");
}
#[test]
fn cargo_workspace() {
let dir = project(&[
("Cargo.toml", "[workspace]\nmembers = [\"crates/*\", \"cli\"]\nexclude = [\"crates/skip\"]\n"),
("crates/core/Cargo.toml", "[package]\nname = \"core\"\n"),
("crates/core/src/lib.rs", ""),
("crates/skip/Cargo.toml", "[package]\nname = \"skip\"\n"),
("crates/skip/src/main.rs", ""),
("cli/Cargo.toml", "[package]\nname = \"openidea-cli\"\n"),
("cli/src/main.rs", ""),
]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["cargo:run:openidea-cli", "cargo:test", "cargo:build"]);
assert_eq!(cmdline(&c[0]), "cargo run -p openidea-cli --bin openidea-cli");
assert_eq!(cmdline(find(&c, "cargo:test")), "cargo test --workspace");
}
#[test]
fn gradle_projects() {
let dir = project(&[
("build.gradle.kts", "plugins {\n application\n id(\"org.springframework.boot\") version \"3.2.0\"\n}\n"),
("gradlew", "#!/bin/sh"),
]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["gradle:build", "gradle:test", "gradle:run", "gradle:bootRun"]);
if cfg!(unix) {
assert_eq!(cmdline(find(&c, "gradle:bootRun")), "./gradlew bootRun");
}
assert!(find(&c, "gradle:test").is_test);
let dir = project(&[("build.gradle", "plugins { id 'java' }\n")]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["gradle:build", "gradle:test"]);
assert_eq!(c[0].command, "gradle");
let dir = project(&[("build.gradle", "apply plugin: 'application'\n")]);
assert!(detect(dir.path()).iter().any(|c| c.id == "gradle:run"));
// Multi-module: Spring Boot applied in a subproject.
let dir = project(&[
("settings.gradle", "include 'app'"),
("build.gradle", ""),
("app/build.gradle", "plugins { id 'org.springframework.boot' version '3.2.0' }"),
]);
assert!(detect(dir.path()).iter().any(|c| c.id == "gradle:bootRun"));
}
#[test]
fn maven_projects() {
let pom = "<project><build><plugins><plugin><groupId>org.springframework.boot</groupId>\
<artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>";
let dir = project(&[("pom.xml", pom), ("mvnw", "#!/bin/sh")]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["maven:package", "maven:test", "maven:spring-boot:run"]);
if cfg!(unix) {
assert_eq!(cmdline(find(&c, "maven:test")), "./mvnw test");
}
assert!(find(&c, "maven:test").is_test);
let dir = project(&[("pom.xml", "<project/>")]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["maven:package", "maven:test"]);
assert_eq!(c[0].command, "mvn");
}
const BOOT_POM: &str = "<project><artifactId>app</artifactId><build><plugins><plugin>\
<artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>";
const PARENT_POM: &str = "<project><packaging>pom</packaging><modules><module>app</module></modules>\
<build><pluginManagement><plugins><plugin>\
<artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></pluginManagement>\
</build></project>";
const BOOT_MAIN: &str = "package org.dromara;\n\nimport org.springframework.boot.SpringApplication;\n\
import org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\
/** Entry. */\n@SpringBootApplication\npublic class DromaraApplication {\n\
\x20 public static void main(String[] args) {\n\
\x20 SpringApplication.run(DromaraApplication.class, args);\n }\n}\n";
/// A folder holding a Maven back end and an npm front end, each its own repository
/// (RuoYi-Vue-Plus + plus-ui): the Maven build is detected where it is.
#[test]
fn builds_nested_in_a_folder_of_repositories() {
let dir = project(&[
("RuoYi-Vue-Plus/pom.xml", PARENT_POM),
("RuoYi-Vue-Plus/ruoyi-admin/pom.xml", BOOT_POM),
("RuoYi-Vue-Plus/ruoyi-admin/src/main/java/org/dromara/DromaraApplication.java", BOOT_MAIN),
("RuoYi-Vue-Plus/ruoyi-admin/src/main/java/org/dromara/web/Controller.java", "package org.dromara.web;"),
("RuoYi-Vue-Plus/ruoyi-common/pom.xml", "<project><packaging>pom</packaging></project>"),
("RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/pom.xml", "<project/>"),
("RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/A.java", "class A {}"),
("plus-ui/package.json", r#"{"scripts":{"dev":"vite"}}"#),
]);
let c = detect(dir.path());
let app = find(&c, "application:RuoYi-Vue-Plus/ruoyi-admin:org.dromara.DromaraApplication");
assert_eq!(app.name, "DromaraApplication");
assert_eq!(app.kind, "spring-boot");
assert_eq!(cmdline(app), "java -cp ${classpath} org.dromara.DromaraApplication");
assert_eq!(app.cwd.as_deref(), Some(Path::new("RuoYi-Vue-Plus/ruoyi-admin")));
assert_eq!(app.extras.module.as_deref(), Some("RuoYi-Vue-Plus/ruoyi-admin"));
assert_eq!(app.debug_adapter.as_deref(), Some("java-debug"));
let package = find(&c, "maven:package:RuoYi-Vue-Plus");
assert_eq!(package.name, "RuoYi-Vue-Plus: mvn package");
assert_eq!(cmdline(package), "mvn package");
assert_eq!(package.cwd.as_deref(), Some(Path::new("RuoYi-Vue-Plus")));
assert!(find(&c, "maven:test:RuoYi-Vue-Plus").is_test);
assert!(ids(&c).contains(&"maven:spring-boot:run:RuoYi-Vue-Plus"));
assert!(ids(&c).contains(&"npm:plus-ui:dev"));
// Modules of the nested build are not builds of their own; only one application.
assert!(!ids(&c).iter().any(|id| id.contains("ruoyi-common")));
assert_eq!(c.iter().filter(|c| c.kind == "spring-boot").count(), 1);
}
/// RuoYi-AI opened from the folder above it (`ites-agent/ruoyi-ai`), or two folders above:
/// its application class builds the `SpringApplication` itself.
#[test]
fn spring_boot_application_of_a_repository_nested_one_or_two_levels_down() {
const MAIN: &str = "package org.ruoyi;\n\nimport org.springframework.boot.SpringApplication;\n\
import org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\
/**\n * 启动程序\n *\n * @author Lion Li\n */\n@SpringBootApplication\n\
public class RuoYiAIApplication {\n\n public static void main(String[] args) {\n\
\x20 SpringApplication application = new SpringApplication(RuoYiAIApplication.class);\n\
\x20 application.run(args);\n\
\x20 System.out.println(\"(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)\");\n }\n}\n";
for prefix in ["", "ites-agent/"] {
let file = |p: &str| format!("{prefix}ruoyi-ai/{p}");
let files = [
(file("pom.xml"), PARENT_POM),
(file("ruoyi-admin/pom.xml"), BOOT_POM),
(file("ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIApplication.java"), MAIN),
(file("ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIServletInitializer.java"), "package org.ruoyi;"),
(file("ruoyi-common/pom.xml"), "<project><packaging>pom</packaging></project>"),
(file("ruoyi-modules/ruoyi-chat/pom.xml"), "<project/>"),
(file("docs/README.md"), "docs"),
];
let files: Vec<(&str, &str)> = files.iter().map(|(p, t)| (p.as_str(), *t)).collect();
let dir = project(&files);
let c = detect(dir.path());
let module = format!("{prefix}ruoyi-ai/ruoyi-admin");
let app = find(&c, &format!("application:{module}:org.ruoyi.RuoYiAIApplication"));
assert_eq!((app.name.as_str(), app.kind.as_str()), ("RuoYiAIApplication", "spring-boot"), "{prefix}");
assert_eq!(app.cwd.as_deref(), Some(Path::new(&module)));
assert!(ids(&c).contains(&format!("maven:spring-boot:run:{prefix}ruoyi-ai").as_str()), "{prefix}");
}
}
#[test]
fn nested_builds_of_every_kind_and_depth() {
let dir = project(&[
("services/api/settings.gradle.kts", "include(\"app\")"),
("services/api/gradlew", "#!/bin/sh"),
("services/api/app/build.gradle.kts", "plugins { id(\"org.springframework.boot\") version \"3.3.0\" }\n"),
(
"services/api/app/src/main/kotlin/com/acme/Api.kt",
"package com.acme\n\n@SpringBootApplication\nclass Api\n\nfun main(args: Array<String>) {\n runApplication<Api>(*args)\n}\n",
),
("billing/pom.xml", BOOT_POM),
("billing/mvnw", "#!/bin/sh"),
(
"billing/src/main/java/Billing.java",
"@SpringBootApplication\nclass Billing { public static void main(String[] a) {} }",
),
// Deeper than a build is looked for.
("a/b/c/d/pom.xml", "<project/>"),
// Build output and sources are not searched.
("web/node_modules/x/pom.xml", "<project/>"),
("tool/src/pom.xml", "<project/>"),
]);
let c = detect(dir.path());
let boot = find(&c, "gradle:bootRun:services/api");
assert_eq!(boot.name, "services/api: gradle bootRun");
assert_eq!(boot.cwd.as_deref(), Some(Path::new("services/api")));
if cfg!(unix) {
assert_eq!(cmdline(boot), "./gradlew bootRun");
assert_eq!(cmdline(find(&c, "maven:package:billing")), "./mvnw package");
}
assert!(find(&c, "gradle:test:services/api").is_test);
let api = find(&c, "application:services/api/app:com.acme.ApiKt");
assert_eq!(api.cwd.as_deref(), Some(Path::new("services/api/app")));
let billing = find(&c, "application:billing:Billing");
assert_eq!(billing.cwd.as_deref(), Some(Path::new("billing")));
let builds: Vec<&str> =
ids(&c).into_iter().filter(|id| id.starts_with("maven:package") || id.starts_with("gradle:build")).collect();
assert_eq!(builds, ["gradle:build:services/api", "maven:package:billing"]);
}
#[test]
fn a_root_build_disables_the_nested_scan() {
let dir = project(&[
("pom.xml", BOOT_POM),
(
"src/main/java/demo/App.java",
"package demo;\n@SpringBootApplication public class App {\n public static void main(String[] args) {}\n}\n",
),
("tools/gen/pom.xml", "<project/>"),
]);
let c = detect(dir.path());
assert!(ids(&c).contains(&"maven:package"));
assert!(!ids(&c).iter().any(|id| id.contains("tools")));
let app = find(&c, "application:demo.App");
assert_eq!(app.cwd, None);
}
#[test]
fn spring_boot_applications_need_the_plugin_and_a_main() {
let main =
"package demo;\n@SpringBootApplication\npublic class App {\n public static void main(String[] args) {}\n}\n";
// A library using Spring Boot without the plugin, a configuration class without `main`,
// the annotation in a comment, a parent POM.
let dir = project(&[
("lib/pom.xml", "<project><dependencies>spring-boot-starter</dependencies></project>"),
("lib/src/main/java/demo/App.java", main),
("svc/pom.xml", BOOT_POM),
("svc/src/main/java/demo/Config.java", "package demo;\n@SpringBootApplication\npublic class Config {}\n"),
(
"svc/src/main/java/demo/Old.java",
"package demo;\n// @SpringBootApplication\nclass Old { public static void main(String[] a) {} }\n",
),
("gr/build.gradle", "dependencies { implementation 'org.springframework.boot:spring-boot-starter' }\n"),
("gr/src/main/java/demo/App.java", main),
]);
let c = detect(dir.path());
assert!(!c.iter().any(|c| c.kind == "spring-boot"), "{:?}", ids(&c));
}
#[test]
fn go_projects() {
let dir = project(&[
("go.mod", "module example.com/x\n"),
("main.go", "// comment\npackage main\n\nfunc main() {}\n"),
("main_test.go", "package main\n"),
("cmd/server/main.go", "package main\n"),
("cmd/lib/lib.go", "package lib\n"),
]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["go:run", "go:run:server", "go:test"]);
assert_eq!(cmdline(find(&c, "go:run:server")), "go run ./cmd/server");
assert_eq!(cmdline(find(&c, "go:test")), "go test ./...");
assert!(find(&c, "go:test").is_test);
assert_eq!(find(&c, "go:run").debug_adapter.as_deref(), Some("delve"));
let dir = project(&[("go.mod", "module x\n"), ("lib.go", "package x\n")]);
assert_eq!(ids(&detect(dir.path())), ["go:test"]);
}
#[test]
fn python_projects() {
let dir = project(&[
("pyproject.toml", "[tool.pytest.ini_options]\naddopts = \"-q\"\n"),
("uv.lock", ""),
("src/mypkg/__init__.py", ""),
("src/mypkg/__main__.py", ""),
("manage.py", ""),
]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["django:runserver", "python:module:mypkg", "pytest"]);
let pytest = find(&c, "pytest");
assert_eq!(cmdline(pytest), "uv run pytest");
assert!(pytest.is_test);
assert_eq!(pytest.debug_adapter.as_deref(), Some("debugpy"));
assert_eq!(cmdline(find(&c, "python:module:mypkg")), "uv run python -m mypkg");
assert_eq!(cmdline(find(&c, "django:runserver")), "uv run python manage.py runserver");
let dir = project(&[("pytest.ini", "[pytest]\n"), ("poetry.lock", "")]);
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), "poetry run pytest");
let dir = project(&[("setup.cfg", "[tool:pytest]\ntestpaths = tests\n")]);
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), "pytest");
let dir = project(&[("requirements.txt", "requests\n"), ("app/__main__.py", "")]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["python:module:app"]);
}
#[cfg(unix)]
#[test]
fn python_virtualenv() {
let dir = project(&[("tox.ini", ""), (".venv/bin/python", ""), (".venv/bin/pytest", ""), ("manage.py", "")]);
let c = detect(dir.path());
assert_eq!(cmdline(find(&c, "pytest")), ".venv/bin/pytest");
assert_eq!(cmdline(find(&c, "django:runserver")), ".venv/bin/python manage.py runserver");
}
#[cfg(unix)]
#[test]
fn bare_virtualenv_marks_a_python_project() {
// No packaging files: the venv alone says which interpreter / pytest to use.
let dir = project(&[(".venv/bin/python", ""), (".venv/bin/pytest", ""), ("worker/test_w.py", "")]);
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), ".venv/bin/pytest");
let dir = project(&[(".venv/bin/python", "")]);
assert!(detect(dir.path()).is_empty());
}
#[test]
fn makefile_targets() {
let makefile = "\
CC := gcc
VERSION = 1.0:beta
.PHONY: all test clean
export PATH := $(PATH)
all: build test
build: main.o
\tgcc -o app main.o
%.o: %.c
\t$(CC) -c $<
test::
\t./run-tests
clean lint: ; rm -rf out
$(OUT)/x: y
out/file: z
define TEMPLATE
inner: x
endef
# comment: no
";
let dir = project(&[("Makefile", makefile)]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["make:all", "make:build", "make:test", "make:clean", "make:lint"]);
assert_eq!(cmdline(find(&c, "make:build")), "make build");
assert!(find(&c, "make:test").is_test);
}
#[test]
fn deno_tasks() {
let dir = project(&[(
"deno.jsonc",
"{\n // tasks\n \"tasks\": {\n \"dev\": \"deno run -A main.ts\", /* inline */\n \"test\": \"deno test\",\n },\n \"url\": \"https://x.dev//y\"\n}\n",
)]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["deno:dev", "deno:test"]);
assert_eq!(cmdline(&c[0]), "deno task dev");
assert!(c[1].is_test);
}
#[test]
fn godot_projects_run_their_main_scene() {
let dir = project(&[("project.godot", "config_version=5\n"), ("demo/project.godot", "config_version=5\n")]);
let c = detect(dir.path());
let root = find(&c, "godot:project");
assert_eq!((root.name.as_str(), root.kind.as_str()), ("Godot Project", "godot"));
assert_eq!(cmdline(root), "godot --path .");
assert_eq!(root.cwd, None);
assert_eq!(root.debug_adapter.as_deref(), Some("godot"));
let demo = find(&c, "godot:project:demo");
assert_eq!(demo.name, "Godot Project (demo)");
assert_eq!(demo.cwd, Some(std::path::PathBuf::from("demo").into()));
assert!(detect(project(&[("main.gd", "extends Node\n")]).path()).is_empty());
}
#[test]
fn just_recipes() {
let justfile = "\
set shell := [\"bash\", \"-c\"]
alias b := build
version := \"1.0\"
export RUST_LOG := \"info\"
# Build it
build target='debug':
cargo build
@test *args: build
cargo test {{args}}
_hidden:
echo
[private]
secret:
echo
serve port=\"8080\": (build \"release\")
./serve
";
let dir = project(&[("justfile", justfile)]);
let c = detect(dir.path());
assert_eq!(ids(&c), ["just:build", "just:test", "just:serve"]);
assert_eq!(cmdline(&c[1]), "just test");
assert!(c[1].is_test);
}
#[test]
fn ordering_is_stable_across_ecosystems() {
let dir = project(&[
("Makefile", "all:\n"),
("package.json", r#"{"scripts":{"dev":"x"}}"#),
("Cargo.toml", "[package]\nname = \"a\"\n"),
("src/main.rs", ""),
("go.mod", "module a\n"),
]);
let first = ids(&detect(dir.path())).into_iter().map(str::to_owned).collect::<Vec<_>>();
assert_eq!(first, ["npm:dev", "cargo:run:a", "cargo:test", "cargo:build", "go:test", "make:all"]);
for _ in 0..3 {
assert_eq!(detect(dir.path()).iter().map(|c| c.id.clone()).collect::<Vec<_>>(), first);
}
assert!(detect(dir.path()).iter().all(|c| c.detected));
}
#[test]
fn nested_packages_in_polyglot_repo() {
let dir = project(&[
("Makefile", "all:\n\techo\n"),
("web/package.json", r#"{"scripts":{"dev":"vite","test":"vitest"}}"#),
("web/pnpm-lock.yaml", ""),
("api/go.mod", "module x\n"),
("api/main.go", "package main\nfunc main() {}\n"),
("node_modules/x/package.json", r#"{"scripts":{"no":"x"}}"#),
]);
let c = detect(dir.path());
let dev = find(&c, "npm:web:dev");
assert_eq!(cmdline(dev), "pnpm run dev");
assert_eq!(dev.cwd.as_deref(), Some(Path::new("web")));
assert_eq!(dev.name, "web: pnpm run dev");
assert!(find(&c, "npm:web:test").is_test);
let run = find(&c, "go:run:api");
assert_eq!(run.cwd.as_deref(), Some(Path::new("api")));
assert_eq!(run.name, "api: go run .");
let test = find(&c, "go:test:api");
assert!(test.is_test);
assert_eq!(cmdline(test), "go test ./...");
assert!(!ids(&c).iter().any(|id| id.contains("no")));
}
#[test]
fn root_package_disables_nested_scan() {
let dir = project(&[
("package.json", r#"{"scripts":{"build":"tsc"}}"#),
("tools/package.json", r#"{"scripts":{"gen":"x"}}"#),
("go.mod", "module x\n"),
("svc/go.mod", "module y\n"),
]);
let c = detect(dir.path());
assert!(!ids(&c).contains(&"npm:tools:gen"));
assert!(ids(&c).contains(&"go:test"));
assert!(!ids(&c).contains(&"go:test:svc"));
}
#[test]
fn cmake_detection_is_read_only_and_preserves_existing_build_directories() {
let dir = project(&[
("CMakeLists.txt", "project(demo)"),
("cmake-build-debug/CMakeCache.txt", "CMAKE_CONFIGURATION_TYPES:STRING=Release;RelWithDebInfo\n"),
]);
let configs = detect(dir.path());
let configure = find(&configs, "cmake:cmake-build-debug:configure");
assert_eq!(configure.args, ["-S", ".", "-B", "cmake-build-debug", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"]);
assert!(!dir.path().join("build").exists());
assert!(!dir.path().join("cmake-build-debug/.cmake").exists());
let build = find(&configs, "cmake:cmake-build-debug:build");
assert!(build.extras.before_launch.is_empty(), "a configured build regenerates by itself");
assert!(build.args.windows(2).any(|a| a == ["--config", "Release"]));
let test = find(&configs, "cmake:cmake-build-debug:test");
assert_eq!(test.extras.before_launch[0].target, build.id);
}
#[test]
fn cmake_configures_before_the_first_build() {
let dir = project(&[("CMakeLists.txt", "project(demo)")]);
let configs = detect(dir.path());
let build = find(&configs, "cmake:build:build");
assert_eq!(build.extras.before_launch[0].target, "cmake:build:configure");
}
#[test]
fn cmake_ignores_malformed_file_api_replies() {
let dir = project(&[
("CMakeLists.txt", "project(demo)"),
("build/CMakeCache.txt", ""),
("build/.cmake/api/v1/reply/index-1.json", "not JSON"),
]);
let configs = detect(dir.path());
assert_eq!(configs.iter().filter(|c| c.kind == "cmake").count(), 3);
assert!(!configs.iter().any(|c| c.kind == "native"));
}
#[test]
fn go_work_members_define_configs_including_deep_and_quoted_directories() {
let dir = project(&[
(
"go.work",
"go 1.22\n// use ./ignored\nuse (\n ./apps/deep/server // member\n \"./module with spaces\"\n)\nuse ./apps/deep/server\n",
),
("apps/deep/server/go.mod", "module example.com/server\ngo 1.22\n"),
("apps/deep/server/main.go", "package main\nfunc main() {}"),
("module with spaces/go.mod", "module example.com/lib\ngo 1.22\n"),
("ignored/go.mod", "module example.com/ignored\ngo 1.22\n"),
]);
let configs = detect(dir.path());
assert_eq!(configs.iter().filter(|c| c.kind == "go" && c.is_test).count(), 2);
assert_eq!(find(&configs, "go:run:apps/deep/server").cwd.as_deref(), Some(Path::new("apps/deep/server")));
assert!(configs.iter().all(|c| !c.id.contains("ignored")));
assert!(configs.iter().any(|c| c.id == "go:test:module with spaces"));
}
#[test]
fn go_work_parser_ignores_other_directives_comments_and_malformed_groups() {
use ide_run::go::work_modules;
assert_eq!(
work_modules("replace use => ./not-a-member\n// use ./also-not\nuse ./real\n"),
vec![std::path::PathBuf::from("./real")]
);
assert!(work_modules("use (\n./missing-close\n").is_empty());
assert!(work_modules("use \"unterminated").is_empty());
}
#[test]
fn go_work_paths_decode_go_string_escapes() {
use ide_run::go::work_modules;
for path in [r#""./\u0061pp""#, r#""./\x61pp""#, r#""./\141pp""#, "`./app`"] {
assert_eq!(work_modules(&format!("use {path}\n")), vec![std::path::PathBuf::from("./app")]);
}
assert!(work_modules(r#"use "./\qinvalid""#).is_empty());
}
#[test]
fn go_default_build_output_is_ignored_by_version_control() {
let dir = project(&[("go.mod", "module x\ngo 1.22\n"), ("main.go", "package main\nfunc main() {}")]);
let mut config = detect(dir.path()).into_iter().find(|c| c.kind == "go" && !c.is_test).unwrap();
config.extras.params.insert("goKind".into(), "package".into());
assert!(ide_run::go::plan(dir.path(), &config).unwrap().is_some());
let ignore = fs::read_to_string(dir.path().join(".openidea/.gitignore")).unwrap();
assert!(ignore.lines().any(|l| l == "/bin/"), "{ignore}");
}