UNPKG

jest-allure2-reporter

Version:
357 lines (298 loc) 11.9 kB
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)); }); }