дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
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
Цей коміт міститься в:
1 джерело
dfa610beb0
коміт
5a48beab07
26 files changed
+2242
-13
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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", "");
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ?? {}) };
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 <3></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");
|
||||
});
|
||||
});
|
||||
@@ -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("|");
|
||||
}
|
||||
@@ -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:<3>. Actual:<2>. </Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Mstest\Test1.cs:line 5
|
||||
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
|
||||
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
|
||||
</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:<1>. Actual:<2>. </Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Mstest\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>
|
||||
<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)
|
||||
Test 'Skipped' was skipped in the test run.
|
||||
</StdOut>
|
||||
</Output>
|
||||
</ResultSummary>
|
||||
</TestRun>
|
||||
@@ -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:<3>. Actual:<2>. '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:<1>. Actual:<2>. '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>
|
||||
@@ -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))
|
||||
Expected: 1
|
||||
But was: 2
|
||||
</Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Nunit\UnitTest1.cs:line 6
|
||||
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)
|
||||
|
||||
1) at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Nunit\UnitTest1.cs:line 6
|
||||
at InvokeStub_CalculatorTests.Parameterized(Object, Span`1)
|
||||
|
||||
</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))
|
||||
Expected: 3
|
||||
But was: 2
|
||||
</Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Nunit\UnitTest1.cs:line 4
|
||||
|
||||
1) at Sample.CalculatorTests.Fails() in C:\fixtures\Nunit\UnitTest1.cs:line 4
|
||||
|
||||
</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
|
||||
Running all tests in C:\fixtures\Nunit\bin\Debug\net9.0\OpenIdea.Nunit.Tests.dll
|
||||
NUnit3TestExecutor discovered 5 of 5 NUnit test cases using Current Discovery mode, Non-Explicit run
|
||||
Skipped: demonstration
|
||||
NUnit Adapter 4.6.0.0: Test execution complete
|
||||
Test 'Skipped' was skipped in the test run.
|
||||
</StdOut>
|
||||
</Output>
|
||||
</ResultSummary>
|
||||
</TestRun>
|
||||
@@ -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
|
||||
Expected: 3
|
||||
Actual: 2</Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Fails() in C:\fixtures\Xunit\UnitTest1.cs:line 4
|
||||
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
|
||||
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
|
||||
Expected: 1
|
||||
Actual: 2</Message>
|
||||
<StackTrace> at Sample.CalculatorTests.Parameterized(Int32 value) in C:\fixtures\Xunit\UnitTest1.cs:line 6
|
||||
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
|
||||
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)
|
||||
[xUnit.net 00:00:00.05] Discovering: OpenIdea.Xunit.Tests
|
||||
[xUnit.net 00:00:00.08] Discovered: OpenIdea.Xunit.Tests
|
||||
[xUnit.net 00:00:00.08] Starting: OpenIdea.Xunit.Tests
|
||||
[xUnit.net 00:00:00.17] Assert.Equal() Failure: Values differ
|
||||
[xUnit.net 00:00:00.17] Expected: 1
|
||||
[xUnit.net 00:00:00.17] Actual: 2
|
||||
[xUnit.net 00:00:00.17] Stack Trace:
|
||||
[xUnit.net 00:00:00.17] C:\fixtures\Xunit\UnitTest1.cs(6,0): at Sample.CalculatorTests.Parameterized(Int32 value)
|
||||
[xUnit.net 00:00:00.17] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
|
||||
[xUnit.net 00:00:00.17] at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
|
||||
[xUnit.net 00:00:00.17] Assert.Equal() Failure: Values differ
|
||||
[xUnit.net 00:00:00.17] Expected: 3
|
||||
[xUnit.net 00:00:00.17] Actual: 2
|
||||
[xUnit.net 00:00:00.17] Stack Trace:
|
||||
[xUnit.net 00:00:00.17] C:\fixtures\Xunit\UnitTest1.cs(4,0): at Sample.CalculatorTests.Fails()
|
||||
[xUnit.net 00:00:00.17] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
|
||||
[xUnit.net 00:00:00.17] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
|
||||
[xUnit.net 00:00:00.17] demonstration
|
||||
[xUnit.net 00:00:00.17] Finished: OpenIdea.Xunit.Tests
|
||||
Test 'Sample.CalculatorTests.Skipped' was skipped in the test run.
|
||||
</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>
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Заблокувати користувача