feat(dotnet): add workspace, NuGet and test workflows (#2303)

* feat(run): show .NET test results and rerun failed methods

* feat(dotnet): add workspace, NuGet and editor test workflows

* fix(dotnet): discard cancelled queries and scan test targets linearly
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-09 17:17:27 +08:00
1 джерело dfa610beb0
коміт 5a48beab07
26 files changed
+2242 -13

No files matched your search

+51
Переглянути файл
@@ -0,0 +1,51 @@
.view {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-3);
overflow: auto;
height: 100%;
color: var(--fg);
}
.view label {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0;
}
.view select {
color: var(--fg);
background: var(--bg-elev);
border: 1px solid var(--border);
padding: var(--space-2);
max-width: 100%;
}
.toolbar {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.list {
padding: 0;
margin: var(--space-2) 0;
list-style: none;
}
.list li {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
padding: var(--space-2) 0;
}
.list li span {
flex: 1 1 100%;
overflow-wrap: anywhere;
}
.error {
color: var(--danger);
white-space: pre-wrap;
}
.output {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
+225
Переглянути файл
@@ -0,0 +1,225 @@
import { executeAction } from "@/platform/actions";
import { useState } from "react";
import { useProject } from "@/platform/project";
import { useTx } from "@/platform/i18n";
import { Button, Input } from "@/ui";
import { useDotnet } from "./store";
import styles from "./Dotnet.module.css";
export function DotnetView() {
const s = useDotnet();
const tx = useTx();
const root = useProject((state) => state.project?.root ?? "");
const relative = (path: string) => (root && path.startsWith(root + "/") ? path.slice(root.length + 1) : path);
const [versions, setVersions] = useState<Record<string, string>>({});
const results = new Map<string, string[]>();
for (const p of s.found) results.set(p.id, [...new Set([...(results.get(p.id) ?? []), p.version])]);
const project = s.model.projects.find((p) => p.path === s.selected);
const action = (id: string, arg?: unknown) => void executeAction(`dotnet.${id}`, arg);
return (
<div className={styles.view}>
<div className={styles.toolbar}>
<Button disabled={s.busy} onClick={() => action("reload")}>
{tx("Reload .NET Workspace")}
</Button>
{s.busy && <Button onClick={() => action("cancel")}>{tx("Cancel")}</Button>}
</div>
{s.error && (
<div role="alert" className={styles.error}>
{s.error}
</div>
)}
{s.model.errors?.map((error) => (
<details key={error} className={styles.error}>
<summary>{tx("Project evaluation failed")}</summary>
{error}
</details>
))}
{s.model.solutions.map((solution) => (
<details key={solution.path}>
<summary>{solution.path.split("/").at(-1)}</summary>
{solution.projects.map((path) => (
<Button key={path} title={path} disabled={s.busy} onClick={() => action("select", { path })}>
{relative(path)}
</Button>
))}
</details>
))}
<label>
{tx(".NET project")}
<select
aria-label={tx(".NET project")}
value={s.selected}
disabled={s.busy}
onChange={(e) => action("select", { path: e.target.value })}
>
{!project && <option value="">{tx("Select a .NET project")}</option>}
{s.model.projects.map((p) => (
<option key={p.path} value={p.path}>
{relative(p.path)}
</option>
))}
</select>
</label>
{project && (
<>
<div className={styles.toolbar}>
<label>
{tx("Configuration")}
<select
aria-label={tx("Configuration")}
disabled={s.busy}
value={s.configuration}
onChange={(e) =>
action("select", { path: s.selected, configuration: e.target.value, framework: s.framework })
}
>
{project.configurations.map((c) => (
<option key={c}>{c}</option>
))}
</select>
</label>
<label>
{tx("Target framework")}
<select
aria-label={tx("Target framework")}
disabled={s.busy}
value={s.framework}
onChange={(e) =>
action("select", { path: s.selected, configuration: s.configuration, framework: e.target.value })
}
>
{project.frameworks.map((f) => (
<option key={f}>{f}</option>
))}
</select>
</label>
</div>
<div className={styles.toolbar}>
{["build", "restore", "clean"].map((op) => (
<Button key={op} disabled={s.busy} onClick={() => action(op)}>
{tx(op === "build" ? "Build" : op === "restore" ? "Restore" : "Clean")}
</Button>
))}
</div>
<details open>
<summary>{tx("Project references")}</summary>
{project.references.map((path) => (
<Button
key={path}
disabled={s.busy || !s.model.projects.some((p) => p.path === path)}
onClick={() => action("select", { path })}
>
{relative(path)}
</Button>
))}
</details>
<details open>
<summary>{tx("NuGet packages")}</summary>
<Button disabled={s.busy} onClick={() => action("packages")}>
{tx("Refresh packages")}
</Button>
<form
className={styles.toolbar}
onSubmit={(e) => {
e.preventDefault();
action("search", { query: s.query });
}}
>
<Input
aria-label={tx("Search NuGet packages")}
value={s.query}
onChange={(e) => useDotnet.setState({ query: e.target.value })}
/>
<Button type="submit" disabled={s.busy || !s.query.trim()}>
{tx("Search")}
</Button>
</form>
<ul className={styles.list}>
{[...results].map(([id, available]) => {
const version = available.includes(versions[id] ?? "") ? versions[id]! : available[0]!;
return (
<li key={id}>
<span>{id}</span>
<select
aria-label={tx("Version")}
value={version}
disabled={s.busy}
onChange={(e) => setVersions({ ...versions, [id]: e.target.value })}
>
{available.map((v) => (
<option key={v}>{v}</option>
))}
</select>
<Button disabled={s.busy} onClick={() => action("packageChange", { id, version })}>
{tx("Install / Update")}
</Button>
<Button disabled={s.busy} onClick={() => action("search", { query: id, exact: true })}>
{tx("Versions")}
</Button>
</li>
);
})}
</ul>
<ul className={styles.list}>
{s.packages.map((p) => (
<li key={`${p.id}:${p.version}`}>
<span>
{p.id} {p.version} {p.transitive ? tx("Transitive") : ""}
</span>
{!p.transitive && (
<>
<Button disabled={s.busy} onClick={() => action("search", { query: p.id, exact: true })}>
{tx("Versions")}
</Button>
<Button disabled={s.busy} onClick={() => action("packageChange", { id: p.id, version: null })}>
{tx("Remove")}
</Button>
</>
)}
</li>
))}
</ul>
</details>
{project.test && (
<details open>
<summary>{tx("Tests")}</summary>
<div className={styles.toolbar}>
<Button disabled={s.busy} onClick={() => action("discover")}>
{tx("Discover .NET Tests")}
</Button>
<Button disabled={s.busy} onClick={() => action("test")}>
{tx("Run all tests")}
</Button>
</div>
{project.runner !== "vstest" && !project.trx && (
<div>
{tx("Install the TRX reporting extension to show MTP results in the test panel.")}
<Button disabled={s.busy} onClick={() => action("installTrx")}>
{tx("Install TRX reporting")}
</Button>
</div>
)}
<ul className={styles.list}>
{s.tests.map((test) => (
<li key={test}>
<span>{test}</span>
<Button disabled={s.busy} onClick={() => action("test", test)}>
{tx("Run test")}
</Button>
</li>
))}
</ul>
</details>
)}
</>
)}
{s.output && (
<details>
<summary>{tx("Output")}</summary>
<pre className={styles.output}>{s.output}</pre>
</details>
)}
</div>
);
}
+125
Переглянути файл
@@ -0,0 +1,125 @@
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { registerToolWindow, toolWindowService } from "@/workbench/contributions";
import { DotnetView } from "./DotnetView";
import {
buildDotnet,
cancelDotnet,
changePackage,
discoverTests,
installTrx,
listPackages,
reloadDotnet,
resetDotnet,
runTests,
searchPackages,
selectDotnet,
} from "./store";
export function activate() {
const offWindow = registerToolWindow({
id: "dotnet",
title: ".NET",
icon: "file-csharp",
anchor: "right",
stripe: "onUse",
component: DotnetView,
});
const show = () => toolWindowService().show("dotnet", true);
const offActions = registerActions([
{
id: "dotnet.show",
title: ".NET Workspace",
category: "Build",
when: "projectOpen",
run: () => {
show();
return reloadDotnet();
},
},
{
id: "dotnet.reload",
title: "Reload .NET Workspace",
category: "Build",
when: "projectOpen",
run: () => reloadDotnet(),
},
{ id: "dotnet.cancel", title: "Cancel .NET Operation", category: "Build", run: () => cancelDotnet() },
{
id: "dotnet.select",
title: "Select .NET Project",
category: "Build",
hidden: true,
run: ({ arg }) => {
const a = arg as { path: string; configuration?: string; framework?: string };
return selectDotnet(a.path, a.configuration, a.framework);
},
},
...(["build", "restore", "clean"] as const).map((op) => ({
id: `dotnet.${op}`,
title: op === "build" ? "Build .NET Project" : op === "restore" ? "Restore .NET Project" : "Clean .NET Project",
category: "Build",
when: "projectOpen",
run: () => buildDotnet(op),
})),
{
id: "dotnet.packages",
title: "Refresh NuGet Packages",
category: "Build",
when: "projectOpen",
run: () => listPackages(),
},
{
id: "dotnet.search",
title: "Search NuGet Packages",
category: "Build",
hidden: true,
run: ({ arg }) => {
const a = arg as { query: string; exact?: boolean };
return searchPackages(a.query, a.exact);
},
},
{
id: "dotnet.packageChange",
title: "Change NuGet Package",
category: "Build",
hidden: true,
run: ({ arg }) => {
const a = arg as { id: string; version: string | null };
return changePackage(a.id, a.version);
},
},
{
id: "dotnet.discover",
title: "Discover .NET Tests",
category: "Run",
when: "projectOpen",
run: () => discoverTests(),
},
{
id: "dotnet.test",
title: "Run .NET Tests",
category: "Run",
when: "projectOpen",
run: async ({ arg }) => {
await runTests(typeof arg === "string" ? arg : undefined);
},
},
{
id: "dotnet.installTrx",
title: "Install TRX reporting",
category: "Build",
when: "projectOpen",
run: () => installTrx(),
},
]);
const offProject = useProject.subscribe((s, old) => {
if (s.project?.root !== old.project?.root) resetDotnet();
});
return () => {
resetDotnet();
offProject();
offActions();
offWindow();
};
}
+97
Переглянути файл
@@ -0,0 +1,97 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cancelDotnet, discoveredTests, packageList, selectDotnet, resetDotnet, useDotnet } from "./store";
import * as platform from "@/platform/dotnet";
afterEach(() => {
resetDotnet();
vi.restoreAllMocks();
});
it("recovers a project that failed the initial workspace scan", async () => {
const project: platform.DotnetProject = {
path: "/repo/Test.csproj",
name: "Test",
frameworks: ["net9.0"],
configurations: ["Debug"],
references: [],
packages: [],
test: true,
runner: "vstest",
adapter: "mstest",
trx: false,
};
vi.spyOn(platform, "evaluateProject").mockResolvedValue(project);
useDotnet.setState({ model: { projects: [], solutions: [], errors: [`${project.path}: failed`] } });
await selectDotnet(project.path);
expect(useDotnet.getState().model).toEqual({ projects: [project], solutions: [], errors: [] });
});
it("ignores an evaluation completed after switching workspaces", async () => {
let complete!: (p: platform.DotnetProject) => void;
vi.spyOn(platform, "evaluateProject").mockImplementation(
() =>
new Promise((resolve) => {
complete = resolve;
}),
);
const pending = selectDotnet("/repo/Test.csproj");
resetDotnet();
complete({ path: "/repo/Test.csproj", frameworks: ["net9.0"] } as platform.DotnetProject);
await pending;
expect(useDotnet.getState().selected).toBe("");
expect(useDotnet.getState().busy).toBe(false);
});
it("ignores an evaluation completed after cancellation", async () => {
let complete!: (p: platform.DotnetProject) => void;
vi.spyOn(platform, "evaluateProject").mockImplementation(
() =>
new Promise((resolve) => {
complete = resolve;
}),
);
const pending = selectDotnet("/repo/Test.csproj");
cancelDotnet();
complete({ path: "/repo/Test.csproj", frameworks: ["net9.0"] } as platform.DotnetProject);
await pending;
expect(useDotnet.getState().selected).toBe("");
expect(useDotnet.getState().busy).toBe(false);
expect(useDotnet.getState().error).toBe("");
});
describe("real dotnet discovery output", () => {
it.each([
"The following Tests are available:\n Passes\n Parameterized (1)\n\nProcess finished with exit code 0",
"The following Tests are available:\nPasses\nParameterized (1)\nTests succeeded: 'Tests.dll' [net9.0|x64]",
"Discovering tests from Tests.dll (net10.0|x64)\nDiscovered 2 tests in assembly - Tests.dll (net10.0|x64)\n Passes\n Parameterized (1)\n\nDiscovered 2 tests.",
])("extracts names without host and runner summaries", (output) => {
expect(discoveredTests(output)).toEqual(["Passes", "Parameterized (1)"]);
});
it("merges target frameworks and assemblies without duplicating names", () => {
expect(
discoveredTests(
"The following Tests are available:\n A.Tests.One\n\nThe following Tests are available:\n A.Tests.One\n B.Tests.Two\n",
),
).toEqual(["A.Tests.One", "B.Tests.Two"]);
});
it("keeps direct and transitive dependencies across frameworks", () => {
expect(
packageList({
projects: [
{
frameworks: [
{
topLevelPackages: [{ id: "Direct", resolvedVersion: "1.0" }],
transitivePackages: [{ id: "Child", resolvedVersion: "2.0" }],
},
{ transitivePackages: [{ id: "Direct", resolvedVersion: "1.0" }] },
],
},
],
}),
).toEqual([
{ id: "Direct", version: "1.0", transitive: false, latest: undefined },
{ id: "Child", version: "2.0", transitive: true, latest: undefined },
]);
});
});
+315
Переглянути файл
@@ -0,0 +1,315 @@
import { create } from "zustand";
import { executeAction } from "@/platform/actions";
import {
dotnet,
dotnetWorkspace,
evaluateProject,
dotnetTestArgs,
jsonOutput,
type DotnetWorkspace,
} from "@/platform/dotnet";
import { dirname, useProject } from "@/platform/project";
import type { RunConfig } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
export interface Package {
id: string;
version: string;
latest?: string;
transitive?: boolean;
}
interface State {
model: DotnetWorkspace;
selected: string;
framework: string;
configuration: string;
busy: boolean;
error: string;
output: string;
query: string;
packages: Package[];
found: Package[];
tests: string[];
}
const initial: State = {
model: { projects: [], solutions: [] },
selected: "",
framework: "",
configuration: "Debug",
busy: false,
error: "",
output: "",
query: "",
packages: [],
found: [],
tests: [],
};
export const useDotnet = create<State>(() => initial);
let controller: AbortController | undefined;
let generation = 0;
export function resetDotnet() {
generation++;
controller?.abort();
useDotnet.setState(initial);
}
export const selectedProject = () =>
useDotnet.getState().model.projects.find((p) => p.path === useDotnet.getState().selected);
async function task(work: (signal: AbortSignal) => Promise<Partial<State>>) {
controller?.abort();
const current = ++generation;
const abort = new AbortController();
controller = abort;
useDotnet.setState({ busy: true, error: "" });
try {
const patch = await work(abort.signal);
if (current === generation && !abort.signal.aborted) useDotnet.setState(patch);
} catch (e) {
if (current === generation && !abort.signal.aborted) useDotnet.setState({ error: String(e) });
} finally {
if (current === generation) {
controller = undefined;
useDotnet.setState({ busy: false });
}
}
}
export function cancelDotnet() {
controller?.abort();
}
export function reloadDotnet() {
const root = useProject.getState().project?.root;
if (!root) return;
return task(async (signal) => {
const model = await dotnetWorkspace(root, signal);
const old = useDotnet.getState();
const project = model.projects.find((p) => p.path === old.selected) ?? model.projects[0];
return {
model,
selected: project?.path ?? "",
configuration: "Debug",
framework: project?.frameworks[0] ?? "",
packages: [],
found: [],
tests: [],
};
});
}
export function selectDotnet(path: string, configuration?: string, framework?: string) {
const state = useDotnet.getState();
return task(async (signal) => {
const project = await evaluateProject(path, configuration ?? state.configuration, framework ?? "", signal);
return {
selected: path,
configuration: configuration ?? state.configuration,
framework: framework ?? project.frameworks[0] ?? "",
model: {
...state.model,
projects: state.model.projects.some((p) => p.path === path)
? state.model.projects.map((p) => (p.path === path ? project : p))
: [...state.model.projects, project],
errors: state.model.errors?.filter((error) => !error.startsWith(`${path}:`)),
},
packages: [],
found: [],
tests: [],
};
});
}
export function buildDotnet(operation: "build" | "restore" | "clean") {
const p = selectedProject();
if (!p) return;
const { framework, configuration } = useDotnet.getState();
return task(async (signal) => {
const output = await dotnet(
[
operation,
p.path,
...(operation === "restore"
? [`-p:Configuration=${configuration}`]
: ["--configuration", configuration, ...(framework ? ["--framework", framework] : [])]),
],
dirname(p.path),
signal,
);
const project = await evaluateProject(p.path, configuration, framework, signal);
const model = useDotnet.getState().model;
return {
output,
model: { ...model, projects: model.projects.map((item) => (item.path === p.path ? project : item)) },
};
});
}
interface PackageJson {
id: string;
resolvedVersion?: string;
requestedVersion?: string;
latestVersion?: string;
}
interface PackagesJson {
projects?: Array<{ frameworks?: Array<{ topLevelPackages?: PackageJson[]; transitivePackages?: PackageJson[] }> }>;
}
export function packageList(data: PackagesJson): Package[] {
const result = new Map<string, Package>();
for (const p of data.projects ?? [])
for (const f of p.frameworks ?? []) {
for (const [key, transitive] of [
["topLevelPackages", false],
["transitivePackages", true],
] as const)
for (const pkg of f[key] ?? [])
result.set(`${pkg.id}:${pkg.resolvedVersion}`, {
id: pkg.id,
version: pkg.resolvedVersion ?? pkg.requestedVersion ?? "",
latest: pkg.latestVersion,
transitive: transitive && result.get(`${pkg.id}:${pkg.resolvedVersion}`)?.transitive !== false,
});
}
return [...result.values()];
}
export function listPackages() {
const p = selectedProject();
if (!p) return;
return task(async (signal) => ({
packages: packageList(
jsonOutput(
await dotnet(["list", p.path, "package", "--include-transitive", "--format", "json"], dirname(p.path), signal),
),
),
}));
}
export function searchPackages(query: string, exact = false) {
const p = selectedProject();
if (!p || !query.trim()) return;
return task(async (signal) => {
const result = jsonOutput<{
problems?: Array<{ text?: string }>;
searchResult?: Array<{ packages?: Array<{ id: string; latestVersion?: string; version?: string }> }>;
}>(
await dotnet(
["package", "search", query.trim(), "--format", "json", "--take", "30", ...(exact ? ["--exact-match"] : [])],
dirname(p.path),
signal,
),
);
const found = (result.searchResult ?? []).flatMap((source) =>
(source.packages ?? []).map((pkg) => ({ id: pkg.id, version: pkg.latestVersion ?? pkg.version ?? "" })),
);
if (result.problems?.length) throw new Error(JSON.stringify(result.problems));
if (exact) found.reverse();
return { found };
});
}
export function changePackage(id: string, version: string | null) {
const p = selectedProject();
if (!p || !id) return;
return task(async (signal) => {
if (id === "Microsoft.Testing.Extensions.TrxReport" && version === "") {
await dotnet(["restore", p.path], dirname(p.path), signal);
const restored = await evaluateProject(
p.path,
useDotnet.getState().configuration,
useDotnet.getState().framework,
signal,
);
if (!restored.platformVersion)
throw new Error(tx("Microsoft.Testing.Platform was not found in the restored project."));
version = restored.platformVersion;
}
const output = await dotnet(
version === null
? ["remove", p.path, "package", id]
: ["add", p.path, "package", id, ...(version ? ["--version", version] : [])],
dirname(p.path),
signal,
);
if (version === null) await dotnet(["restore", p.path], dirname(p.path), signal);
const project = await evaluateProject(
p.path,
useDotnet.getState().configuration,
useDotnet.getState().framework,
signal,
);
const packages = packageList(
jsonOutput(
await dotnet(["list", p.path, "package", "--include-transitive", "--format", "json"], dirname(p.path), signal),
),
);
return {
output,
packages,
model: {
...useDotnet.getState().model,
projects: useDotnet.getState().model.projects.map((x) => (x.path === p.path ? project : x)),
},
};
});
}
export function discoveredTests(output: string): string[] {
const lines = output.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "").split(/\r?\n/);
const tests = new Set<string>();
let listing = false;
for (const line of lines) {
if (/following Tests are available|Discovered tests:|Discovered \d+ tests? in assembly/i.test(line)) {
listing = true;
continue;
}
if (!listing) continue;
const name = line.trim();
if (
!name ||
/^(?:Process finished|Tests succeeded:|Tests failed:|Test run summary:|Discovered \d+ tests?\.|Total:)/.test(name)
) {
listing = false;
continue;
}
if (!/^(?:at |\[|.*\.dll\b)/.test(name)) tests.add(name);
}
return [...tests];
}
export function discoverTests() {
const p = selectedProject();
if (!p) return;
const s = useDotnet.getState();
return task(async (signal) => {
const output = await dotnet(
dotnetTestArgs(p, s.framework, ["--list-tests"], s.configuration),
dirname(p.path),
signal,
);
return { output, tests: discoveredTests(output) };
});
}
export function runTests(test?: string) {
const p = selectedProject();
if (!p) return;
const s = useDotnet.getState();
const method = test?.replace(/\(.*$/, "").trim();
const config: RunConfig = {
id: `dotnet:${p.path}:tests`,
name: test ?? p.name,
kind: "dotnet-test",
command: "dotnet",
args: [],
cwd: dirname(p.path),
env: {},
detected: true,
isTest: true,
debugAdapter: null,
params: {
project: p.path,
framework: s.framework,
configuration: s.configuration,
runner: p.runner,
adapter: p.adapter,
programArgs: "",
...(method ? { testMethods: JSON.stringify([method]) } : {}),
},
};
return executeAction("run.runConfig", config);
}
export function installTrx() {
return changePackage("Microsoft.Testing.Extensions.TrxReport", "");
}
+2
Переглянути файл
@@ -21,6 +21,7 @@ import {
} from "./vcs";
import { activate as terminal } from "./terminal";
import { activate as run } from "./run";
import { activate as dotnet } from "./dotnet";
import { activate as runAnything } from "./run-anything";
import { activate as macros } from "./macros";
import { activate as debuggerFeature } from "./debugger";
@@ -70,6 +71,7 @@ const features: Array<[string, Activate]> = [
["vcs", vcs],
["terminal", terminal],
["run", run],
["dotnet", dotnet],
["run-anything", runAnything],
["macros", macros],
["debugger", debuggerFeature],
+23 -2
Переглянути файл
@@ -12,11 +12,14 @@ import { isWindows } from "./jvmTypes";
import { typeFor } from "./types";
import { runTemplate } from "./runTemplates";
import { appendScriptArgs } from "./scriptArgs";
import { csharpTargets } from "./csharpTargets";
export type FileRole = "test" | "main" | "script" | null;
/** `django`: a Django project, whose default test module is `tests.py` (elsewhere a script). */
export function fileRole(relPath: string, content?: string, windows = isWindows(), django = false): FileRole {
if (/\.cs$/i.test(relPath) && content && csharpTargets(content).some((target) => target.kind === "test"))
return "test";
const name = relPath.split("/").pop() ?? relPath;
if (
/\.(test|spec)\.[cm]?[jt]sx?$/.test(name) ||
@@ -44,6 +47,7 @@ const stem = (p: string) => (p.split("/").pop() ?? p).replace(/\.[^.]+$/, "");
/** Kinds whose test configs can run a file of this extension. */
function testKinds(relPath: string): string[] {
if (/\.cs$/i.test(relPath)) return ["dotnet-test"];
if (/\.[cm]?[jt]sx?$/.test(relPath)) return ["npm", "deno", "bun"];
if (/\.py$/.test(relPath)) return ["pytest", "python", "django-test"];
if (/\.php$/.test(relPath)) return ["phpunit"];
@@ -57,6 +61,8 @@ function testKinds(relPath: string): string[] {
/** The module owning a configuration's targets, relative to the project root. */
export function configScope(config: RunConfig): string {
if (config.kind === "dotnet" || config.kind === "dotnet-test")
return dirOf((config.params?.project || "").replace(/\\/g, "/"));
return (
config.kind === "go"
? (config.params?.module ?? config.module ?? config.cwd ?? "")
@@ -210,11 +216,26 @@ export function testBasesFor(relPath: string, configs: RunConfig[]): RunConfig[]
* File-level test configuration for `relPath` (the closest matching test config,
* or `base`, narrowed to the file).
*/
export function testConfigFor(relPath: string, configs: RunConfig[], base?: RunConfig): RunConfig | null {
export function testConfigFor(
relPath: string,
configs: RunConfig[],
base?: RunConfig,
content?: string,
): RunConfig | null {
base ??= testBasesFor(relPath, configs)[0];
if (!base) return null;
const file = relToCwd(relPath, base);
switch (base.kind) {
case "dotnet-test": {
const methods = csharpTargets(content ?? "")
.filter((t) => t.kind === "test")
.map((t) => t.path.join("."));
return methods.length
? derived(base, relPath, {
params: { ...base.params, testMethods: JSON.stringify(methods) },
})
: null;
}
case "npm":
case "bun": {
const args = appendScriptArgs(base.command, base.args, [file]);
@@ -409,7 +430,7 @@ export function contextConfig(
windows = isWindows(),
): RunConfig | null {
const role = fileRole(relPath, content, windows, hasDjangoTests(configs));
if (role === "test") return testConfigFor(relPath, configs);
if (role === "test") return testConfigFor(relPath, configs, undefined, content);
if (role === "main") return mainConfigFor(relPath, configs) ?? scriptConfigFor(relPath, configs, windows, content);
if (role === "script") return scriptConfigFor(relPath, configs, windows, content);
return null;
+106
Переглянути файл
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { csharpTargets } from "./csharpTargets";
import { targetConfig } from "./targets";
import { produceContext, produceFile, OPTIMISTIC } from "./producers";
import { dotnetTestType } from "./dotnetTypes";
import type { RunConfig } from "@/platform/ipc";
const config = (project: string): RunConfig => ({
id: project,
name: project,
command: "dotnet",
args: ["test", project],
kind: "dotnet-test",
params: { project, framework: "net9.0" },
cwd: null,
env: {},
detected: true,
isTest: true,
debugAdapter: null,
});
const source = `namespace Demo.Tests;
public class Calculator {
// [Fact] public void NotATest() {}
[Xunit.Fact]
public void Adds() { var s = "}"; }
[Theory, InlineData(1)]
public async Task Values(int n) { }
public class Nested {
[TestMethod] public void Child() {}
}
}`;
describe("C# editor test targets", () => {
it("tracks lines across multiline tokens and separate test classes", () => {
const text = `namespace Demo {
/* a comment
across lines */
class First {
[Fact]
void One(
) {}
}
class Second {
[TestCase(
1)]
void Two(int value) {}
}
}`;
expect(csharpTargets(text).map((t) => [t.line, t.path])).toEqual([
[3, ["Demo.First"]],
[5, ["Demo.First", "One"]],
[8, ["Demo.Second"]],
[11, ["Demo.Second", "Two"]],
]);
});
it("runs only methods declared in the chosen file", () => {
const result = produceFile(
{ path: "/repo/two/Tests.cs", root: "/repo", content: source, configs: [config("/repo/two/Two.csproj")] },
OPTIMISTIC,
);
expect(JSON.parse(result!.config!.params!.testMethods!)).toEqual([
"Demo.Tests.Calculator.Adds",
"Demo.Tests.Calculator.Values",
"Demo.Tests.Calculator+Nested.Child",
]);
});
it("finds qualified and multiline attributes, nested types, and skips literals/comments", () => {
expect(
csharpTargets(source)
.filter((t) => t.kind === "test")
.map((t) => [t.line, t.path]),
).toEqual([
[4, ["Demo.Tests.Calculator", "Adds"]],
[6, ["Demo.Tests.Calculator", "Values"]],
[8, ["Demo.Tests.Calculator+Nested", "Child"]],
]);
expect(csharpTargets("namespace A { namespace B { class C { [Test] void T() {} } } }").at(-1)?.path).toEqual([
"A.B.C",
"T",
]);
});
it("selects the owning project when same-named tests exist in sibling projects", () => {
const targets = csharpTargets(source);
const configs = [config("one/One.csproj"), config("two/Two.csproj")];
const result = targetConfig(
targets.find((t) => t.name === "Adds")!,
"two/Tests.cs",
configs,
source,
)!;
expect(result.params?.project).toBe("two/Two.csproj");
expect(dotnetTestType.commandLine(result.params!)?.args).toContain("FullyQualifiedName=Demo.Tests.Calculator.Adds");
});
it("normalizes absolute workspace project paths for caret/gutter launch", () => {
const result = produceContext(
{
path: "/repo/two/Tests.cs",
root: "/repo",
content: source,
line: 4,
configs: [config("/repo/one/One.csproj"), config("/repo/two/Two.csproj")],
},
OPTIMISTIC,
);
expect(result?.config?.params?.project).toBe("two/Two.csproj");
});
});
+64
Переглянути файл
@@ -0,0 +1,64 @@
import type { RunTarget } from "./targets";
/** Mask comments/literals without changing UTF-16 offsets or line breaks. */
export function csharpTargets(text: string): RunTarget[] {
const code = text.replace(
/\/\*[\s\S]*?\*\/|\/\/[^\n]*|@"(?:""|[^"])*"|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g,
(value) => value.replace(/[^\r\n]/g, " "),
);
const tokens =
/\bnamespace\s+([\w.]+)\s*([;{])|\b(?:class|record|struct)\s+(\w+)[^;{}]*\{|\[([^\]]*)\]|\b(\w+)\s*\([^;{}]*\)\s*(?=\{|=>)|[{};]/g;
const scopes: Array<{ name: string; depth: number; line: number; kind: "namespace" | "class" }> = [];
const out: RunTarget[] = [];
const suites = new Set<string>();
let depth = 0;
let line = 0;
let offset = 0;
let fileNamespace = "";
let test = false;
for (const match of code.matchAll(tokens)) {
const value = match[0];
while (offset < match.index) {
if (code.charCodeAt(offset) === 10) line++;
offset++;
}
if (match[1]) {
if (match[2] === ";") fileNamespace = match[1];
else scopes.push({ name: match[1], depth: ++depth, line, kind: "namespace" });
} else if (match[3]) {
scopes.push({ name: match[3], depth: ++depth, line, kind: "class" });
test = false;
} else if (match[4] !== undefined) {
test ||=
/(?:^|[.,\s])(?:Fact|Theory|Test|TestCase|TestCaseSource|TestMethod|DataTestMethod)(?:Attribute)?\b/.test(
match[4],
);
} else if (match[5]) {
if (test && scopes.some((s) => s.kind === "class")) {
const className = [
fileNamespace,
...scopes.filter((s) => s.kind === "namespace").map((s) => s.name),
scopes
.filter((s) => s.kind === "class")
.map((s) => s.name)
.join("+"),
]
.filter(Boolean)
.join(".");
const owner = scopes.filter((s) => s.kind === "class").at(-1)!;
if (!suites.has(className)) {
suites.add(className);
out.push({ line: owner.line, kind: "suite", name: owner.name, path: [className] });
}
out.push({ line, kind: "test", name: match[5], path: [className, match[5]] });
}
test = false;
} else if (value === "{") depth++;
else if (value === "}") {
depth--;
while (scopes.length && scopes.at(-1)!.depth > depth) scopes.pop();
test = false;
} else test = false;
}
return out;
}
+44 -2
Переглянути файл
@@ -2,6 +2,7 @@ import { call, commands, type RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import type { RunConfigTypeContribution } from "@/workbench/contributions";
import { parseArgs } from "./configs";
import { prepareDotnetTest } from "@/platform/dotnet";
function profile(p: Readonly<Record<string, string>>): {
applicationUrl?: string;
@@ -20,6 +21,7 @@ export function dotnetCommand(p: Readonly<Record<string, string>>, test = false)
args: [
...(test ? ["test", p.project || ""] : ["run", "--project", p.project || ""]),
...(p.framework ? ["--framework", p.framework] : []),
...(p.configuration ? ["--configuration", p.configuration] : []),
...(!test ? (p.launchProfile ? ["--launch-profile", p.launchProfile] : ["--no-launch-profile"]) : []),
...(p.programArgs?.trim() ? ["--", ...parseArgs(p.programArgs)] : []),
],
@@ -125,10 +127,50 @@ export const dotnetTestType: RunConfigTypeContribution = {
fields: [...dotnetProjectType.fields.slice(0, 2), { key: "programArgs", label: "Test arguments", mono: true }],
commandLine(p) {
const line = dotnetCommand({ ...p, programArgs: "" }, true);
return { ...line, args: [...line.args, ...parseArgs(p.programArgs || "")] };
if (p.runner === "mtp") line.args.splice(1, 0, "--project");
if (p.runner === "mtp-legacy")
line.args.push("-p:TestingPlatformCaptureOutput=false", "-p:TestingPlatformDotnetTestSupport=true");
let extra = parseArgs(p.programArgs || "");
if (p.runner === "mtp-legacy") {
const boundary = extra.indexOf("--");
if (boundary >= 0) {
line.args.push(...extra.slice(0, boundary));
extra = extra.slice(boundary + 1);
} else {
const runner: string[] = [];
const options = new Set(["--configuration", "-c", "--framework", "-f", "--runtime", "-r", "--verbosity", "-v"]);
for (let i = 0; i < extra.length; i++) {
const arg = extra[i]!;
if (options.has(arg)) line.args.push(arg, extra[++i] ?? "");
else if (/^(?:--no-build$|--no-restore$|-p:|-property:|--property:)/.test(arg)) line.args.push(arg);
else runner.push(arg);
}
extra = runner;
}
}
if (p.testMethods) {
const methods = JSON.parse(p.testMethods) as string[];
const flags = new Set(["--filter", "--filter-method"]);
extra = extra.filter((a, i, all) => !flags.has(a) && !flags.has(all[i - 1] ?? "") && !a.startsWith("--filter="));
const selection =
p.runner !== "vstest" && p.runner && p.adapter === "xunit"
? methods.flatMap((m) => ["--filter-method", m.includes(".") ? m : `*.${m}`])
: [
"--filter",
methods
.map((m) => `${m.includes(".") ? "FullyQualifiedName" : "Name"}=${m.replace(/[\\()&|=!~]/g, "\\$&")}`)
.join("|"),
];
const boundary = extra.indexOf("--");
extra.splice(boundary < 0 ? extra.length : boundary, 0, ...selection);
}
return {
...line,
args: [...line.args, ...(p.runner === "mtp-legacy" && extra[0] !== "--" ? ["--"] : []), ...extra],
};
},
async prepareLaunch(config) {
const resolved = await call(commands.runDotnetResolve(config, false));
const resolved = await prepareDotnetTest(await call(commands.runDotnetResolve(config, false)));
return { ...resolved, ...dotnetTestType.commandLine(resolved.params ?? {}) };
},
};
+5 -2
Переглянути файл
@@ -126,6 +126,9 @@ function scopedInput<T extends Pick<ContextInput, "root" | "configs">>(input: T)
...c,
cwd: c.cwd == null ? c.cwd : relative(c.cwd),
...(c.module == null ? {} : { module: relative(c.module) }),
...(c.kind.startsWith("dotnet") && c.params?.project
? { params: { ...c.params, project: relative(c.params.project) } }
: {}),
...(c.kind === "go" && c.params?.module != null
? { params: { ...c.params, module: relative(c.params.module) } }
: {}),
@@ -743,7 +746,7 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
// Project-aware: test files through the project's test runner, Cargo / Go / Gradle / Maven mains.
const role = fileRole(rel, input.content, undefined, hasDjangoTests(configs));
if (role === "test") {
const found = testConfigFor(rel, configs);
const found = testConfigFor(rel, configs, undefined, input.content);
const test = found && goTestNamed(found, input);
if (test) return { name: test.name, scope: "file", config: test };
}
@@ -1102,7 +1105,7 @@ export function produceContextChoices(input: ContextInput, env: ToolEnv): Produc
for (const base of bases) {
const found = element
? targetConfig(element, rel, [...input.configs], input.content, base)
: testConfigFor(rel, [...input.configs], base);
: testConfigFor(rel, [...input.configs], base, input.content);
const config = found && goTestNamed(found, input, element?.name);
if (!config || out.some((o) => o.config?.id === config.id)) continue;
out.push({ name: config.name, scope: produced.scope, config, detail: base.name });
+19 -1
Переглянути файл
@@ -45,6 +45,7 @@ import {
} from "./testing";
import { PluginTestOutput } from "./testing/plugin";
import { GoTestOutput } from "./testing/go";
import { DotnetReports } from "./testing/dotnet";
import { moveToReportedClasses } from "./testing/jvm";
import { RubyTestOutput } from "./testing/ruby";
import { TeamcityOutput } from "./testing/teamcity";
@@ -94,6 +95,7 @@ export interface RunSession {
}
interface SessionRuntime {
dotnetReports?: DotnetReports;
resolveRequest?: { id: RequestId; controller: AbortController };
console: ConsoleModel;
tests: TestResults | null;
@@ -653,6 +655,7 @@ function newRuntime(config: RunConfig): SessionRuntime {
if (pluginTests && kind !== "system") pluginTests.feed(text);
if (parser && tests && kind !== "system" && !(kind === "stdout" && stdout)) tests.apply(parser.feed(text));
if (kind !== "system") for (const l of runtime.lineListeners) l(text);
if (kind !== "system") runtime.dotnetReports?.feed(text);
},
}),
queue: [],
@@ -894,12 +897,16 @@ export async function startRun(
if ((options.launch || launch.isTest) && launch.emulateTerminal) launch = { ...launch, emulateTerminal: false };
if (launch.emulateTerminal) rt.terminal = new TerminalOutput();
patchSession(sessionId, { terminal: !!launch.emulateTerminal });
const started =
let started =
launch.params?.framework === "plugin"
? launch
: launch.isTest
? await gradleTestLaunch(withTestReporting(launch))
: await gradleBootLaunch(launch);
if (launch.isTest && frameworkFor(launch) === "dotnet" && launch.params?.framework !== "plugin") {
rt.dotnetReports = new DotnetReports();
started = rt.dotnetReports.launch(started);
}
if (runtimes.get(sessionId)?.generation !== generation) return getSession(sessionId) ?? null;
const processStarted = performance.now();
const info = await startWithRemedies(started, () => call(commands.runStart(started, channel)));
@@ -1028,6 +1035,15 @@ function finishSession(sessionId: string, code: number | null) {
if (runtimes.get(sessionId) !== rt) return;
if (rt.tests && (rt.parser || rt.pluginTests)) {
rt.tests.apply(rt.parser?.end?.() ?? []);
if (rt.dotnetReports) {
try {
const events = await rt.dotnetReports.read((path) => call(commands.fsReadText(path)));
if (runtimes.get(sessionId) !== rt) return;
rt.tests.apply(events);
} catch (e) {
rt.console.append({ kind: "system", text: tx`Could not read .NET test results: ${String(e)}\n` });
}
}
// Surefire / Gradle print only failures and class totals: per-test results come from the XML reports.
if (frameworkFor(session.config) === "jvm") {
const reports = await jvmReports(session);
@@ -1120,6 +1136,8 @@ export function onRunExited(runId: RunId, code: number | null) {
const session = useRun.getState().sessions.find((s) => s.runId === runId);
if (!session) {
pendingExits.set(runId, code);
// Background project queries also use the process host without attaching a Run tab.
if (pendingExits.size > 128) pendingExits.delete(pendingExits.keys().next().value!);
return;
}
finishSession(session.id, code);
+15 -2
Переглянути файл
@@ -7,6 +7,7 @@
* one target derived from the project's detected test configurations.
*/
import { goTargets } from "./goTargets";
import { csharpTargets } from "./csharpTargets";
import { formatArgs } from "./configs";
import type { RunConfig } from "@/platform/ipc";
import { applicationConfig, jvmTestConfig } from "./application";
@@ -43,9 +44,10 @@ export interface RunTarget {
/** Above this size we skip detection (generated / minified files). */
export const MAX_SCAN = 400_000;
type Lang = "rust" | "js" | "python" | "php" | "go" | "jvm" | "ruby" | null;
type Lang = "rust" | "js" | "python" | "php" | "go" | "jvm" | "ruby" | "csharp" | null;
export function targetLanguage(relPath: string): Lang {
if (/\.cs$/i.test(relPath)) return "csharp";
if (/\.rs$/.test(relPath)) return "rust";
if (/\.[cm]?[jt]sx?$/.test(relPath)) return "js";
if (/\.py$/.test(relPath)) return "python";
@@ -355,6 +357,8 @@ export function detectRunTargets(text: string, relPath: string, django = false):
if (text.length > MAX_SCAN) return [];
const lines = text.split("\n");
switch (targetLanguage(relPath)) {
case "csharp":
return csharpTargets(text);
case "rust":
return rustTargets(lines, relPath);
case "js":
@@ -409,7 +413,7 @@ export function targetConfig(
return applicationConfig({ className: target.className, spring: target.spring }, relPath, configs);
return contextConfig(relPath, configs, content);
}
const file = testConfigFor(relPath, configs, base);
const file = testConfigFor(relPath, configs, base, content);
if (!file) return null;
if (target.className && (file.kind === "maven" || file.kind === "gradle"))
return jvmTestConfig(
@@ -427,6 +431,15 @@ export function targetConfig(
detected: true,
});
switch (file.kind) {
case "dotnet-test": {
const methods =
target.kind === "suite" && content
? csharpTargets(content)
.filter((t) => t.kind === "test" && t.path[0] === target.path[0])
.map((t) => t.path.join("."))
: [target.path.join(".")];
return { ...named(file.args), params: { ...file.params, testMethods: JSON.stringify(methods) } };
}
case "npm":
case "bun":
case "deno":
+146
Переглянути файл
@@ -0,0 +1,146 @@
import { describe, expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { DotnetReports, trxEvents } from "./dotnet";
import { frameworkFor, TestResults } from "./index";
import { failedTestPaths, rerunFailedConfig } from "./rerun";
import { dotnetTestType } from "../dotnetTypes";
import xunitReport from "./fixtures/xunit.trx?raw";
import nunitReport from "./fixtures/nunit.trx?raw";
import mstestReport from "./fixtures/mstest.trx?raw";
import mtpReport from "./fixtures/mtp.trx?raw";
const config: RunConfig = {
id: "dotnet:tests",
name: "Tests",
kind: "dotnet-test",
command: "dotnet",
args: ["test", "Tests.csproj"],
cwd: null,
env: {},
detected: true,
isTest: true,
debugAdapter: null,
params: {
project: "Tests.csproj",
framework: "net9.0",
programArgs: "--filter Category=Unit --no-restore -- RunConfiguration.MaxCpuCount=1",
},
};
function report(adapter: string, source = "C:/My Project/bin/net9.0/Tests.dll") {
return `<TestRun xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Results>
<UnitTestResult testId="a" testName="adds (1)" outcome="Passed" duration="00:00:00.0015"/>
<UnitTestResult testId="b" testName="adds (2)" outcome="Failed" duration="00:01:02.5"><Output><ErrorInfo>
<Message>Expected &lt;3&gt;</Message><StackTrace>at Sample.Tests.Adds(Int32 value) in C:\\My Project\\Tests.cs:line 12</StackTrace>
</ErrorInfo><StdOut>captured output</StdOut></Output></UnitTestResult>
<UnitTestResult testId="c" testName="skipped" outcome="NotExecuted"/>
</Results>
<TestDefinitions>
<UnitTest id="a"><TestMethod codeBase="${source}" adapterTypeName="${adapter}" className="Sample.Tests" name="Adds"/></UnitTest>
<UnitTest id="b"><TestMethod codeBase="${source}" adapterTypeName="${adapter}" className="Sample.Tests" name="Adds"/></UnitTest>
<UnitTest id="c"><TestMethod codeBase="${source}" adapterTypeName="${adapter}" className="Sample.Tests" name="Skipped"/></UnitTest>
</TestDefinitions>
</TestRun>`;
}
describe(".NET test reports", () => {
it("finds source locations in MTP stack traces without the VSTest line prefix", () => {
expect(trxEvents(mtpReport)).toContainEqual(
expect.objectContaining({
type: "location",
location: { path: "C:\\Fixture\\Native\\Test1.cs", line: 5, resolve: "path" },
}),
);
});
it.each([xunitReport, nunitReport, mstestReport, mtpReport])(
"reads actual VSTest reports and selects failed methods",
async (xml) => {
const results = new TestResults();
results.apply(trxEvents(xml));
expect(results.counts()).toEqual({ passed: 2, failed: 2, skipped: 1, running: 0, total: 5 });
const rerun = await rerunFailedConfig(config, failedTestPaths(results));
const args = dotnetTestType.commandLine(rerun!.params!)!.args;
expect(args[args.indexOf("--filter") + 1]!.split("|").sort()).toEqual([
"FullyQualifiedName=Sample.CalculatorTests.Fails",
"FullyQualifiedName=Sample.CalculatorTests.Parameterized",
]);
},
);
it.each([
"executor://xunit/VsTestRunner2/netcoreapp",
"executor://nunit3testexecutor/",
"executor://mstestadapter/v2",
])("reads outcomes, parameterized names, duration and source from %s", (adapter) => {
const results = new TestResults();
results.apply(trxEvents(report(adapter)));
expect(results.counts()).toEqual({ passed: 1, failed: 1, skipped: 1, running: 0, total: 3 });
const failed = results.firstFailure()!;
expect(failed.name).toBe("adds (2)");
expect(failed.durationMs).toBe(62500);
expect(failed.output).toContain("Expected <3>");
expect(failed.output).toContain("captured output");
expect(failed.location).toEqual({ path: "C:\\My Project\\Tests.cs", line: 12, resolve: "path" });
});
it("keeps assemblies and target frameworks separate", () => {
const results = new TestResults();
for (const source of ["/a/bin/net9.0/Tests.dll", "/a/bin/net8.0/Tests.dll", "/b/bin/net9.0/Tests.dll"])
results.apply(trxEvents(report("adapter", source)));
expect(results.roots).toHaveLength(3);
expect(results.counts().total).toBe(9);
});
it("rejects incomplete reports", () => {
expect(() => trxEvents("<TestRun>")).toThrow("Invalid TRX");
expect(() => trxEvents("<html/>")).toThrow("Invalid TRX");
});
it("inserts the logger before runsettings and reads only this invocation's announced paths", async () => {
const reports = new DotnetReports();
const launched = reports.launch({ ...config, args: ["test", "Tests.csproj", "--", "x=y"] });
expect(launched.args).toEqual([
"test",
"Tests.csproj",
"--logger",
`trx;LogFilePrefix=${reports.prefix}`,
"--",
"x=y",
]);
const windows = `C:\\My Project\\TestResults\\${reports.prefix}_net9.0.trx`;
const unix = `/tmp/My Project/${reports.prefix}_net8.0.trx`;
reports.feed("Results File: C:\\old.trx");
reports.feed(`结果文件: ${windows}`);
reports.feed(`Results File: ${windows}`);
reports.feed(`Results File: ${unix}`);
const read = vi.fn().mockResolvedValue(report("adapter"));
expect(await reports.read(read)).not.toHaveLength(0);
expect(read.mock.calls).toEqual([[windows], [unix]]);
});
it("detects typed configurations and direct dotnet test commands", () => {
expect(frameworkFor(config)).toBe("dotnet");
expect(frameworkFor({ ...config, kind: "shell", command: "C:\\Program Files\\dotnet\\dotnet.exe" })).toBe("dotnet");
expect(frameworkFor({ ...config, kind: "shell", args: ["build"] })).toBe("auto");
});
it("keeps a method filter through .NET command regeneration and preserves runsettings", async () => {
const results = new TestResults();
results.apply(trxEvents(report("adapter")));
const selected = await rerunFailedConfig(config, failedTestPaths(results));
expect(selected).not.toBeNull();
const line = dotnetTestType.commandLine(selected!.params!);
expect(line?.args).toEqual([
"test",
"Tests.csproj",
"--framework",
"net9.0",
"--no-restore",
"--filter",
"FullyQualifiedName=Sample.Tests.Adds",
"--",
"RunConfiguration.MaxCpuCount=1",
]);
expect(config.params?.programArgs).toContain("Category=Unit");
});
});
+95
Переглянути файл
@@ -0,0 +1,95 @@
import type { RunConfig } from "@/platform/ipc";
import type { TestEvent } from "./types";
/** Only this invocation's reports are read, including each target framework's report. */
export class DotnetReports {
readonly prefix = `openidea-${crypto.randomUUID()}`;
private readonly paths = new Set<string>();
private mtp = false;
launch(config: RunConfig): RunConfig {
this.mtp = config.params?.runner === "mtp" || config.params?.runner === "mtp-legacy";
if (this.mtp) {
if (config.params?.trx === "false") return config;
const args = [...config.args];
if (config.params?.runner === "mtp-legacy" && !args.includes("--")) args.push("--");
if (!args.includes("--report-trx")) args.push("--report-trx");
return { ...config, args };
}
const boundary = config.args.indexOf("--");
const at = boundary < 0 ? config.args.length : boundary;
return {
...config,
args: [...config.args.slice(0, at), "--logger", `trx;LogFilePrefix=${this.prefix}`, ...config.args.slice(at)],
};
}
feed(line: string) {
// Match the absolute report path, independently of VSTest's UI language.
const path = /(?:[A-Za-z]:[\\/]|\/).*\.trx\s*$/i.exec(line)?.[0].trim();
if (path && (this.mtp || path.replace(/\\/g, "/").split("/").at(-1)?.startsWith(this.prefix))) this.paths.add(path);
}
async read(readText: (path: string) => Promise<string>): Promise<TestEvent[]> {
const events: TestEvent[] = [];
for (const path of this.paths) events.push(...trxEvents(await readText(path)));
return events;
}
}
const elements = (node: Document | Element, name: string) => Array.from(node.getElementsByTagNameNS("*", name));
const attr = (node: Element, name: string) => node.getAttribute(name) ?? "";
export function trxEvents(xml: string): TestEvent[] {
const doc = new DOMParser().parseFromString(xml, "application/xml");
if (elements(doc, "parsererror").length || doc.documentElement.localName !== "TestRun")
throw new Error("Invalid TRX report");
const definitions = new Map(
elements(doc, "UnitTest").map((test) => [attr(test, "id"), elements(test, "TestMethod")[0]]),
);
const events: TestEvent[] = [];
for (const result of elements(doc, "UnitTestResult")) {
const method = definitions.get(attr(result, "testId"));
if (!method) continue;
const className = attr(method, "className").split(",")[0]!;
const name = attr(method, "name").replace(/\(.*$/s, "").trim();
const source = attr(method, "codeBase").replace(/\\/g, "/");
if (!className || !name || !source) continue;
const display = (attr(result, "testName") || name).replace(
new RegExp(`^${className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.`),
"",
);
const path = [source, className, name, ...(display === name ? [] : [display])];
const outcome = attr(result, "outcome");
const status =
outcome === "Passed" ? "passed" : /^(NotExecuted|Inconclusive|Pending)$/.test(outcome) ? "skipped" : "failed";
const duration = /^(\d+):(\d+):(\d+(?:\.\d+)?)$/.exec(attr(result, "duration"));
const durationMs = duration
? (Number(duration[1]) * 3600 + Number(duration[2]) * 60 + Number(duration[3])) * 1000
: undefined;
events.push({ type: "result", path, status, durationMs });
for (const tag of ["Message", "StackTrace", "StdOut", "StdErr"]) {
for (const node of elements(result, tag)) {
for (const line of (node.textContent ?? "").split(/\r?\n/).slice(0, 200)) {
events.push({ type: "output", path, line });
const location = /\bin (.+\.(?:cs|fs|vb)):(?:line )?(\d+)/.exec(line);
if (location)
events.push({
type: "location",
path,
location: { path: location[1]!, line: Number(location[2]), resolve: "path" },
});
}
}
}
}
return events;
}
/** A parameterized case reruns its method, using adapter-independent TRX method identities. */
export function dotnetFilter(paths: string[][]): string | null {
if (paths.some((path) => path.length < 3)) return null;
const names = [...new Set(paths.map((path) => `${path[1]}.${path[2]}`))];
if (!names.length) return null;
return names.map((name) => `FullyQualifiedName=${name.replace(/[\\()&|=!~]/g, "\\$&")}`).join("|");
}
+81
Переглянути файл
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="4f4d0116-da34-4a70-b983-520ff35f1ee3" name="user@host 2026-11-09 15:30:22" runUser="host\user" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-09T15:30:22.0343603+08:00" queuing="2026-11-09T15:30:22.0343604+08:00" start="2026-11-09T15:30:21.6172521+08:00" finish="2026-11-09T15:30:22.0381244+08:00" />
<TestSettings name="default" id="efc46603-0a80-4af5-84e8-99efe216b685">
<Deployment runDeploymentRoot="user_host_2026-11-09_15_30_22" />
</TestSettings>
<Results>
<UnitTestResult executionId="a6aaf02e-b817-4528-8a65-a90a48fabb66" testId="3b2113bb-4c9e-76f3-cc23-ab79647789d4" testName="Parameterized (1)" computerName="host" duration="00:00:00.0001506" startTime="2026-11-09T15:30:21.9384780+08:00" endTime="2026-11-09T15:30:21.9479967+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="a6aaf02e-b817-4528-8a65-a90a48fabb66" />
<UnitTestResult executionId="dc9367d9-629b-429e-b893-948d6bdc6f71" testId="c5910b1b-ff19-2b97-141e-56e6f6c34aae" testName="Skipped" computerName="host" startTime="2026-11-09T15:30:21.9385124+08:00" endTime="2026-11-09T15:30:21.9406618+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="NotExecuted" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="dc9367d9-629b-429e-b893-948d6bdc6f71">
<Output>
<ErrorInfo>
<Message>demonstration</Message>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="b8788804-9f8a-4cdd-81d6-d00dcf77c3dc" testId="7d19e91c-5580-3412-cbca-804c6c9aad48" testName="Fails" computerName="host" duration="00:00:00.0242319" startTime="2026-11-09T15:30:21.9384945+08:00" endTime="2026-11-09T15:30:21.9692535+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="b8788804-9f8a-4cdd-81d6-d00dcf77c3dc">
<Output>
<ErrorInfo>
<Message>Assert.AreEqual failed. Expected:&lt;3&gt;. Actual:&lt;2&gt;. </Message>
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Mstest\Test1.cs:line 5&#xD;
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)&#xD;
</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="7d898d82-c3eb-4c65-b0b4-d79e5a456e6e" testId="82759888-07e6-a8c5-f14e-7d93542bd7f7" testName="Parameterized (2)" computerName="host" duration="00:00:00.0225438" startTime="2026-11-09T15:30:21.9384879+08:00" endTime="2026-11-09T15:30:21.9692510+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="7d898d82-c3eb-4c65-b0b4-d79e5a456e6e">
<Output>
<ErrorInfo>
<Message>Assert.AreEqual failed. Expected:&lt;1&gt;. Actual:&lt;2&gt;. </Message>
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Mstest\Test1.cs:line 7&#xD;
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)&#xD;
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)&#xD;
</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="6b8a19a3-e077-49cf-ae6a-ed568e077b51" testId="bfa6ea7d-3483-2c06-6e5a-cf49d98a6463" testName="Passes" computerName="host" duration="00:00:00.0018384" startTime="2026-11-09T15:30:21.9385027+08:00" endTime="2026-11-09T15:30:21.9479924+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6b8a19a3-e077-49cf-ae6a-ed568e077b51" />
</Results>
<TestDefinitions>
<UnitTest name="Fails" storage="C:\fixtures\mstest\bin\debug\net9.0\openidea.mstest.tests.dll" id="7d19e91c-5580-3412-cbca-804c6c9aad48">
<Execution id="b8788804-9f8a-4cdd-81d6-d00dcf77c3dc" />
<TestMethod codeBase="C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll" adapterTypeName="executor://mstestadapter/v2" className="Sample.CalculatorTests" name="Fails" />
</UnitTest>
<UnitTest name="Passes" storage="C:\fixtures\mstest\bin\debug\net9.0\openidea.mstest.tests.dll" id="bfa6ea7d-3483-2c06-6e5a-cf49d98a6463">
<Execution id="6b8a19a3-e077-49cf-ae6a-ed568e077b51" />
<TestMethod codeBase="C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll" adapterTypeName="executor://mstestadapter/v2" className="Sample.CalculatorTests" name="Passes" />
</UnitTest>
<UnitTest name="Parameterized (2)" storage="C:\fixtures\mstest\bin\debug\net9.0\openidea.mstest.tests.dll" id="82759888-07e6-a8c5-f14e-7d93542bd7f7">
<Execution id="7d898d82-c3eb-4c65-b0b4-d79e5a456e6e" />
<TestMethod codeBase="C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll" adapterTypeName="executor://mstestadapter/v2" className="Sample.CalculatorTests" name="Parameterized" />
</UnitTest>
<UnitTest name="Parameterized (1)" storage="C:\fixtures\mstest\bin\debug\net9.0\openidea.mstest.tests.dll" id="3b2113bb-4c9e-76f3-cc23-ab79647789d4">
<Execution id="a6aaf02e-b817-4528-8a65-a90a48fabb66" />
<TestMethod codeBase="C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll" adapterTypeName="executor://mstestadapter/v2" className="Sample.CalculatorTests" name="Parameterized" />
</UnitTest>
<UnitTest name="Skipped" storage="C:\fixtures\mstest\bin\debug\net9.0\openidea.mstest.tests.dll" id="c5910b1b-ff19-2b97-141e-56e6f6c34aae">
<Execution id="dc9367d9-629b-429e-b893-948d6bdc6f71" />
<TestMethod codeBase="C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll" adapterTypeName="executor://mstestadapter/v2" className="Sample.CalculatorTests" name="Skipped" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="3b2113bb-4c9e-76f3-cc23-ab79647789d4" executionId="a6aaf02e-b817-4528-8a65-a90a48fabb66" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="c5910b1b-ff19-2b97-141e-56e6f6c34aae" executionId="dc9367d9-629b-429e-b893-948d6bdc6f71" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="7d19e91c-5580-3412-cbca-804c6c9aad48" executionId="b8788804-9f8a-4cdd-81d6-d00dcf77c3dc" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="82759888-07e6-a8c5-f14e-7d93542bd7f7" executionId="7d898d82-c3eb-4c65-b0b4-d79e5a456e6e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="bfa6ea7d-3483-2c06-6e5a-cf49d98a6463" executionId="6b8a19a3-e077-49cf-ae6a-ed568e077b51" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Failed">
<Counters total="5" executed="4" passed="2" failed="2" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>Test Parallelization enabled for C:\fixtures\Mstest\bin\Debug\net9.0\OpenIdea.Mstest.Tests.dll (Workers: 32, Scope: MethodLevel)&#xD;
Test 'Skipped' was skipped in the test run.&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
+85
Переглянути файл
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="7ba330b2-4d2b-4252-bff2-c830b4419419" name="tester@testerS 2026-11-09 08:20:26.5318025" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-09T08:20:26.4131456Z" queuing="2026-11-09T08:20:26.4131456Z" start="2026-11-09T08:20:26.4131456Z" finish="2026-11-09T08:20:26.532145Z" />
<TestSettings name="default" id="b2c5ea89-0c8e-4215-bf21-255314713779">
<Deployment runDeploymentRoot="tester_testerS_2026-11-09_08_20_26.5318025" />
</TestSettings>
<Results>
<UnitTestResult executionId="ba20361c-0fd9-4459-91ba-dd3fce5113ee" testId="1a494a91-d607-8a83-ba91-8f7c369bcf4b" testName="Passes" computerName="testerS" duration="00:00:00.0039472" startTime="2026-11-09T08:20:26.4953435+00:00" endTime="2026-11-09T08:20:26.5104697+00:00" testType="13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B" outcome="Passed" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" relativeResultsDirectory="ba20361c-0fd9-4459-91ba-dd3fce5113ee" />
<UnitTestResult executionId="5a3992a8-81b9-4792-bba3-d11b23a7f817" testId="1b2eb6ed-b075-83ed-89ca-7b681a892bfe" testName="Fails" computerName="testerS" duration="00:00:00.0084826" startTime="2026-11-09T08:20:26.5132704+00:00" endTime="2026-11-09T08:20:26.5220913+00:00" testType="13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B" outcome="Failed" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" relativeResultsDirectory="5a3992a8-81b9-4792-bba3-d11b23a7f817">
<Output>
<ErrorInfo>
<Message>Assert.AreEqual failed. Expected:&lt;3&gt;. Actual:&lt;2&gt;. 'expected' expression: '3', 'actual' expression: '1 + 1'.</Message>
<StackTrace> at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowAssertAreEqualFailed(Object expected, Object actual, String userMessage) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 665
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual[T](T expected, T actual, IEqualityComparer`1 comparer, String message, String expectedExpression, String actualExpression) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 492
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual[T](T expected, T actual, String message, String expectedExpression, String actualExpression) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 405
at Sample.CalculatorTests.Fails() in C:\Fixture\Native\Test1.cs:line 5
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="75ac56ba-d7ad-462c-b299-71d9d0f33569" testId="1cb231b0-d0fa-81b3-ab3e-bc60f4639b8c" testName="Skipped" computerName="testerS" duration="00:00:00.0000000" startTime="2026-11-09T08:20:26.5222673+00:00" endTime="2026-11-09T08:20:26.5224786+00:00" testType="13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B" outcome="NotExecuted" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" relativeResultsDirectory="75ac56ba-d7ad-462c-b299-71d9d0f33569">
<Output>
<ErrorInfo>
<Message>demonstration</Message>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="1e66e9bf-4efd-4aad-9432-a2275cd88bfe" testId="139db31e-318e-8f04-91bc-5a0a5b61f929" testName="Parameterized (1)" computerName="testerS" duration="00:00:00.0000465" startTime="2026-11-09T08:20:26.5227177+00:00" endTime="2026-11-09T08:20:26.5240179+00:00" testType="13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B" outcome="Passed" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" relativeResultsDirectory="1e66e9bf-4efd-4aad-9432-a2275cd88bfe" />
<UnitTestResult executionId="edda94ba-80df-4d23-b753-c01400520c31" testId="1ab155a7-5304-8dd0-a469-414f028392a5" testName="Parameterized (2)" computerName="testerS" duration="00:00:00.0003644" startTime="2026-11-09T08:20:26.5240566+00:00" endTime="2026-11-09T08:20:26.5252897+00:00" testType="13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B" outcome="Failed" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" relativeResultsDirectory="edda94ba-80df-4d23-b753-c01400520c31">
<Output>
<ErrorInfo>
<Message>Assert.AreEqual failed. Expected:&lt;1&gt;. Actual:&lt;2&gt;. 'expected' expression: '1', 'actual' expression: 'value'.</Message>
<StackTrace> at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowAssertAreEqualFailed(Object expected, Object actual, String userMessage) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 665
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual[T](T expected, T actual, IEqualityComparer`1 comparer, String message, String expectedExpression, String actualExpression) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 492
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual[T](T expected, T actual, String message, String expectedExpression, String actualExpression) in /_/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs:line 405
at Sample.CalculatorTests.Parameterized(Int32 value) in C:\Fixture\Native\Test1.cs:line 7
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
</Results>
<TestDefinitions>
<UnitTest name="Passes" storage="C:\Fixture\native\bin\debug\net10.0\native.dll" id="1a494a91-d607-8a83-ba91-8f7c369bcf4b">
<Execution id="ba20361c-0fd9-4459-91ba-dd3fce5113ee" />
<TestMethod codeBase="C:\Fixture\Native\bin\Debug\net10.0\Native.dll" adapterTypeName="executor://MSTestExtension/4.0.0" className="Sample.CalculatorTests" name="Passes" />
</UnitTest>
<UnitTest name="Fails" storage="C:\Fixture\native\bin\debug\net10.0\native.dll" id="1b2eb6ed-b075-83ed-89ca-7b681a892bfe">
<Execution id="5a3992a8-81b9-4792-bba3-d11b23a7f817" />
<TestMethod codeBase="C:\Fixture\Native\bin\Debug\net10.0\Native.dll" adapterTypeName="executor://MSTestExtension/4.0.0" className="Sample.CalculatorTests" name="Fails" />
</UnitTest>
<UnitTest name="Skipped" storage="C:\Fixture\native\bin\debug\net10.0\native.dll" id="1cb231b0-d0fa-81b3-ab3e-bc60f4639b8c">
<Execution id="75ac56ba-d7ad-462c-b299-71d9d0f33569" />
<TestMethod codeBase="C:\Fixture\Native\bin\Debug\net10.0\Native.dll" adapterTypeName="executor://MSTestExtension/4.0.0" className="Sample.CalculatorTests" name="Skipped" />
</UnitTest>
<UnitTest name="Parameterized (1)" storage="C:\Fixture\native\bin\debug\net10.0\native.dll" id="139db31e-318e-8f04-91bc-5a0a5b61f929">
<Execution id="1e66e9bf-4efd-4aad-9432-a2275cd88bfe" />
<TestMethod codeBase="C:\Fixture\Native\bin\Debug\net10.0\Native.dll" adapterTypeName="executor://MSTestExtension/4.0.0" className="Sample.CalculatorTests" name="Parameterized (1)" />
</UnitTest>
<UnitTest name="Parameterized (2)" storage="C:\Fixture\native\bin\debug\net10.0\native.dll" id="1ab155a7-5304-8dd0-a469-414f028392a5">
<Execution id="edda94ba-80df-4d23-b753-c01400520c31" />
<TestMethod codeBase="C:\Fixture\Native\bin\Debug\net10.0\Native.dll" adapterTypeName="executor://MSTestExtension/4.0.0" className="Sample.CalculatorTests" name="Parameterized (2)" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="1a494a91-d607-8a83-ba91-8f7c369bcf4b" executionId="ba20361c-0fd9-4459-91ba-dd3fce5113ee" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
<TestEntry testId="1b2eb6ed-b075-83ed-89ca-7b681a892bfe" executionId="5a3992a8-81b9-4792-bba3-d11b23a7f817" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
<TestEntry testId="1cb231b0-d0fa-81b3-ab3e-bc60f4639b8c" executionId="75ac56ba-d7ad-462c-b299-71d9d0f33569" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
<TestEntry testId="139db31e-318e-8f04-91bc-5a0a5b61f929" executionId="1e66e9bf-4efd-4aad-9432-a2275cd88bfe" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
<TestEntry testId="1ab155a7-5304-8dd0-a469-414f028392a5" executionId="edda94ba-80df-4d23-b753-c01400520c31" testListId="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8C84FA94-04C1-424b-9868-57A2D4851A1D" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Failed">
<Counters total="5" executed="4" passed="2" failed="2" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="1" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<RunInfos>
<RunInfo computerName="testerS" outcome="Error" timestamp="2026-11-09T08:20:26.5377983">
<Text>Exit code indicates failure: '2'. Please refer to https://aka.ms/testingplatform/exitcodes for more information.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>
+96
Переглянути файл
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="90ac1e90-367b-4ef0-9626-f509a293b66f" name="user@host 2026-11-09 15:29:36" runUser="host\user" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-09T15:29:36.6222274+08:00" queuing="2026-11-09T15:29:36.6222276+08:00" start="2026-11-09T15:29:35.9112000+08:00" finish="2026-11-09T15:29:36.6258453+08:00" />
<TestSettings name="default" id="c4a8bf0e-5e08-4b32-a8fc-8bac196f28bf">
<Deployment runDeploymentRoot="user_host_2026-11-09_15_29_36" />
</TestSettings>
<Results>
<UnitTestResult executionId="3a5c8c54-b5a7-4b63-99c5-6aa007b68f06" testId="02a81555-5865-e7f8-12f4-5fc78b67a49c" testName="Parameterized(2)" computerName="host" duration="00:00:00.0003440" startTime="2026-11-09T15:29:36.5471392+08:00" endTime="2026-11-09T15:29:36.5474827+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="3a5c8c54-b5a7-4b63-99c5-6aa007b68f06">
<Output>
<ErrorInfo>
<Message> Assert.That(value, NUnit.Framework.Is.EqualTo(1))&#xD;
Expected: 1&#xD;
But was: 2&#xD;
</Message>
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Nunit\UnitTest1.cs:line 6&#xD;
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)&#xD;
1) at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Nunit\UnitTest1.cs:line 6&#xD;
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)&#xD;
</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="cd0ce076-d902-4bc5-8eb6-efac66f22f76" testId="4662c40d-3831-20b9-b93f-3d46f4c3745e" testName="Fails" computerName="host" duration="00:00:00.0197860" startTime="2026-11-09T15:29:36.5267595+08:00" endTime="2026-11-09T15:29:36.5465230+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="cd0ce076-d902-4bc5-8eb6-efac66f22f76">
<Output>
<ErrorInfo>
<Message> Assert.That(1 + 1, NUnit.Framework.Is.EqualTo(3))&#xD;
Expected: 3&#xD;
But was: 2&#xD;
</Message>
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Nunit\UnitTest1.cs:line 4&#xD;
1) at Sample.CalculatorTests.Fails() in C:\fixtures\Nunit\UnitTest1.cs:line 4&#xD;
</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="08316b31-c9f2-4e05-82d9-a6ac58f1bc7c" testId="2025c34d-a3f9-811e-8437-7eef0caa4776" testName="Passes" computerName="host" duration="00:00:00.0000550" startTime="2026-11-09T15:29:36.5479936+08:00" endTime="2026-11-09T15:29:36.5480483+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="08316b31-c9f2-4e05-82d9-a6ac58f1bc7c" />
<UnitTestResult executionId="41afb0d9-6a86-4345-b814-57bbe8e235a6" testId="c67f2006-84d4-f2e9-b3a1-3d8a46e07d6c" testName="Parameterized(1)" computerName="host" duration="00:00:00.0001110" startTime="2026-11-09T15:29:36.5470161+08:00" endTime="2026-11-09T15:29:36.5471267+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="41afb0d9-6a86-4345-b814-57bbe8e235a6" />
<UnitTestResult executionId="cab1aa72-f4f8-418d-b66a-353c4d14cbc9" testId="09a0e53e-a28a-d7cb-4241-5e576bbe92bd" testName="Skipped" computerName="host" duration="00:00:00.0000780" startTime="2026-11-09T15:29:36.5480515+08:00" endTime="2026-11-09T15:29:36.5481298+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="NotExecuted" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="cab1aa72-f4f8-418d-b66a-353c4d14cbc9">
<Output>
<StdOut>demonstration</StdOut>
<ErrorInfo>
<Message>demonstration</Message>
</ErrorInfo>
</Output>
</UnitTestResult>
</Results>
<TestDefinitions>
<UnitTest name="Skipped" storage="C:\fixtures\nunit\bin\debug\net9.0\openidea.nunit.tests.dll" id="09a0e53e-a28a-d7cb-4241-5e576bbe92bd">
<Execution id="cab1aa72-f4f8-418d-b66a-353c4d14cbc9" />
<TestMethod codeBase="C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll" adapterTypeName="executor://nunit3testexecutor/" className="Sample.CalculatorTests" name="Skipped" />
</UnitTest>
<UnitTest name="Parameterized(1)" storage="C:\fixtures\nunit\bin\debug\net9.0\openidea.nunit.tests.dll" id="c67f2006-84d4-f2e9-b3a1-3d8a46e07d6c">
<Execution id="41afb0d9-6a86-4345-b814-57bbe8e235a6" />
<TestMethod codeBase="C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll" adapterTypeName="executor://nunit3testexecutor/" className="Sample.CalculatorTests" name="Parameterized(1)" />
</UnitTest>
<UnitTest name="Parameterized(2)" storage="C:\fixtures\nunit\bin\debug\net9.0\openidea.nunit.tests.dll" id="02a81555-5865-e7f8-12f4-5fc78b67a49c">
<Execution id="3a5c8c54-b5a7-4b63-99c5-6aa007b68f06" />
<TestMethod codeBase="C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll" adapterTypeName="executor://nunit3testexecutor/" className="Sample.CalculatorTests" name="Parameterized(2)" />
</UnitTest>
<UnitTest name="Fails" storage="C:\fixtures\nunit\bin\debug\net9.0\openidea.nunit.tests.dll" id="4662c40d-3831-20b9-b93f-3d46f4c3745e">
<Execution id="cd0ce076-d902-4bc5-8eb6-efac66f22f76" />
<TestMethod codeBase="C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll" adapterTypeName="executor://nunit3testexecutor/" className="Sample.CalculatorTests" name="Fails" />
</UnitTest>
<UnitTest name="Passes" storage="C:\fixtures\nunit\bin\debug\net9.0\openidea.nunit.tests.dll" id="2025c34d-a3f9-811e-8437-7eef0caa4776">
<Execution id="08316b31-c9f2-4e05-82d9-a6ac58f1bc7c" />
<TestMethod codeBase="C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll" adapterTypeName="executor://nunit3testexecutor/" className="Sample.CalculatorTests" name="Passes" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="02a81555-5865-e7f8-12f4-5fc78b67a49c" executionId="3a5c8c54-b5a7-4b63-99c5-6aa007b68f06" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="4662c40d-3831-20b9-b93f-3d46f4c3745e" executionId="cd0ce076-d902-4bc5-8eb6-efac66f22f76" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="2025c34d-a3f9-811e-8437-7eef0caa4776" executionId="08316b31-c9f2-4e05-82d9-a6ac58f1bc7c" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="c67f2006-84d4-f2e9-b3a1-3d8a46e07d6c" executionId="41afb0d9-6a86-4345-b814-57bbe8e235a6" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="09a0e53e-a28a-d7cb-4241-5e576bbe92bd" executionId="cab1aa72-f4f8-418d-b66a-353c4d14cbc9" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Failed">
<Counters total="5" executed="4" passed="2" failed="2" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>NUnit Adapter 4.6.0.0: Test execution started&#xD;
Running all tests in C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll&#xD;
NUnit3TestExecutor discovered 5 of 5 NUnit test cases using Current Discovery mode, Non-Explicit run&#xD;
Skipped: demonstration&#xD;
NUnit Adapter 4.6.0.0: Test execution complete&#xD;
Test 'Skipped' was skipped in the test run.&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
+113
Переглянути файл
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="6730bf1d-c08d-4929-bb05-3e13f571e02b" name="user@host 2026-11-09 15:31:14" runUser="host\user" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-09T15:31:14.9191388+08:00" queuing="2026-11-09T15:31:14.9191389+08:00" start="2026-11-09T15:31:14.3506101+08:00" finish="2026-11-09T15:31:14.9238336+08:00" />
<TestSettings name="default" id="3df9757c-2b02-4a46-b1db-4ee1a8fa15dd">
<Deployment runDeploymentRoot="user_host_2026-11-09_15_31_14" />
</TestSettings>
<Results>
<UnitTestResult executionId="45bed8c3-6316-465f-9ebd-b1ce3e829452" testId="f52bc9c9-8df9-6fd9-934e-9b7c2f64f597" testName="Sample.CalculatorTests.Passes" computerName="host" duration="00:00:00.0000540" startTime="2026-11-09T15:31:14.8478668+08:00" endTime="2026-11-09T15:31:14.8478669+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="45bed8c3-6316-465f-9ebd-b1ce3e829452" />
<UnitTestResult executionId="b092dfdf-2c5c-48fd-b642-21df8c45636d" testId="7c64262f-2dfc-ba5e-c769-0c95cf182a76" testName="Sample.CalculatorTests.Fails" computerName="host" duration="00:00:00.0001171" startTime="2026-11-09T15:31:14.8483013+08:00" endTime="2026-11-09T15:31:14.8483014+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="b092dfdf-2c5c-48fd-b642-21df8c45636d">
<Output>
<ErrorInfo>
<Message>Assert.Equal() Failure: Values differ&#xD;
Expected: 3&#xD;
Actual: 2</Message>
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Xunit\UnitTest1.cs:line 4&#xD;
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="cd875b78-eac0-4818-8568-9f94369a9f82" testId="0352e892-15b1-279b-751c-4ef8fa3b78a4" testName="Sample.CalculatorTests.Parameterized(value: 1)" computerName="host" duration="00:00:00.0008224" startTime="2026-11-09T15:31:14.8477406+08:00" endTime="2026-11-09T15:31:14.8477407+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="cd875b78-eac0-4818-8568-9f94369a9f82" />
<UnitTestResult executionId="d881161e-8fea-4081-be74-367b5a2b1cdd" testId="dba96a25-d6a4-9ba0-5868-d87d247475d6" testName="Sample.CalculatorTests.Skipped" computerName="host" duration="00:00:00.0010000" startTime="2026-11-09T15:31:14.8488028+08:00" endTime="2026-11-09T15:31:14.8488029+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="NotExecuted" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="d881161e-8fea-4081-be74-367b5a2b1cdd">
<Output>
<ErrorInfo>
<Message>demonstration</Message>
</ErrorInfo>
</Output>
</UnitTestResult>
<UnitTestResult executionId="4f7a68de-fbed-4a0f-bb49-d33262b6b321" testId="342fa793-ffa3-5734-772a-79a8f289629f" testName="Sample.CalculatorTests.Parameterized(value: 2)" computerName="host" duration="00:00:00.0592671" startTime="2026-11-09T15:31:14.8455506+08:00" endTime="2026-11-09T15:31:14.8455607+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4f7a68de-fbed-4a0f-bb49-d33262b6b321">
<Output>
<ErrorInfo>
<Message>Assert.Equal() Failure: Values differ&#xD;
Expected: 1&#xD;
Actual: 2</Message>
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Xunit\UnitTest1.cs:line 6&#xD;
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
</Results>
<TestDefinitions>
<UnitTest name="Sample.CalculatorTests.Parameterized(value: 2)" storage="C:\fixtures\xunit\bin\debug\net9.0\openidea.xunit.tests.dll" id="342fa793-ffa3-5734-772a-79a8f289629f">
<Execution id="4f7a68de-fbed-4a0f-bb49-d33262b6b321" />
<TestMethod codeBase="C:\fixtures\Xunit\bin\Debug\net9.0\OpenIdea.Xunit.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="Sample.CalculatorTests" name="Parameterized" />
</UnitTest>
<UnitTest name="Sample.CalculatorTests.Skipped" storage="C:\fixtures\xunit\bin\debug\net9.0\openidea.xunit.tests.dll" id="dba96a25-d6a4-9ba0-5868-d87d247475d6">
<Execution id="d881161e-8fea-4081-be74-367b5a2b1cdd" />
<TestMethod codeBase="C:\fixtures\Xunit\bin\Debug\net9.0\OpenIdea.Xunit.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="Sample.CalculatorTests" name="Skipped" />
</UnitTest>
<UnitTest name="Sample.CalculatorTests.Passes" storage="C:\fixtures\xunit\bin\debug\net9.0\openidea.xunit.tests.dll" id="f52bc9c9-8df9-6fd9-934e-9b7c2f64f597">
<Execution id="45bed8c3-6316-465f-9ebd-b1ce3e829452" />
<TestMethod codeBase="C:\fixtures\Xunit\bin\Debug\net9.0\OpenIdea.Xunit.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="Sample.CalculatorTests" name="Passes" />
</UnitTest>
<UnitTest name="Sample.CalculatorTests.Parameterized(value: 1)" storage="C:\fixtures\xunit\bin\debug\net9.0\openidea.xunit.tests.dll" id="0352e892-15b1-279b-751c-4ef8fa3b78a4">
<Execution id="cd875b78-eac0-4818-8568-9f94369a9f82" />
<TestMethod codeBase="C:\fixtures\Xunit\bin\Debug\net9.0\OpenIdea.Xunit.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="Sample.CalculatorTests" name="Parameterized" />
</UnitTest>
<UnitTest name="Sample.CalculatorTests.Fails" storage="C:\fixtures\xunit\bin\debug\net9.0\openidea.xunit.tests.dll" id="7c64262f-2dfc-ba5e-c769-0c95cf182a76">
<Execution id="b092dfdf-2c5c-48fd-b642-21df8c45636d" />
<TestMethod codeBase="C:\fixtures\Xunit\bin\Debug\net9.0\OpenIdea.Xunit.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="Sample.CalculatorTests" name="Fails" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="f52bc9c9-8df9-6fd9-934e-9b7c2f64f597" executionId="45bed8c3-6316-465f-9ebd-b1ce3e829452" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="7c64262f-2dfc-ba5e-c769-0c95cf182a76" executionId="b092dfdf-2c5c-48fd-b642-21df8c45636d" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="0352e892-15b1-279b-751c-4ef8fa3b78a4" executionId="cd875b78-eac0-4818-8568-9f94369a9f82" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="dba96a25-d6a4-9ba0-5868-d87d247475d6" executionId="d881161e-8fea-4081-be74-367b5a2b1cdd" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="342fa793-ffa3-5734-772a-79a8f289629f" executionId="4f7a68de-fbed-4a0f-bb49-d33262b6b321" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Failed">
<Counters total="5" executed="4" passed="2" failed="2" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.17)&#xD;
[xUnit.net 00:00:00.05] Discovering: OpenIdea.Xunit.Tests&#xD;
[xUnit.net 00:00:00.08] Discovered: OpenIdea.Xunit.Tests&#xD;
[xUnit.net 00:00:00.08] Starting: OpenIdea.Xunit.Tests&#xD;
[xUnit.net 00:00:00.17] Assert.Equal() Failure: Values differ&#xD;
[xUnit.net 00:00:00.17] Expected: 1&#xD;
[xUnit.net 00:00:00.17] Actual: 2&#xD;
[xUnit.net 00:00:00.17] Stack Trace:&#xD;
[xUnit.net 00:00:00.17] C:\fixtures\Xunit\UnitTest1.cs(6,0): at Sample.CalculatorTests.Parameterized(Int32 value)&#xD;
[xUnit.net 00:00:00.17] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
[xUnit.net 00:00:00.17] at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)&#xD;
[xUnit.net 00:00:00.17] Assert.Equal() Failure: Values differ&#xD;
[xUnit.net 00:00:00.17] Expected: 3&#xD;
[xUnit.net 00:00:00.17] Actual: 2&#xD;
[xUnit.net 00:00:00.17] Stack Trace:&#xD;
[xUnit.net 00:00:00.17] C:\fixtures\Xunit\UnitTest1.cs(4,0): at Sample.CalculatorTests.Fails()&#xD;
[xUnit.net 00:00:00.17] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
[xUnit.net 00:00:00.17] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)&#xD;
[xUnit.net 00:00:00.17] demonstration&#xD;
[xUnit.net 00:00:00.17] Finished: OpenIdea.Xunit.Tests&#xD;
Test 'Sample.CalculatorTests.Skipped' was skipped in the test run.&#xD;
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="host" outcome="Error" timestamp="2026-11-09T15:31:14.8441178+08:00">
<Text>[xUnit.net 00:00:00.17] Sample.CalculatorTests.Parameterized(value: 2) [FAIL]</Text>
</RunInfo>
<RunInfo computerName="host" outcome="Error" timestamp="2026-11-09T15:31:14.8482465+08:00">
<Text>[xUnit.net 00:00:00.17] Sample.CalculatorTests.Fails [FAIL]</Text>
</RunInfo>
<RunInfo computerName="host" outcome="Warning" timestamp="2026-11-09T15:31:14.8487414+08:00">
<Text>[xUnit.net 00:00:00.17] Sample.CalculatorTests.Skipped [SKIP]</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>
+6 -1
Переглянути файл
@@ -23,11 +23,13 @@ export function combinedParser(parsers: TestOutputParser[]): TestOutputParser {
};
}
export type Framework = "cargo" | "js" | "pytest" | "unittest" | "phpunit" | "go" | "jvm" | "ruby" | "auto";
export type Framework = "cargo" | "js" | "pytest" | "unittest" | "phpunit" | "go" | "jvm" | "ruby" | "dotnet" | "auto";
export function frameworkFor(config: Pick<RunConfig, "kind" | "command" | "args">): Framework {
const cmd = [config.command, ...config.args].join(" ");
switch (config.kind) {
case "dotnet-test":
return "dotnet";
case "cargo":
return "cargo";
case "npm":
@@ -53,6 +55,7 @@ export function frameworkFor(config: Pick<RunConfig, "kind" | "command" | "args"
case "rspec":
return "ruby";
}
if (/(^|[/\\])dotnet(?:\.exe)?$/i.test(config.command) && config.args[0] === "test") return "dotnet";
if (/\bcargo\b/.test(cmd)) return "cargo";
if (/\b(jest|vitest|npm|pnpm|yarn|bun|npx|deno)\b/.test(cmd)) return "js";
if (/\bpytest\b/.test(cmd)) return "pytest";
@@ -110,6 +113,8 @@ export function withTestReporting<T extends Pick<RunConfig, "kind" | "command" |
export function createTestParser(config: Pick<RunConfig, "kind" | "command" | "args">): TestOutputParser {
switch (frameworkFor(config)) {
case "dotnet":
return { feed: () => [] };
case "cargo":
return cargoParser();
case "js":
+27 -1
Переглянути файл
@@ -5,11 +5,12 @@
import { selectPluginTests } from "@/workbench/contributions";
import { phpunitArgs, phpunitFilter } from "./phpunit";
import type { RunConfig } from "@/platform/ipc";
import { formatArgs } from "../configs";
import { formatArgs, parseArgs } from "../configs";
import { appendScriptArgs } from "../scriptArgs";
import { djangoTestsOf } from "../djangoTypes";
import { goTestParts, withoutGoSelection } from "../goArguments";
import { frameworkFor } from "./index";
import { dotnetFilter } from "./dotnet";
import { nodeStatus, type TestNode, type TestResults } from "./results";
/** Paths of failed tests: failed leaves, plus failed suites without any failed children reported. */
@@ -67,6 +68,11 @@ function cargoArgs(base: string[], paths: string[][]): string[] | null {
return [...before, "--", ...rest, ...names, "--exact"];
}
function dotnetArgs(base: string[], filter: string): string[] {
const { before, after } = splitDashDash(base);
return [...dropFlag(before, ["--filter"]), "--filter", filter, ...(after ? ["--", ...after] : [])];
}
function jsArgs(config: RunConfig, paths: string[][]): string[] | null {
const base = config.args;
const files = unique(paths.map((p) => p[0]!).filter(Boolean));
@@ -202,6 +208,26 @@ function builtinTestsConfig(config: RunConfig, paths: string[][], name: string):
if (!paths.length) return null;
let args: string[] | null;
switch (frameworkFor(config)) {
case "dotnet": {
const filter = dotnetFilter(paths);
if (!filter) return null;
args = dotnetArgs(config.args, filter);
if (config.kind === "dotnet-test") {
return {
...config,
args,
name,
detected: true,
env: { ...config.env },
params: {
...config.params,
testMethods: JSON.stringify([...new Set(paths.map((p) => `${p[1]}.${p[2]}`))]),
programArgs: formatArgs(dotnetArgs(parseArgs(config.params?.programArgs || ""), filter)),
},
};
}
break;
}
case "cargo":
args = cargoArgs(config.args, paths);
break;