jest-allure2-reporter
Version:
Idiomatic Jest reporter for Allure Framework
357 lines (298 loc) • 11.9 kB
text/typescript
import path from 'node:path';
import type {
AggregatedResult,
Config,
ReporterOnStartOptions,
Test,
TestCaseResult,
TestContext,
TestResult,
} from '@jest/reporters';
import type { TestInvocationMetadata } from 'jest-metadata';
import JestMetadataReporter from 'jest-metadata/reporter';
import type {
AllureTestCaseMetadata,
AllureTestFileMetadata,
AllureTestRunMetadata,
GlobalExtractorContext,
Helpers,
PropertyExtractorContext,
ReporterOptions,
TestCaseExtractorContext,
TestFileExtractorContext,
TestRunExtractorContext,
} from 'jest-allure2-reporter';
import { type AllureWriter } from 'allure-store';
import { type ReporterConfig, resolveOptions, resolveWriter } from '../options';
import { AllureMetadataProxy, MetadataSquasher } from '../metadata';
import {
compactArray,
DeferredTaskQueue,
FileNavigatorCache,
memoize,
stringifyValues,
} from '../utils';
import { log, optimizeForTracing } from '../logger';
import * as fallbacks from './fallbacks';
import { postProcessMetadata } from './postProcessMetadata';
import { writeTest } from './allureCommons';
import { resolvePromisedItem, resolvePromisedTestCase } from './resolveTestItem';
const NOT_INITIALIZED = null as any;
const __TID = optimizeForTracing((test?: Test) => ({
tid: ['jest-allure2-reporter', test ? test.path : 'run'],
}));
const __TID_NAME = optimizeForTracing((test: Test, testCaseResult: TestCaseResult) => ({
tid: ['jest-allure2-reporter', test.path],
fullName: testCaseResult.fullName,
}));
interface QueuedTestResult {
test: Test;
testCaseResult: TestCaseResult;
testInvocationMetadata: TestInvocationMetadata;
}
export class JestAllure2Reporter extends JestMetadataReporter {
private readonly _globalConfig: Config.GlobalConfig;
private readonly _options: ReporterOptions;
private _globalContext: GlobalExtractorContext = NOT_INITIALIZED;
private _writer: AllureWriter = NOT_INITIALIZED;
private _config: ReporterConfig<void> = NOT_INITIALIZED;
private _globalMetadataProxy: AllureMetadataProxy<AllureTestRunMetadata> = NOT_INITIALIZED;
private readonly _taskQueue = new DeferredTaskQueue<QueuedTestResult, TestInvocationMetadata>({
getThreadName: (item) => item.test.path,
getPayloadKey: (item) => item.testInvocationMetadata,
execute: (item) =>
log.trace.complete(
__TID_NAME(item.test, item.testCaseResult),
item.testCaseResult.title,
() => this.#writeTestResult(item),
),
});
constructor(globalConfig: Config.GlobalConfig, options: ReporterOptions) {
super(globalConfig);
this._globalConfig = globalConfig;
this._options = options;
}
async #init() {
this._config = await resolveOptions(this._globalConfig.rootDir, this._options);
this._writer = await resolveWriter(this._globalConfig.rootDir, this._config);
await this._writer.init?.();
const testRunMetadata = JestAllure2Reporter.query.globalMetadata();
this._globalMetadataProxy = new AllureMetadataProxy<AllureTestRunMetadata>(testRunMetadata);
this._globalMetadataProxy.set('config', {
resultsDir: this._config.resultsDir,
overwrite: this._config.overwrite,
attachments: this._config.attachments,
injectGlobals: this._config.injectGlobals,
});
}
async #attempt(name: string, function_: () => unknown) {
try {
await function_.call(this);
} catch (error: unknown) {
log.error(error, `Caught unhandled error in JestAllure2Reporter#${name}`);
}
}
async #attemptSync(name: string, function_: () => unknown) {
try {
function_.call(this);
} catch (error: unknown) {
log.error(error, `Caught unhandled error in JestAllure2Reporter#${name}`);
}
}
async onRunStart(
aggregatedResult: AggregatedResult,
options: ReporterOnStartOptions,
): Promise<void> {
await super.onRunStart(aggregatedResult, options);
const attemptRunStart = this.#attempt.bind(this, 'onRunStart()', this.#onRunStart);
log.trace.begin(__TID(), 'jest-allure2-reporter');
await log.trace.complete(__TID(), 'init', this.#init.bind(this));
await log.trace.complete(__TID(), 'onRunStart', attemptRunStart);
}
async #onRunStart() {
const reporterConfig = this._config;
const allureWriter = this._writer;
const globalContext = {
$: {} as Helpers,
globalConfig: this._globalConfig,
reporterConfig,
value: undefined,
};
globalContext.$ = await resolvePromisedItem(globalContext, reporterConfig.helpers, '$');
if (reporterConfig.sourceCode.enabled) {
const { factories, options, plugins } = reporterConfig.sourceCode;
const maybePlugins = await Promise.all(
Object.entries(factories).map(async ([key, factory]) =>
factory({
...globalContext,
value: options[key],
}),
),
);
plugins.push(...compactArray(maybePlugins));
}
this._globalContext = globalContext;
const environment = await reporterConfig.environment(globalContext);
if (environment) {
await allureWriter.writeEnvironmentInfo(stringifyValues(environment));
}
const executor = await reporterConfig.executor(globalContext);
if (executor) {
await allureWriter.writeExecutorInfo(executor);
}
const categories = await reporterConfig.categories(globalContext);
if (categories) {
await allureWriter.writeCategories(categories);
}
}
async onTestFileStart(test: Test) {
super.onTestFileStart(test);
const execute = this.#onTestFileStart.bind(this, test);
const attempt = this.#attemptSync.bind(this, 'onTestFileStart()', execute);
const testPath = path.relative(this._globalConfig.rootDir, test.path);
log.trace.begin(__TID(test), testPath);
await log.trace.complete(__TID(test), 'onTestFileStart', attempt);
}
async #onTestFileStart(test: Test) {
await FileNavigatorCache.instance.scanSourcemap(test.path);
const rawMetadata = JestAllure2Reporter.query.test(test);
const testFileMetadata = new AllureMetadataProxy<AllureTestFileMetadata>(rawMetadata);
fallbacks.onTestFileStart(test, testFileMetadata);
}
onTestCaseStart(test: Test, testCaseResult: TestCaseResult) {
super.onTestCaseStart(test, testCaseResult);
this._taskQueue.startPending(test.path);
}
onTestCaseResult(test: Test, testCaseResult: TestCaseResult) {
super.onTestCaseResult(test, testCaseResult);
const testEntryMetadata = JestAllure2Reporter.query.testCaseResult(testCaseResult);
const testInvocationMetadata = testEntryMetadata.lastInvocation!;
fallbacks.onTestCaseResult(
test,
testCaseResult,
new AllureMetadataProxy<AllureTestCaseMetadata>(testInvocationMetadata),
);
this._taskQueue.enqueue({
test,
testCaseResult,
testInvocationMetadata,
});
}
async #writeTestResult({ test, testCaseResult, testInvocationMetadata }: QueuedTestResult) {
await this.#scanSourceMaps(test.path);
await postProcessMetadata(this._globalContext, testInvocationMetadata);
const squasher = new MetadataSquasher();
const testCaseContext: PropertyExtractorContext<TestCaseExtractorContext, void> = {
...this._globalContext,
filePath: path.relative(this._globalConfig.rootDir, test.path).split(path.sep),
result: {},
testCase: testCaseResult,
testCaseMetadata: squasher.testInvocation(testInvocationMetadata),
testFileMetadata: squasher.testFile(testInvocationMetadata.file),
testRunMetadata: this._globalMetadataProxy.get(),
value: undefined,
};
const allureTest = await resolvePromisedTestCase(testCaseContext, this._config.testCase);
if (!allureTest) {
return;
}
const invocationIndex =
testInvocationMetadata.definition.invocations.indexOf(testInvocationMetadata);
await writeTest({
resultsDir: this._config.resultsDir,
containerName: `${testCaseResult.fullName} (${invocationIndex})`,
writer: this._writer,
test: allureTest,
});
}
async onTestFileResult(test: Test, testResult: TestResult, aggregatedResult: AggregatedResult) {
super.onTestFileResult(test, testResult, aggregatedResult);
const execute = this.#onTestFileResult.bind(this, test, testResult);
const attempt = this.#attempt.bind(this, 'onTestFileResult()', execute);
await log.trace.complete(__TID(test), 'onTestFileResult', attempt);
log.trace.end(__TID(test));
}
async #onTestFileResult(test: Test, testResult: TestResult) {
const rawMetadata = JestAllure2Reporter.query.test(test);
const testFileMetadata = new AllureMetadataProxy<AllureTestFileMetadata>(rawMetadata);
fallbacks.onTestFileResult(test, testFileMetadata);
for (const testCaseResult of testResult.testResults) {
const invocations =
JestAllure2Reporter.query.testCaseResult(testCaseResult).invocations || [];
for (const testInvocationMetadata of invocations) {
this._taskQueue.enqueue({
test,
testCaseResult,
testInvocationMetadata,
});
}
}
await this._taskQueue.awaitCompletion();
const config = this._config;
const squasher = new MetadataSquasher();
const globalMetadataProxy = this._globalMetadataProxy;
const testFileContext: PropertyExtractorContext<TestFileExtractorContext, void> = {
...this._globalContext,
filePath: path.relative(this._globalConfig.rootDir, testResult.testFilePath).split(path.sep),
result: {},
testFile: testResult,
testFileMetadata: squasher.testFile(JestAllure2Reporter.query.testResult(testResult)),
testRunMetadata: globalMetadataProxy.get(),
value: undefined,
};
const allureFileTest = await resolvePromisedTestCase(testFileContext, config.testFile);
if (allureFileTest) {
await writeTest({
resultsDir: config.resultsDir,
containerName: `${testResult.testFilePath}`,
writer: this._writer,
test: allureFileTest,
});
}
}
async onRunComplete(
testContexts: Set<TestContext>,
aggregatedResult: AggregatedResult,
): Promise<void> {
await super.onRunComplete(testContexts, aggregatedResult);
const execute = this.#onRunComplete.bind(this, aggregatedResult);
const attempt = this.#attempt.bind(this, 'onRunComplete()', execute);
await log.trace.complete(__TID(), 'onRunComplete', attempt);
await log.trace.end(__TID());
}
async #onRunComplete(aggregatedResult: AggregatedResult) {
const globalMetadataProxy = this._globalMetadataProxy;
globalMetadataProxy.set('stop', Date.now());
const allureWriter = this._writer;
const config = this._config;
const globalContext = this._globalContext;
const testRunContext: PropertyExtractorContext<TestRunExtractorContext, void> = {
...globalContext,
aggregatedResult,
result: {},
testRunMetadata: globalMetadataProxy.get(),
value: undefined,
};
const allureRunTest = await resolvePromisedTestCase(testRunContext, config.testRun);
if (allureRunTest) {
await writeTest({
resultsDir: config.resultsDir,
containerName: `Test Run (${process.pid})`,
writer: allureWriter,
test: allureRunTest,
});
}
await allureWriter.cleanup?.();
}
// Executing this once per file should be enough to scan source maps of auxiliary files
#scanSourceMaps = memoize(async (_testPath: string) => {
const globalMetadataProxy = this._globalMetadataProxy;
const loadedFiles = globalMetadataProxy
.get<string[]>('loadedFiles', [])
.filter(FileNavigatorCache.instance.hasScannedSourcemap);
if (loadedFiles.length === 0) {
return;
}
return Promise.all(loadedFiles.map(FileNavigatorCache.instance.scanSourcemap));
});
}