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

Compatibility with Mocha's parallel mode #967

Merged
merged 6 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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 @@ -31,7 +31,10 @@ import {
createTestResult,
getTestResultHistoryId,
getTestResultTestCaseId,
resolveWriter,
} from "./utils.js";
import type { WellKnownWriters } 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
6 changes: 6 additions & 0 deletions packages/allure-js-commons/src/sdk/node/ReporterRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { extname } from "path";
import { AttachmentOptions, TestResult } from "../../model.js";
import { Config } from "../Config.js";
import { ReporterRuntime } from "../ReporterRuntime.js";
import { WellKnownWriters } from "../utils.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 +61,8 @@ export class AllureNodeReporterRuntime extends ReporterRuntime {
labels: (result.labels ?? []).concat(getGlobalLabels()),
});
}

protected getWellKnownWriters(): WellKnownWriters {
return Object.assign({}, super.getWellKnownWriters(), wellKnownNodeWriters);
}
}
29 changes: 29 additions & 0 deletions packages/allure-js-commons/src/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
TestResultContainer,
} 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 +179,30 @@ export const readImageAsBase64 = async (filePath: string): Promise<string | unde
return undefined;
}
};

export type WellKnownWriters = {
epszaw marked this conversation as resolved.
Show resolved Hide resolved
[key: string]: (new (...args: readonly unknown[]) => Writer) | 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();
epszaw marked this conversation as resolved.
Show resolved Hide resolved
};
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)}`);
epszaw marked this conversation as resolved.
Show resolved Hide resolved

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(),
];
38 changes: 38 additions & 0 deletions packages/allure-mocha/test/fixtures/AllureMochaParallelWriter.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint @typescript-eslint/no-require-imports: 0 */
/* eslint @typescript-eslint/no-var-requires: 0 */
/* eslint no-underscore-dangle: 0 */
const ParallelBuffered = require("mocha/lib/nodejs/reporters/parallel-buffered.js");
const { MessageAllureWriter } = require("allure-js-commons/sdk/node");

class AllureMochaParallelWriter extends MessageAllureWriter {
constructor() {
super();
this.events = [];
}

sendData(path, type, data) {
const event = { path, type, data: data.toString("base64") };
this.events.push(JSON.stringify(event));
}
}

const writer = new AllureMochaParallelWriter();

const originalDone = ParallelBuffered.prototype.done;
ParallelBuffered.prototype.done = function (failures, callback) {
for (const event of this.events) {
if (event.originalError) {
// workaround the "Converting circular structure to JSON" error
if (event.originalError.multiple) {
event.originalError.multiple = event.originalError.multiple.filter((e) => !Object.is(e, event.originalError));
}
}
}
return originalDone.call(this, failures, (r) => {
r.__allure__ = writer.events;
writer.events = [];
callback(r);
});
};

module.exports = writer;
Loading
Loading