Skip to content

Commit

Permalink
Compatibility with Mocha's parallel mode (#967)
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie authored May 23, 2024
1 parent f4c6d9b commit 7c66920
Show file tree
Hide file tree
Showing 17 changed files with 295 additions and 97 deletions.
6 changes: 6 additions & 0 deletions packages/allure-js-commons/src/model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Writer } from "./sdk/Writer.js";

export const ALLURE_METADATA_CONTENT_TYPE = "application/vnd.allure.metadata+json";
export const ALLURE_IMAGEDIFF_CONTENT_TYPE = "application/vnd.allure.image.diff";
export const ALLURE_SKIPPED_BY_TEST_PLAN_LABEL = "allure-skipped-by-test-plan";
Expand Down Expand Up @@ -287,3 +289,7 @@ export type ExtensionMessage<T extends string> = T extends MessageTypes<RuntimeM

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Messages<T> = T extends RuntimeMessage | ExtensionMessage<infer _> ? T : never;

export type WellKnownWriters = {
[key: string]: (new (...args: readonly unknown[]) => Writer) | undefined;
};
4 changes: 3 additions & 1 deletion packages/allure-js-commons/src/sdk/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export interface LinkConfig {
urlTemplate: string;
}

export type WriterDescriptor = [cls: string, ...args: readonly unknown[]] | string;

export interface Config {
readonly resultsDir?: string;
readonly writer: Writer;
readonly writer: Writer | WriterDescriptor;
// TODO: handle lifecycle hooks here
readonly testMapper?: (test: TestResult) => TestResult | null;
readonly links?: LinkConfig[];
Expand Down
9 changes: 8 additions & 1 deletion packages/allure-js-commons/src/sdk/ReporterRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Stage,
StepResult,
TestResult,
WellKnownWriters,
} from "../model.js";
import { deepClone, typeToExtension } from "../utils.js";
import { Config, LinkConfig } from "./Config.js";
Expand All @@ -31,7 +32,9 @@ import {
createTestResult,
getTestResultHistoryId,
getTestResultTestCaseId,
resolveWriter,
} from "./utils.js";
import * as wellKnownCommonWriters from "./writers/index.js";

type StartScopeOpts = {
/**
Expand Down Expand Up @@ -170,7 +173,7 @@ export class ReporterRuntime {
}: Config & {
crypto: Crypto;
}) {
this.writer = writer;
this.writer = resolveWriter(this.getWellKnownWriters(), writer);
this.notifier = new Notifier({ listeners });
this.crypto = crypto;
this.links = links;
Expand Down Expand Up @@ -760,6 +763,10 @@ export class ReporterRuntime {
};
}

protected getWellKnownWriters() {
return wellKnownCommonWriters as WellKnownWriters;
}

private handleBuiltInMessage = <T>(message: Messages<T>, targets: MessageTargets) => {
switch (message.type) {
case "metadata":
Expand Down
7 changes: 6 additions & 1 deletion packages/allure-js-commons/src/sdk/node/ReporterRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { extname } from "path";
import { AttachmentOptions, TestResult } from "../../model.js";
import { AttachmentOptions, TestResult, WellKnownWriters } from "../../model.js";
import { Config } from "../Config.js";
import { ReporterRuntime } from "../ReporterRuntime.js";
import { AllureNodeCrypto } from "./Crypto.js";
import { getGlobalLabels } from "./utils.js";
import * as wellKnownNodeWriters from "./writers/index.js";

export class AllureNodeReporterRuntime extends ReporterRuntime {
constructor({ writer, listeners, links, environmentInfo, categories }: Config) {
Expand Down Expand Up @@ -59,4 +60,8 @@ export class AllureNodeReporterRuntime extends ReporterRuntime {
labels: (result.labels ?? []).concat(getGlobalLabels()),
});
}

protected getWellKnownWriters(): WellKnownWriters {
return Object.assign({}, super.getWellKnownWriters(), wellKnownNodeWriters);
}
}
26 changes: 26 additions & 0 deletions packages/allure-js-commons/src/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import {
StepResult,
TestResult,
TestResultContainer,
WellKnownWriters,
} from "../model.js";
import { typeToExtension } from "../utils.js";
import type { WriterDescriptor } from "./Config.js";
import { Crypto } from "./Crypto.js";
import type { Writer } from "./Writer.js";

export const createTestResultContainer = (uuid: string): TestResultContainer => {
return {
Expand Down Expand Up @@ -177,3 +180,26 @@ export const readImageAsBase64 = async (filePath: string): Promise<string | unde
return undefined;
}
};

export const resolveWriter = (wellKnownWriters: WellKnownWriters, value: Writer | WriterDescriptor): Writer => {
if (typeof value === "string") {
return createWriter(wellKnownWriters, value);
} else if (value instanceof Array) {
return createWriter(wellKnownWriters, value[0], value.slice(1));
}
return value;
};

const createWriter = (wellKnownWriters: WellKnownWriters, nameOrPath: string, args: readonly unknown[] = []) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
const ctorOrInstance = getKnownWriterCtor(wellKnownWriters, nameOrPath) ?? requireWriterCtor(nameOrPath);
return typeof ctorOrInstance === "function" ? new ctorOrInstance(...args) : ctorOrInstance;
};

const getKnownWriterCtor = (wellKnownWriters: WellKnownWriters, name: string) =>
(wellKnownWriters as unknown as { [key: string]: Writer | undefined })[name];

const requireWriterCtor = (modulePath: string): (new (...args: readonly unknown[]) => Writer) | Writer => {
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
return require(modulePath);
};
34 changes: 15 additions & 19 deletions packages/allure-mocha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@

---

**Allure API doesn't work in parallel mode**! If you want to use the functionality, please switch
back to single thread mode!

## Installation

```bash
Expand Down Expand Up @@ -51,34 +48,33 @@ If you want to provide extra information, such as steps and attachments, import
into your code:

```javascript
const { allure } = require("allure-mocha/runtime");
const { epic } = require("allure-js-commons");

it("is a test", () => {
allure.epic("Some info");
it("is a test", async () => {
await epic("Some info");
});
```

### Parameters usage

```ts
const { allure } = require("allure-mocha/runtime");
const { parameter } = require("allure-js-commons");

it("is a test", () => {
allure.parameter("parameterName", "parameterValue");
it("is a test", async () => {
await parameter("parameterName", "parameterValue");
});
```

Also `parameter` method takes an third optional argument with the hidden and excluded options:
`mode: "hidden" | "masked"` - `masked` hide parameter value to secure sensitive data, and `hidden`
entirely hide parameter from report
The `parameter` method may also take the third argument with the `hidden` and `excluded` options:
`mode: "hidden" | "masked"` - `masked` replaces the value with `*` characters to secure sensitive data, and `hidden` hides the parameter from report.

`excluded: true` - excludes parameter from the history
`excluded: true` - excludes the parameter from the history.

```ts
import { allure } from "allure-mocha/runtime";
import { parameter } from "allure-js-commons";

it("is a test", () => {
allure.parameter("parameterName", "parameterValue", {
it("is a test", async () => {
await parameter("parameterName", "parameterValue", {
mode: "hidden",
excluded: true,
});
Expand All @@ -87,10 +83,10 @@ it("is a test", () => {

## Decorators Support

To make tests more readable and avoid explicit API calls, you can use a special
To make tests more readable and avoid explicit API calls, you may use a special
extension - [ts-test-decorators](https://github.com/sskorol/ts-test-decorators).

## Examples

[mocha-allure-example](https://github.com/vovsemenv/mocha-allure-example) - minimal setup for using
mocha with allure
[mocha-allure-example](https://github.com/vovsemenv/mocha-allure-example) - a minimal setup for using
Mocha with Allure.
4 changes: 3 additions & 1 deletion packages/allure-mocha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"generate-report": "allure generate ./out/allure-results -o ./out/allure-report --clean",
"lint": "eslint ./src ./test --ext .ts",
"lint:fix": "eslint ./src ./test --ext .ts --fix",
"test": "vitest run ./test/spec/**/*.test.ts"
"test": "yarn run test:serial && yarn run test:parallel",
"test:serial": "vitest run ./test/spec/**/*.test.ts",
"test:parallel": "ALLURE_MOCHA_TEST_PARALLEL=true vitest run ./test/spec/**/*.test.ts"
},
"dependencies": {
"allure-js-commons": "workspace:*"
Expand Down
10 changes: 6 additions & 4 deletions packages/allure-mocha/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import commonjsPlugin from "@rollup/plugin-commonjs";
import resolvePlugin from "@rollup/plugin-node-resolve";
/* eslint @typescript-eslint/no-unsafe-argument: 0 */
import typescriptPlugin from "@rollup/plugin-typescript";
import { join, relative } from "node:path";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "rollup";

Expand All @@ -13,9 +12,12 @@ const createNodeEntry = (inputFile) => {
"mocha",
"node:os",
"node:fs",
"node:path",
"node:process",
"node:worker_threads",
"allure-js-commons",
"allure-js-commons/sdk/node",
"mocha/lib/nodejs/reporters/parallel-buffered.js",
];

return [
Expand Down Expand Up @@ -53,5 +55,5 @@ const createNodeEntry = (inputFile) => {
};

export default () => {
return [createNodeEntry("src/index.ts")].flat();
return [createNodeEntry("src/index.ts"), createNodeEntry("src/setupAllureMochaParallel.ts")].flat();
};
33 changes: 10 additions & 23 deletions packages/allure-mocha/src/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import * as Mocha from "mocha";
import { hostname } from "node:os";
import { env } from "node:process";
import "allure-js-commons";
import {
AllureNodeReporterRuntime,
Config,
FileSystemAllureWriter,
Label,
LabelName,
Stage,
Status,
ensureSuiteLabels,
Expand All @@ -16,7 +12,7 @@ import {
getStatusFromError,
} from "allure-js-commons/sdk/node";
import { setUpTestRuntime } from "./ContextBasedTestRuntime.js";
import { getSuitesOfMochaTest } from "./utils.js";
import { getInitialLabels, getSuitesOfMochaTest, resolveParallelModeSetupFile } from "./utils.js";

const {
EVENT_SUITE_BEGIN,
Expand All @@ -30,33 +26,28 @@ const {
EVENT_HOOK_END,
} = Mocha.Runner.constants;

type ParallelRunner = Mocha.Runner & {
linkPartialObjects?: (val: boolean) => ParallelRunner;
};

export class MochaAllureReporter extends Mocha.reporters.Base {
private static readonly hostname = env.ALLURE_HOST_NAME || hostname();
private readonly runtime: AllureNodeReporterRuntime;

constructor(runner: ParallelRunner, opts: Mocha.MochaOptions) {
constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions) {
super(runner, opts);

this.runner = runner;
const { resultsDir = "allure-results", writer, ...restOptions }: Config = opts.reporterOptions || {};
this.runtime = new AllureNodeReporterRuntime({
writer: writer || new FileSystemAllureWriter({ resultsDir }),
...restOptions,
});
setUpTestRuntime(this.runtime); // Covers the serial mode

if (runner.linkPartialObjects) {
runner.linkPartialObjects(true);
}
setUpTestRuntime(this.runtime);

this.applyAsyncListeners();
if (opts.parallel) {
opts.require = [...(opts.require ?? []), resolveParallelModeSetupFile()];
} else {
this.applyListeners();
}
}

private applyAsyncListeners = () => {
private applyListeners = () => {
this.runner
.on(EVENT_SUITE_BEGIN, this.onSuite)
.on(EVENT_SUITE_END, this.onSuiteEnd)
Expand All @@ -79,11 +70,7 @@ export class MochaAllureReporter extends Mocha.reporters.Base {

private onTest = (test: Mocha.Test) => {
let fullName = "";
const labels: Label[] = [
{ name: LabelName.LANGUAGE, value: "javascript" },
{ name: LabelName.FRAMEWORK, value: "mocha" },
{ name: LabelName.HOST, value: MochaAllureReporter.hostname },
];
const labels = getInitialLabels();

if (test.file) {
const testPath = getRelativePath(test.file);
Expand Down
12 changes: 12 additions & 0 deletions packages/allure-mocha/src/setupAllureMochaParallel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Mocha from "mocha";
// @ts-ignore
import { default as ParallelBuffered } from "mocha/lib/nodejs/reporters/parallel-buffered.js";
import { MochaAllureReporter } from "./reporter.js";

const originalCreateListeners: (runner: Mocha.Runner) => Mocha.reporters.Base =
ParallelBuffered.prototype.createListeners;

ParallelBuffered.prototype.createListeners = function (runner: Mocha.Runner) {
new MochaAllureReporter(runner, this.options as Mocha.MochaOptions);
return originalCreateListeners.call(this, runner);
};
29 changes: 28 additions & 1 deletion packages/allure-mocha/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as Mocha from "mocha";
import { StatusDetails } from "allure-js-commons/sdk/node";
import { hostname } from "node:os";
import { extname, join } from "node:path";
import { env, pid } from "node:process";
import { isMainThread, threadId } from "node:worker_threads";
import { Label, LabelName, StatusDetails } from "allure-js-commons/sdk/node";

export const errorToStatusDetails = (error: unknown): StatusDetails | undefined => {
if (error instanceof Error) {
Expand All @@ -15,3 +19,26 @@ export const errorToStatusDetails = (error: unknown): StatusDetails | undefined
};

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

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

export const resolveMochaWorkerId = () => env.MOCHA_WORKER_ID ?? (isMainThread ? pid : threadId).toString();

const allureHostName = env.ALLURE_HOST_NAME || hostname();

export const getHostLabel = (): Label => ({
name: LabelName.HOST,
value: allureHostName,
});

export const getWorkerIdLabel = (): Label => ({
name: LabelName.THREAD,
value: resolveMochaWorkerId(),
});

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

0 comments on commit 7c66920

Please sign in to comment.