Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allure Mocha: testplan support, env info and categories, metadata #1003

Merged
merged 8 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/allure-mocha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"compile:fixup": "node ./scripts/fixup.mjs",
"lint": "eslint ./src ./test --ext .ts",
"lint:fix": "eslint ./src ./test --ext .ts --fix",
"test": "run-s 'test:*'",
"test": "run-s --print-name 'test:*'",
"test:serial": "vitest run",
"test:parallel": "ALLURE_MOCHA_TEST_PARALLEL=true vitest run",
"test:runner": "ALLURE_MOCHA_TEST_RUNNER=cjs ALLURE_MOCHA_TEST_SPEC_FORMAT=cjs vitest run",
Expand Down
72 changes: 49 additions & 23 deletions packages/allure-mocha/src/AllureMochaReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ import {
import { setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
import { MochaTestRuntime } from "./MochaTestRuntime.js";
import { setLegacyApiRuntime } from "./legacyUtils.js";
import { getInitialLabels, getSuitesOfMochaTest, resolveParallelModeSetupFile } from "./utils.js";
import {
applyTestPlan,
createTestPlanIndices,
getAllureDisplayName,
getAllureFullName,
getAllureMetaLabels,
getInitialLabels,
getSuitesOfMochaTest,
getTestCaseId,
isIncludedInTestRun,
resolveParallelModeSetupFile,
} from "./utils.js";
import type { TestPlanIndices } from "./utils.js";

const {
EVENT_SUITE_BEGIN,
Expand All @@ -30,6 +42,7 @@ const {

export class AllureMochaReporter extends Mocha.reporters.Base {
private readonly runtime: ReporterRuntime;
private readonly testplan?: TestPlanIndices;

constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions) {
super(runner, opts);
Expand All @@ -40,6 +53,8 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
writer: writer || new FileSystemWriter({ resultsDir }),
...restOptions,
});
this.testplan = createTestPlanIndices();

const testRuntime = new MochaTestRuntime(this.runtime);

setGlobalTestRuntime(testRuntime);
Expand All @@ -52,6 +67,12 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
}
}

override done(failures: number, fn?: ((failures: number) => void) | undefined): void {
this.runtime.writeEnvironmentInfo();
this.runtime.writeCategoriesDefinitions();
return fn?.(failures);
}

private applyListeners = () => {
this.runner
.on(EVENT_SUITE_BEGIN, this.onSuite)
Expand All @@ -65,7 +86,10 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
.on(EVENT_HOOK_END, this.onHookEnd);
};

private onSuite = () => {
private onSuite = (suite: Mocha.Suite) => {
if (!suite.parent && this.testplan) {
applyTestPlan(this.testplan.idIndex, this.testplan.fullNameIndex, suite);
}
this.runtime.startScope();
};

Expand All @@ -74,26 +98,24 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
};

private onTest = (test: Mocha.Test) => {
let fullName = "";
const globalLabels = getEnvironmentLabels().filter((label) => !!label.value);
const initialLabels: Label[] = getInitialLabels();
const labels = globalLabels.concat(initialLabels);
const metaLabels = getAllureMetaLabels(test);
const labels = globalLabels.concat(initialLabels, metaLabels);

if (test.file) {
const testPath = getRelativePath(test.file);
fullName = `${testPath!}: `;
const packageLabelFromPath: Label = getPackageLabelFromPath(testPath);
labels.push(packageLabelFromPath);
}

fullName += test.titlePath().join(" > ");

this.runtime.startTest(
{
name: test.title,
name: getAllureDisplayName(test),
stage: Stage.RUNNING,
fullName,
fullName: getAllureFullName(test),
labels,
testCaseId: getTestCaseId(test),
},
{ dedicatedScope: true },
);
Expand All @@ -116,23 +138,27 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
};

private onPending = (test: Mocha.Test) => {
this.onTest(test);
this.runtime.updateTest((r) => {
r.status = Status.SKIPPED;
r.statusDetails = {
message: "Test skipped",
};
});
if (isIncludedInTestRun(test)) {
this.onTest(test);
this.runtime.updateTest((r) => {
r.status = Status.SKIPPED;
r.statusDetails = {
message: "Test skipped",
};
});
}
};

private onTestEnd = (test: Mocha.Test) => {
const defaultSuites = getSuitesOfMochaTest(test);
this.runtime.updateTest((t) => {
ensureSuiteLabels(t, defaultSuites);
t.stage = Stage.FINISHED;
});
this.runtime.stopTest();
this.runtime.writeTest();
if (isIncludedInTestRun(test)) {
const defaultSuites = getSuitesOfMochaTest(test);
this.runtime.updateTest((t) => {
ensureSuiteLabels(t, defaultSuites);
t.stage = Stage.FINISHED;
});
this.runtime.stopTest();
this.runtime.writeTest();
}
};

private onHookStart = (hook: Mocha.Hook) => {
Expand Down
108 changes: 91 additions & 17 deletions packages/allure-mocha/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,110 @@
import type * as Mocha from "mocha";
import { hostname } from "node:os";
import { dirname, extname, join } from "node:path";
import { env, pid } from "node:process";
import { env } from "node:process";
import { fileURLToPath } from "node:url";
import { isMainThread, threadId } from "node:worker_threads";
import type { Label } from "allure-js-commons";
import { LabelName } from "allure-js-commons";
import type { TestPlanV1, TestPlanV1Test } from "allure-js-commons/sdk";
import { extractMetadataFromString } from "allure-js-commons/sdk";
import { getHostLabel, getRelativePath, getThreadLabel, md5, parseTestPlan } from "allure-js-commons/sdk/reporter";

const filename = fileURLToPath(import.meta.url);

export const getSuitesOfMochaTest = (test: Mocha.Test) => test.titlePath().slice(0, -1);
const allureMochaDataKey = Symbol("Used to access Allure extra data in Mocha objects");

export const resolveParallelModeSetupFile = () =>
join(dirname(filename), `setupAllureMochaParallel${extname(filename)}`);
type AllureMochaTestData = {
isIncludedInTestRun: boolean;
fullName: string;
labels: readonly Label[];
displayName: string;
};

const getAllureData = (item: Mocha.Test): AllureMochaTestData => {
const data = (item as any)[allureMochaDataKey];
if (!data) {
const meta = extractMetadataFromString(item.title);
const defaultData: AllureMochaTestData = {
isIncludedInTestRun: true,
fullName: createAllureFullName(item),
labels: meta.labels,
displayName: meta.cleanTitle,
};
(item as any)[allureMochaDataKey] = defaultData;
return defaultData;
}
return data;
};

const createAllureFullName = (test: Mocha.Test) => {
const titlePath = test.titlePath().join(" > ");
return test.file ? `${getRelativePath(test.file)}: ${titlePath}` : titlePath;
};

const createTestPlanSelectorIndex = (testplan: TestPlanV1) => createTestPlanIndex((e) => e.selector, testplan);

const createTestPlanIdIndex = (testplan: TestPlanV1) => createTestPlanIndex((e) => e.id?.toString(), testplan);

const createTestPlanIndex = <T>(keySelector: (entry: TestPlanV1Test) => T, testplan: TestPlanV1) =>
new Set(testplan.tests.map((e) => keySelector(e)).filter((v) => v) as readonly T[]);

export type TestPlanIndices = {
fullNameIndex: ReadonlySet<string>;
idIndex: ReadonlySet<string>;
};

export const resolveMochaWorkerId = () => env.MOCHA_WORKER_ID ?? (isMainThread ? pid : threadId).toString();
export const createTestPlanIndices = (): TestPlanIndices | undefined => {
const testplan = parseTestPlan();
if (testplan) {
return {
fullNameIndex: createTestPlanSelectorIndex(testplan),
idIndex: createTestPlanIdIndex(testplan),
};
}
};

const allureHostName = env.ALLURE_HOST_NAME || hostname();
export const getAllureFullName = (test: Mocha.Test) => getAllureData(test).fullName;

export const getHostLabel = (): Label => ({
name: LabelName.HOST,
value: allureHostName,
});
export const isIncludedInTestRun = (test: Mocha.Test) => getAllureData(test).isIncludedInTestRun;

export const getWorkerIdLabel = (): Label => ({
name: LabelName.THREAD,
value: resolveMochaWorkerId(),
});
export const getAllureMetaLabels = (test: Mocha.Test) => getAllureData(test).labels;

export const getAllureId = (data: AllureMochaTestData) => {
const values = data.labels.filter((l) => l.name === LabelName.ALLURE_ID).map((l) => l.value);
if (values.length) {
return values[0];
}
};

export const getAllureDisplayName = (test: Mocha.Test) => getAllureData(test).displayName;

export const getSuitesOfMochaTest = (test: Mocha.Test) => test.titlePath().slice(0, -1);

export const resolveParallelModeSetupFile = () =>
join(dirname(filename), `setupAllureMochaParallel${extname(filename)}`);

export const getInitialLabels = (): Label[] => [
{ name: LabelName.LANGUAGE, value: "javascript" },
{ name: LabelName.FRAMEWORK, value: "mocha" },
getHostLabel(),
getWorkerIdLabel(),
getThreadLabel(env.MOCHA_WORKER_ID),
];

export const getTestCaseId = (test: Mocha.Test) => {
const suiteTitles = test.titlePath().slice(0, -1);
return md5(JSON.stringify([...suiteTitles, getAllureDisplayName(test)]));
};

export const applyTestPlan = (ids: ReadonlySet<string>, selectors: ReadonlySet<string>, rootSuite: Mocha.Suite) => {
const suiteQueue = [];
for (let s: Mocha.Suite | undefined = rootSuite; s; s = suiteQueue.shift()) {
for (const test of s.tests) {
const allureData = getAllureData(test);
const allureId = getAllureId(allureData);
if (!selectors.has(allureData.fullName) && (!allureId || !ids.has(allureId))) {
allureData.isIncludedInTestRun = false;
test.pending = true;
}
}
suiteQueue.push(...s.suites);
}
};
10 changes: 6 additions & 4 deletions packages/allure-mocha/test/fixtures/reporter.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/* eslint @typescript-eslint/no-unsafe-argument: 0 */
const AllureMochaReporter = require("allure-mocha");
const path = require("path");

class ProcessMessageAllureReporter extends AllureMochaReporter {
constructor(runner, opts) {
if (opts.reporterOptions?.emitFiles !== "true") {
opts.reporterOptions = {
writer: opts.parallel ? path.join(__dirname, "./AllureMochaParallelWriter.cjs") : "MessageWriter",
};
(opts.reporterOptions ??= {}).writer = "MessageWriter";
}
for (const key of ["environmentInfo", "categories"]) {
if (typeof opts.reporterOptions?.[key] === "string") {
opts.reporterOptions[key] = JSON.parse(Buffer.from(opts.reporterOptions[key], "base64Url").toString());
}
}
super(runner, opts);
}
Expand Down
19 changes: 14 additions & 5 deletions packages/allure-mocha/test/fixtures/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
let emitFiles = false;
let parallel = false;
const requires = [];
const reporterOptions = {};

const sepIndex = process.argv.indexOf("--");
const args = sepIndex === -1 ? [] : process.argv.splice(sepIndex);
for (const arg of args) {
switch (arg) {
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--emit-files":
emitFiles = true;
break;
Expand All @@ -28,14 +29,22 @@ for (const arg of args) {
// esm: await import("./setupParallel.cjs");
requires.push(path.join(dirname, "setupParallel.cjs"));
break;
case "--environment-info":
reporterOptions.environmentInfo = JSON.parse(Buffer.from(args[++i], "base64url").toString());
break;
case "--categories":
reporterOptions.categories = JSON.parse(Buffer.from(args[++i], "base64url").toString());
break;
}
}

if (!emitFiles) {
reporterOptions.writer = "MessageWriter";
}

const mocha = new Mocha({
reporter: AllureReporter,
reporterOptions: {
writer: emitFiles ? undefined : "MessageWriter",
},
reporterOptions,
parallel,
require: requires,
color: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// cjs: const { it } = require("mocha");
// esm: import { it } from "mocha";

it("a test a changing meta @allure.label.foo:bar", async () => {});

it("a test a changing meta @allure.label.foo:baz", async () => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// cjs: const { it } = require("mocha");
// esm: import { it } from "mocha";

it("a test with ID 1004 @allure.id:1004", async () => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// cjs: const { it } = require("mocha");
// esm: import { it } from "mocha";

it("a test with ID 1005 @allure.id:1005", async () => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// cjs: const { it } = require("mocha");
// esm: import { it } from "mocha";

it("a test two embedded custom label @allure.label.foo:bar @allure.label.baz:qux", async () => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// cjs: const { it } = require("mocha");
// esm: import { it } from "mocha";

it("a test with an embedded custom label @allure.label.foo:bar", async () => {});
Loading
Loading