дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Run or Debug on a Python file started it as a script (`python src/tasks/cli.py`), so a module of a package that imports relatively (`from .models import TaskList`) always failed with "attempted relative import with no known parent package". A file with relative imports now runs as `python -m package.module` from the directory above its top package (the enclosing directories with `__init__.py`, read once and re-read when one is added or removed), with a venv-relative interpreter resolved against the project root. Files without relative imports still run as scripts.
930 рядки
41 KiB
TypeScript
930 рядки
41 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { HierarchyItem, RunConfig } from "@/platform/ipc";
|
|
import {
|
|
innermostTarget,
|
|
nodeTypeStripping,
|
|
OPTIMISTIC,
|
|
produceContext,
|
|
produceContextChoices,
|
|
produceDirectory,
|
|
produceFile,
|
|
shebang,
|
|
shellWord,
|
|
fileTargets,
|
|
type ContextInput,
|
|
type LocatedTool,
|
|
type ToolEnv,
|
|
} from "./producers";
|
|
import { clearInheritedMain, resolveCandidates, unresolvedCandidates } from "./inheritedMain";
|
|
|
|
afterEach(() => clearInheritedMain());
|
|
|
|
const ROOT = "/work/app";
|
|
const superItem: Omit<HierarchyItem, "name" | "path"> = {
|
|
kind: "class",
|
|
detail: null,
|
|
range: { start: { line: 0, col: 0 }, end: { line: 1, col: 0 } },
|
|
selectionRange: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } },
|
|
fromRanges: [],
|
|
};
|
|
const SCRATCH = "/Users/me/Library/Application Support/ai.l0ng.openidea/scratches";
|
|
|
|
const cfg = (over: Partial<RunConfig> & { id: string; kind: string }): RunConfig => ({
|
|
name: over.id,
|
|
command: over.kind,
|
|
args: [],
|
|
cwd: null,
|
|
env: {},
|
|
detected: true,
|
|
isTest: false,
|
|
debugAdapter: null,
|
|
...over,
|
|
});
|
|
|
|
/** Tools at `/bin/<name>` with optional versions; names not listed are not installed. */
|
|
function env(tools: Record<string, string | null>, extra: Partial<ToolEnv> = {}): ToolEnv {
|
|
return {
|
|
tool: (n): LocatedTool | null => (n in tools ? { path: `/bin/${n}`, version: tools[n] ?? null } : null),
|
|
compileDir: "/tmp/openidea-run",
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
const ALL = env({
|
|
node: "24.11.0",
|
|
python3: null,
|
|
bash: null,
|
|
zsh: null,
|
|
sh: null,
|
|
ruby: null,
|
|
php: null,
|
|
perl: null,
|
|
lua: null,
|
|
Rscript: null,
|
|
go: null,
|
|
rustc: "1.85.0",
|
|
java: null,
|
|
kotlin: null,
|
|
swift: null,
|
|
clang: null,
|
|
"clang++": null,
|
|
dart: null,
|
|
elixir: null,
|
|
"scala-cli": null,
|
|
deno: "2.1.0",
|
|
bun: "1.1.0",
|
|
});
|
|
|
|
const scratch = (name: string, content?: string, line?: number): ContextInput => ({
|
|
path: `${SCRATCH}/${name}`,
|
|
root: ROOT,
|
|
configs: [],
|
|
content,
|
|
line,
|
|
});
|
|
|
|
const run = (input: ContextInput, e: ToolEnv = ALL) => {
|
|
const p = produceContext(input, e);
|
|
return p?.config ? { name: p.name, command: p.config.command, args: p.config.args, cwd: p.config.cwd } : p;
|
|
};
|
|
|
|
describe("single-file producers (scratches)", () => {
|
|
it("JavaScript: node <file> from the file's directory, debuggable with js-debug", () => {
|
|
const p = produceContext(scratch("hi.js", "console.log(1)"), ALL)!;
|
|
expect(p.name).toBe("hi.js");
|
|
expect(p.config).toMatchObject({ command: "/bin/node", args: ["hi.js"], cwd: SCRATCH, debugAdapter: "js-debug" });
|
|
expect(p.config!.id).toBe(`context:file:${SCRATCH}/hi.js`);
|
|
});
|
|
|
|
it("TypeScript: node type stripping by version, then tsx / ts-node, else what is missing", () => {
|
|
expect(run(scratch("hi.ts"), env({ node: "23.6.0" }))).toMatchObject({ command: "/bin/node", args: ["hi.ts"] });
|
|
expect(run(scratch("hi.ts"), env({ node: "22.18.0" }))).toMatchObject({ args: ["hi.ts"] });
|
|
expect(run(scratch("hi.ts"), env({ node: "22.6.0" }))).toMatchObject({
|
|
args: ["--experimental-strip-types", "hi.ts"],
|
|
});
|
|
expect(run(scratch("hi.ts"), env({ node: "23.5.0" }))).toMatchObject({
|
|
args: ["--experimental-strip-types", "hi.ts"],
|
|
});
|
|
expect(run(scratch("hi.ts"), env({ node: "20.11.0", tsx: null }))).toMatchObject({
|
|
command: "/bin/tsx",
|
|
args: ["hi.ts"],
|
|
});
|
|
expect(run(scratch("hi.ts"), env({ node: "20.11.0", "ts-node": null }))).toMatchObject({ command: "/bin/ts-node" });
|
|
const missing = produceContext(scratch("hi.ts"), env({ node: "20.11.0" }))!;
|
|
expect(missing.name).toBe("hi.ts");
|
|
expect(missing.config).toBeUndefined();
|
|
expect(missing.missing!.body).toMatch(/22\.6\+.*Found Node\.js 20\.11\.0.*npm install -D tsx/);
|
|
expect(produceContext(scratch("hi.js"), env({}))!.missing!.body).toMatch(/Node\.js was not found.*nodejs\.org/);
|
|
});
|
|
|
|
it("type stripping versions", () => {
|
|
expect(nodeTypeStripping("22.5.1")).toBeNull();
|
|
expect(nodeTypeStripping("22.6.0")).toBe("flag");
|
|
expect(nodeTypeStripping("22.17.9")).toBe("flag");
|
|
expect(nodeTypeStripping("22.18.0")).toBe("native");
|
|
expect(nodeTypeStripping("23.5.0")).toBe("flag");
|
|
expect(nodeTypeStripping("23.6.0")).toBe("native");
|
|
expect(nodeTypeStripping("v?")).toBeNull();
|
|
});
|
|
|
|
it("Deno / Bun projects run JS and TS with their runtime", () => {
|
|
const input: ContextInput = { path: `${ROOT}/scripts/x.ts`, root: ROOT, configs: [] };
|
|
expect(run(input, { ...ALL, deno: true })).toMatchObject({
|
|
command: "/bin/deno",
|
|
args: ["run", "scripts/x.ts"],
|
|
cwd: null,
|
|
});
|
|
expect(produceContext(input, { ...ALL, deno: true })!.config!.debugAdapter).toBe("js-debug");
|
|
expect(run(input, { ...ALL, bun: true })).toMatchObject({ command: "/bin/bun", args: ["run", "scripts/x.ts"] });
|
|
// Bun's inspector is WebKit-only: js-debug can't attach.
|
|
expect(produceContext(input, { ...ALL, bun: true })!.config!.debugAdapter).toBeNull();
|
|
// Not for scratches outside the project.
|
|
expect(run(scratch("x.ts"), { ...ALL, deno: true })).toMatchObject({ command: "/bin/node" });
|
|
});
|
|
|
|
it("Python: the interpreter from settings, then the project's, then python3", () => {
|
|
expect(run(scratch("hi.py"), { ...ALL, python: "/work/app/.venv/bin/python" })).toMatchObject({
|
|
command: "/work/app/.venv/bin/python",
|
|
args: ["hi.py"],
|
|
cwd: SCRATCH,
|
|
});
|
|
const venv = [cfg({ id: "python:module:app", kind: "python", command: ".venv/bin/python", args: ["-m", "app"] })];
|
|
expect(run({ ...scratch("hi.py"), configs: venv })).toMatchObject({ command: "/work/app/.venv/bin/python" });
|
|
expect(run({ path: `${ROOT}/tools/gen.py`, root: ROOT, configs: venv })).toMatchObject({
|
|
command: ".venv/bin/python",
|
|
args: ["tools/gen.py"],
|
|
cwd: null,
|
|
});
|
|
expect(produceContext(scratch("hi.py"), ALL)!.config).toMatchObject({
|
|
command: "/bin/python3",
|
|
debugAdapter: "debugpy",
|
|
});
|
|
expect(produceContext(scratch("hi.py"), env({}))!.missing!.title).toBe("Cannot run 'hi.py'");
|
|
});
|
|
|
|
it("Python: a module with relative imports runs with -m from above its top package", () => {
|
|
const venv = [cfg({ id: "pytest", kind: "pytest", command: ".venv/bin/pytest" })];
|
|
const packages = new Set([`${ROOT}/src/tasks`, `${ROOT}/src/tasks/sub`]);
|
|
const at = (path: string, content: string) =>
|
|
run({ path, root: ROOT, configs: venv, content, pythonPackage: (d) => packages.has(d) });
|
|
const relative = "from .models import TaskList\n";
|
|
expect(at(`${ROOT}/src/tasks/cli.py`, relative)).toMatchObject({
|
|
command: `${ROOT}/.venv/bin/python`,
|
|
args: ["-m", "tasks.cli"],
|
|
cwd: "src",
|
|
});
|
|
expect(at(`${ROOT}/src/tasks/sub/__main__.py`, "from ..models import X\n")).toMatchObject({
|
|
args: ["-m", "tasks.sub"],
|
|
cwd: "src",
|
|
});
|
|
// A directory without `__init__.py` is still the module's package; a top-level one runs from the root.
|
|
expect(at(`${ROOT}/scripts/gen.py`, relative)).toMatchObject({ args: ["-m", "scripts.gen"], cwd: null });
|
|
// Without relative imports, the file runs as a script.
|
|
expect(at(`${ROOT}/src/tasks/cli.py`, "import argparse\n")).toMatchObject({
|
|
command: ".venv/bin/python",
|
|
args: ["src/tasks/cli.py"],
|
|
cwd: null,
|
|
});
|
|
});
|
|
|
|
it("shell scripts: the shell of the #! line, else by extension; extensionless files by #!", () => {
|
|
expect(run(scratch("a.sh", "#!/usr/bin/env bash\necho hi"))).toMatchObject({
|
|
command: "/bin/bash",
|
|
args: ["a.sh"],
|
|
});
|
|
expect(run(scratch("a.sh", "#!/bin/zsh -f\necho"))).toMatchObject({ command: "/bin/zsh", args: ["-f", "a.sh"] });
|
|
expect(run(scratch("a.sh", "echo"))).toMatchObject({ command: "/bin/sh" });
|
|
expect(run(scratch("a.zsh", "echo"))).toMatchObject({ command: "/bin/zsh" });
|
|
expect(run(scratch("tool", "#!/usr/bin/env ruby\nputs 1"))).toMatchObject({
|
|
name: "tool",
|
|
command: "/bin/ruby",
|
|
args: ["tool"],
|
|
});
|
|
expect(run(scratch("tool", "#!/usr/bin/env -S deno run -A\n"))).toMatchObject({
|
|
command: "/bin/deno",
|
|
args: ["run", "-A", "tool"],
|
|
});
|
|
expect(produceContext(scratch("tool", "#!/usr/bin/env -S deno run -A\n"), ALL)!.config!.debugAdapter).toBe(
|
|
"js-debug",
|
|
);
|
|
expect(run(scratch("tool", "#!/usr/bin/env python3\n"))).toMatchObject({ command: "/bin/python3", args: ["tool"] });
|
|
expect(run(scratch("build.bat"), { ...ALL, windows: true })).toMatchObject({
|
|
command: "cmd.exe",
|
|
args: ["/c", "build.bat"],
|
|
cwd: SCRATCH,
|
|
});
|
|
expect(run({ path: `${ROOT}/scripts/run.cmd`, root: ROOT, configs: [] }, { ...ALL, windows: true })).toMatchObject({
|
|
command: "cmd.exe",
|
|
args: ["/c", "run.cmd"],
|
|
cwd: "scripts",
|
|
});
|
|
expect(produceContext(scratch("build.bat"), ALL)).toBeNull();
|
|
expect(produceContext(scratch("notes", "just text"), ALL)).toBeNull();
|
|
expect(shebang("#!/usr/bin/env node")).toEqual(["node"]);
|
|
});
|
|
|
|
it("Ruby, PHP, Perl, Lua, R, Swift, Dart, Elixir, Scala CLI, Kotlin script", () => {
|
|
expect(run(scratch("a.rb"))).toMatchObject({ command: "/bin/ruby", args: ["a.rb"] });
|
|
expect(run(scratch("a.php"))).toMatchObject({ command: "/bin/php", args: ["a.php"] });
|
|
expect(run(scratch("a.pl"))).toMatchObject({ command: "/bin/perl" });
|
|
expect(run(scratch("a.lua"))).toMatchObject({ command: "/bin/lua" });
|
|
expect(run(scratch("a.R"))).toMatchObject({ command: "/bin/Rscript", args: ["a.R"] });
|
|
expect(run(scratch("a.swift"))).toMatchObject({ command: "/bin/swift", args: ["a.swift"] });
|
|
expect(run(scratch("a.dart"))).toMatchObject({ command: "/bin/dart", args: ["run", "a.dart"] });
|
|
expect(run(scratch("a.exs"))).toMatchObject({ command: "/bin/elixir", args: ["a.exs"] });
|
|
expect(run(scratch("a.sc"))).toMatchObject({ command: "/bin/scala-cli", args: ["run", "a.sc"] });
|
|
expect(run(scratch("a.kts"))).toMatchObject({ command: "/bin/kotlin", args: ["a.kts"] });
|
|
expect(run(scratch("a.kts"), env({ kotlinc: null }))).toMatchObject({
|
|
command: "/bin/kotlinc",
|
|
args: ["-script", "a.kts"],
|
|
});
|
|
expect(produceContext(scratch("a.rb"), env({}))!.missing!.body).toMatch(/brew install ruby/);
|
|
expect(produceContext(scratch("build.gradle.kts"), ALL)).toBeNull();
|
|
});
|
|
|
|
it("Go: go run <file> for package main outside a module", () => {
|
|
expect(run(scratch("hi.go", "package main\nfunc main() {}"))).toMatchObject({
|
|
command: "/bin/go",
|
|
args: ["run", "hi.go"],
|
|
cwd: SCRATCH,
|
|
});
|
|
expect(produceContext(scratch("lib.go", "package lib"), ALL)).toBeNull();
|
|
expect(produceContext(scratch("hi.go", "package main"), ALL)!.config!.debugAdapter).toBe("delve");
|
|
});
|
|
|
|
it("Go: the module's go run for packages in the project", () => {
|
|
const go = [cfg({ id: "go:run", kind: "go", command: "go", args: ["run", "."] })];
|
|
const p = produceContext(
|
|
{ path: `${ROOT}/cmd/tool/main.go`, root: ROOT, configs: go, content: "package main\nfunc main() {}" },
|
|
ALL,
|
|
)!;
|
|
expect(p.config).toMatchObject({ args: ["run", "./cmd/tool"] });
|
|
});
|
|
|
|
it("Rust: rustc into the compile directory, then the binary; codelldb debugs the binary", () => {
|
|
const p = produceContext(scratch("hihi.rs", 'fn main() {\n println!("hi");\n}'), ALL)!;
|
|
expect(p.name).toBe("hihi.rs");
|
|
expect(p.config!.cwd).toBe(SCRATCH);
|
|
expect(p.config!.args).toEqual([]);
|
|
expect(p.config!.command).toMatch(
|
|
/^mkdir -p \/tmp\/openidea-run && \/bin\/rustc -g --edition 2021 -o \/tmp\/openidea-run\/hihi-\w+ hihi\.rs && \/tmp\/openidea-run\/hihi-\w+$/,
|
|
);
|
|
expect(p.config!.kind).toBe("rustc");
|
|
expect(p.debug!.build.command).toMatch(/rustc -g .* hihi\.rs$/);
|
|
expect(p.debug!.config).toMatchObject({ kind: "native", debugAdapter: "codelldb", args: [] });
|
|
expect(p.debug!.config.command).toMatch(/^\/tmp\/openidea-run\/hihi-\w+$/);
|
|
expect(produceContext(scratch("hihi.rs", "fn main() {}"), env({}))!.missing!.body).toMatch(/rustup/);
|
|
});
|
|
|
|
it("Rust: a test at the caret of a standalone file builds with --test and filters", () => {
|
|
const src = [
|
|
"fn main() {}",
|
|
"#[cfg(test)]",
|
|
"mod tests {",
|
|
" #[test]",
|
|
" fn adds() {",
|
|
" assert!(true);",
|
|
" }",
|
|
"}",
|
|
].join("\n");
|
|
const p = produceContext(scratch("calc.rs", src, 5), ALL)!;
|
|
expect(p).toMatchObject({ name: "tests::adds", scope: "element" });
|
|
expect(p.config!.command).toMatch(
|
|
/rustc -g --edition 2021 --test -o \S+-test calc\.rs && \S+-test tests::adds --exact$/,
|
|
);
|
|
expect(p.config!.isTest).toBe(true);
|
|
// On the module line: the whole module.
|
|
expect(produceContext(scratch("calc.rs", src, 2), ALL)!.config!.command).toMatch(/-test tests::$/);
|
|
// Outside: the file (main).
|
|
expect(produceContext(scratch("calc.rs", src, 0), ALL)!.name).toBe("calc.rs");
|
|
});
|
|
|
|
it("Rust in a Cargo project: cargo run --bin / --example, not rustc", () => {
|
|
const cargo = [
|
|
cfg({ id: "cargo:run:server", kind: "cargo", command: "cargo", args: ["run", "--bin", "server"] }),
|
|
cfg({ id: "cargo:test", kind: "cargo", command: "cargo", args: ["test"], isTest: true }),
|
|
];
|
|
const bin = produceContext(
|
|
{ path: `${ROOT}/src/bin/server.rs`, root: ROOT, configs: cargo, content: "fn main() {}" },
|
|
ALL,
|
|
)!;
|
|
expect(bin.config!.id).toBe("cargo:run:server");
|
|
const ex = produceContext(
|
|
{ path: `${ROOT}/examples/demo.rs`, root: ROOT, configs: cargo, content: "fn main() {}" },
|
|
ALL,
|
|
)!;
|
|
expect(ex.config).toMatchObject({ args: ["run", "--example", "demo"], name: "Run demo" });
|
|
expect(
|
|
produceContext({ path: `${ROOT}/src/lib.rs`, root: ROOT, configs: cargo, content: "pub fn x() {}" }, ALL),
|
|
).toBeNull();
|
|
const test = produceContext(
|
|
{ path: `${ROOT}/src/lib.rs`, root: ROOT, configs: cargo, content: "#[test]\nfn works() {\n}\n", line: 2 },
|
|
ALL,
|
|
)!;
|
|
expect(test.config).toMatchObject({ command: "cargo", args: ["test", "--", "works", "--exact"], name: "works" });
|
|
});
|
|
|
|
it("Java: java <File>.java outside a build project; the Application configuration inside", () => {
|
|
const src = "public class Hi {\n public static void main(String[] args) {\n System.out.println(1);\n }\n}";
|
|
expect(run(scratch("Hi.java", src))).toMatchObject({ command: "/bin/java", args: ["Hi.java"], cwd: SCRATCH });
|
|
expect(produceContext(scratch("Hi.java", src), ALL)!.config!.kind).toBe("java-file");
|
|
expect(produceContext(scratch("Lib.java", "class Lib {}"), ALL)).toBeNull();
|
|
const gradle = [cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"], isTest: true })];
|
|
const p = produceContext(
|
|
{
|
|
path: `${ROOT}/src/main/java/demo/Hi.java`,
|
|
root: ROOT,
|
|
configs: gradle,
|
|
content: `package demo;\n${src}`,
|
|
line: 3,
|
|
},
|
|
ALL,
|
|
)!;
|
|
expect(p.config).toMatchObject({ kind: "application", name: "Hi" });
|
|
expect(p.config!.args.at(-1)).toBe("demo.Hi");
|
|
});
|
|
|
|
it("Java: a main in a Gradle module outside the project runs on the module's classpath", () => {
|
|
// `includeFlat 'billing'`: the module folder is a sibling of the project folder.
|
|
const svc = "/work/billing/modules/core";
|
|
const boot =
|
|
"package com.example;\nimport org.springframework.boot.SpringApplication;\n@SpringBootApplication\n" +
|
|
"public class ShopApplication {\n public static void main(String[] args) {\n" +
|
|
" SpringApplication.run(ShopApplication.class, args);\n }\n}";
|
|
const gradle = [cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"], isTest: true })];
|
|
const input = (contentRoots?: string[]) => ({
|
|
path: `${svc}/src/main/java/com/example/ShopApplication.java`,
|
|
root: ROOT,
|
|
configs: gradle,
|
|
content: boot,
|
|
line: 4,
|
|
contentRoots,
|
|
});
|
|
expect(produceContext(input(), ALL)!.config!.kind).toBe("java-file");
|
|
const p = produceContext(input(["/work/billing", svc]), ALL)!;
|
|
expect(p.config).toMatchObject({ kind: "spring-boot", module: svc, cwd: svc });
|
|
expect(p.config!.args).toEqual(["-cp", "${classpath}", "com.example.ShopApplication"]);
|
|
});
|
|
|
|
it("Java: a class whose superclass declares main runs as a Spring Boot application", async () => {
|
|
const path = `${ROOT}/src/main/java/demo/AiChatApplication.java`;
|
|
const content = "package demo;\n\n@SpringBootApplication\npublic class AiChatApplication extends AtomStarter {}\n";
|
|
const gradle = [cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"], isTest: true })];
|
|
const input = { path, root: ROOT, configs: gradle, content, line: 3 };
|
|
expect(fileTargets(input, content)).toEqual([]);
|
|
await resolveCandidates(path, unresolvedCandidates(path, content), {
|
|
supertypes: async () => [{ ...superItem, name: "AtomStarter", path: "jdt://lib/AtomStarter.class" }],
|
|
text: async () => "package x; public class AtomStarter { public static void main(String[] a) {} }",
|
|
});
|
|
expect(fileTargets(input, content)).toMatchObject([
|
|
{ kind: "main", className: "demo.AiChatApplication", spring: true },
|
|
]);
|
|
expect(produceContext(input, ALL)!.config).toMatchObject({ kind: "spring-boot" });
|
|
expect(produceContext(input, ALL)!.config!.args.at(-1)).toBe("demo.AiChatApplication");
|
|
});
|
|
|
|
describe("Java / Kotlin mains in a build nested in a folder of repositories", () => {
|
|
// RuoYi-Vue-Plus (Maven) next to plus-ui (npm), opened together.
|
|
const boot =
|
|
"package org.dromara;\n\nimport org.springframework.boot.SpringApplication;\n\n@SpringBootApplication\n" +
|
|
"public class DromaraApplication {\n public static void main(String[] args) {\n" +
|
|
" SpringApplication.run(DromaraApplication.class, args);\n }\n}\n";
|
|
const nested = (kind: "maven" | "gradle", dir: string) => [
|
|
cfg({ id: "npm:plus-ui:dev", kind: "npm", command: "npm", args: ["run", "dev"], cwd: "plus-ui" }),
|
|
cfg({ id: `${kind}:test:${dir}`, kind, args: ["test"], cwd: dir, isTest: true }),
|
|
];
|
|
const at = (path: string, configs: RunConfig[], content = boot, line = 6): ContextInput => ({
|
|
path: `${ROOT}/${path}`,
|
|
root: ROOT,
|
|
configs,
|
|
content,
|
|
line,
|
|
});
|
|
const cases = [
|
|
{
|
|
kind: "maven",
|
|
dir: "RuoYi-Vue-Plus",
|
|
file: "RuoYi-Vue-Plus/ruoyi-admin/src/main/java/org/dromara/DromaraApplication.java",
|
|
},
|
|
{
|
|
kind: "gradle",
|
|
dir: "services/api",
|
|
file: "services/api/app/src/main/java/org/dromara/DromaraApplication.java",
|
|
},
|
|
] as const;
|
|
for (const { kind, dir, file } of cases) {
|
|
it(`${kind}: the Spring Boot application in its module, from the gutter and from the file`, () => {
|
|
const module = file.slice(0, file.indexOf("/src/"));
|
|
for (const p of [
|
|
produceContext(at(file, nested(kind, dir)), ALL),
|
|
produceFile(at(file, nested(kind, dir)), ALL),
|
|
]) {
|
|
expect(p!.config).toMatchObject({
|
|
id: `application:${module}:org.dromara.DromaraApplication`,
|
|
kind: "spring-boot",
|
|
name: "DromaraApplication",
|
|
command: "java",
|
|
cwd: module,
|
|
});
|
|
expect(p!.config!.args).toEqual(["-cp", "${classpath}", "org.dromara.DromaraApplication"]);
|
|
}
|
|
});
|
|
}
|
|
|
|
it("a file outside every build still runs as a single source file", () => {
|
|
const src = "public class Hi {\n public static void main(String[] args) {}\n}\n";
|
|
const p = produceContext(at("scripts/Hi.java", nested("maven", "RuoYi-Vue-Plus"), src, 1), ALL)!;
|
|
expect(p.config).toMatchObject({ kind: "java-file", args: ["scripts/Hi.java"] });
|
|
});
|
|
|
|
it("Kotlin and test classes use the build that contains them", () => {
|
|
const kt = "package demo\n\nfun main() {\n println(1)\n}\n";
|
|
const p = produceContext(at("backend/app/src/main/kotlin/demo/App.kt", nested("gradle", "backend"), kt, 3), ALL)!;
|
|
expect(p.config).toMatchObject({ kind: "application", cwd: "backend/app" });
|
|
expect(p.config!.args.at(-1)).toBe("demo.AppKt");
|
|
const test = "package demo;\nclass AppTest {\n @Test\n void works() {\n }\n}\n";
|
|
const t = produceContext(
|
|
at("backend/app/src/test/java/demo/AppTest.java", nested("gradle", "backend"), test, 4),
|
|
ALL,
|
|
)!;
|
|
expect(t.config).toMatchObject({ kind: "gradle", cwd: "backend" });
|
|
expect(t.config!.args).toEqual(["test", "--tests", "demo.AppTest.works"]);
|
|
});
|
|
});
|
|
|
|
it("C / C++: compile with debug info into the compile directory", () => {
|
|
const c = produceContext(scratch("a.c", "int main(void) { return 0; }"), ALL)!;
|
|
expect(c.config!.command).toMatch(
|
|
/^mkdir -p \/tmp\/openidea-run && \/bin\/clang -g -o \/tmp\/openidea-run\/a-\w+ a\.c && \/tmp\/openidea-run\/a-\w+$/,
|
|
);
|
|
const cpp = produceContext(scratch("a.cpp", "int main() {}"), env({ "g++": null }))!;
|
|
expect(cpp.config!.command).toMatch(/\/bin\/g\+\+ -g -std=c\+\+17 -o/);
|
|
expect(cpp.debug!.config.debugAdapter).toBe("lldb-dap");
|
|
expect(c.debug!.config.debugAdapter).toBe("lldb-dap");
|
|
expect(produceContext(scratch("lib.c", "int add(int a, int b);"), ALL)).toBeNull();
|
|
});
|
|
|
|
it("compiled binaries of same-named files in different directories don't collide", () => {
|
|
const a = produceContext({ ...scratch("x.rs", "fn main() {}"), path: "/a/x.rs" }, ALL)!;
|
|
const b = produceContext({ ...scratch("x.rs", "fn main() {}"), path: "/b/x.rs" }, ALL)!;
|
|
expect(a.debug!.config.command).not.toBe(b.debug!.config.command);
|
|
});
|
|
|
|
it("names come out without runtime lookups (menus)", () => {
|
|
expect(produceContext(scratch("hi.ts"), OPTIMISTIC)!.name).toBe("hi.ts");
|
|
expect(produceContext(scratch("hi.ts"), OPTIMISTIC)!.config!.command).toBe("npx");
|
|
expect(produceContext(scratch("hi.js"), OPTIMISTIC)!.config!.command).toBe("node");
|
|
});
|
|
});
|
|
|
|
describe("project producers", () => {
|
|
const configs = [
|
|
cfg({ id: "npm:test", kind: "npm", command: "npm", args: ["run", "test"], isTest: true }),
|
|
cfg({ id: "pytest", kind: "pytest", command: ".venv/bin/pytest", args: [], isTest: true }),
|
|
];
|
|
|
|
it("Godot scenes run with godot --path in the project holding them (Current Scene)", () => {
|
|
const godot = [
|
|
cfg({ id: "godot:project", kind: "godot", command: "godot", args: ["--path", "."], debugAdapter: "godot" }),
|
|
cfg({ id: "godot:project:demo", kind: "godot", command: "godot", args: ["--path", "."], cwd: "demo" }),
|
|
];
|
|
const root = produceContext({ path: `${ROOT}/levels/one.tscn`, root: ROOT, configs: godot }, OPTIMISTIC)!;
|
|
expect(root).toMatchObject({ name: "one.tscn", scope: "file" });
|
|
expect(root.config).toMatchObject({
|
|
kind: "godot",
|
|
command: "godot",
|
|
args: ["--path", ".", "res://levels/one.tscn"],
|
|
cwd: null,
|
|
debugAdapter: "godot",
|
|
});
|
|
const nested = produceContext({ path: `${ROOT}/demo/main.tscn`, root: ROOT, configs: godot }, OPTIMISTIC)!;
|
|
expect(nested.config).toMatchObject({ args: ["--path", ".", "res://main.tscn"], cwd: "demo" });
|
|
// Outside a Godot project there is nothing to run.
|
|
expect(produceContext({ path: `${ROOT}/a.tscn`, root: ROOT, configs: [] }, OPTIMISTIC)).toBeNull();
|
|
});
|
|
|
|
it("keeps package.json test files on the project's runner", () => {
|
|
const p = produceContext(
|
|
{ path: `${ROOT}/src/a.test.ts`, root: ROOT, configs, content: "test('x', () => {})" },
|
|
ALL,
|
|
)!;
|
|
expect(p).toMatchObject({ name: "a.test.ts", scope: "file" });
|
|
expect(p.config!.args).toEqual(["run", "test", "--", "src/a.test.ts"]);
|
|
});
|
|
|
|
it("⌃⇧R picks the innermost context: test > describe > file", () => {
|
|
const src = [
|
|
"describe('math', () => {",
|
|
" it('adds', () => {",
|
|
" expect(1).toBe(1);",
|
|
" });",
|
|
"",
|
|
"});",
|
|
"",
|
|
"const x = 1;",
|
|
].join("\n");
|
|
const at = (line: number) =>
|
|
produceContext({ path: `${ROOT}/src/m.test.ts`, root: ROOT, configs, content: src, line }, ALL)!;
|
|
expect(at(2)).toMatchObject({ name: "math > adds", scope: "element" });
|
|
expect(at(4)).toMatchObject({ name: "math", scope: "element" });
|
|
expect(at(7)).toMatchObject({ name: "m.test.ts", scope: "file" });
|
|
});
|
|
|
|
it("does not offer configurations produced by an earlier run as choices", () => {
|
|
const earlier = cfg({
|
|
id: "context:target:npm:test:src/m.test.ts:other",
|
|
name: "other",
|
|
kind: "npm",
|
|
command: "npm",
|
|
args: ["run", "test", "--", "src/m.test.ts"],
|
|
isTest: true,
|
|
});
|
|
const src = "describe('math', () => {\n it('adds', () => {\n });\n});\n";
|
|
const input = { path: `${ROOT}/src/m.test.ts`, root: ROOT, configs: [...configs, earlier], content: src, line: 1 };
|
|
expect(produceContextChoices(input, ALL).map((c) => c.name)).toEqual(["math > adds"]);
|
|
});
|
|
|
|
it("several equally close test configurations: one choice each, the default first", () => {
|
|
const withUnit = [
|
|
...configs,
|
|
cfg({
|
|
id: "npm:test:unit",
|
|
name: "npm run test:unit",
|
|
kind: "npm",
|
|
command: "npm",
|
|
args: ["run", "test:unit"],
|
|
isTest: true,
|
|
}),
|
|
];
|
|
const src = "describe('math', () => {\n it('adds', () => {\n });\n});\n";
|
|
const input = { path: `${ROOT}/src/m.test.ts`, root: ROOT, configs: withUnit, content: src, line: 1 };
|
|
const choices = produceContextChoices(input, ALL);
|
|
expect(choices.map((c) => [c.name, c.detail, c.scope])).toEqual([
|
|
["math > adds", "npm:test", "element"],
|
|
["math > adds", "npm run test:unit", "element"],
|
|
]);
|
|
expect(choices[1]!.config!.args.slice(0, 2)).toEqual(["run", "test:unit"]);
|
|
// The file context offers the same choice; a single matching configuration needs none.
|
|
expect(produceContextChoices({ ...input, line: undefined }, ALL).map((c) => c.detail)).toEqual([
|
|
"npm:test",
|
|
"npm run test:unit",
|
|
]);
|
|
expect(produceContextChoices({ ...input, configs }, ALL)).toHaveLength(1);
|
|
});
|
|
|
|
it("django: tests.py, TestCase classes and test packages run through manage.py test", () => {
|
|
const django = [
|
|
cfg({ id: "django:runserver", kind: "python", command: ".venv/bin/python", args: ["manage.py", "runserver"] }),
|
|
cfg({
|
|
id: "django:test",
|
|
kind: "django-test",
|
|
command: ".venv/bin/python",
|
|
args: ["manage.py", "test", "--keepdb"],
|
|
isTest: true,
|
|
}),
|
|
];
|
|
const src = [
|
|
"from django.test import TestCase",
|
|
"",
|
|
"class QuestionTests(TestCase):",
|
|
" def test_recent(self):",
|
|
" pass",
|
|
].join("\n");
|
|
const input = { path: `${ROOT}/polls/tests.py`, root: ROOT, configs: django, content: src };
|
|
expect(produceContext({ ...input, line: 4 }, ALL)).toMatchObject({
|
|
name: "QuestionTests.test_recent",
|
|
config: { kind: "django-test", args: ["manage.py", "test", "polls.tests.QuestionTests.test_recent", "--keepdb"] },
|
|
});
|
|
expect(produceContext({ ...input, line: 2 }, ALL)!.config!.args).toEqual([
|
|
"manage.py",
|
|
"test",
|
|
"polls.tests.QuestionTests",
|
|
"--keepdb",
|
|
]);
|
|
expect(produceContext(input, ALL)).toMatchObject({
|
|
name: "tests.py",
|
|
config: { args: ["manage.py", "test", "polls.tests", "--keepdb"] },
|
|
});
|
|
expect(produceDirectory({ path: `${ROOT}/polls/tests`, root: ROOT, configs: django })).toMatchObject({
|
|
name: "Tests in 'tests'",
|
|
config: { args: ["manage.py", "test", "polls.tests", "--keepdb"] },
|
|
});
|
|
expect(produceDirectory({ path: `${ROOT}/polls/migrations`, root: ROOT, configs: django })).toBeNull();
|
|
// Outside Django projects `tests.py` is a script: no test targets, and the file runs as one.
|
|
const plain = { ...input, configs: [configs[1]!] };
|
|
expect(fileTargets(plain, src)).toEqual([]);
|
|
expect(produceContext(plain, ALL)).toMatchObject({ name: "tests.py", config: { kind: "python", isTest: false } });
|
|
// pytest detected as well (pytest-django): the user chooses, pytest first.
|
|
const both = [configs[1]!, ...django];
|
|
expect(produceContextChoices({ ...input, configs: both, line: 4 }, ALL).map((c) => c.detail)).toEqual([
|
|
"pytest",
|
|
"django:test",
|
|
]);
|
|
});
|
|
|
|
it("python: test function > test class > file; __main__ runs the file", () => {
|
|
const src = [
|
|
"class TestMath:",
|
|
" def test_add(self):",
|
|
" assert 1",
|
|
"",
|
|
" x = 1",
|
|
"",
|
|
"def helper():",
|
|
" pass",
|
|
].join("\n");
|
|
const at = (line: number) =>
|
|
produceContext({ path: `${ROOT}/tests/test_m.py`, root: ROOT, configs, content: src, line }, ALL)!.name;
|
|
expect(at(2)).toBe("TestMath.test_add");
|
|
expect(at(4)).toBe("TestMath");
|
|
expect(at(7)).toBe("test_m.py");
|
|
const main = 'print(1)\nif __name__ == "__main__":\n main()\n';
|
|
expect(produceContext(scratch("hi.py", main, 2), ALL)!).toMatchObject({ name: "hi.py", scope: "element" });
|
|
});
|
|
|
|
it("directories: Tests in '<dir>' and All Tests", () => {
|
|
expect(produceDirectory({ path: `${ROOT}/tests`, root: ROOT, configs })).toMatchObject({
|
|
name: "Tests in 'tests'",
|
|
config: { args: ["tests"], command: ".venv/bin/pytest" },
|
|
});
|
|
expect(produceDirectory({ path: `${ROOT}/src/__tests__`, root: ROOT, configs })!.config!.args).toEqual([
|
|
"run",
|
|
"test",
|
|
"--",
|
|
"src/__tests__",
|
|
]);
|
|
const gradle = [cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"], isTest: true })];
|
|
expect(produceDirectory({ path: `${ROOT}/src/test/java`, root: ROOT, configs: gradle })).toMatchObject({
|
|
name: "All Tests",
|
|
});
|
|
expect(
|
|
produceDirectory({ path: `${ROOT}/src/test/java/com/acme`, root: ROOT, configs: gradle })!.config!.args,
|
|
).toEqual(["test", "--tests", "com.acme.*"]);
|
|
expect(produceDirectory({ path: `${ROOT}/src/main`, root: ROOT, configs })).toBeNull();
|
|
expect(produceDirectory({ path: ROOT, root: ROOT, configs })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("file-level gutter targets", () => {
|
|
it("scratches: Python __main__, Go main, Rust fn main, C main; no JS", () => {
|
|
const at = (name: string, text: string) =>
|
|
fileTargets({ path: `${SCRATCH}/${name}`, root: ROOT, configs: [] }, text).map((t) => [t.line, t.kind]);
|
|
expect(at("hi.py", 'x = 1\nif __name__ == "__main__":\n print(x)')).toEqual([[1, "main"]]);
|
|
expect(at("hi.go", "package main\n\nfunc main() {\n}")).toEqual([[2, "main"]]);
|
|
expect(at("hihi.rs", "use std::io;\nfn main() {\n}")).toEqual([[1, "main"]]);
|
|
expect(at("a.c", "#include <stdio.h>\nint main(void) {\n}")).toEqual([[1, "main"]]);
|
|
expect(at("hi.js", "console.log(1)")).toEqual([]);
|
|
expect(at("hi.ts", "console.log(123)")).toEqual([]);
|
|
});
|
|
|
|
it("innermost target uses declaration extents", () => {
|
|
const src = "fn main() {\n}\n#[test]\nfn t() {\n x();\n}\n";
|
|
const targets = fileTargets({ path: `${SCRATCH}/a.rs`, root: ROOT, configs: [] }, src);
|
|
expect(innermostTarget(src, targets, 4)?.name).toBe("t");
|
|
expect(innermostTarget(src, targets, 2)).toBeNull();
|
|
expect(innermostTarget(src, targets, 0)?.kind).toBe("main");
|
|
});
|
|
});
|
|
|
|
describe("produceFile", () => {
|
|
it("ignores unknown files and declaration files", () => {
|
|
expect(produceFile(scratch("readme.md"), ALL)).toBeNull();
|
|
expect(produceFile(scratch("types.d.ts"), ALL)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Kotlin single file (.kt)", () => {
|
|
const KT = env({ kotlinc: null, java: null });
|
|
const hello = 'package demo\n\nfun main(args: Array<String>) {\n println("hi")\n}';
|
|
|
|
it("kotlinc into a runnable jar, then java -jar; no debugger", () => {
|
|
const p = produceContext(scratch("hello.kt", hello), KT)!;
|
|
expect(p.name).toBe("hello.kt");
|
|
expect(p.config).toMatchObject({ kind: "kotlinc", cwd: SCRATCH, args: [], debugAdapter: null });
|
|
expect(p.config!.command).toMatch(
|
|
/^mkdir -p \/tmp\/openidea-run && \/bin\/kotlinc -include-runtime -d \/tmp\/openidea-run\/hello-\w+\.jar hello\.kt && \/bin\/java -jar \/tmp\/openidea-run\/hello-\w+\.jar$/,
|
|
);
|
|
expect(p.debug).toBeUndefined();
|
|
// `suspend fun main()` and the gutter on it (scratch mains with a class name).
|
|
expect(produceContext(scratch("s.kt", "suspend fun main() {}"), KT)!.config!.kind).toBe("kotlinc");
|
|
const targets = fileTargets({ path: `${SCRATCH}/hello.kt`, root: ROOT, configs: [] }, hello);
|
|
expect(targets.map((t) => [t.line, t.kind])).toEqual([[2, "main"]]);
|
|
expect(produceContext(scratch("hello.kt", hello, 3), KT)!.config!.kind).toBe("kotlinc");
|
|
});
|
|
|
|
it("nothing to run without a top-level main; missing kotlinc / java are explained", () => {
|
|
expect(produceContext(scratch("lib.kt", "fun helper() = 42"), KT)).toBeNull();
|
|
const noKotlinc = produceContext(scratch("hello.kt", hello), env({ java: null }))!;
|
|
expect(noKotlinc.config).toBeUndefined();
|
|
expect(noKotlinc.missing!.title).toBe("Cannot run 'hello.kt'");
|
|
expect(noKotlinc.missing!.body).toMatch(
|
|
/The Kotlin compiler was not found \(looked for `kotlinc`\)\..*brew install kotlin/,
|
|
);
|
|
expect(produceContext(scratch("hello.kt", hello), env({ kotlinc: null }))!.missing!.body).toMatch(
|
|
/Java was not found.*JDK/,
|
|
);
|
|
});
|
|
|
|
it("a Gradle / Maven project runs .kt mains through the build tool", () => {
|
|
const gradle = [cfg({ id: "gradle:run", kind: "gradle", command: "./gradlew", args: ["run"] })];
|
|
const p = produceContext(
|
|
{ path: `${ROOT}/src/main/kotlin/App.kt`, root: ROOT, configs: gradle, content: hello },
|
|
KT,
|
|
)!;
|
|
expect(p.config!.kind).not.toBe("kotlinc");
|
|
});
|
|
});
|
|
|
|
describe("compile-and-run lines on Windows", () => {
|
|
const WIN = (tools: Record<string, string | null>) =>
|
|
({
|
|
tool: (n: string): LocatedTool | null =>
|
|
n in tools ? { path: `C:\\Program Files\\Tools\\${n}.exe`, version: tools[n] ?? null } : null,
|
|
compileDir: "C:\\Users\\me\\AppData\\Local\\Temp\\openidea-run",
|
|
windows: true,
|
|
}) satisfies ToolEnv;
|
|
const DIR = String.raw`C:\Users\me\AppData\Local\Temp\openidea-run`;
|
|
/** The command with the per-file hash replaced by `H`. */
|
|
const line = (p: { config?: { command: string } } | null) =>
|
|
p!.config!.command.replace(/-[0-9a-z]+(-test)?\.(exe|jar)/g, "-H$1.$2");
|
|
|
|
it("cmd quoting: double quotes only where needed, never POSIX single quotes", () => {
|
|
expect(shellWord(String.raw`C:\Program Files\x.exe`, true)).toBe(String.raw`"C:\Program Files\x.exe"`);
|
|
expect(shellWord(String.raw`C:\tools\x.exe`, true)).toBe(String.raw`C:\tools\x.exe`);
|
|
expect(shellWord("a&b", true)).toBe('"a&b"');
|
|
expect(shellWord("it's", true)).toBe(`"it's"`);
|
|
expect(shellWord("", true)).toBe('""');
|
|
expect(shellWord("it's")).toBe(String.raw`'it'\''s'`);
|
|
});
|
|
|
|
it("rustc: .exe output, backslash paths, cmd mkdir", () => {
|
|
const p = produceContext(scratch("hihi.rs", "fn main() {}"), WIN({ rustc: "1.85.0" }))!;
|
|
expect(line(p)).toBe(
|
|
`(if not exist ${DIR} mkdir ${DIR}) && "C:\\Program Files\\Tools\\rustc.exe" -g --edition 2021 -o ${DIR}\\hihi-H.exe hihi.rs && ${DIR}\\hihi-H.exe`,
|
|
);
|
|
expect(line(p.debug!)).toBe(`${DIR}\\hihi-H.exe`);
|
|
const test = produceContext(scratch("calc.rs", "#[test]\nfn adds() {}", 1), WIN({ rustc: null }))!;
|
|
expect(line(test)).toMatch(/--test -o \S+\\calc-H-test\.exe calc\.rs && \S+\\calc-H-test\.exe adds --exact$/);
|
|
});
|
|
|
|
it("clang / gcc and kotlinc", () => {
|
|
const c = produceContext(scratch("a.c", "int main(void) {}"), WIN({ gcc: null }))!;
|
|
expect(c.config!.command).toMatch(
|
|
/&& "C:\\Program Files\\Tools\\gcc\.exe" -g -o \S+\\a-\w+\.exe a\.c && \S+\\a-\w+\.exe$/,
|
|
);
|
|
const kt = produceContext(scratch("hello.kt", "fun main() {}"), WIN({ kotlinc: null, java: null }))!;
|
|
expect(kt.config!.command).toMatch(
|
|
/&& "C:\\Program Files\\Tools\\kotlinc\.exe" -include-runtime -d \S+\\hello-\w+\.jar hello\.kt && "C:\\Program Files\\Tools\\java\.exe" -jar \S+\\hello-\w+\.jar$/,
|
|
);
|
|
});
|
|
|
|
it("batch-file compilers (kotlinc.bat) are run with call", () => {
|
|
const e: ToolEnv = {
|
|
...WIN({}),
|
|
tool: (n) =>
|
|
({
|
|
kotlinc: { path: String.raw`C:\kotlin\bin\kotlinc.bat`, version: null },
|
|
java: { path: String.raw`C:\jdk\bin\java.exe`, version: null },
|
|
})[n] ?? null,
|
|
};
|
|
expect(line(produceContext(scratch("hello.kt", "fun main() {}"), e))).toBe(
|
|
`(if not exist ${DIR} mkdir ${DIR}) && call C:\\kotlin\\bin\\kotlinc.bat -include-runtime -d ${DIR}\\hello-H.jar hello.kt && C:\\jdk\\bin\\java.exe -jar ${DIR}\\hello-H.jar`,
|
|
);
|
|
});
|
|
|
|
it("a compile directory with spaces is quoted for cmd", () => {
|
|
const e = { ...WIN({ clang: null }), compileDir: "C:/Users/John Smith/Temp/openidea-run/" };
|
|
const p = produceContext(scratch("a.c", "int main() {}"), e)!;
|
|
expect(p.config!.command).toMatch(
|
|
/^\(if not exist "C:\\Users\\John Smith\\Temp\\openidea-run" mkdir "C:\\Users\\John Smith\\Temp\\openidea-run"\) && /,
|
|
);
|
|
expect(p.config!.command).toMatch(
|
|
/ -o "C:\\Users\\John Smith\\Temp\\openidea-run\\a-\w+\.exe" a\.c && "C:\\Users\\John Smith\\Temp\\openidea-run\\a-\w+\.exe"$/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Go test names (GoLand)", () => {
|
|
const GO = [
|
|
cfg({ id: "go:run", kind: "go", command: "go", args: ["run", "."], debugAdapter: "delve" }),
|
|
cfg({ id: "go:test", kind: "go", command: "go", args: ["test", "./..."], isTest: true, debugAdapter: "delve" }),
|
|
];
|
|
const src = [
|
|
"package calc",
|
|
"",
|
|
'import "testing"',
|
|
"",
|
|
"func TestAdd(t *testing.T) {",
|
|
"}",
|
|
"",
|
|
"func TestSub(t *testing.T) {",
|
|
'\tt.Run("zero case", func(t *testing.T) {',
|
|
"\t})",
|
|
"}",
|
|
"",
|
|
"func TestTable(t *testing.T) {",
|
|
"\ttests := []struct{ name string }{",
|
|
'\t\t{name: "one plus one"},',
|
|
"\t}",
|
|
"\tfor _, tt := range tests {",
|
|
"\t\tt.Run(tt.name, func(t *testing.T) {})",
|
|
"\t}",
|
|
"}",
|
|
].join("\n");
|
|
const modules = (dir: string) => (dir === ROOT ? "example.com/fixture" : undefined);
|
|
const at = (line: number | undefined, goModule?: ContextInput["goModule"]) =>
|
|
produceContext({ path: `${ROOT}/calc/calc_test.go`, root: ROOT, configs: GO, content: src, line, goModule }, ALL)!;
|
|
|
|
it("a test function: 'TestFoo in <import path>' with -run '^TestFoo$'", () => {
|
|
const p = at(5, modules);
|
|
expect(p.name).toBe("TestAdd in example.com/fixture/calc");
|
|
expect(p.config).toMatchObject({
|
|
args: ["test", "./calc", "-run", "^TestAdd$"],
|
|
debugAdapter: "delve",
|
|
isTest: true,
|
|
});
|
|
});
|
|
|
|
it("subtests and table-driven entries", () => {
|
|
expect(at(8, modules)).toMatchObject({ name: "TestSub/zero_case in example.com/fixture/calc" });
|
|
expect(at(8, modules).config!.args).toEqual(["test", "./calc", "-run", "^TestSub$/^zero_case$"]);
|
|
expect(at(14, modules).config!.args).toEqual(["test", "./calc", "-run", "^TestTable$/^one_plus_one$"]);
|
|
// Inside the t.Run body of a table test: the whole test.
|
|
expect(at(17, modules).config!.args).toEqual(["test", "./calc", "-run", "^TestTable$"]);
|
|
});
|
|
|
|
it("the file and the directory: 'go test <import path>'", () => {
|
|
expect(at(undefined, modules).name).toBe("go test example.com/fixture/calc");
|
|
expect(at(undefined, modules).config!.args).toEqual(["test", "./calc"]);
|
|
const dir = produceDirectory({ path: `${ROOT}/internal/tests`, root: ROOT, configs: GO, goModule: modules })!;
|
|
expect(dir.name).toBe("go test example.com/fixture/internal/tests/...");
|
|
expect(dir.config!.args).toEqual(["test", "./internal/tests/..."]);
|
|
expect(dir.config!.name).toBe(dir.name);
|
|
});
|
|
|
|
it("without go.mod read yet: the package clause", () => {
|
|
expect(at(5).name).toBe("TestAdd in calc");
|
|
expect(at(undefined).name).toBe("go test calc");
|
|
});
|
|
|
|
it("nested modules use their own go.mod", () => {
|
|
const nested = [
|
|
cfg({ id: "go:test:api", kind: "go", command: "go", args: ["test", "./..."], cwd: "api", isTest: true }),
|
|
];
|
|
const p = produceContext(
|
|
{
|
|
path: `${ROOT}/api/calc_test.go`,
|
|
root: ROOT,
|
|
configs: nested,
|
|
content: src,
|
|
line: 5,
|
|
goModule: (d) => (d === `${ROOT}/api` ? "example.com/api" : undefined),
|
|
},
|
|
ALL,
|
|
)!;
|
|
expect(p.name).toBe("TestAdd in example.com/api");
|
|
expect(p.config!.args).toEqual(["test", ".", "-run", "^TestAdd$"]);
|
|
});
|
|
});
|
|
|
|
describe("CMake file context", () => {
|
|
const configure = cfg({
|
|
id: "cmake:build:configure",
|
|
kind: "cmake",
|
|
command: "cmake",
|
|
args: ["-S", ".", "-B", "build"],
|
|
});
|
|
const native = cfg({
|
|
id: "cmake:build:Debug:app",
|
|
name: "app",
|
|
kind: "native",
|
|
command: "/p/build/app",
|
|
debugAdapter: "lldb-dap",
|
|
params: { cmakeSources: "main.cpp\nmath.cpp" },
|
|
beforeLaunch: [{ kind: "run", target: "cmake:build:Debug:app:build" }],
|
|
});
|
|
const input = { path: "/p/main.cpp", root: "/p", configs: [configure, native], content: "int main() {}", line: 0 };
|
|
it("runs the owning executable and keeps its build task", () => {
|
|
expect(produceContext(input, OPTIMISTIC)?.config).toEqual(native);
|
|
});
|
|
it("matches absolute source paths from a linked workspace member", () => {
|
|
const target = { ...native, params: { cmakeSources: "/workspace/native/main.cpp" } };
|
|
const p = produceContext(
|
|
{ ...input, path: "/workspace/native/main.cpp", root: "/workspace", configs: [configure, target] },
|
|
OPTIMISTIC,
|
|
);
|
|
expect(p?.config).toEqual(target);
|
|
});
|
|
it("offers all executable targets sharing the source", () => {
|
|
const second = { ...native, id: "other", name: "other" };
|
|
expect(
|
|
produceContextChoices({ ...input, configs: [configure, native, second] }, OPTIMISTIC).map((p) => p.config?.id),
|
|
).toEqual([native.id, second.id]);
|
|
});
|
|
it("never compiles a CMake source as a standalone file when the target is unknown", () => {
|
|
const p = produceContext({ ...input, configs: [configure] }, OPTIMISTIC);
|
|
expect(p?.config).toBeUndefined();
|
|
expect(p?.missing?.body).toContain("CMake Configure");
|
|
});
|
|
});
|