Skip to content

Commit

Permalink
feat(mocha): testplan support, env info and categories, metadata (via #…
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie authored Jun 17, 2024
1 parent 6295a36 commit 347e34b
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 58 deletions.
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

0 comments on commit 347e34b

Please sign in to comment.