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]>
Цей коміт міститься в:
Shohruh Ismoilovтаshohruh-genius authored and GitHub committed 2026-11-08 00:12:09 +08:00
1 джерело 0d939c774d
коміт a232850b88
7 files changed
+264 -52

No files matched your search

+63
Переглянути файл
@@ -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", "./..."],
]);
});
+14 -5
Переглянути файл
@@ -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;
}
+14 -12
Переглянути файл
@@ -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;
}
+15 -2
Переглянути файл
@@ -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",
});
});
});
+2 -1
Переглянути файл
@@ -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;
}
+108 -15
Переглянути файл
@@ -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"]);
});
});
+48 -17
Переглянути файл
@@ -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" },
}