дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(run): rerun exact Go tests in each package (#1978)
Go test reruns now keep full subtest paths (each segment anchored) and run each package with its own selection; tests and benchmarks use their own filters (-run / -bench with -run '^$'), and stale tool or test-binary filters can no longer override reruns or gutter launches. File configurations keep their file target and original run configuration. Boolean test flag aliases are parsed before package targets. Co-authored-by: shohruh-genius <[email protected]>
Цей коміт міститься в:
1 джерело
0d939c774d
коміт
a232850b88
7 files changed
+264
-52
No files matched your search
@@ -80,3 +80,66 @@ test("⌘F2 stops a running process", async ({ workbench: page }) => {
|
||||
await expect(run.getByRole("button", { name: "Stop" })).toBeDisabled();
|
||||
await expect(run.getByRole("tab", { name: "dev" })).toHaveAttribute("data-tooltip", /exit code 143$/);
|
||||
});
|
||||
|
||||
test("Go reruns keep each package's exact failures and the original configuration", async ({ workbench: page }) => {
|
||||
await stubDesktop(page);
|
||||
await page.evaluate(async () => {
|
||||
const config = {
|
||||
id: "go:test",
|
||||
name: "All tests",
|
||||
kind: "go",
|
||||
command: "go",
|
||||
args: ["test", "-json", "./..."],
|
||||
cwd: "/dev/acme-api",
|
||||
env: {},
|
||||
detected: false,
|
||||
isTest: true,
|
||||
debugAdapter: "delve",
|
||||
};
|
||||
const h = window.__ipc.handlers;
|
||||
h.run_configs = () => [config];
|
||||
h.run_apply_settings = (args) => args.configs;
|
||||
let id = 20;
|
||||
h.run_start = (args) => {
|
||||
const run = id++;
|
||||
setTimeout(() => {
|
||||
for (const [pkg, name] of [
|
||||
["example.com/a", "TestA/one"],
|
||||
["example.com/b", "TestB/two/leaf"],
|
||||
]) {
|
||||
for (const event of [
|
||||
{ Action: "run", Package: pkg, Test: name },
|
||||
{ Action: "fail", Package: pkg, Test: name },
|
||||
{ Action: "fail", Package: pkg },
|
||||
])
|
||||
window.__ipc.send(args.onOutput, { kind: "stdout", text: JSON.stringify(event) + "\n" });
|
||||
}
|
||||
window.__ipc.emit({ type: "runExited", run, code: 1 });
|
||||
}, 50);
|
||||
return { id: run, configId: config.id, name: config.name, pid: 4242, running: true, pty: false };
|
||||
};
|
||||
const module = "/src/features/run/store.ts";
|
||||
const { loadConfigs, selectConfig } = await import(/* @vite-ignore */ module);
|
||||
await loadConfigs();
|
||||
selectConfig(config.id);
|
||||
});
|
||||
await page.keyboard.press("Control+r");
|
||||
const run = page.getByRole("region", { name: "Run", exact: true });
|
||||
await expect(run.getByRole("button", { name: "Stop", exact: true })).toBeDisabled();
|
||||
await run.getByRole("button", { name: "Rerun Failed Tests", exact: true }).click();
|
||||
await expect.poll(async () => (await ipcCalls(page, "run_start")).length).toBe(3);
|
||||
const calls = await ipcCalls(page, "run_start");
|
||||
expect(calls.slice(1).map((call) => (call.config as { args: string[] }).args)).toEqual([
|
||||
["test", "-json", "-run", "^TestA$/^one$", "example.com/a"],
|
||||
["test", "-json", "-run", "^TestB$/^two$/^leaf$", "example.com/b"],
|
||||
]);
|
||||
const originals = await page.evaluate(async () => {
|
||||
const module = "/src/features/run/store.ts";
|
||||
const { useRun } = await import(/* @vite-ignore */ module);
|
||||
return useRun.getState().sessions.map((session: { baseConfig?: { args: string[] } }) => session.baseConfig?.args);
|
||||
});
|
||||
expect(originals).toEqual([
|
||||
["test", "-json", "./..."],
|
||||
["test", "-json", "./..."],
|
||||
]);
|
||||
});
|
||||
@@ -34,7 +34,8 @@ export function goTestParts(args: readonly string[]) {
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
flags.push(arg);
|
||||
if (!arg.includes("=") && !BOOLEAN_FLAGS.has(arg) && args[i + 1] !== undefined) flags.push(args[++i]!);
|
||||
if (!arg.includes("=") && !BOOLEAN_FLAGS.has(arg.replace(/^-test\./, "-")) && args[i + 1] !== undefined)
|
||||
flags.push(args[++i]!);
|
||||
} else targets.push(arg);
|
||||
}
|
||||
return { flags, targets, program };
|
||||
@@ -42,13 +43,21 @@ export function goTestParts(args: readonly string[]) {
|
||||
|
||||
export function withoutGoSelection(flags: readonly string[]): string[] {
|
||||
const out: string[] = [];
|
||||
let program = false;
|
||||
for (let i = 0; i < flags.length; i++) {
|
||||
if (["-run", "-skip", "-bench", "-fuzz"].includes(flags[i]!)) {
|
||||
i++;
|
||||
const arg = flags[i]!;
|
||||
if (arg === "--") {
|
||||
out.push(...flags.slice(i));
|
||||
break;
|
||||
}
|
||||
if (arg === "-args") program = true;
|
||||
const selector = program ? /^-test\.(run|skip|bench|fuzz)(=|$)/ : /^-(?:test\.)?(run|skip|bench|fuzz)(=|$)/;
|
||||
const match = selector.exec(arg);
|
||||
if (match) {
|
||||
if (match[2] !== "=") i++;
|
||||
continue;
|
||||
}
|
||||
if (/^-(run|skip|bench|fuzz)=/.test(flags[i]!)) continue;
|
||||
out.push(flags[i]!);
|
||||
out.push(arg);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ import { RubyTestOutput } from "./testing/ruby";
|
||||
import { TeamcityOutput } from "./testing/teamcity";
|
||||
import type { TestStdout } from "./testing/types";
|
||||
import { recordTestStates } from "./testing/states";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfig, testsConfig } from "./testing/rerun";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfigs, testsConfigs } from "./testing/rerun";
|
||||
import { compoundChildren } from "./configs";
|
||||
import { remoteLabel, startWithRemedies } from "./remote";
|
||||
import { TerminalOutput } from "./terminalOutput";
|
||||
@@ -1158,9 +1158,9 @@ export function rerunFailed(sessionId: string): boolean {
|
||||
const session = getSession(sessionId);
|
||||
if (!session || isLive(session)) return false;
|
||||
const base = baseConfigOf(session);
|
||||
const derived = rerunFailedConfig(base, failedTests(sessionId));
|
||||
if (!derived) return false;
|
||||
void startRun(derived, sessionId, base);
|
||||
const derived = rerunFailedConfigs(base, failedTests(sessionId));
|
||||
if (!derived.length) return false;
|
||||
derived.forEach((config, index) => void startRun(config, index === 0 ? sessionId : undefined, base));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1180,14 +1180,16 @@ export async function runSelectedTests(
|
||||
const paths = leafTestPaths(nodes);
|
||||
const base = baseConfigOf(session);
|
||||
const name = nodes.length === 1 ? nodes[0]!.name : `${paths.length} tests`;
|
||||
const derived = testsConfig(base, paths, name);
|
||||
if (!derived) return false;
|
||||
const config = adoptTemporary({
|
||||
...derived,
|
||||
id: `context:tests:${base.id}:${paths.map((p) => p.join("/")).join("|")}`,
|
||||
});
|
||||
if (debug) await executeAction("run.debugConfig", config);
|
||||
else await startRun(config);
|
||||
const derived = testsConfigs(base, paths, name);
|
||||
if (!derived.length) return false;
|
||||
for (const [index, next] of derived.entries()) {
|
||||
const config = adoptTemporary({
|
||||
...next,
|
||||
id: `context:tests:${base.id}:${index}:${paths.map((p) => p.join("/")).join("|")}`,
|
||||
});
|
||||
if (debug) await executeAction("run.debugConfig", config);
|
||||
else await startRun(config, undefined, base);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -245,14 +245,26 @@ describe("Go test targets", () => {
|
||||
id: "go:typed",
|
||||
kind: "go",
|
||||
cwd: "runtime",
|
||||
args: ["test", "-tags", "demo", "-run", "Old", ".", "-args", "-test.short"],
|
||||
args: [
|
||||
"test",
|
||||
"-tags",
|
||||
"demo",
|
||||
"-run",
|
||||
"Old",
|
||||
".",
|
||||
"-args",
|
||||
"-test.run=Old",
|
||||
"-test.bench",
|
||||
"OldBench",
|
||||
"-test.short",
|
||||
],
|
||||
params: {
|
||||
goKind: "test",
|
||||
module: "app",
|
||||
scope: "package",
|
||||
target: ".",
|
||||
pattern: "Old",
|
||||
programArgs: "-test.short",
|
||||
programArgs: "-test.run=Old -test.bench OldBench -test.short",
|
||||
},
|
||||
});
|
||||
const text = 'package calc\nfunc TestNew(t *testing.T) {\n t.Run("child case", func(t *testing.T) {})\n}';
|
||||
@@ -273,6 +285,7 @@ describe("Go test targets", () => {
|
||||
scope: "directory",
|
||||
target: ".",
|
||||
pattern: "^TestNew$/^child_case$",
|
||||
programArgs: "-test.short",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -530,13 +530,14 @@ export function targetConfig(
|
||||
...withoutGoSelection(parts.flags),
|
||||
...parts.targets,
|
||||
...selection,
|
||||
...parts.program,
|
||||
...withoutGoSelection(parts.program),
|
||||
]);
|
||||
if (file.params?.goKind)
|
||||
result.params = {
|
||||
...file.params,
|
||||
pattern: benchmark ? "^$" : pattern,
|
||||
toolArgs: formatArgs([...withoutGoSelection(parts.flags), ...(benchmark ? ["-bench", pattern] : [])]),
|
||||
programArgs: formatArgs(withoutGoSelection(parts.program).slice(1)),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { TestResults } from "./results";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfig, testsConfig } from "./rerun";
|
||||
import {
|
||||
failedTestPaths,
|
||||
leafTestPaths,
|
||||
rerunFailedConfig,
|
||||
rerunFailedConfigs,
|
||||
testsConfig,
|
||||
testsConfigs,
|
||||
} from "./rerun";
|
||||
|
||||
const cfg = (kind: string, command: string, args: string[]): RunConfig => ({
|
||||
id: `${kind}:test`,
|
||||
@@ -129,20 +136,17 @@ describe("rerunFailedConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("go: -run with top-level names and packages", () => {
|
||||
const c = rerunFailedConfig(cfg("go", "go", ["test", "-v", "./..."]), [
|
||||
it("go: keeps full subtest paths separate for each package", () => {
|
||||
const configs = rerunFailedConfigs(cfg("go", "go", ["test", "-v", "./..."]), [
|
||||
["example.com/m/pkg", "TestAdd"],
|
||||
["example.com/m/pkg", "TestSub", "case_1"],
|
||||
["example.com/m/other", "TestX"],
|
||||
])!;
|
||||
expect(c.args).toEqual([
|
||||
"test",
|
||||
"-v",
|
||||
"-run",
|
||||
"^(TestAdd|TestSub|TestX)$",
|
||||
"example.com/m/pkg",
|
||||
"example.com/m/other",
|
||||
]);
|
||||
expect(configs.map((config) => config.args)).toEqual([
|
||||
["test", "-v", "-run", "^TestAdd$|^TestSub$/^case_1$", "example.com/m/pkg"],
|
||||
["test", "-v", "-run", "^TestX$", "example.com/m/other"],
|
||||
]);
|
||||
expect(new Set(configs.map((config) => config.id)).size).toBe(2);
|
||||
});
|
||||
|
||||
it("gradle and maven filters", () => {
|
||||
@@ -204,6 +208,15 @@ describe("withTestReporting", () => {
|
||||
});
|
||||
|
||||
describe("Go rerun argument boundaries", () => {
|
||||
it.each(["-test.v", "-test.short", "-test.benchmem", "-test.failfast", "-test.fullpath"])(
|
||||
"does not retain package targets after the boolean flag %s",
|
||||
(flag) => {
|
||||
const config = cfg("go", "go", ["test", flag, "./..."]);
|
||||
const rerun = rerunFailedConfig(config, [["example.com/selected", "TestOnly"]])!;
|
||||
expect(rerun.args).toEqual(["test", flag, "-run", "^TestOnly$", "example.com/selected"]);
|
||||
},
|
||||
);
|
||||
|
||||
it("replaces import-path targets without losing flag values or binary arguments", () => {
|
||||
const config = cfg("go", "go", ["test", "-tags", "demo", "-run", "old", "example.com/old", "-args", "-test.short"]);
|
||||
const rerun = rerunFailedConfig(config, [["example.com/new", "TestChild", "case"]])!;
|
||||
@@ -218,18 +231,98 @@ describe("Go rerun argument boundaries", () => {
|
||||
"-test.short",
|
||||
]);
|
||||
});
|
||||
it("retains preparation of file-scoped configurations", () => {
|
||||
it.each(["command-line-arguments", "example.com/app"])("retains file-scoped preparation for %s", (packageName) => {
|
||||
const config = {
|
||||
...cfg("go", "go", ["test", "main_test.go"]),
|
||||
params: { goKind: "test", scope: "file", target: "main_test.go", module: "app", pattern: "", useTags: "true" },
|
||||
};
|
||||
const rerun = rerunFailedConfig(config, [["command-line-arguments", "TestFail"]])!;
|
||||
const rerun = rerunFailedConfig(config, [[packageName, "TestFail"]])!;
|
||||
expect(rerun.params).toMatchObject({
|
||||
goKind: "test",
|
||||
scope: "file",
|
||||
target: "main_test.go",
|
||||
pattern: "^(TestFail)$",
|
||||
pattern: "^TestFail$",
|
||||
});
|
||||
expect(rerun.args).toEqual(["test", "-run", "^(TestFail)$", "main_test.go"]);
|
||||
expect(rerun.args).toEqual(["test", "-run", "^TestFail$", "main_test.go"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exact Go test and benchmark selections", () => {
|
||||
it("keeps benchmark selectors and removes stale selectors from binary arguments", () => {
|
||||
const base = cfg("go", "go", [
|
||||
"test",
|
||||
"-tags=demo",
|
||||
"-run=Old",
|
||||
"-bench",
|
||||
"Old",
|
||||
"./...",
|
||||
"-args",
|
||||
"-test.run",
|
||||
"Old",
|
||||
"-test.bench=Old",
|
||||
"-test.skip=New",
|
||||
"-custom",
|
||||
"keep",
|
||||
]);
|
||||
const [config] = testsConfigs(base, [["example.com/p", "BenchmarkNew", "case"]], "selected");
|
||||
expect(config!.args).toEqual([
|
||||
"test",
|
||||
"-tags=demo",
|
||||
"-run",
|
||||
"^$",
|
||||
"-bench",
|
||||
"^BenchmarkNew$/^case$",
|
||||
"example.com/p",
|
||||
"-args",
|
||||
"-custom",
|
||||
"keep",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps independent alternatives for tests of different depths", () => {
|
||||
const [config] = testsConfigs(
|
||||
cfg("go", "go", ["test", "."]),
|
||||
[
|
||||
["p", "TestA", "one"],
|
||||
["p", "TestB", "two", "leaf.with.dot"],
|
||||
["p", "BenchmarkC"],
|
||||
],
|
||||
"selected",
|
||||
);
|
||||
expect(config!.args).toEqual([
|
||||
"test",
|
||||
"-run",
|
||||
"^TestA$/^one$|^TestB$/^two$/^leaf\\.with\\.dot$",
|
||||
"-bench",
|
||||
"^BenchmarkC$",
|
||||
"p",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps typed file benchmark flags and clears overridden binary selectors", () => {
|
||||
const base = {
|
||||
...cfg("go", "go", ["test", "chosen_test.go", "-args", "-test.run=Old", "-custom", "keep"]),
|
||||
params: {
|
||||
goKind: "test",
|
||||
scope: "file",
|
||||
target: "chosen_test.go",
|
||||
module: "app",
|
||||
programArgs: "-test.run=Old -custom keep",
|
||||
},
|
||||
};
|
||||
const [config] = testsConfigs(base, [["example.com/p", "BenchmarkNew"]], "selected");
|
||||
expect(config!.params).toMatchObject({
|
||||
target: "chosen_test.go",
|
||||
pattern: "^$",
|
||||
toolArgs: "-bench '^BenchmarkNew$'",
|
||||
programArgs: "-custom keep",
|
||||
});
|
||||
expect(config!.args).toContain("chosen_test.go");
|
||||
});
|
||||
|
||||
it("reruns an entire failed package with its original runner options", () => {
|
||||
const base = cfg("go", "go", ["test", "-race", "-bench", ".", "./..."]);
|
||||
const [config] = testsConfigs(base, [["example.com/p"]], "selected");
|
||||
expect(config!.args).toEqual(["test", "-race", "-bench", ".", "example.com/p"]);
|
||||
});
|
||||
});
|
||||
@@ -86,23 +86,32 @@ function pytestArgs(base: string[], paths: string[][]): string[] | null {
|
||||
return [...kept, ...ids];
|
||||
}
|
||||
|
||||
function goArgs(base: string[], paths: string[][]): string[] | null {
|
||||
function goArgs(base: string[], paths: string[][], file?: string): string[] | null {
|
||||
const pkgs = unique(paths.map((p) => p[0]!).filter(Boolean));
|
||||
if (!pkgs.length) return null;
|
||||
const tests = paths.filter((p) => p.length > 1);
|
||||
if (pkgs.length !== 1) return null;
|
||||
const parts = goTestParts(base);
|
||||
const top = unique(tests.map((p) => p[1]!));
|
||||
const pattern =
|
||||
tests.length === 1 && tests[0]!.length > 2
|
||||
? tests[0]!
|
||||
const targets = file ? [file] : pkgs.includes("command-line-arguments") ? parts.targets : pkgs;
|
||||
if (paths.some((path) => path.length === 1)) return ["test", ...parts.flags, ...targets, ...parts.program];
|
||||
const pattern = (selected: string[][]) =>
|
||||
unique(
|
||||
selected.map((path) =>
|
||||
path
|
||||
.slice(1)
|
||||
.map((name) => `^${escapeRegExp(name)}$`)
|
||||
.join("/")
|
||||
: `^(${top.map(escapeRegExp).join("|")})$`;
|
||||
const run = top.length ? ["-run", pattern] : [];
|
||||
// A file-scoped run is reported under this synthetic import path; keep its file targets.
|
||||
const targets = pkgs.includes("command-line-arguments") ? parts.targets : pkgs;
|
||||
return ["test", ...withoutGoSelection(parts.flags), ...run, ...targets, ...parts.program];
|
||||
.join("/"),
|
||||
),
|
||||
).join("|");
|
||||
const benchmarks = paths.filter((path) => path[1]?.startsWith("Benchmark"));
|
||||
const tests = paths.filter((path) => !path[1]?.startsWith("Benchmark"));
|
||||
return [
|
||||
"test",
|
||||
...withoutGoSelection(parts.flags),
|
||||
"-run",
|
||||
tests.length ? pattern(tests) : "^$",
|
||||
...(benchmarks.length ? ["-bench", pattern(benchmarks)] : []),
|
||||
...targets,
|
||||
...withoutGoSelection(parts.program),
|
||||
];
|
||||
}
|
||||
|
||||
function jvmArgs(config: RunConfig, paths: string[][]): string[] | null {
|
||||
@@ -141,8 +150,29 @@ export function leafTestPaths(nodes: readonly TestNode[]): string[][] {
|
||||
|
||||
/** Config running only `paths` (failed test paths from the results tree), or null when unsupported. */
|
||||
export function rerunFailedConfig(config: RunConfig, paths: string[][]): RunConfig | null {
|
||||
const configs = rerunFailedConfigs(config, paths);
|
||||
return configs.length === 1 ? configs[0]! : null;
|
||||
}
|
||||
|
||||
export function rerunFailedConfigs(config: RunConfig, paths: string[][]): RunConfig[] {
|
||||
const baseName = config.name.replace(/^Failed: /, "");
|
||||
return testsConfig(config, paths, `Failed: ${baseName}`);
|
||||
return testsConfigs(config, paths, `Failed: ${baseName}`);
|
||||
}
|
||||
|
||||
/** Go filters apply to every package in an invocation, so each package keeps its own selection. */
|
||||
export function testsConfigs(config: RunConfig, paths: string[][], name: string): RunConfig[] {
|
||||
const groups = new Map<string, string[][]>();
|
||||
for (const path of paths) {
|
||||
const key = frameworkFor(config) === "go" ? (path[0] ?? "") : "";
|
||||
const group = groups.get(key) ?? [];
|
||||
group.push(path);
|
||||
groups.set(key, group);
|
||||
}
|
||||
return [...groups].flatMap(([pkg, selected]) => {
|
||||
const derived = testsConfig(config, selected, groups.size > 1 ? `${name} (${pkg})` : name);
|
||||
if (!derived) return [];
|
||||
return [{ ...derived, id: groups.size > 1 ? `context:tests:${config.id}:${pkg}` : derived.id }];
|
||||
});
|
||||
}
|
||||
|
||||
/** Config running only the tests at `paths` (test tree paths), named `name`; null when unsupported. */
|
||||
@@ -169,7 +199,7 @@ export function testsConfig(config: RunConfig, paths: string[][], name: string):
|
||||
args = phpunitArgs(config.args, { filter: phpunitFilter(paths) });
|
||||
break;
|
||||
case "go":
|
||||
args = goArgs(config.args, paths);
|
||||
args = goArgs(config.args, paths, config.params?.scope === "file" ? config.params.target : undefined);
|
||||
break;
|
||||
case "jvm":
|
||||
args = jvmArgs(config, paths);
|
||||
@@ -191,8 +221,9 @@ export function testsConfig(config: RunConfig, paths: string[][], name: string):
|
||||
config.params.scope === "file"
|
||||
? {
|
||||
...config.params,
|
||||
pattern: args[args.indexOf("-run") + 1] ?? "",
|
||||
toolArgs: formatArgs(withoutGoSelection(goTestParts(args).flags)),
|
||||
pattern: args.includes("-run") ? args[args.indexOf("-run") + 1]! : "",
|
||||
toolArgs: formatArgs(dropFlag(goTestParts(args).flags, ["-run", "-test.run"])),
|
||||
programArgs: formatArgs(goTestParts(args).program.slice(1)),
|
||||
}
|
||||
: { module: config.params.module ?? "", useTags: config.params.useTags ?? "false" },
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача