Skip to content

Commit

Permalink
feat(mocha): add support for extra reporters (fixes #1134, via #1182)
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie authored Nov 1, 2024
1 parent 93fbdd1 commit a84cc84
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 14 deletions.
16 changes: 11 additions & 5 deletions packages/allure-mocha/src/AllureMochaReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Stage, Status } from "allure-js-commons";
import type { Category, RuntimeMessage } from "allure-js-commons/sdk";
import { getMessageAndTraceFromError, getStatusFromError } from "allure-js-commons/sdk";
import { getHostLabel, getThreadLabel } from "allure-js-commons/sdk/reporter";
import type { ReporterConfig } from "allure-js-commons/sdk/reporter";
import {
ReporterRuntime,
createDefaultWriter,
Expand All @@ -17,8 +16,9 @@ import {
} from "allure-js-commons/sdk/reporter";
import { setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
import { MochaTestRuntime } from "./MochaTestRuntime.js";
import { doneAll, enableExtraReporters } from "./extraReporters.js";
import { setLegacyApiRuntime } from "./legacyUtils.js";
import type { TestPlanIndices } from "./types.js";
import type { AllureMochaReporterConfig, TestPlanIndices } from "./types.js";
import {
applyTestPlan,
createTestPlanIndices,
Expand Down Expand Up @@ -55,11 +55,13 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
protected currentTest?: string;
protected currentHook?: string;
private readonly isInWorker: boolean;
readonly #extraReporters: Mocha.reporters.Base[] = [];

constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions, isInWorker: boolean = false) {
super(runner, opts);

const { resultsDir, ...restOptions }: ReporterConfig = opts.reporterOptions || {};
const allureConfig: AllureMochaReporterConfig = opts.reporterOptions ?? {};
const { resultsDir, extraReporters, ...restOptions } = allureConfig;

this.isInWorker = isInWorker;
this.runtime = new ReporterRuntime({
Expand All @@ -78,6 +80,10 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
} else {
this.applyListeners();
}

if (!isInWorker && extraReporters) {
this.#extraReporters = enableExtraReporters(runner, opts, extraReporters);
}
}

applyRuntimeMessages = (...message: RuntimeMessage[]) => {
Expand Down Expand Up @@ -123,10 +129,10 @@ export class AllureMochaReporter extends Mocha.reporters.Base {
this.runtime.writeAttachment(root, null, name, buffer, { ...opts, wrapInStep: false });
};

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

private applyListeners = () => {
Expand Down
153 changes: 153 additions & 0 deletions packages/allure-mocha/src/extraReporters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as Mocha from "mocha";
import { createRequire } from "node:module";
import path from "node:path";
import type { ReporterDoneFn, ReporterEntry, ReporterModuleOrCtor, ReporterOptions } from "./types.js";

type CanonicalReporterEntry = readonly [ReporterModuleOrCtor, ReporterOptions];
type ShortReporterEntry = readonly [ReporterModuleOrCtor];
type LoadedReporterEntry = readonly [Mocha.ReporterConstructor, ReporterOptions];

// There is no global require in ESM, and we can't use dynamic import (which returns a promise) because it's called from the reporter's constructor; therefore, it must be synchronous.
const localRequire = typeof require === "function" ? require : createRequire(import.meta.url);

export const enableExtraReporters = (
runner: Mocha.Runner,
options: Mocha.MochaOptions,
extraReportersConfig: ReporterEntry | readonly ReporterEntry[],
) => {
const extraReporterEntries = Array.from(generateCanonicalizedReporterEntries(extraReportersConfig));
const loadedReporterEntries = loadReporters(extraReporterEntries);
return instantiateReporters(runner, options, loadedReporterEntries);
};

export const doneAll = (
reporters: readonly Mocha.reporters.Base[],
failures: number,
fn?: ((failures: number) => void) | undefined,
) => {
const doneCallbacks = collectDoneCallbacks(reporters);
let callbacksToWait = doneCallbacks.length + 1;
const onReporterIsDone = () => {
if (--callbacksToWait === 0) {
fn?.(failures);
}
};

for (const done of doneCallbacks) {
done(failures, onReporterIsDone);
}

onReporterIsDone(); // handle the synchronous completion
};

const generateCanonicalizedReporterEntries = function* (
reporters: ReporterEntry | readonly ReporterEntry[] | undefined,
): Generator<CanonicalReporterEntry, void, undefined> {
if (reporters) {
if (!(reporters instanceof Array)) {
yield [reporters, {}];
} else {
if (isReporterArrayEntry(reporters)) {
yield resolveReporterArrayEntry(reporters);
} else {
yield* reporters.map((e) => {
return resolveReporterEntry(e);
});
}
}
}
};

const loadReporters = (reporterEntries: readonly CanonicalReporterEntry[]): LoadedReporterEntry[] =>
reporterEntries.map(([moduleOrCtor, options]) => [loadReporterModule(moduleOrCtor), options]);

const instantiateReporters = (
runner: Mocha.Runner,
options: Mocha.MochaOptions,
entries: readonly LoadedReporterEntry[],
) => {
const reporters: Mocha.reporters.Base[] = [];
for (const [Reporter, reporterOptions] of entries) {
const optionsForReporter = {
...options,
reporterOptions,
// eslint-disable-next-line quote-props
reporterOption: reporterOptions,
"reporter-option": reporterOptions,
};
reporters.push(new Reporter(runner, optionsForReporter));
}
return reporters;
};

const collectDoneCallbacks = (reporters: readonly Mocha.reporters.Base[]) => {
const doneCallbacks: ReporterDoneFn[] = [];
for (const reporter of reporters) {
if (reporter.done) {
doneCallbacks.push(reporter.done.bind(reporter));
}
}
return doneCallbacks;
};

const isReporterArrayEntry = (
reporters: ShortReporterEntry | CanonicalReporterEntry | readonly ReporterEntry[],
): reporters is ShortReporterEntry | CanonicalReporterEntry => {
const [maybeReporterModuleOrCtor, maybeReporterOptions = {}] = reporters;
return (
!(maybeReporterModuleOrCtor instanceof Array) &&
typeof maybeReporterOptions === "object" &&
!(maybeReporterOptions instanceof Array)
);
};

const loadReporterModule = (moduleOrCtor: ReporterModuleOrCtor) => {
if (typeof moduleOrCtor === "string") {
const builtInReporters = Mocha.reporters as Record<string, Mocha.ReporterConstructor>;
const builtInReporterCtor = builtInReporters[moduleOrCtor];
if (builtInReporterCtor) {
return builtInReporterCtor;
}

const reporterModulePath = getReporterModulePath(moduleOrCtor);
try {
return localRequire(reporterModulePath) as Mocha.ReporterConstructor;
} catch (e: any) {
throw new Error(`Can't load the '${moduleOrCtor}' reporter from ${reporterModulePath}: ${e.message}`);
}
}

if (typeof moduleOrCtor !== "function") {
throw new Error(`A reporter value must be a string or a constructor. Got ${typeof moduleOrCtor}`);
}

return moduleOrCtor;
};

const getReporterModulePath = (module: string) => {
try {
return localRequire.resolve(module);
} catch (e) {}

try {
return path.resolve(module);
} catch (e: any) {
throw new Error(`Can't resolve the '${module}' reporter's path: ${e.message}`);
}
};

const resolveReporterEntry = (reporterEntry: ReporterEntry): CanonicalReporterEntry => {
return reporterEntry instanceof Array ? resolveReporterArrayEntry(reporterEntry) : [reporterEntry, {}];
};

const resolveReporterArrayEntry = (
reporterEntry: ShortReporterEntry | CanonicalReporterEntry,
): CanonicalReporterEntry => {
if (reporterEntry.length < 1 || reporterEntry.length > 2) {
throw new Error(
`If an extra reporter entry is an array, it must contain one or two elements. ${reporterEntry.length} found`,
);
}

return reporterEntry.length === 1 ? [...reporterEntry, {}] : [...reporterEntry];
};
14 changes: 14 additions & 0 deletions packages/allure-mocha/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ReporterConstructor } from "mocha";
import type { Label } from "allure-js-commons";
import type { ReporterConfig } from "allure-js-commons/sdk/reporter";

export type TestPlanIndices = {
fullNameIndex: ReadonlySet<string>;
Expand All @@ -18,3 +20,15 @@ export type HookCategory = "before" | "after";
export type HookScope = "all" | "each";

export type HookType = [category?: HookCategory, scope?: HookScope];

export type AllureMochaReporterConfig = ReporterConfig & {
extraReporters?: ReporterEntry | ReporterEntry[];
};

export type ReporterModuleOrCtor = ReporterConstructor | string;

export type ReporterOptions = Record<string, any>;

export type ReporterEntry = ReporterModuleOrCtor | [ReporterModuleOrCtor] | [ReporterModuleOrCtor, ReporterOptions];

export type ReporterDoneFn = (failures: number, fn?: ((failures: number) => void) | undefined) => void;
13 changes: 13 additions & 0 deletions packages/allure-mocha/test/samples/customReporter.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Mocha = require("mocha");

class CustomReporter extends Mocha.reporters.Base {
constructor(runner, opts) {
super(runner, opts);
runner.on("start", () => {
// eslint-disable-next-line no-console
console.log(JSON.stringify(opts.reporterOptions));
});
}
}

module.exports = CustomReporter;
2 changes: 1 addition & 1 deletion packages/allure-mocha/test/samples/reporter.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ProcessMessageAllureReporter extends AllureMochaReporter {
if (opts.reporterOptions?.emitFiles !== "true") {
(opts.reporterOptions ??= {}).writer = "MessageWriter";
}
for (const key of ["environmentInfo", "categories"]) {
for (const key of ["environmentInfo", "categories", "extraReporters"]) {
if (typeof opts.reporterOptions?.[key] === "string") {
opts.reporterOptions[key] = JSON.parse(Buffer.from(opts.reporterOptions[key], "base64Url").toString());
}
Expand Down
3 changes: 3 additions & 0 deletions packages/allure-mocha/test/samples/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ for (let i = 0; i < args.length; i++) {
case "--categories":
reporterOptions.categories = JSON.parse(Buffer.from(args[++i], "base64url").toString());
break;
case "--extra-reporters":
reporterOptions.extraReporters = JSON.parse(Buffer.from(args[++i], "base64url").toString());
break;
}
}

Expand Down
Loading

0 comments on commit a84cc84

Please sign in to comment.