дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Initial open-source release
Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
Цей коміт міститься в:
коміт
9a2025b76e
2034 files changed
+243381
No files matched your search
@@ -0,0 +1,300 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
|
||||
import {
|
||||
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 { 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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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("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",
|
||||
cwd: "m",
|
||||
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 command lines", () => {
|
||||
const info: JavaLaunchInfo = {
|
||||
build: "succeeded",
|
||||
projectRoot: "/p",
|
||||
classPaths: ["/p/target/classes", "/m2/a.jar"],
|
||||
modulePaths: [],
|
||||
javaHome: "/jdk21",
|
||||
javaExec: "/jdk21/bin/java",
|
||||
};
|
||||
|
||||
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"] }, ";").args.slice(0, 2)).toEqual([
|
||||
"--module-path",
|
||||
"/m/x.jar",
|
||||
]);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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: "B.java", line: 42, resolve: "name" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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:/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("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([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача