Skip to content

Commit

Permalink
feat(mocha): implement extra reporters
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie committed Oct 30, 2024
1 parent 9cd29df commit 4244bd0
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 5 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
150 changes: 150 additions & 0 deletions packages/allure-mocha/src/extraReporters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as Mocha from "mocha";
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];

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 {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
return require(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 entry must be a module name or a constructor. Got ${typeof moduleOrCtor}`);
}

return moduleOrCtor;
};

const getReporterModulePath = (module: string) => {
try {
return require.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];
};

0 comments on commit 4244bd0

Please sign in to comment.