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

feat(mocha): add support for extra reporters #1182

Merged
merged 7 commits into from
Nov 1, 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
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
Loading