дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Configurations created for a main class, and the Spring Boot applications detected in a module, set their working directory to the module directory. A relative path in the VM options (such as --module-path=app/server/target/dependency) then resolved against the module and the JVM failed to start. They now leave the working directory empty, so the process starts in the project root, which is what an empty working directory means elsewhere. The module is still stored separately and keeps supplying the classpath and the debugger's sources. Fixes #1197
543 рядки
23 KiB
TypeScript
543 рядки
23 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
|
|
import {
|
|
formatCompileErrors,
|
|
joinJavaArgs,
|
|
jdwpListeningPort,
|
|
parseJavaArgs,
|
|
resolveJavaCommand,
|
|
} from "@/platform/javaLaunch";
|
|
import { useProject } from "@/platform/project";
|
|
import { applicationConfig, jvmModuleDir, jvmTestConfig } from "./application";
|
|
import { buildRows } from "./ConfigChooser";
|
|
import { jdwpLaunch } from "./javaDebug";
|
|
import { firstTopLevelType, jvmMainCandidates, scanJvmTargets } from "./jvmTargets";
|
|
import { adoptTemporary, MAX_TEMPORARY, saveTemporary, testReportEvents, useRun } from "./store";
|
|
import { detectRunTargets, targetConfig } from "./targets";
|
|
|
|
const cfg = (over: Partial<RunConfig> & { id: string; kind: string }): RunConfig => ({
|
|
name: over.id,
|
|
command: over.kind === "maven" ? "./mvnw" : over.kind,
|
|
args: [],
|
|
cwd: null,
|
|
env: {},
|
|
detected: true,
|
|
isTest: false,
|
|
debugAdapter: null,
|
|
...over,
|
|
});
|
|
|
|
const pick = (src: string, rel: string, kotlin = false) =>
|
|
scanJvmTargets(src, rel, kotlin).map((t) => [t.line, t.kind, t.className, t.method ?? null, t.spring ?? false]);
|
|
|
|
describe("scanJvmTargets (Java)", () => {
|
|
it("finds a Spring Boot main with annotations on earlier lines", () => {
|
|
const src = [
|
|
"package com.example.demo;", // 0
|
|
"",
|
|
"import org.springframework.boot.SpringApplication;",
|
|
"",
|
|
"@SpringBootApplication(", // 4
|
|
' scanBasePackages = "com.example" // not a ) here',
|
|
")",
|
|
"public class SpringDemoApplication {", // 7
|
|
" /* public static void main(String[] args) in a comment */",
|
|
' static String s = "void main(String[] a) {";',
|
|
" public static void", // 10
|
|
" main(",
|
|
" final String... args) {",
|
|
" SpringApplication.run(SpringDemoApplication.class, args);",
|
|
" }",
|
|
"}",
|
|
].join("\n");
|
|
expect(pick(src, "src/main/java/com/example/demo/SpringDemoApplication.java")).toEqual([
|
|
[11, "main", "com.example.demo.SpringDemoApplication", null, true],
|
|
]);
|
|
});
|
|
|
|
it("ignores non-main methods named main and Java 25 instance mains count", () => {
|
|
expect(pick("class A {\n int main(String[] a) { return 0; }\n static void main(int x) {}\n}", "A.java")).toEqual(
|
|
[],
|
|
);
|
|
expect(pick("class B {\n void main() {}\n}", "B.java")).toEqual([[1, "main", "B", null, false]]);
|
|
});
|
|
|
|
it("finds JUnit tests, nested @Nested classes and multi-line annotations", () => {
|
|
const src = [
|
|
"package a.b;", // 0
|
|
"class OrderTest {", // 1
|
|
" @Test",
|
|
" void plain() {}", // 3
|
|
" @ParameterizedTest(",
|
|
' name = "{0}")',
|
|
" @ValueSource(ints = {1, 2})",
|
|
" void param(int x) {", // 7
|
|
" Runnable r = new Runnable() { public void run() {} };",
|
|
" }",
|
|
" void helper() {}",
|
|
" @Nested",
|
|
" class WhenEmpty {", // 12
|
|
" @org.junit.jupiter.api.Test void fails() {}", // 13
|
|
" }",
|
|
" static class Fixture {",
|
|
" @Test void inner() {}", // 16
|
|
" }",
|
|
"}",
|
|
].join("\n");
|
|
expect(pick(src, "src/test/java/a/b/OrderTest.java")).toEqual([
|
|
[1, "suite", "a.b.OrderTest", null, false],
|
|
[3, "test", "a.b.OrderTest", "plain", false],
|
|
[7, "test", "a.b.OrderTest", "param", false],
|
|
[12, "suite", "a.b.OrderTest$WhenEmpty", null, false],
|
|
[13, "test", "a.b.OrderTest$WhenEmpty", "fails", false],
|
|
[15, "suite", "a.b.OrderTest$Fixture", null, false],
|
|
[16, "test", "a.b.OrderTest$Fixture", "inner", false],
|
|
]);
|
|
});
|
|
|
|
it("treats the public void methods of a TestNG class-level @Test as tests", () => {
|
|
const src = [
|
|
"package a.b;", // 0
|
|
"import org.testng.annotations.*;",
|
|
'@Test(groups = "unit")',
|
|
"public class PriceTest {", // 3
|
|
" @BeforeMethod public void setUp() {}",
|
|
" public void rounds() {}", // 5
|
|
' @Test(dataProvider = "rows")',
|
|
" public void parses(String row) {}", // 7
|
|
" @DataProvider public Object[][] rows() { return null; }",
|
|
' @DataProvider(name = "more") public void notATest() {}',
|
|
" void packagePrivate() {}",
|
|
" private void util() {}",
|
|
" public static void shared() {}",
|
|
" public int value() { return 1; }",
|
|
"}",
|
|
].join("\n");
|
|
expect(pick(src, "src/test/java/a/b/PriceTest.java")).toEqual([
|
|
[3, "suite", "a.b.PriceTest", null, false],
|
|
[5, "test", "a.b.PriceTest", "rounds", false],
|
|
[7, "test", "a.b.PriceTest", "parses", false],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("firstTopLevelType", () => {
|
|
it("is the first top-level class, interface, enum, record or annotation, with the package", () => {
|
|
const src = [
|
|
"package demo;",
|
|
"import java.util.List; // class Commented {}",
|
|
'/* enum Old {} */ @Tag(type = List.class, name = "class Str")',
|
|
"interface Shape { class Nested {} }",
|
|
"public class Test { public static void main(String[] args) {} }",
|
|
].join("\n");
|
|
expect(firstTopLevelType(src)).toBe("demo.Shape");
|
|
expect(firstTopLevelType("record Point(int x) {}\nclass Test {}")).toBe("Point");
|
|
expect(firstTopLevelType("@interface Marker {}\nclass Test {}")).toBe("Marker");
|
|
expect(firstTopLevelType("void main() {\n IO.println(1);\n}\n")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("scanJvmTargets (Kotlin)", () => {
|
|
it("top-level main runs the file facade, objects and companions with @JvmStatic", () => {
|
|
expect(pick("package app\n\nfun main(args: Array<String>) {}", "src/main/kotlin/app/server.kt", true)).toEqual([
|
|
[2, "main", "app.ServerKt", null, false],
|
|
]);
|
|
expect(pick('@file:JvmName("Boot")\npackage app\nfun main() {}', "src/main/kotlin/app/x.kt", true)).toEqual([
|
|
[2, "main", "app.Boot", null, false],
|
|
]);
|
|
const companion = [
|
|
"package app",
|
|
"@SpringBootApplication",
|
|
"class Demo",
|
|
"class Tool {",
|
|
" companion object {",
|
|
" @JvmStatic",
|
|
" fun main(args: Array<String>) {}", // 6
|
|
" }",
|
|
"}",
|
|
"object Cli { @JvmStatic fun main(args: Array<String>) {} }", // 9
|
|
].join("\n");
|
|
expect(pick(companion, "src/main/kotlin/app/Tool.kt", true)).toEqual([
|
|
[6, "main", "app.Tool", null, false],
|
|
[9, "main", "app.Cli", null, false],
|
|
]);
|
|
});
|
|
|
|
it("finds backticked test names", () => {
|
|
const kt = "class CalcTest {\n @Test\n fun `adds numbers`() {}\n}";
|
|
expect(pick(kt, "src/test/kotlin/CalcTest.kt", true)).toEqual([
|
|
[0, "suite", "CalcTest", null, false],
|
|
[2, "test", "CalcTest", "adds numbers", false],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("application configurations", () => {
|
|
const maven = [cfg({ id: "maven:test", kind: "maven", args: ["test"], isTest: true })];
|
|
|
|
it("derives the module directory", () => {
|
|
expect(jvmModuleDir("src/main/java/a/App.java")).toBe("");
|
|
expect(jvmModuleDir("services/api/src/test/kotlin/a/T.kt")).toBe("services/api");
|
|
expect(jvmModuleDir("odd/App.java", [cfg({ id: "m", kind: "maven", cwd: "odd" })])).toBe("odd");
|
|
});
|
|
|
|
it("takes the innermost module the build model knows, like IDEA's file index", () => {
|
|
const modules = ["", "/w/billing", "/w/billing/modules/core", "legacy"];
|
|
// `sourceSets.main.java.srcDirs = ['src']`: no `src/main/java` to guess from.
|
|
expect(jvmModuleDir("legacy/src/a/App.java", [], modules)).toBe("legacy");
|
|
expect(jvmModuleDir("/w/billing/modules/core/src/main/java/a/App.java", [], modules)).toBe(
|
|
"/w/billing/modules/core",
|
|
);
|
|
// A module the model does not know yet (before the first sync): the source layout.
|
|
expect(jvmModuleDir("services/api/src/main/java/a/App.java", [], modules)).toBe("services/api");
|
|
});
|
|
|
|
it("adds JDWP to a script explicitly assigned the Kotlin debugger", () => {
|
|
const rc = cfg({ id: "kotlin-script", kind: "shell", command: "./run.sh", debugAdapter: "kotlin-debug-adapter" });
|
|
expect(jdwpLaunch(rc)?.env.JAVA_TOOL_OPTIONS).toContain("-agentlib:jdwp=");
|
|
});
|
|
|
|
it("routes Kotlin facade and @file:JvmName Application configurations to Kotlin", () => {
|
|
for (const className of ["org.example.MainKt", "org.example.CustomFacade"]) {
|
|
expect(applicationConfig({ className }, "src/main/kotlin/Main.kt")).toMatchObject({
|
|
debugAdapter: "kotlin-debug-adapter",
|
|
args: ["-cp", "${classpath}", className],
|
|
});
|
|
}
|
|
});
|
|
|
|
it("creates an Application / Spring Boot config named after the class", () => {
|
|
const rel = "src/main/java/com/example/demo/SpringDemoApplication.java";
|
|
const src =
|
|
"package com.example.demo;\n@SpringBootApplication\nclass SpringDemoApplication {\n public static void main(String[] a) {}\n}";
|
|
const main = detectRunTargets(src, rel).find((t) => t.kind === "main")!;
|
|
const c = targetConfig(main, rel, maven, src)!;
|
|
expect(c).toMatchObject({
|
|
id: "application:com.example.demo.SpringDemoApplication",
|
|
name: "SpringDemoApplication",
|
|
kind: "spring-boot",
|
|
command: "java",
|
|
args: ["-cp", "${classpath}", "com.example.demo.SpringDemoApplication"],
|
|
cwd: null,
|
|
debugAdapter: "java-debug",
|
|
isTest: false,
|
|
});
|
|
expect(applicationConfig({ className: "a.Tool" }, "m/src/test/java/a/Tool.java")).toMatchObject({
|
|
kind: "application",
|
|
module: "m",
|
|
cwd: null,
|
|
args: ["-cp", "${testClasspath}", "a.Tool"],
|
|
});
|
|
});
|
|
|
|
it("scopes Maven and Gradle test configs to a class or method", () => {
|
|
const file = cfg({ id: "f", kind: "maven", args: ["test", "-Dtest=OrderTest"], isTest: true });
|
|
expect(jvmTestConfig(file, "a.OrderTest$Nested", "fails")).toMatchObject({
|
|
name: "Nested.fails",
|
|
args: ["test", "-Dtest=a.OrderTest$Nested#fails", "-Dsurefire.failIfNoSpecifiedTests=false"],
|
|
});
|
|
const gradle = cfg({ id: "g", kind: "gradle", args: ["test", "--tests", "*OrderTest"], isTest: true });
|
|
expect(jvmTestConfig(gradle, "a.OrderTest", undefined)!.args).toEqual(["test", "--tests", "a.OrderTest"]);
|
|
});
|
|
});
|
|
|
|
describe("java compile errors", () => {
|
|
const base: Omit<JavaLaunchInfo, "errors" | "errorCount" | "hints"> = {
|
|
build: "withErrors",
|
|
projectRoot: "/p",
|
|
classPaths: [],
|
|
modulePaths: [],
|
|
pathListSeparator: ":",
|
|
javaHome: null,
|
|
javaExec: null,
|
|
shortenCommandLine: null,
|
|
};
|
|
|
|
it("lists the first errors, counts the rest and adds hints", () => {
|
|
const report = formatCompileErrors({
|
|
...base,
|
|
errors: [{ path: "/p/src/A.java", line: 72, column: 5, message: "The method getUsername() is undefined" }],
|
|
errorCount: 3,
|
|
hints: ["Install Lombok"],
|
|
});
|
|
expect(report).toBe(
|
|
"/p/src/A.java:72:5: error: The method getUsername() is undefined\n" +
|
|
"… and 2 more errors (see the Problems tool window)\nInstall Lombok\n",
|
|
);
|
|
expect(formatCompileErrors({ ...base, errors: [], errorCount: 0, hints: [] })).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("java command lines", () => {
|
|
const info: JavaLaunchInfo = {
|
|
build: "succeeded",
|
|
projectRoot: "/p",
|
|
classPaths: ["/p/target/classes", "/m2/a.jar"],
|
|
modulePaths: [],
|
|
pathListSeparator: ":",
|
|
javaHome: "/jdk21",
|
|
javaExec: "/jdk21/bin/java",
|
|
shortenCommandLine: "none",
|
|
errors: [],
|
|
errorCount: 0,
|
|
hints: [],
|
|
};
|
|
|
|
it("parses VM options, main class and program arguments", () => {
|
|
expect(parseJavaArgs(["-Xmx1g", "--add-opens", "a/b=ALL", "-cp", "${classpath}", "a.Main", "-v", "x"])).toEqual({
|
|
vmArgs: ["-Xmx1g", "--add-opens", "a/b=ALL"],
|
|
mainClass: "a.Main",
|
|
programArgs: ["-v", "x"],
|
|
classpath: "runtime",
|
|
explicitClasspath: null,
|
|
});
|
|
expect(parseJavaArgs(["-cp", "lib/*", "a.Main"]).explicitClasspath).toBe("lib/*");
|
|
expect(parseJavaArgs(["-Dx=1"]).mainClass).toBeNull();
|
|
});
|
|
|
|
it("substitutes the classpath and the project JDK", () => {
|
|
const c = applicationConfig({ className: "a.Main" }, "src/main/java/a/Main.java");
|
|
const r = resolveJavaCommand({ ...c, args: ["-Dx=1", ...c.args, "arg"] }, info);
|
|
expect(r.command).toBe("/jdk21/bin/java");
|
|
expect(r.args).toEqual(["-Dx=1", "-cp", "/p/target/classes:/m2/a.jar", "a.Main", "arg"]);
|
|
expect(
|
|
resolveJavaCommand(c, { ...info, modulePaths: ["/m/x.jar"], pathListSeparator: ";" }).args.slice(0, 2),
|
|
).toEqual(["--module-path", "/m/x.jar"]);
|
|
});
|
|
|
|
it("colours a Spring Boot console unless the configuration sets the property", () => {
|
|
const boot = (args: string[]) => ({
|
|
...applicationConfig({ className: "a.Main" }, "src/main/java/a/Main.java"),
|
|
kind: "spring-boot" as const,
|
|
args,
|
|
});
|
|
expect(resolveJavaCommand(boot(["-Xmx1g", "a.Main"]), info).args[0]).toBe("-Dspring.output.ansi.enabled=always");
|
|
expect(resolveJavaCommand(boot(["-Dspring.output.ansi.enabled=never", "a.Main"]), info).args).toEqual([
|
|
"-Dspring.output.ansi.enabled=never",
|
|
"a.Main",
|
|
]);
|
|
const plain = applicationConfig({ className: "a.Main" }, "src/main/java/a/Main.java");
|
|
expect(resolveJavaCommand(plain, info).args).not.toContain("-Dspring.output.ansi.enabled=always");
|
|
});
|
|
|
|
it("joins arguments for java-debug and reads JDWP's port", () => {
|
|
expect(joinJavaArgs(["a", "b c", 'q"'])).toBe('a "b c" "q\\""');
|
|
expect(jdwpListeningPort("Listening for transport dt_socket at address: 50123")).toBe(50123);
|
|
expect(jdwpListeningPort("Listening for transport dt_socket at address: 127.0.0.1:5005")).toBe(5005);
|
|
expect(jdwpListeningPort("[INFO] Listening")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("debugging build tool configurations", () => {
|
|
it("adds a waiting JDWP agent to spring-boot:run, Surefire and Gradle", () => {
|
|
const boot = jdwpLaunch(cfg({ id: "b", kind: "maven", args: ["spring-boot:run"] }))!;
|
|
expect(boot.args[1]).toMatch(
|
|
/^-Dspring-boot\.run\.jvmArguments=-agentlib:jdwp=.*suspend=y.*address=127\.0\.0\.1:0$/,
|
|
);
|
|
const merged = jdwpLaunch(
|
|
cfg({ id: "b", kind: "maven", args: ["spring-boot:run", "-Dspring-boot.run.jvmArguments=-Xmx1g"] }),
|
|
)!;
|
|
expect(merged.args[1]).toMatch(/^-Dspring-boot\.run\.jvmArguments=-Xmx1g -agentlib:jdwp=/);
|
|
const test = jdwpLaunch(cfg({ id: "t", kind: "maven", args: ["test", "-Dtest=A#b"], isTest: true }))!;
|
|
expect(test.args.some((a) => a.startsWith("-Dmaven.surefire.debug=-agentlib:jdwp="))).toBe(true);
|
|
expect(jdwpLaunch(cfg({ id: "g", kind: "gradle", args: ["bootRun"] }))!.args).toEqual(["bootRun", "--debug-jvm"]);
|
|
expect(jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["package"] }))).toBeNull();
|
|
expect(jdwpLaunch(cfg({ id: "n", kind: "npm", args: ["test"] }))).toBeNull();
|
|
});
|
|
|
|
const AGENT = /-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127\.0\.0\.1:0$/;
|
|
|
|
it.each([
|
|
["tomcat7:run"],
|
|
["org.apache.tomcat.maven:tomcat7-maven-plugin:2.2:run"],
|
|
["jetty:run"],
|
|
["jetty:run-war"],
|
|
["exec:java"],
|
|
])("debugs Maven's own JVM for the in-process goal %s", (goal) => {
|
|
const out = jdwpLaunch(cfg({ id: "m", kind: "maven", args: ["clean", goal], env: { MAVEN_OPTS: "-Xmx1g" } }))!;
|
|
expect(out.args).toEqual(["clean", goal]);
|
|
expect(out.env.MAVEN_OPTS).toMatch(/^-Xmx1g -agentlib:jdwp=/);
|
|
expect(out.env.MAVEN_OPTS).toMatch(AGENT);
|
|
});
|
|
|
|
it("keeps plain Maven builds out of the debugger unless set to the Java debugger", () => {
|
|
expect(jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["exec:exec"] }))).toBeNull();
|
|
const chosen = jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["package"], debugAdapter: "java-debug" }))!;
|
|
expect(chosen.env.MAVEN_OPTS).toMatch(AGENT);
|
|
});
|
|
|
|
it.each([
|
|
["shell", "C:\\apache-tomcat-9.0.98\\bin\\catalina.bat", ["run"]],
|
|
["shell", "/opt/tomcat9/bin/catalina.sh", ["run"]],
|
|
["program", "java", ["-jar", "app.war"]],
|
|
])("starts a %s configuration set to the Java debugger with an agent in JAVA_TOOL_OPTIONS", (kind, command, args) => {
|
|
const rc = cfg({ id: "tomcat9", kind, command, args, debugAdapter: "java-debug" });
|
|
const out = jdwpLaunch(rc)!;
|
|
expect(out.command).toBe(command);
|
|
expect(out.args).toEqual(args);
|
|
expect(out.env.JAVA_TOOL_OPTIONS).toMatch(AGENT);
|
|
const merged = jdwpLaunch({ ...rc, env: { JAVA_TOOL_OPTIONS: "-Dfile.encoding=UTF-8" } })!;
|
|
expect(merged.env.JAVA_TOOL_OPTIONS).toMatch(/^-Dfile\.encoding=UTF-8 -agentlib:jdwp=/);
|
|
});
|
|
|
|
it("debugs a single Java file: the agent before the file, or for java only on a javac line", () => {
|
|
const source = jdwpLaunch(cfg({ id: "f", kind: "java-file", command: "/bin/java", args: ["Hi.java"] }))!;
|
|
expect(source.args).toEqual([expect.stringMatching(AGENT), "Hi.java"]);
|
|
const line = "mkdir -p /tmp/c && /bin/javac -g -d /tmp/c Test.java && /bin/java -cp /tmp/c Test";
|
|
const compiled = jdwpLaunch(
|
|
cfg({ id: "f", kind: "java-file", command: line, env: { JDK_JAVA_OPTIONS: "-Xmx1g" } }),
|
|
)!;
|
|
expect(compiled).toMatchObject({ command: line, args: [] });
|
|
expect(compiled.env.JDK_JAVA_OPTIONS).toMatch(/^-Xmx1g -agentlib:jdwp=/);
|
|
expect(compiled.env.JAVA_TOOL_OPTIONS).toBeUndefined();
|
|
});
|
|
|
|
it("leaves Application configurations and other debuggers alone", () => {
|
|
for (const kind of ["application", "spring-boot"])
|
|
expect(jdwpLaunch(cfg({ id: "a", kind, command: "java", debugAdapter: "java-debug" }))).toBeNull();
|
|
expect(jdwpLaunch(cfg({ id: "s", kind: "shell", command: "./run.sh", debugAdapter: "debugpy" }))).toBeNull();
|
|
expect(jdwpLaunch(cfg({ id: "g", kind: "gradle", args: ["build"], debugAdapter: "java-debug" }))).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("JUnit XML reports", () => {
|
|
it("become tree results with failure output and source location", () => {
|
|
const events = testReportEvents([
|
|
{ className: "a.B", name: "ok", status: "passed", durationMs: 3, message: null, output: null },
|
|
{
|
|
className: "a.B$Inner",
|
|
name: "bad",
|
|
status: "failed",
|
|
durationMs: null,
|
|
message: "expected 1",
|
|
output: "AssertionError\n\tat org.junit.Assert.fail(Assert.java:1)\n\tat a.B$Inner.bad(B.java:42)",
|
|
},
|
|
]);
|
|
expect(events[0]).toEqual({ type: "result", path: ["a.B", "ok"], status: "passed", durationMs: 3 });
|
|
expect(events).toContainEqual({ type: "output", path: ["a.B$Inner", "bad"], line: "expected 1" });
|
|
expect(events).toContainEqual({
|
|
type: "location",
|
|
path: ["a.B$Inner", "bad"],
|
|
location: { path: "a/B.java", line: 42, resolve: "name", className: "a.B$Inner" },
|
|
});
|
|
});
|
|
|
|
it("locate Kotlin tests with spaces in their names, behind a class loader prefix", () => {
|
|
const events = testReportEvents([
|
|
{
|
|
className: "a.LibraryTest",
|
|
name: "cannot lend twice",
|
|
status: "failed",
|
|
durationMs: null,
|
|
message: null,
|
|
output: "AssertionFailedError\n\tat app//a.LibraryTest.cannot lend twice(LibraryTest.kt:43)",
|
|
},
|
|
]);
|
|
expect(events).toContainEqual({
|
|
type: "location",
|
|
path: ["a.LibraryTest", "cannot lend twice"],
|
|
location: { path: "a/LibraryTest.kt", line: 43, resolve: "name", className: "a.LibraryTest" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("temporary configurations", () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
useProject.setState({ project: { root: "/p" } } as never);
|
|
useRun.setState({
|
|
configs: [cfg({ id: "maven:spring-boot:run", kind: "maven" })],
|
|
temporary: [],
|
|
selectedId: null,
|
|
sessions: [],
|
|
});
|
|
});
|
|
|
|
it("are added, selected, capped and persisted", () => {
|
|
const apps = Array.from({ length: MAX_TEMPORARY + 2 }, (_, i) =>
|
|
applicationConfig({ className: `a.App${i}` }, "src/main/java/a/App.java"),
|
|
);
|
|
for (const a of apps) adoptTemporary(a);
|
|
const s = useRun.getState();
|
|
expect(s.temporary).toHaveLength(MAX_TEMPORARY);
|
|
expect(s.temporary).not.toContain(apps[0]!.id);
|
|
expect(s.selectedId).toBe(apps[apps.length - 1]!.id);
|
|
expect(s.configs.filter((c) => c.kind === "application")).toHaveLength(MAX_TEMPORARY);
|
|
expect(JSON.parse(localStorage.getItem("openidea.run.temporary.v2:/p")!)).toHaveLength(MAX_TEMPORARY);
|
|
// Reusing an existing temporary moves it last instead of duplicating it.
|
|
adoptTemporary(apps[3]!);
|
|
expect(useRun.getState().temporary.at(-1)).toBe(apps[3]!.id);
|
|
expect(useRun.getState().configs.filter((c) => c.id === apps[3]!.id)).toHaveLength(1);
|
|
});
|
|
|
|
it("reuse a saved configuration with the same id", () => {
|
|
const saved = cfg({ id: "application:a.Main", kind: "application", name: "Mine", detected: false });
|
|
useRun.setState({ configs: [saved] });
|
|
const used = adoptTemporary(applicationConfig({ className: "a.Main" }, "src/main/java/a/Main.java"));
|
|
expect(used.name).toBe("Mine");
|
|
expect(useRun.getState().temporary).toEqual([]);
|
|
});
|
|
|
|
it("reuse a configuration of the same main class and module, whatever its id", () => {
|
|
const own = (module: string) =>
|
|
cfg({
|
|
id: `user:${module}`,
|
|
kind: "spring-boot",
|
|
name: `Mine ${module}`,
|
|
args: ["-Xmx1g", "-cp", "${classpath}", "a.Main"],
|
|
module,
|
|
detected: false,
|
|
});
|
|
useRun.setState({ configs: [own("other"), own("app")] });
|
|
expect(adoptTemporary(applicationConfig({ className: "a.Main" }, "app/src/main/java/a/Main.java")).name).toBe(
|
|
"Mine app",
|
|
);
|
|
expect(useRun.getState().temporary).toEqual([]);
|
|
const fresh = adoptTemporary(applicationConfig({ className: "a.Other" }, "app/src/main/java/a/Other.java"));
|
|
expect(useRun.getState().temporary).toEqual([fresh.id]);
|
|
});
|
|
|
|
it("offer Save in the chooser while selected", async () => {
|
|
const app = adoptTemporary(applicationConfig({ className: "a.Main" }, "src/main/java/a/Main.java"));
|
|
const { configs, temporary } = useRun.getState();
|
|
const rows = buildRows(configs, "", app, temporary);
|
|
expect(rows[0]).toMatchObject({ type: "save", config: { id: app.id } });
|
|
expect(buildRows(configs, "x", app, temporary)[0]?.type).not.toBe("save");
|
|
// Not a desktop app in tests: saving fails at the IPC layer but the config stops being temporary.
|
|
await saveTemporary(app.id).catch(() => undefined);
|
|
expect(useRun.getState().temporary).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("jvmMainCandidates", () => {
|
|
const names = (src: string) => jvmMainCandidates(src, "a/B.java").map((c) => c.className);
|
|
|
|
it("finds the class from the report, at its name", () => {
|
|
const src = "package demo;\n\n@SpringBootApplication\npublic class AiChatApplication extends AtomStarter {}\n";
|
|
expect(jvmMainCandidates(src, "a/B.java")).toEqual([
|
|
{ line: 3, col: 13, className: "demo.AiChatApplication", base: "AtomStarter", spring: true },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["static nested class", "class A { static class B extends Base {} }", ["A$B"]],
|
|
["inner (non-static) class", "class A { class B extends Base {} }", []],
|
|
["abstract class", "abstract class A extends Base {}", ["A"]],
|
|
["own main", "class A extends Base { public static void main(String[] a) {} }", []],
|
|
["implements only", "class A implements Runnable {}", []],
|
|
["interface extends", "interface A extends Base {}", []],
|
|
["enum and record", "enum E implements I {}\nrecord R() implements I {}", []],
|
|
["multi-line generic header", "class A<T extends Number>\n extends Base<T>\n{}", ["A"]],
|
|
["type bound only", "class A<T extends Number> {}", []],
|
|
["extends in comment and string", '// class A extends B\nclass C { String s = "class D extends E {"; }', []],
|
|
])("%s", (_, src, expected) => expect(names(src)).toEqual(expected));
|
|
|
|
it("accepts java.lang.String[] and flags instance main", () => {
|
|
const [fq] = scanJvmTargets("class A { public static void main(java.lang.String[] a) {} }", "A.java", false);
|
|
expect(fq).toMatchObject({ kind: "main", className: "A" });
|
|
const [inst] = scanJvmTargets("class A { void main() {} }", "A.java", false);
|
|
expect(inst).toMatchObject({ kind: "main", instance: true });
|
|
expect(fq?.instance).toBeUndefined();
|
|
});
|
|
});
|