Skip to content

Commit d49b9ab

Browse files
authored
Fix Allure metadata attribution in concurrent Vitest tests (#1471)
Co-authored-by: Dmitry Baev <baev@users.noreply.github.com>
1 parent 5628e04 commit d49b9ab

9 files changed

Lines changed: 284 additions & 52 deletions

File tree

packages/allure-vitest/README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,13 @@ Install Allure Report separately when you want to render the generated `allure-r
5454
5555
## Usage
5656
57-
Add next changes to your config file if you want to use vitest to run NodeJS tests only:
57+
Add the reporter to your config file if you want to use Vitest to run Node.js tests only:
5858
5959
```diff
6060
import { defineConfig } from "vitest/config";
6161
6262
export default defineConfig({
6363
test: {
64-
+ setupFiles: ["allure-vitest/setup"],
6564
reporters: [
6665
"default",
6766
"allure-vitest/reporter",
@@ -70,15 +69,17 @@ export default defineConfig({
7069
});
7170
```
7271
73-
In case if you want to use [vitest for browser testing](https://vitest.dev/guide/browser/) add next changes:
72+
The reporter registers the Allure setup module automatically. If your project already lists `allure-vitest/setup` in
73+
`setupFiles`, you can keep it for compatibility or remove it from the config.
74+
75+
If you want to use [Vitest for browser testing](https://vitest.dev/guide/browser/), add the browser provider config:
7476
7577
```diff
7678
import { defineConfig } from "vitest/config";
77-
+ import { commands } from "allure-vitest/browser"
79+
+ import { playwright } from "@vitest/browser-playwright";
7880
7981
export default defineConfig({
8082
test: {
81-
+ setupFiles: ["allure-vitest/browser/setup"],
8283
reporters: [
8384
"default",
8485
"allure-vitest/reporter",
@@ -91,13 +92,17 @@ export default defineConfig({
9192
instances: [
9293
{ browser: "chromium" },
9394
],
94-
+ commands: {
95-
+ ...commands,
96-
+ }
9795
},
9896
});
9997
```
10098
99+
The reporter also registers `allure-vitest/browser/setup` and the Allure browser command automatically when browser
100+
mode is enabled. If your project already lists them in the config, you can keep them for compatibility or remove them.
101+
102+
Browser mode does not have a stable async context primitive equivalent to Node.js `AsyncLocalStorage`, so Allure runtime
103+
API calls in `describe.concurrent` browser tests may still be attributed incorrectly after async boundaries. Prefer
104+
non-concurrent browser tests when using the context-free Allure runtime API.
105+
101106
### View the report
102107
103108
Use Allure Report 2:

packages/allure-vitest/src/reporter.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fileURLToPath } from "node:url";
2+
13
import { Stage, Status } from "allure-js-commons";
24
import type { RuntimeMessage } from "allure-js-commons/sdk";
35
import { getMessageAndTraceFromError, getStatusFromError } from "allure-js-commons/sdk";
@@ -16,12 +18,20 @@ import {
1618
md5,
1719
} from "allure-js-commons/sdk/reporter";
1820
import type { RunnerTask as Task } from "vitest";
19-
import type { TestModule } from "vitest/node";
21+
import type { TestModule, Vitest } from "vitest/node";
2022
import type { Reporter } from "vitest/reporters";
2123

24+
import { commands as allureBrowserCommands } from "./browser/index.js";
2225
import { takeGlobalRuntimeMessages } from "./runtime.js";
2326
import { getTestMetadata } from "./utils.js";
2427

28+
const setupModulePath = fileURLToPath(new URL("./setup.js", import.meta.url));
29+
30+
const browserSetupModulePath = fileURLToPath(new URL("./browser/setup.js", import.meta.url));
31+
32+
const normalizeSetupFilePath = (setupFilePath: string) =>
33+
setupFilePath.startsWith("file://") ? fileURLToPath(setupFilePath) : setupFilePath;
34+
2535
export default class AllureVitestReporter implements Reporter {
2636
private allureReporterRuntime?: ReporterRuntime;
2737
private config: ReporterConfig;
@@ -31,7 +41,9 @@ export default class AllureVitestReporter implements Reporter {
3141
this.config = config;
3242
}
3343

34-
onInit() {
44+
onInit(vitest: Vitest) {
45+
this.registerSetupFile(vitest);
46+
3547
const { listeners, resultsDir, ...config } = this.config;
3648

3749
this.allureReporterRuntime = new ReporterRuntime({
@@ -45,6 +57,28 @@ export default class AllureVitestReporter implements Reporter {
4557
this.globalRuntimeMessages = [];
4658
}
4759

60+
private registerSetupFile(vitest: Vitest) {
61+
for (const project of vitest.projects) {
62+
const setupFilePath = project.config.browser.enabled ? browserSetupModulePath : setupModulePath;
63+
64+
const hasSetupFile = project.config.setupFiles.some(
65+
(setupFile) => normalizeSetupFilePath(setupFile) === setupFilePath,
66+
);
67+
68+
if (!hasSetupFile) {
69+
project.config.setupFiles.unshift(setupFilePath);
70+
}
71+
72+
if (project.config.browser.enabled) {
73+
project.config.browser.commands ??= {};
74+
75+
for (const [name, command] of Object.entries(allureBrowserCommands)) {
76+
project.config.browser.commands[name] ??= command;
77+
}
78+
}
79+
}
80+
}
81+
4882
// eslint-disable-next-line @typescript-eslint/array-type
4983
onTestRunEnd(tests: ReadonlyArray<TestModule>) {
5084
for (const test of tests) {

packages/allure-vitest/src/runtime.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const ALLURE_VITEST_GLOBAL_RUNTIME_MESSAGES_META_KEY = "allureGlobalRunti
99

1010
export const ALLURE_VITEST_RUNTIME_MESSAGES_META_KEY = "allureRuntimeMessages";
1111

12+
export const ALLURE_VITEST_ASYNC_CONTEXT_KEY = "__allureVitestAsyncContext";
13+
1214
export type RuntimeMessageMetaKey =
1315
| typeof ALLURE_VITEST_GLOBAL_RUNTIME_MESSAGES_META_KEY
1416
| typeof ALLURE_VITEST_RUNTIME_MESSAGES_META_KEY;
@@ -18,6 +20,27 @@ export type RuntimeMessageTaskMeta = TaskMeta & {
1820
[ALLURE_VITEST_RUNTIME_MESSAGES_META_KEY]?: RuntimeMessage[];
1921
};
2022

23+
type AllureVitestAsyncContext = {
24+
currentTaskStorage: {
25+
getStore: () => Task | undefined;
26+
};
27+
activeTasks?: {
28+
has: (task: Task) => boolean;
29+
};
30+
};
31+
32+
const getCurrentTask = (): Task | undefined => {
33+
const holder = globalThis as unknown as Record<string, AllureVitestAsyncContext | undefined>;
34+
const asyncContext = holder[ALLURE_VITEST_ASYNC_CONTEXT_KEY];
35+
const task = asyncContext?.currentTaskStorage.getStore();
36+
37+
if (task) {
38+
return (asyncContext?.activeTasks?.has(task) ?? true) ? task : undefined;
39+
}
40+
41+
return getCurrentTest();
42+
};
43+
2144
export const addGlobalMessage = (message: RuntimeMessage) => {
2245
const holder = globalThis as unknown as Record<string, RuntimeMessage[] | undefined>;
2346
const messages = (holder[ALLURE_VITEST_GLOBAL_RUNTIME_MESSAGES_KEY] ??= []);
@@ -101,7 +124,7 @@ export class BaseVitestTestRuntime extends BaseMessageTestRuntime {
101124
}
102125

103126
protected processMessage(message: RuntimeMessage): boolean {
104-
const currentTest = getCurrentTest();
127+
const currentTest = getCurrentTask();
105128

106129
if (isGlobalRuntimeMessage(message)) {
107130
if (currentTest) {

packages/allure-vitest/src/setup.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,98 @@
1+
import { AsyncLocalStorage } from "node:async_hooks";
2+
3+
import type { Task, Test } from "@vitest/runner";
14
import { parseTestPlan } from "allure-js-commons/sdk/reporter";
25
import { setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
36
import { afterAll, afterEach, beforeAll, beforeEach } from "vitest";
7+
import { VitestTestRunner } from "vitest/runners";
48

59
import { allureVitestLegacyApi } from "./legacy.js";
10+
import { ALLURE_VITEST_ASYNC_CONTEXT_KEY } from "./runtime.js";
611
import { existsInTestPlan } from "./utils.js";
712
import { VitestTestRuntime } from "./VitestTestRuntime.js";
813

14+
const ALLURE_VITEST_RUNNER_PATCHED_KEY = Symbol.for("allure-vitest.runnerPatched");
15+
16+
type AllureVitestAsyncContext = {
17+
currentTaskStorage: AsyncLocalStorage<Test>;
18+
activeTasks: WeakSet<Test>;
19+
};
20+
21+
type PatchedRunner = typeof VitestTestRunner.prototype & {
22+
[ALLURE_VITEST_RUNNER_PATCHED_KEY]?: true;
23+
onAfterRetryTask?: (test: Task, retryInfo: { retry: number; repeats: number }) => unknown;
24+
};
25+
26+
const getAsyncContext = (): AllureVitestAsyncContext => {
27+
const holder = globalThis as unknown as Record<string, AllureVitestAsyncContext | undefined>;
28+
29+
return (holder[ALLURE_VITEST_ASYNC_CONTEXT_KEY] ??= {
30+
currentTaskStorage: new AsyncLocalStorage<Test>(),
31+
activeTasks: new WeakSet<Test>(),
32+
});
33+
};
34+
35+
const patchVitestRunner = () => {
36+
const prototype = VitestTestRunner.prototype as PatchedRunner;
37+
38+
if (prototype[ALLURE_VITEST_RUNNER_PATCHED_KEY]) {
39+
return;
40+
}
41+
42+
const originalOnBeforeRunTask = prototype.onBeforeRunTask;
43+
const originalOnAfterRetryTask = prototype.onAfterRetryTask;
44+
const originalOnAfterRunTask = prototype.onAfterRunTask;
45+
46+
const releaseTask = (test: Task) => {
47+
getAsyncContext().activeTasks.delete(test as Test);
48+
};
49+
50+
const releaseNonRunnableTask = (test: Task) => {
51+
if (test.mode !== "run" && test.mode !== "queued") {
52+
releaseTask(test);
53+
}
54+
};
55+
56+
const releaseDynamicallySkippedTask = (test: Task) => {
57+
const result = (test as Test).result as { pending?: boolean; state?: string } | undefined;
58+
59+
if (result?.pending || result?.state === "skip") {
60+
releaseTask(test);
61+
}
62+
};
63+
64+
prototype.onBeforeRunTask = async function allureOnBeforeRunTask(test: Task) {
65+
const asyncContext = getAsyncContext();
66+
67+
asyncContext.activeTasks.add(test as Test);
68+
asyncContext.currentTaskStorage.enterWith(test as Test);
69+
70+
await originalOnBeforeRunTask.call(this, test);
71+
72+
releaseNonRunnableTask(test);
73+
};
74+
75+
prototype.onAfterRetryTask = function allureOnAfterRetryTask(test: Task, retryInfo) {
76+
try {
77+
return originalOnAfterRetryTask?.call(this, test, retryInfo);
78+
} finally {
79+
releaseDynamicallySkippedTask(test);
80+
}
81+
};
82+
83+
prototype.onAfterRunTask = async function allureOnAfterRunTask(test: Task) {
84+
try {
85+
return await originalOnAfterRunTask.call(this, test);
86+
} finally {
87+
releaseTask(test);
88+
}
89+
};
90+
91+
prototype[ALLURE_VITEST_RUNNER_PATCHED_KEY] = true;
92+
};
93+
94+
patchVitestRunner();
95+
996
beforeAll(() => {
1097
globalThis.allureTestPlan = parseTestPlan();
1198
setGlobalTestRuntime(new VitestTestRuntime());

packages/allure-vitest/test/spec/categories.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

3-
import {
4-
type TestFileAccessor,
5-
browserSetupModulePath,
6-
reporterModulePath,
7-
runVitestInlineTest,
8-
setupModulePath,
9-
} from "../utils.js";
3+
import { type TestFileAccessor, reporterModulePath, runVitestInlineTest } from "../utils.js";
104

115
describe("categories", () => {
126
for (const env of ["node", "browser"]) {
@@ -21,7 +15,6 @@ describe("categories", () => {
2115
2216
export default defineConfig({
2317
test: {
24-
setupFiles: ["${setupModulePath}"],
2518
reporters: [
2619
"default",
2720
[
@@ -42,13 +35,11 @@ describe("categories", () => {
4235
}
4336
return `
4437
import { defineConfig } from "vitest/config";
45-
import { commands } from "allure-vitest/browser";
4638
import { playwright } from "@vitest/browser-playwright";
4739
4840
export default defineConfig({
4941
test: {
5042
openTelemetry: { enabled: false },
51-
setupFiles: ["${browserSetupModulePath}"],
5243
reporters: [
5344
"default",
5445
[
@@ -68,7 +59,6 @@ describe("categories", () => {
6859
enabled: true,
6960
headless: true,
7061
instances: [{ browser: "chromium" }],
71-
commands: { ...commands },
7262
},
7363
},
7464
});

packages/allure-vitest/test/spec/environmentInfo.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

3-
import {
4-
type TestFileAccessor,
5-
browserSetupModulePath,
6-
reporterModulePath,
7-
runVitestInlineTest,
8-
setupModulePath,
9-
} from "../utils.js";
3+
import { type TestFileAccessor, reporterModulePath, runVitestInlineTest } from "../utils.js";
104

115
describe("environment info", () => {
126
for (const env of ["node", "browser"]) {
@@ -21,7 +15,6 @@ describe("environment info", () => {
2115
2216
export default defineConfig({
2317
test: {
24-
setupFiles: ["${setupModulePath}"],
2518
reporters: [
2619
"default",
2720
[
@@ -41,13 +34,11 @@ describe("environment info", () => {
4134
}
4235
return `
4336
import { defineConfig } from "vitest/config";
44-
import { commands } from "allure-vitest/browser";
4537
import { playwright } from "@vitest/browser-playwright";
4638
4739
export default defineConfig({
4840
test: {
4941
openTelemetry: { enabled: false },
50-
setupFiles: ["${browserSetupModulePath}"],
5142
reporters: [
5243
"default",
5344
[
@@ -66,7 +57,6 @@ describe("environment info", () => {
6657
enabled: true,
6758
headless: true,
6859
instances: [{ browser: "chromium" }],
69-
commands: { ...commands },
7060
},
7161
},
7262
});

0 commit comments

Comments
 (0)