дзеркало
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;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { solutionPaths, jsonOutput, dotnetTestArgs, globalRunner, type DotnetProject } from "./dotnet";
|
||||
import { dotnetTestType } from "@/features/run/dotnetTypes";
|
||||
import { DotnetReports } from "@/features/run/testing/dotnet";
|
||||
|
||||
const project: DotnetProject = {
|
||||
path: "C:/My Repo/Tests.csproj",
|
||||
name: "Tests",
|
||||
frameworks: ["net9.0"],
|
||||
configurations: ["Debug", "Release"],
|
||||
references: [],
|
||||
packages: [],
|
||||
test: true,
|
||||
runner: "vstest",
|
||||
adapter: "mstest",
|
||||
trx: true,
|
||||
};
|
||||
describe(".NET workspace formats and runners", () => {
|
||||
it("reads global.json comments and trailing commas without changing string values", () => {
|
||||
expect(
|
||||
globalRunner(
|
||||
'{ // comment\n "test": { "runner": "Microsoft.Testing.Platform", }, "unused": "https://example.com/,}", }',
|
||||
),
|
||||
).toBe("mtp");
|
||||
});
|
||||
it("keeps SDK options before the legacy MTP argument boundary", () => {
|
||||
const line = dotnetTestType.commandLine({
|
||||
project: project.path,
|
||||
runner: "mtp-legacy",
|
||||
programArgs: "--no-build --filter Name=One",
|
||||
testMethods: '["A.Tests.Two"]',
|
||||
})!;
|
||||
expect(line.args.indexOf("--no-build")).toBeLessThan(line.args.indexOf("--"));
|
||||
expect(line.args.slice(line.args.indexOf("--") + 1)).toEqual(["--filter", "FullyQualifiedName=A.Tests.Two"]);
|
||||
});
|
||||
it("resolves solution projects, XML entities and external references", () => {
|
||||
expect(
|
||||
solutionPaths(
|
||||
'Project("id") = "Tests", "..\\outside\\Tests.csproj", "id"\nProject("id") = "Folder", "Folder", "id"',
|
||||
false,
|
||||
"C:/repo",
|
||||
),
|
||||
).toEqual(["C:/outside/Tests.csproj"]);
|
||||
expect(
|
||||
solutionPaths(
|
||||
'<Solution><Folder Name="src"><Project Path="A&B/App.csproj" /></Folder></Solution>',
|
||||
true,
|
||||
"/repo",
|
||||
),
|
||||
).toEqual(["/repo/A&B/App.csproj"]);
|
||||
});
|
||||
it("reads JSON despite SDK banners", () =>
|
||||
expect(jsonOutput('Welcome\n{"Properties":{"TargetFramework":"net9.0"}}\n')).toEqual({
|
||||
Properties: { TargetFramework: "net9.0" },
|
||||
}));
|
||||
it.each(["vstest", "mtp", "mtp-legacy"] as const)(
|
||||
"keeps %s discovery arguments in the correct command layer",
|
||||
(runner) => {
|
||||
const args = dotnetTestArgs({ ...project, runner }, "net9.0", ["--list-tests"], "Release");
|
||||
expect(args.includes("--project")).toBe(runner === "mtp");
|
||||
expect(args.includes("--")).toBe(runner === "mtp-legacy");
|
||||
expect(args.at(-1)).toBe("--list-tests");
|
||||
},
|
||||
);
|
||||
it.each(["mtp", "mtp-legacy"])("launches %s with TRX and adapter-native selectors", (runner) => {
|
||||
const p = {
|
||||
project: project.path,
|
||||
framework: "net9.0",
|
||||
runner,
|
||||
adapter: "xunit",
|
||||
testMethods: JSON.stringify(["A.Tests.One", "A.Tests.Two"]),
|
||||
};
|
||||
const line = dotnetTestType.commandLine(p)!;
|
||||
expect(line.args).toContain("--filter-method");
|
||||
expect(line.args).not.toContain("--filter");
|
||||
const reporting = new DotnetReports().launch({
|
||||
id: "test",
|
||||
name: "test",
|
||||
kind: "dotnet-test",
|
||||
...line,
|
||||
params: p,
|
||||
cwd: null,
|
||||
env: {},
|
||||
isTest: true,
|
||||
detected: true,
|
||||
debugAdapter: null,
|
||||
});
|
||||
expect(reporting.args).toContain("--report-trx");
|
||||
expect(reporting.args).not.toContain("--logger");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import { call, commands, ideEventsReady, onIdeEvent, type OutputChunk, type RunConfig, type RunId } from "./ipc";
|
||||
import { dirname, useProject } from "./project";
|
||||
import { tx } from "./i18n";
|
||||
|
||||
export type DotnetRunner = "vstest" | "mtp" | "mtp-legacy";
|
||||
export interface DotnetProject {
|
||||
path: string;
|
||||
name: string;
|
||||
frameworks: string[];
|
||||
configurations: string[];
|
||||
references: string[];
|
||||
packages: Array<{ id: string; version: string }>;
|
||||
test: boolean;
|
||||
runner: DotnetRunner;
|
||||
adapter: string;
|
||||
trx: boolean;
|
||||
platformVersion?: string;
|
||||
}
|
||||
export interface DotnetSolution {
|
||||
path: string;
|
||||
projects: string[];
|
||||
}
|
||||
export interface DotnetWorkspace {
|
||||
projects: DotnetProject[];
|
||||
solutions: DotnetSolution[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export function dotnetConfig(args: string[], cwd: string): RunConfig {
|
||||
return {
|
||||
id: `dotnet-task:${crypto.randomUUID()}`,
|
||||
name: `dotnet ${args[0]}`,
|
||||
kind: "shell",
|
||||
command: "dotnet",
|
||||
args,
|
||||
cwd,
|
||||
env: { DOTNET_CLI_UI_LANGUAGE: "en" },
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
emulateTerminal: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Uses the same trusted, remote-aware process host and cancellation as the Run window. */
|
||||
export async function dotnet(args: string[], cwd: string, signal?: AbortSignal): Promise<string> {
|
||||
signal?.throwIfAborted();
|
||||
await ideEventsReady();
|
||||
let id: RunId | undefined;
|
||||
let output = "";
|
||||
let overflow = false;
|
||||
const exits = new Map<RunId, number | null>();
|
||||
let finish!: (code: number | null) => void;
|
||||
const exited = new Promise<number | null>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
const off = onIdeEvent("runExited", (event) => {
|
||||
if (id === undefined) exits.set(event.run, event.code);
|
||||
else if (event.run === id) finish(event.code);
|
||||
});
|
||||
const stop = () => {
|
||||
if (id !== undefined) void call(commands.runStop(id)).catch(() => {});
|
||||
};
|
||||
const channel = new Channel<OutputChunk>();
|
||||
channel.onmessage = (chunk) => {
|
||||
if (output.length + chunk.text.length > 8_000_000) {
|
||||
overflow = true;
|
||||
stop();
|
||||
} else output += chunk.text;
|
||||
};
|
||||
signal?.addEventListener("abort", stop);
|
||||
try {
|
||||
const info = await call(commands.runStart(dotnetConfig(args, cwd), channel));
|
||||
id = info.id;
|
||||
if (signal?.aborted || overflow) stop();
|
||||
if (exits.has(id)) finish(exits.get(id)!);
|
||||
const code = await exited;
|
||||
signal?.throwIfAborted();
|
||||
if (overflow) throw new Error(tx(".NET output exceeded the size limit"));
|
||||
if (code !== 0) throw new Error(output.trim() || tx`dotnet exited with code ${code}`);
|
||||
return output;
|
||||
} finally {
|
||||
off();
|
||||
signal?.removeEventListener("abort", stop);
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonOutput<T = Record<string, unknown>>(text: string): T {
|
||||
const start = text.indexOf("{");
|
||||
const end = text.lastIndexOf("}");
|
||||
return JSON.parse(text.slice(start, end + 1));
|
||||
}
|
||||
|
||||
function absolute(base: string, relative: string): string {
|
||||
const path = relative.replace(/\\/g, "/");
|
||||
if (/^(?:[A-Za-z]:\/|\/)/.test(path)) return path;
|
||||
const parts = base.replace(/\\/g, "/").split("/");
|
||||
for (const part of path.split("/")) {
|
||||
if (part === "..") parts.pop();
|
||||
else if (part && part !== ".") parts.push(part);
|
||||
}
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
export function solutionPaths(text: string, xml: boolean, dir: string): string[] {
|
||||
const names = xml
|
||||
? Array.from(new DOMParser().parseFromString(text, "application/xml").getElementsByTagName("Project")).map(
|
||||
(p) => p.getAttribute("Path") || "",
|
||||
)
|
||||
: Array.from(text.matchAll(/^Project\([^\r\n]+?=\s*"[^"]*",\s*"([^"]+)"/gm), (m) => m[1]!);
|
||||
return [...new Set(names.filter((p) => /\.(?:cs|fs|vb)proj$/i.test(p)).map((p) => absolute(dir, p)))];
|
||||
}
|
||||
|
||||
export function globalRunner(text: string): DotnetRunner {
|
||||
const json = JSON.parse(
|
||||
text.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|,\s*(?=[}\]])/g, (token) =>
|
||||
token.startsWith('"') ? token : "",
|
||||
),
|
||||
);
|
||||
return json.test?.runner === "Microsoft.Testing.Platform" ? "mtp" : "vstest";
|
||||
}
|
||||
|
||||
export async function runnerMode(dir: string): Promise<DotnetRunner> {
|
||||
for (let current = dir; current;) {
|
||||
try {
|
||||
return globalRunner(await call(commands.fsReadText(`${current}/global.json`)));
|
||||
} catch {
|
||||
/* No global.json at this level. */
|
||||
}
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
return "vstest";
|
||||
}
|
||||
|
||||
export async function evaluateProject(
|
||||
path: string,
|
||||
configuration = "Debug",
|
||||
framework = "",
|
||||
signal?: AbortSignal,
|
||||
): Promise<DotnetProject> {
|
||||
const data = jsonOutput<{ Properties: Record<string, string>; Items: Record<string, Array<Record<string, string>>> }>(
|
||||
await dotnet(
|
||||
[
|
||||
"msbuild",
|
||||
path,
|
||||
"-nologo",
|
||||
`-p:Configuration=${configuration}`,
|
||||
...(framework ? [`-p:TargetFramework=${framework}`] : []),
|
||||
"-getProperty:MSBuildProjectName,TargetFramework,TargetFrameworks,Configurations,IsTestProject,IsTestingPlatformApplication,TestingPlatformDotnetTestSupport,EnableMSTestRunner,ProjectAssetsFile",
|
||||
"-getItem:ProjectReference,PackageReference",
|
||||
],
|
||||
dirname(path),
|
||||
signal,
|
||||
),
|
||||
);
|
||||
const p = data.Properties ?? {};
|
||||
if (!framework && p.TargetFrameworks)
|
||||
return evaluateProject(path, configuration, p.TargetFrameworks.split(";")[0], signal);
|
||||
const packages = (data.Items?.PackageReference ?? []).map((x) => ({
|
||||
id: String(x.Identity),
|
||||
version: String(x.Version ?? ""),
|
||||
}));
|
||||
const mode = await runnerMode(dirname(path));
|
||||
const mtp =
|
||||
[p.IsTestingPlatformApplication, p.TestingPlatformDotnetTestSupport, p.EnableMSTestRunner].includes("true") ||
|
||||
packages.some((x) => /xunit\.v3\.mtp/i.test(x.id));
|
||||
let trx = packages.some((x) => /^Microsoft.Testing.Extensions.TrxReport$/i.test(x.id));
|
||||
let platformVersion: string | undefined;
|
||||
if (p.ProjectAssetsFile) {
|
||||
try {
|
||||
const assets = JSON.parse(await call(commands.fsReadText(p.ProjectAssetsFile)));
|
||||
const libraries = Object.keys(assets.libraries ?? {});
|
||||
trx ||= libraries.some((key) => /^Microsoft.Testing.Extensions.TrxReport\//i.test(key));
|
||||
platformVersion = libraries.find((key) => /^Microsoft.Testing.Platform\//i.test(key))?.split("/")[1];
|
||||
} catch {
|
||||
/* Not restored yet. */
|
||||
}
|
||||
}
|
||||
return {
|
||||
path,
|
||||
name: p.MSBuildProjectName || path.split("/").at(-1) || path,
|
||||
frameworks: (p.TargetFrameworks || p.TargetFramework || "").split(";").filter(Boolean),
|
||||
configurations: (p.Configurations || "Debug;Release").split(";"),
|
||||
references: (data.Items?.ProjectReference ?? []).map((x) => absolute(dirname(path), x.Identity!)),
|
||||
packages,
|
||||
test:
|
||||
p.IsTestProject === "true" ||
|
||||
mtp ||
|
||||
(p.IsTestProject !== "false" &&
|
||||
packages.some((x) => /^(?:Microsoft.NET.Test.Sdk|MSTest|NUnit|xunit(?:\.v3(?:\.mtp-v[12])?)?)$/i.test(x.id))),
|
||||
runner: mode === "mtp" ? mode : mtp ? "mtp-legacy" : "vstest",
|
||||
adapter: packages.some((x) => /^xunit/i.test(x.id))
|
||||
? "xunit"
|
||||
: packages.some((x) => /^nunit/i.test(x.id))
|
||||
? "nunit"
|
||||
: "mstest",
|
||||
trx,
|
||||
platformVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export async function dotnetWorkspace(root: string, signal?: AbortSignal): Promise<DotnetWorkspace> {
|
||||
const paths = new Set<string>();
|
||||
const solutions: DotnetSolution[] = [];
|
||||
const queue = [root];
|
||||
while (queue.length) {
|
||||
signal?.throwIfAborted();
|
||||
const dir = queue.shift()!;
|
||||
for (const entry of await call(commands.fsReadDir(dir))) {
|
||||
if (
|
||||
entry.kind === "dir" &&
|
||||
!entry.excluded &&
|
||||
!/^(?:\.|bin$|obj$|node_modules$|target$|packages$)/.test(entry.name)
|
||||
)
|
||||
queue.push(entry.path);
|
||||
else if (/\.(?:cs|fs|vb)proj$/i.test(entry.name)) paths.add(entry.path);
|
||||
else if (/\.slnx?$/i.test(entry.name)) {
|
||||
const projects = solutionPaths(
|
||||
await call(commands.fsReadText(entry.path)),
|
||||
entry.name.endsWith(".slnx"),
|
||||
dirname(entry.path),
|
||||
);
|
||||
solutions.push({ path: entry.path, projects });
|
||||
projects.forEach((p) => paths.add(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
const projects: DotnetProject[] = [];
|
||||
const errors: string[] = [];
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const project = await evaluateProject(path, "Debug", "", signal);
|
||||
projects.push(project);
|
||||
project.references.forEach((reference) => paths.add(reference));
|
||||
} catch (e) {
|
||||
signal?.throwIfAborted();
|
||||
errors.push(`${path}: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
return { projects, solutions, errors };
|
||||
}
|
||||
|
||||
export function dotnetTestArgs(
|
||||
project: DotnetProject,
|
||||
framework: string,
|
||||
extra: string[] = [],
|
||||
configuration = "Debug",
|
||||
): string[] {
|
||||
return [
|
||||
"test",
|
||||
...(project.runner === "mtp" ? ["--project", project.path] : [project.path]),
|
||||
"--configuration",
|
||||
configuration,
|
||||
...(framework ? ["--framework", framework] : []),
|
||||
...(project.runner === "mtp-legacy"
|
||||
? [
|
||||
"-p:TestingPlatformCaptureOutput=false",
|
||||
"-p:TestingPlatformDotnetTestSupport=true",
|
||||
...(extra.length ? ["--"] : []),
|
||||
]
|
||||
: []),
|
||||
...extra,
|
||||
];
|
||||
}
|
||||
|
||||
export async function prepareDotnetTest(config: RunConfig): Promise<RunConfig> {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root || !config.params?.project) return config;
|
||||
let project = await evaluateProject(
|
||||
absolute(root, config.params.project),
|
||||
config.params.configuration,
|
||||
config.params.framework,
|
||||
);
|
||||
if (project.runner !== "vstest" && !project.platformVersion) {
|
||||
await dotnet(["restore", project.path], dirname(project.path));
|
||||
project = await evaluateProject(project.path, config.params.configuration, config.params.framework);
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
params: { ...config.params, runner: project.runner, adapter: project.adapter, trx: String(project.trx) },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
const host = vi.hoisted(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
listeners: new Set<(e: { run: number; code: number | null }) => void>(),
|
||||
}));
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
Channel: class {
|
||||
onmessage = (_chunk: unknown) => {};
|
||||
},
|
||||
}));
|
||||
vi.mock("./ipc", () => ({
|
||||
commands: { runStart: host.start, runStop: host.stop },
|
||||
call: <T>(promise: Promise<T>) => promise,
|
||||
ideEventsReady: () => Promise.resolve(),
|
||||
onIdeEvent: (_kind: string, fn: (e: { run: number; code: number | null }) => void) => {
|
||||
host.listeners.add(fn);
|
||||
return () => host.listeners.delete(fn);
|
||||
},
|
||||
}));
|
||||
vi.mock("./project", () => ({
|
||||
dirname: (p: string) => p.slice(0, p.lastIndexOf("/")),
|
||||
useProject: { getState: () => ({ project: null }) },
|
||||
}));
|
||||
import { dotnet } from "./dotnet";
|
||||
const exit = (run: number, code: number | null) => host.listeners.forEach((fn) => fn({ run, code }));
|
||||
afterEach(() => {
|
||||
host.start.mockReset();
|
||||
host.stop.mockReset();
|
||||
expect(host.listeners.size).toBe(0);
|
||||
});
|
||||
describe(".NET query lifecycle", () => {
|
||||
it("captures a fast exit before runStart resolves, ignoring another run", async () => {
|
||||
host.start.mockImplementation(async (_config, channel) => {
|
||||
channel.onmessage({ text: '{"ok":true}' });
|
||||
exit(99, 1);
|
||||
exit(7, 0);
|
||||
return { id: 7 };
|
||||
});
|
||||
expect(await dotnet(["msbuild"], "/repo")).toBe('{"ok":true}');
|
||||
});
|
||||
it("reports process failures and releases its event subscription", async () => {
|
||||
host.start.mockImplementation(async () => {
|
||||
exit(7, 1);
|
||||
return { id: 7 };
|
||||
});
|
||||
await expect(dotnet(["msbuild"], "/repo")).rejects.toThrow("dotnet exited with code 1");
|
||||
});
|
||||
it("stops a process cancelled while runStart is pending", async () => {
|
||||
const abort = new AbortController();
|
||||
host.start.mockImplementation(async () => {
|
||||
abort.abort();
|
||||
return { id: 7 };
|
||||
});
|
||||
host.stop.mockImplementation(async (id) => exit(id, null));
|
||||
await expect(dotnet(["msbuild"], "/repo", abort.signal)).rejects.toThrow();
|
||||
expect(host.stop).toHaveBeenCalledWith(7);
|
||||
});
|
||||
it("releases its subscription if starting the process fails", async () => {
|
||||
host.start.mockRejectedValue(new Error("SDK missing"));
|
||||
await expect(dotnet(["msbuild"], "/repo")).rejects.toThrow("SDK missing");
|
||||
});
|
||||
});
|
||||
@@ -681,5 +681,36 @@
|
||||
"Untagged Images": "Untagged Images",
|
||||
"Show Processes": "Show Processes",
|
||||
"Open Log in Editor": "Open Log in Editor",
|
||||
"Open Container Log in Editor": "Open Container Log in Editor"
|
||||
"Open Container Log in Editor": "Open Container Log in Editor",
|
||||
"Could not read .NET test results: {error}\n": "Could not read .NET test results: {error}\n",
|
||||
"Invalid TRX report": "Invalid TRX report",
|
||||
".NET Workspace": ".NET Workspace",
|
||||
"Reload .NET Workspace": "Reload .NET Workspace",
|
||||
"Cancel .NET Operation": "Cancel .NET Operation",
|
||||
"Select .NET Project": "Select .NET Project",
|
||||
"Build .NET Project": "Build .NET Project",
|
||||
"Restore .NET Project": "Restore .NET Project",
|
||||
"Clean .NET Project": "Clean .NET Project",
|
||||
"Refresh NuGet Packages": "Refresh NuGet Packages",
|
||||
"Search NuGet Packages": "Search NuGet Packages",
|
||||
"Change NuGet Package": "Change NuGet Package",
|
||||
"Discover .NET Tests": "Discover .NET Tests",
|
||||
"Run .NET Tests": "Run .NET Tests",
|
||||
"Install TRX reporting": "Install TRX reporting",
|
||||
"Project references": "Project references",
|
||||
"NuGet packages": "NuGet packages",
|
||||
"Refresh packages": "Refresh packages",
|
||||
"Search NuGet packages": "Search NuGet packages",
|
||||
"Install / Update": "Install / Update",
|
||||
"Versions": "Versions",
|
||||
"Transitive": "Transitive",
|
||||
"Run all tests": "Run all tests",
|
||||
"Install the TRX reporting extension to show MTP results in the test panel.": "Install the TRX reporting extension to show MTP results in the test panel.",
|
||||
".NET output exceeded the size limit": ".NET output exceeded the size limit",
|
||||
"Clean": "Clean",
|
||||
"Run test": "Run test",
|
||||
"Select a .NET project": "Select a .NET project",
|
||||
"Project evaluation failed": "Project evaluation failed",
|
||||
"dotnet exited with code {0}": "dotnet exited with code {0}",
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "Microsoft.Testing.Platform was not found in the restored project."
|
||||
}
|
||||
@@ -7021,5 +7021,35 @@
|
||||
"Untagged Images": "未标记的镜像",
|
||||
"Show Processes": "显示进程",
|
||||
"Open Log in Editor": "在编辑器中打开日志",
|
||||
"Open Container Log in Editor": "在编辑器中打开容器日志"
|
||||
"Open Container Log in Editor": "在编辑器中打开容器日志",
|
||||
"Could not read .NET test results: {error}\n": "无法读取 .NET 测试结果:{error}\n",
|
||||
"Invalid TRX report": "无效的 TRX 报告",
|
||||
".NET Workspace": ".NET 工作区",
|
||||
"Reload .NET Workspace": "重新加载 .NET 工作区",
|
||||
"Cancel .NET Operation": "取消 .NET 操作",
|
||||
"Select .NET Project": "选择 .NET 项目",
|
||||
"Build .NET Project": "构建 .NET 项目",
|
||||
"Restore .NET Project": "还原 .NET 项目",
|
||||
"Clean .NET Project": "清理 .NET 项目",
|
||||
"Refresh NuGet Packages": "刷新 NuGet 包",
|
||||
"Search NuGet Packages": "搜索 NuGet 包",
|
||||
"Change NuGet Package": "修改 NuGet 包",
|
||||
"Discover .NET Tests": "发现 .NET 测试",
|
||||
"Run .NET Tests": "运行 .NET 测试",
|
||||
"Install TRX reporting": "安装 TRX 报告支持",
|
||||
"Project references": "项目引用",
|
||||
"NuGet packages": "NuGet 包",
|
||||
"Refresh packages": "刷新包列表",
|
||||
"Search NuGet packages": "搜索 NuGet 包",
|
||||
"Install / Update": "安装 / 更新",
|
||||
"Versions": "版本",
|
||||
"Transitive": "间接依赖",
|
||||
"Run all tests": "运行所有测试",
|
||||
"Install the TRX reporting extension to show MTP results in the test panel.": "安装 TRX 报告扩展,以在测试面板中显示 MTP 结果。",
|
||||
".NET output exceeded the size limit": ".NET 输出超过大小限制",
|
||||
"Clean": "清理",
|
||||
"Run test": "运行测试",
|
||||
"Project evaluation failed": "项目求值失败",
|
||||
"dotnet exited with code {0}": "dotnet 已退出,退出码 {0}",
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "还原后的项目中未找到 Microsoft.Testing.Platform。"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача