Skip to content

Commit

Permalink
feat(allure-jasmine): selective run implementation (via #1063)
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie authored Jul 17, 2024
1 parent b17a493 commit 8ffa35d
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 60 deletions.
9 changes: 5 additions & 4 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/allure-jasmine/babel.cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
],
["@babel/preset-env", { "modules": "commonjs" }]
],
"plugins": ["babel-plugin-add-module-exports"],
"plugins": ["babel-plugin-add-module-exports", "babel-plugin-transform-import-meta"],
"targets": {
"esmodules": false,
"node": 18
Expand Down
1 change: 1 addition & 0 deletions packages/allure-jasmine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"allure-commandline": "^2.29.0",
"allure-vitest": "workspace:*",
"babel-plugin-add-module-exports": "^1.0.4",
"babel-plugin-transform-import-meta": "^2.2.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
Expand Down
46 changes: 27 additions & 19 deletions packages/allure-jasmine/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cwd, env } from "node:process";
import { env } from "node:process";
import * as allure from "allure-js-commons";
import { Stage, Status } from "allure-js-commons";
import type { RuntimeMessage } from "allure-js-commons/sdk";
Expand All @@ -10,10 +10,12 @@ import {
ReporterRuntime,
getEnvironmentLabels,
getSuiteLabels,
hasSkipLabel,
} from "allure-js-commons/sdk/reporter";
import { MessageTestRuntime, setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
import type { JasmineBeforeAfterFn } from "./model.js";
import { findAnyError, findMessageAboutThrow, last } from "./utils.js";
import { enableAllureJasmineTestPlan } from "./testplan.js";
import { findAnyError, findMessageAboutThrow, getAllureNamesAndLabels, last } from "./utils.js";

class AllureJasmineTestRuntime extends MessageTestRuntime {
constructor(private readonly allureJasmineReporter: AllureJasmineReporter) {
Expand Down Expand Up @@ -51,7 +53,7 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter {

setGlobalTestRuntime(testRuntime);

this.#enableAllureFixtures();
this.#enableAllureFeatures();

// the best place to start global container for hooks and nested suites
const scopeUuid = this.allureRuntime.startScope();
Expand All @@ -71,13 +73,6 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter {
)[0] as string[];
}

private getSpecFullName(spec: jasmine.SpecResult & { filename?: string }) {
const specFilename = (spec.filename || "").replace(cwd(), "").replace(/^[/\\]/, "");
const specPath = this.getCurrentSpecPath().concat(spec.description).join(" > ");

return `${specFilename}#${specPath}`;
}

getAllureInterface() {
return allure;
}
Expand Down Expand Up @@ -125,16 +120,24 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter {
this.#stopScope();
}

specStarted(spec: jasmine.SpecResult): void {
this.#startScope();
this.currentAllureTestUuid = this.allureRuntime.startTest(
{
name: spec.description,
fullName: this.getSpecFullName(spec),
stage: Stage.RUNNING,
},
this.scopesStack,
specStarted(spec: jasmine.SpecResult & { filename?: string }): void {
const { fullName, labels, name } = getAllureNamesAndLabels(
spec.filename,
this.getCurrentSpecPath(),
spec.description,
);
if (!hasSkipLabel(labels)) {
this.#startScope();
this.currentAllureTestUuid = this.allureRuntime.startTest(
{
name,
fullName,
labels,
stage: Stage.RUNNING,
},
this.scopesStack,
);
}
}

specDone(spec: jasmine.SpecResult): void {
Expand Down Expand Up @@ -228,6 +231,11 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter {
}
};

#enableAllureFeatures = () => {
this.#enableAllureFixtures();
enableAllureJasmineTestPlan();
};

#enableAllureFixtures(): void {
const jasmineBeforeAll: JasmineBeforeAfterFn = global.beforeAll;
const jasmineAfterAll: JasmineBeforeAfterFn = global.afterAll;
Expand Down
11 changes: 10 additions & 1 deletion packages/allure-jasmine/src/model.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export type JasmineBeforeAfterFn = typeof beforeEach;
export type JasmineBeforeAfterFn = typeof global.beforeEach;

export type JasmineSpecFn = typeof global.it;

export type JasmineSuiteFn = typeof global.describe;

export type TestPlanIndex = {
ids: ReadonlySet<string>;
fullNames: ReadonlySet<string>;
};
113 changes: 113 additions & 0 deletions packages/allure-jasmine/src/testplan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { LabelName } from "allure-js-commons";
import type { Label } from "allure-js-commons";
import { addSkipLabelAsMeta, parseTestPlan } from "allure-js-commons/sdk/reporter";
import type { JasmineSpecFn, JasmineSuiteFn, TestPlanIndex } from "./model.js";
import { getAllureNamesAndLabels } from "./utils.js";

const dirname = path.dirname(fileURLToPath(import.meta.url));

const getCallerFileStackTraceFormatFn = (_: Error, stackTraces: NodeJS.CallSite[]): string | undefined => {
return stackTraces[0]?.getFileName();
};

const getCallerFile = <TFn extends (...args: any) => any>(fn: TFn) => {
const originalPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = getCallerFileStackTraceFormatFn;
try {
const obj = {};
Error.captureStackTrace(obj, fn);
return (obj as any).stack as ReturnType<typeof getCallerFileStackTraceFormatFn>;
} finally {
Error.prepareStackTrace = originalPrepareStackTrace;
}
};

const originalErrorToString = (e: Error) => Error.prototype.toString.call(e);

const defaultPrepareStackTrace = (error: Error, stackTraces: NodeJS.CallSite[]): string =>
stackTraces.length === 0
? originalErrorToString(error)
: [originalErrorToString(error), ...stackTraces].join("\n at ");

const isAllureJasmineFrame = (frame: NodeJS.CallSite) => frame.getFileName()?.startsWith(dirname + path.sep) ?? false;

const createStackFilter =
(prepareStackTrace: (error: Error, stackTraces: NodeJS.CallSite[]) => any) =>
(error: Error, stackTraces: NodeJS.CallSite[]) =>
prepareStackTrace(
error,
stackTraces.filter((frame) => !isAllureJasmineFrame(frame)),
);

const hideAllureFramesFromFunc = <TArgs extends any[], TReturn, TFn extends (...args: TArgs) => TReturn>(
target: TFn,
...args: TArgs
) => {
const originalPrepareStackTrace = Error.prepareStackTrace;
const underlyingPrepareStackTrace = originalPrepareStackTrace ?? defaultPrepareStackTrace;
Error.prepareStackTrace = createStackFilter(underlyingPrepareStackTrace);
try {
return target(...args);
} finally {
Error.prepareStackTrace = originalPrepareStackTrace;
}
};

const getIndexedTestPlan = (): TestPlanIndex | undefined => {
const testplan = parseTestPlan();
if (testplan) {
return {
ids: new Set(testplan.tests.filter((e) => e.id).map((e) => e.id!.toString())),
fullNames: new Set(testplan.tests.filter((e) => e.selector).map((e) => e.selector!)),
};
}
};

const isInTestPlan = (testplan: TestPlanIndex | undefined, fullName: string, labels: readonly Label[]) => {
if (testplan && !testplan.fullNames.has(fullName)) {
const allureId = labels.find((l) => l.name === LabelName.ALLURE_ID)?.value;
return allureId && testplan.ids.has(allureId);
}
return true;
};

export const enableAllureJasmineTestPlan = () => {
const jasmineDescribe: JasmineSuiteFn = global.describe;
const jasmineIt: JasmineSpecFn = global.it;
const jasmineXit: JasmineSpecFn = global.xit;

const suites: string[] = [];
let currentFile: string | undefined;
const testplan = getIndexedTestPlan();

global.describe = (description: string, specDefinitions: () => void) => {
const callerFile = getCallerFile(global.describe);
if (!callerFile) {
return hideAllureFramesFromFunc(jasmineDescribe, description, specDefinitions);
} else {
if (callerFile !== currentFile) {
currentFile = callerFile;
suites.splice(0, suites.length);
}

suites.push(description);
try {
return hideAllureFramesFromFunc(jasmineDescribe, description, specDefinitions);
} finally {
suites.pop();
}
}
};

global.it = (expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number) => {
const filename = currentFile ?? getCallerFile(global.it);
const { fullName, labels } = getAllureNamesAndLabels(filename, suites, expectation);
if (isInTestPlan(testplan, fullName, labels)) {
return hideAllureFramesFromFunc(jasmineIt, expectation, assertion, timeout);
} else {
return hideAllureFramesFromFunc(jasmineXit, addSkipLabelAsMeta(expectation), assertion, timeout);
}
};
};
20 changes: 19 additions & 1 deletion packages/allure-jasmine/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// eslint-disable-next-line no-undef
import { cwd } from "node:process";
import { extractMetadataFromString } from "allure-js-commons/sdk";

import FailedExpectation = jasmine.FailedExpectation;

export const findAnyError = (expectations?: FailedExpectation[]): FailedExpectation | null => {
Expand All @@ -14,3 +16,19 @@ export const findMessageAboutThrow = (expectations?: FailedExpectation[]) => {
};

export const last = <T>(arr: readonly T[]) => (arr.length ? arr[arr.length - 1] : undefined);

export const getAllureNamesAndLabels = (
filename: string | undefined,
suites: readonly string[],
rawSpecName: string,
) => {
const filePart = (filename || "").replace(cwd(), "").replace(/^[/\\]/, "");
const { cleanTitle: specName, labels } = extractMetadataFromString(rawSpecName);
const specPart = [...suites, specName].join(" > ");

return {
name: specName,
fullName: `${filePart}#${specPart}`,
labels,
};
};
32 changes: 0 additions & 32 deletions packages/allure-jasmine/test/spec/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: expect.objectContaining({
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
}),
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -458,10 +454,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: expect.objectContaining({
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
}),
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -497,10 +489,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: expect.objectContaining({
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
}),
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -535,10 +523,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: expect.objectContaining({
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
}),
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -573,10 +557,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
},
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -610,10 +590,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
},
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -647,10 +623,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
},
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down Expand Up @@ -685,10 +657,6 @@ describe("hook failures", () => {
expect.objectContaining({
name: "bar",
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("Not run because a beforeAll function failed"),
trace: expect.anything(),
},
}),
]);
expect(groups, "has broken fixture").toEqual([
Expand Down
Loading

0 comments on commit 8ffa35d

Please sign in to comment.