diff --git a/e2e/jest.config.js b/e2e/jest.config.js index f50e424..2654eaa 100644 --- a/e2e/jest.config.js +++ b/e2e/jest.config.js @@ -50,6 +50,12 @@ const jestAllure2ReporterOptions = { tms: 'https://test.testrail.io/index.php?/cases/view/{{name}}', }, }, + testRun: { + ignored: false, + }, + testFile: { + ignored: false, + }, testStep: { ignored: ({ testStepMetadata }) => testStepMetadata.displayName?.includes('(Jest)'), }, diff --git a/e2e/presets/code-snippets.js b/e2e/presets/code-snippets.js index d5fc51a..5d6149c 100644 --- a/e2e/presets/code-snippets.js +++ b/e2e/presets/code-snippets.js @@ -3,8 +3,12 @@ const path = require('node:path'); const description = async ({ $, testRunMetadata, testFileMetadata, testCaseMetadata }) => { const metadata = testCaseMetadata ?? testFileMetadata ?? testRunMetadata; const text = metadata.description?.join('\n\n') ?? ''; - const codes = await $.extractSourceCode(metadata, true); - const snippets = codes.map($.source2markdown); + const steps = metadata.steps || []; + const before = steps.filter(s => s.hookType?.startsWith('before')); + const after = steps.filter(s => s.hookType?.startsWith('after')); + const allMetadata = [...before, metadata, ...after]; + const codes = await Promise.all(allMetadata.map(m => $.extractSourceCode(m, true))); + const snippets = codes.filter(Boolean).map($.source2markdown); return [text, ...snippets].filter(t => t != null).join('\n\n'); }; diff --git a/e2e/test_plan.md b/e2e/test_plan.md index 9bcc51b..ec7cbe0 100644 --- a/e2e/test_plan.md +++ b/e2e/test_plan.md @@ -1,17 +1,6 @@ # Test Plan 1. Test cases: - * by **status**: - * passed - * failed - * broken - * skipped - * unknown - * by **severity**: - * default - * custom - * for an individual test - * for a test suite * by **category**: * Product defect * Test defect @@ -26,19 +15,6 @@ * from a broken "afterAll" hook * from broken test suite files * broken due to a failed test environment setup - * with **labels**: - * flaky - * custom tag (value) - * custom label (key=value) - * maintainer? - * lead? - * JIRA - * TMS - * custom - * with **Description** - * with **Parameters** - * with **Links** - * with **Environment** * with **Execution**: * Set up * Test body @@ -66,11 +42,6 @@ * Package * ... 3. Report-scoped features: - * Environment - * Full - * Zero - * Filtered (strings) - * Filtered (callback) * Executor (build agent name) 4. Test run history * Duration diff --git a/e2e/tests/sanity/decorators/Step.ts b/e2e/tests/sanity/decorators/Step.ts index ce52f4f..c6affef 100644 --- a/e2e/tests/sanity/decorators/Step.ts +++ b/e2e/tests/sanity/decorators/Step.ts @@ -1,4 +1,4 @@ -import { Step } from '../../../../dist/api'; +import { Step } from 'jest-allure2-reporter/api'; describe('Step', () => { class FactorialCalculator { diff --git a/e2e/tests/sanity/runtime/createAttachment.js b/e2e/tests/sanity/runtime/createAttachment.js index bfe3207..519fb3f 100644 --- a/e2e/tests/sanity/runtime/createAttachment.js +++ b/e2e/tests/sanity/runtime/createAttachment.js @@ -1,13 +1,13 @@ const os = require('node:os'); test('should wrap a function with automatic attachment of its result', async () => { - const diagnostics = () => JSON.stringify({ available_memory: os.freemem() }); + const diagnostics = () => ({ available_memory: os.freemem() }); const wrapped = allure.createAttachment(diagnostics, 'diagnostics-{{0}}.json'); // Creates 'diagnostics-before.json' attachment - expect(wrapped('before')).toMatch(/{"available_memory":\d+}/); + expect(wrapped('before')).toEqual({ available_memory: expect.any(Number) }); // Consume some memory expect(Array.from({ length: 100000 }, () => Math.random())).toHaveLength(100000); // Creates 'diagnostics-after.json' attachment; the returned value is not changed by the wrapper - expect(wrapped('after')).toMatch(/{"available_memory":\d+}/); + expect(wrapped('after')).toEqual({ available_memory: expect.any(Number) }); }); diff --git a/index.d.ts b/index.d.ts index 23ad72b..4eec7bf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -37,11 +37,6 @@ declare module 'jest-allure2-reporter' { * Configure how external attachments are attached to the report. */ attachments?: AttachmentsOptions; - /** - * Tweak the way markdown is processed. - * You can enable or disable the processor, add remark plugins, etc. - */ - markdown?: MarkdownProcessorOptions; /** * Tweak the way source code and docblocks are extracted from test files. */ @@ -119,13 +114,6 @@ declare module 'jest-allure2-reporter' { /** @see {@link AttachmentsOptions#contentHandler} */ export type BuiltinContentAttachmentHandler = 'write'; - export interface MarkdownProcessorOptions { - enabled?: boolean; - keepSource?: boolean; - remarkPlugins?: MaybeWithOptions[]; - rehypePlugins?: MaybeWithOptions[]; - } - export interface SourceCodeProcessorOptions { enabled?: boolean; plugins?: Record; @@ -140,11 +128,11 @@ declare module 'jest-allure2-reporter' { export interface SourceCodePlugin { readonly name: string; - extractDocblock?(context: Readonly): MaybePromise; - extractSourceCode?(context: Readonly): MaybePromise; + extractDocblock?(context: Readonly): MaybePromise; + extractSourceCode?(location: Readonly, includeComments: boolean): MaybePromise; } - export interface SourceCodeExtractionContext extends AllureTestItemSourceLocation { + export interface DocblockExtractionContext extends AllureTestItemSourceLocation { transformedCode?: string; } @@ -408,30 +396,19 @@ declare module 'jest-allure2-reporter' { /** * Provides an optimized way to navigate through the test file content. * Accepts a file path in a string or a split array format. - * Returns undefined if the file is not found or cannot be read. + * @param filePath - the path to the file to navigate, split by directory separators or as a single string. + * @returns a file navigator object or undefined if the file is not found or cannot be read. */ getFileNavigator(filePath: string | string[]): Promise; /** - * Extracts the source code of the current test case, test step or a test file. - * Pass `true` as the second argument to extract source code recursively from all steps. - * + * Extracts the source code of the current test case or step. + * @param metadata - the metadata object of the test case or step. + * @param includeComments - whether to include comments before the actual code. + * @returns the extracted source code or undefined if the source code is not found. * @example * ({ $, testFileMetadata }) => $.extractSourceCode(testFileMetadata) - * @example - * ({ $, testCaseMetadata }) => $.extractSourceCode(testCaseMetadata, true) */ - extractSourceCode: ExtractSourceCodeHelper; - /** - * Extracts the executor information from the current environment. - * Pass `true` as the argument to include local executor information. - * By default, supports GitHub Actions and Buildkite. - * - * @example - * ({ $ }) => $.getExecutorInfo() - * @example - * ({ $ }) => $.getExecutorInfo(true) - */ - getExecutorInfo: GetExecutorInfoHelper; + extractSourceCode(metadata: AllureTestItemMetadata, includeComments?: boolean): Promise; /** * Extracts the manifest of the current project or a specific package. * Pass a callback to extract specific data from the manifest – this way you can omit async/await. @@ -446,6 +423,13 @@ declare module 'jest-allure2-reporter' { * ({ $ }) => (await $.manifest('jest')).version */ manifest: ManifestHelper; + /** + * Strips ANSI escape codes from the given string or object. + * @example + * $.stripAnsi('Hello, \u001b[31mworld\u001b[0m!') + * @example + * $.stripAnsi({ message: 'Hello, \u001b[31mworld\u001b[0m!' }) + */ stripAnsi: StripAnsiHelper; } @@ -461,17 +445,6 @@ declare module 'jest-allure2-reporter' { readLine(lineNumber?: number): string; } - export interface ExtractSourceCodeHelper { - (metadata: AllureTestItemMetadata, recursive?: false): Promise; - (metadata: AllureTestItemMetadata, recursive: true): Promise; - (metadata: AllureTestItemMetadata, recursive: boolean): Promise | undefined>; - } - - export interface GetExecutorInfoHelper { - (): MaybePromise; - (includeLocal: true): MaybePromise; - } - export interface ManifestHelper { (packageName?: string): Promise | undefined>; (extractor: string[] | ManifestHelperExtractor): Promise; @@ -672,7 +645,7 @@ declare module 'jest-allure2-reporter' { export interface AllureTestCaseResult { uuid?: string; - ignored: boolean; + ignored?: boolean; historyId: Primitive; displayName: string; fullName: string; @@ -691,7 +664,7 @@ declare module 'jest-allure2-reporter' { } export interface AllureTestStepResult { - ignored: boolean; + ignored?: boolean; hookType?: AllureTestStepMetadata['hookType']; displayName: string; start: number; diff --git a/src/environment/listener.ts b/src/environment/listener.ts index 93a5efb..9b7aeff 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -95,19 +95,23 @@ function addSourceCode({ event }: TestEnvironmentCircusEvent) { let code = ''; if (event.name === 'add_hook') { const { hookType, fn } = event; - code = `${hookType}(${fn});`; + const functionCode = String(fn); - if (code.includes("during setup, this cannot be null (and it's fine to explode if it is)")) { + if ( + functionCode.includes("during setup, this cannot be null (and it's fine to explode if it is)") + ) { code = ''; realm.runtimeContext .getCurrentMetadata() .set('displayName', 'Reset mocks, modules and timers (Jest)'); + } else { + code = `${hookType}(${autoIndent(functionCode)});`; } } if (event.name === 'add_test') { const { testName, fn } = event; - code = `test(${JSON.stringify(testName)}, ${fn});`; + code = `test(${JSON.stringify(testName)}, ${autoIndent(String(fn))});`; } if (code) { diff --git a/src/logger/logger.ts b/src/logger/logger.ts index bf581a1..78b5744 100644 --- a/src/logger/logger.ts +++ b/src/logger/logger.ts @@ -1,10 +1,17 @@ -import { bunyamin, threadGroups } from 'bunyamin'; +import { bunyamin, type BunyaminLogRecordFields, isDebug, threadGroups } from 'bunyamin'; export const log = bunyamin.child({ cat: 'jest-allure2-reporter', tid: 'jest-allure2-reporter', }); +const nofields: BunyaminLogRecordFields = {}; +const noop = () => nofields; + +export const optimizeForTracing = isDebug('jest-allure2-reporter') + ? BunyaminLogRecordFields>(function_: T): T => function_ + : () => noop; + threadGroups.add({ id: 'jest-allure2-reporter', displayName: 'jest-allure2-reporter', diff --git a/src/options/default/index.ts b/src/options/default/index.ts index 542b644..11fe577 100644 --- a/src/options/default/index.ts +++ b/src/options/default/index.ts @@ -1,8 +1,8 @@ +import * as sourceCode from '../../source-code'; import type { ReporterConfig } from '../types'; import * as common from '../common'; import * as custom from '../custom'; import * as helpers from '../helpers'; -import * as sourceCode from '../source-code'; import { categories } from './categories'; import { testRun } from './testRun'; @@ -24,12 +24,6 @@ export function defaultOptions(): ReporterConfig { enabled: true, plugins: sourceCode, }), - markdown: { - enabled: true, - keepSource: true, - remarkPlugins: ['remark-gfm'], - rehypePlugins: ['rehype-highlight'], - }, helpers: custom.helpers(helpers)!, testRun: custom.testCase(testRun), testFile: custom.testCase(testFile), @@ -37,6 +31,6 @@ export function defaultOptions(): ReporterConfig { testStep: custom.testStep(testStep), categories: common.constant(categories), environment: () => ({}), - executor: ({ $ }) => $.getExecutorInfo(true), + executor: () => ({}), }; } diff --git a/src/options/default/testRun.ts b/src/options/default/testRun.ts index 9452c3e..a5348d7 100644 --- a/src/options/default/testRun.ts +++ b/src/options/default/testRun.ts @@ -6,7 +6,7 @@ import { compose2 } from '../common'; export const testRun: TestCaseCustomizer = { ignored: true, historyId: ({ testRunMetadata, result }) => testRunMetadata.historyId ?? result.fullName, - fullName: async ({ $, testRunMetadata }) => + fullName: ({ $, testRunMetadata }) => testRunMetadata.fullName ?? $.manifest(['name'], 'untitled project'), displayName: ({ testRunMetadata }) => testRunMetadata.displayName ?? '(test run)', description: ({ testRunMetadata }) => testRunMetadata.description?.join('\n\n') ?? '', diff --git a/src/options/extendOptions.ts b/src/options/extendOptions.ts index 5df8821..d0b3c50 100644 --- a/src/options/extendOptions.ts +++ b/src/options/extendOptions.ts @@ -19,12 +19,6 @@ export function extendOptions( contentHandler: custom?.attachments?.contentHandler ?? base.attachments.contentHandler, fileHandler: custom?.attachments?.fileHandler ?? base.attachments.fileHandler, }, - markdown: { - enabled: custom?.markdown?.enabled ?? base.markdown.enabled, - keepSource: custom?.markdown?.keepSource ?? base.markdown.keepSource, - remarkPlugins: [...base.markdown.remarkPlugins, ...(custom?.markdown?.remarkPlugins ?? [])], - rehypePlugins: [...base.markdown.rehypePlugins, ...(custom?.markdown?.rehypePlugins ?? [])], - }, sourceCode: custom?.sourceCode ? mergeSourceCodeConfigs(base.sourceCode, customizers.sourceCode(custom.sourceCode)) : base.sourceCode, diff --git a/src/options/helpers/executor/index.ts b/src/options/helpers/executor/index.ts deleted file mode 100644 index a6202d1..0000000 --- a/src/options/helpers/executor/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ExecutorInfo, KeyedHelperCustomizer } from 'jest-allure2-reporter'; - -import { - BuildkiteInfoProvider, - type ExecutorInfoProvider, - GitHubInfoProvider, - LocalInfoProvider, -} from './providers'; - -const isEnabled = (provider: ExecutorInfoProvider) => provider.enabled; - -async function getExecutorInfoImpl(): Promise; -async function getExecutorInfoImpl(includeLocal: true): Promise; -async function getExecutorInfoImpl(includeLocal = false) { - const environment = process.env as Record; - - const providers: ExecutorInfoProvider[] = [ - new BuildkiteInfoProvider(environment), - new GitHubInfoProvider(environment), - new LocalInfoProvider(includeLocal), - ]; - - return providers.find(isEnabled)?.getExecutorInfo(); -} - -export const getExecutorInfo: KeyedHelperCustomizer<'getExecutorInfo'> = () => getExecutorInfoImpl; diff --git a/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.test.ts b/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.test.ts deleted file mode 100644 index 27b2791..0000000 --- a/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BuildkiteInfoProvider } from './BuildkiteInfoProvider'; - -describe('BuildkiteInfoProvider', () => { - const environment = { - BUILDKITE: 'true', - BUILDKITE_AGENT_NAME: 'Buildkite Agent', - BUILDKITE_BUILD_URL: 'https://buildkite.com/owner/repo/builds/123', - BUILDKITE_BUILD_NUMBER: '123', - }; - - it('should return Buildkite info when enabled', async () => { - const provider = new BuildkiteInfoProvider(environment); - expect(provider.enabled).toBe(true); - - const info = await provider.getExecutorInfo(); - expect(info).toEqual({ - name: expect.stringContaining('Buildkite Agent'), - type: 'buildkite', - buildUrl: 'https://buildkite.com/owner/repo/builds/123', - buildOrder: 123, - buildName: '#123', - }); - }); - - it('should return default info when not enabled', async () => { - const provider = new BuildkiteInfoProvider({}); - expect(provider.enabled).toBe(false); - - const info = await provider.getExecutorInfo(); - expect(info).toEqual({ - name: expect.stringContaining('Buildkite'), - type: 'buildkite', - buildUrl: undefined, - buildOrder: Number.NaN, - buildName: '#undefined', - }); - }); -}); diff --git a/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.ts b/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.ts deleted file mode 100644 index d61090c..0000000 --- a/src/options/helpers/executor/providers/buildkite/BuildkiteInfoProvider.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import { type ExecutorInfoProvider, getOSDetails } from '../common'; - -export interface BuildkiteEnvironment { - BUILDKITE?: string; - BUILDKITE_AGENT_NAME?: string; - BUILDKITE_BUILD_URL?: string; - BUILDKITE_BUILD_NUMBER?: string; -} - -export class BuildkiteInfoProvider implements ExecutorInfoProvider { - constructor(private readonly environment: Partial) {} - - get enabled() { - return !!this.environment.BUILDKITE; - } - - async getExecutorInfo(): Promise { - const { BUILDKITE_AGENT_NAME, BUILDKITE_BUILD_URL, BUILDKITE_BUILD_NUMBER } = this.environment; - const agentName = BUILDKITE_AGENT_NAME || 'Buildkite'; - - return { - name: `${agentName} (${getOSDetails()})`, - type: 'buildkite', - buildUrl: BUILDKITE_BUILD_URL, - buildOrder: Number(BUILDKITE_BUILD_NUMBER), - buildName: `#${BUILDKITE_BUILD_NUMBER}`, - }; - } -} diff --git a/src/options/helpers/executor/providers/buildkite/index.ts b/src/options/helpers/executor/providers/buildkite/index.ts deleted file mode 100644 index abf652e..0000000 --- a/src/options/helpers/executor/providers/buildkite/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BuildkiteInfoProvider'; diff --git a/src/options/helpers/executor/providers/common/ExecutorInfoProvider.ts b/src/options/helpers/executor/providers/common/ExecutorInfoProvider.ts deleted file mode 100644 index 8abfe6a..0000000 --- a/src/options/helpers/executor/providers/common/ExecutorInfoProvider.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -export interface ExecutorInfoProvider { - readonly enabled: boolean; - getExecutorInfo(): Promise; -} diff --git a/src/options/helpers/executor/providers/common/getOSDetails.ts b/src/options/helpers/executor/providers/common/getOSDetails.ts deleted file mode 100644 index 7a7397a..0000000 --- a/src/options/helpers/executor/providers/common/getOSDetails.ts +++ /dev/null @@ -1,5 +0,0 @@ -import os from 'node:os'; - -export function getOSDetails() { - return `${os.type()} ${os.release()}/${os.arch()}`; -} diff --git a/src/options/helpers/executor/providers/common/index.ts b/src/options/helpers/executor/providers/common/index.ts deleted file mode 100644 index df3d472..0000000 --- a/src/options/helpers/executor/providers/common/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ExecutorInfoProvider'; -export * from './getOSDetails'; diff --git a/src/options/helpers/executor/providers/github/GitHubInfoProvider.test.ts b/src/options/helpers/executor/providers/github/GitHubInfoProvider.test.ts deleted file mode 100644 index 7c061f9..0000000 --- a/src/options/helpers/executor/providers/github/GitHubInfoProvider.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports */ -jest.mock('node-fetch'); - -import { type GitHubEnvironment, GitHubInfoProvider } from './GitHubInfoProvider'; - -describe('GitHubInfoProvider', () => { - const apiUrl = 'https://api.github.com/repos/owner/repo/actions/runs/123/attempts/1/jobs'; - - let fetch: jest.MockedFunction; - let environment: Partial; - - beforeEach(() => { - environment = { - GITHUB_ACTIONS: 'true', - GITHUB_JOB: 'test_job', - GITHUB_REPOSITORY: 'owner/repo', - GITHUB_RUN_ATTEMPT: '1', - GITHUB_RUN_ID: '123', - GITHUB_RUN_NUMBER: '10', - GITHUB_SERVER_URL: 'https://github.com', - GITHUB_TOKEN: 'my_token', - RUNNER_NAME: 'GitHub Runner', - }; - }); - - beforeEach(() => { - fetch = jest.requireMock('node-fetch'); - fetch.mockClear(); - }); - - const mockSuccessResponse = (jobs: any[]) => ({ - ok: true, - json: jest.fn().mockResolvedValue({ jobs }), - }); - - it('should be disabled if GITHUB_ACTIONS env var is undefined', () => { - const provider = new GitHubInfoProvider({ - ...environment, - GITHUB_ACTIONS: '', - }); - - expect(provider.enabled).toBe(false); - }); - - it('should be enabled if GITHUB_ACTIONS env var is defined', () => { - const provider = new GitHubInfoProvider({ - ...environment, - GITHUB_ACTIONS: '', - }); - - expect(provider.enabled).toBe(false); - }); - - it('should return executor info when API request is successful', async () => { - const jobInfo = { - id: 'job_id', - name: 'Test Job', - html_url: 'https://github.com/owner/repo/actions/runs/123#test_job', - }; - - fetch.mockResolvedValueOnce(mockSuccessResponse([jobInfo]) as any); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: jobInfo.html_url, - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when job is not found', async () => { - fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when API request fails', async () => { - const mockError = new Error('API error'); - fetch.mockRejectedValueOnce(mockError); - jest.spyOn(console, 'error').mockReturnValueOnce(); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when token is not provided', async () => { - delete environment.GITHUB_TOKEN; - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should include authorization header when githubToken is provided', async () => { - fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); - - const provider = new GitHubInfoProvider(environment); - await provider.getExecutorInfo(); - - expect(fetch).toHaveBeenCalledWith(apiUrl, { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: 'token my_token', - }, - }); - }); -}); diff --git a/src/options/helpers/executor/providers/github/GitHubInfoProvider.ts b/src/options/helpers/executor/providers/github/GitHubInfoProvider.ts deleted file mode 100644 index 4902962..0000000 --- a/src/options/helpers/executor/providers/github/GitHubInfoProvider.ts +++ /dev/null @@ -1,99 +0,0 @@ -import fetch from 'node-fetch'; -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import { snakeCase } from '../../../../../utils'; -import { type ExecutorInfoProvider, getOSDetails } from '../common'; - -export interface GitHubEnvironment { - GITHUB_ACTIONS: string; - GITHUB_JOB: string; - GITHUB_REPOSITORY: string; - GITHUB_RUN_ATTEMPT: string; - GITHUB_RUN_ID: string; - GITHUB_RUN_NUMBER: string; - GITHUB_SERVER_URL: string; - GITHUB_TOKEN: string; - RUNNER_NAME?: string; -} - -type Job = { - id: string; - name: string; - html_url: string; -}; - -export class GitHubInfoProvider implements ExecutorInfoProvider { - constructor(private readonly environment: Partial) {} - - get enabled() { - return !!this.environment.GITHUB_ACTIONS; - } - - async getExecutorInfo(): Promise { - const job = await this._fetchJob(); - - const { - GITHUB_RUN_ATTEMPT, - GITHUB_RUN_ID, - GITHUB_RUN_NUMBER, - GITHUB_JOB, - RUNNER_NAME, - GITHUB_SERVER_URL, - GITHUB_REPOSITORY, - } = this.environment; - - const runnerName = RUNNER_NAME || 'GitHub Actions'; - const buildUrl = job - ? job.html_url - : `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - - return { - name: `${runnerName} (${getOSDetails()})`, - type: 'github', - buildUrl, - buildOrder: Number(GITHUB_RUN_NUMBER) * 10 + Number(GITHUB_RUN_ATTEMPT), - buildName: `${GITHUB_RUN_ID}#${GITHUB_JOB}`, - }; - } - - private async _fetchJob(): Promise { - const url = this._buildApiUrl(); - - try { - const data = await this._fetchJobs(url); - return this._findJob(data.jobs); - } catch (error: unknown) { - console.error(`Failed to fetch job ID from: ${url}\nReason:`, error); - return; - } - } - - private async _fetchJobs(url: string) { - if (!this.environment.GITHUB_TOKEN) { - return { jobs: [] }; - } - - const headers = { - Accept: 'application/vnd.github.v3+json', - Authorization: `token ${this.environment.GITHUB_TOKEN}`, - }; - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}`); - } - - return await response.json(); - } - - private _buildApiUrl(): string { - const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT } = this.environment; - return `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs`; - } - - private _findJob(jobs: Job[]): Job | undefined { - const { GITHUB_JOB } = this.environment; - - return jobs.length === 1 ? jobs[0] : jobs.find((job) => snakeCase(job.name) === GITHUB_JOB); - } -} diff --git a/src/options/helpers/executor/providers/github/index.ts b/src/options/helpers/executor/providers/github/index.ts deleted file mode 100644 index 32c0cc2..0000000 --- a/src/options/helpers/executor/providers/github/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './GitHubInfoProvider'; diff --git a/src/options/helpers/executor/providers/index.ts b/src/options/helpers/executor/providers/index.ts deleted file mode 100644 index 4c735a9..0000000 --- a/src/options/helpers/executor/providers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { ExecutorInfoProvider } from './common'; - -export * from './buildkite'; -export * from './github'; -export * from './local'; diff --git a/src/options/helpers/executor/providers/local/LocalInfoProvider.test.ts b/src/options/helpers/executor/providers/local/LocalInfoProvider.test.ts deleted file mode 100644 index a79aa95..0000000 --- a/src/options/helpers/executor/providers/local/LocalInfoProvider.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LocalInfoProvider } from './LocalInfoProvider'; - -describe('LocalInfoProvider', () => { - it('should return local info', async () => { - const provider = new LocalInfoProvider(true); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringMatching(/.* \(.*\/.*\)/), - type: expect.any(String), - }); - }); -}); diff --git a/src/options/helpers/executor/providers/local/LocalInfoProvider.ts b/src/options/helpers/executor/providers/local/LocalInfoProvider.ts deleted file mode 100644 index 0774fdb..0000000 --- a/src/options/helpers/executor/providers/local/LocalInfoProvider.ts +++ /dev/null @@ -1,16 +0,0 @@ -import os from 'node:os'; - -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import { type ExecutorInfoProvider, getOSDetails } from '../common'; - -export class LocalInfoProvider implements ExecutorInfoProvider { - constructor(public readonly enabled: boolean) {} - - async getExecutorInfo(): Promise { - return { - name: `${os.hostname()} (${getOSDetails()})`, - type: `${os.platform()}-${os.arch()}`, - }; - } -} diff --git a/src/options/helpers/executor/providers/local/index.ts b/src/options/helpers/executor/providers/local/index.ts deleted file mode 100644 index 355f792..0000000 --- a/src/options/helpers/executor/providers/local/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './LocalInfoProvider'; diff --git a/src/options/helpers/extractSourceCode.ts b/src/options/helpers/extractSourceCode.ts index d836a18..82734c0 100644 --- a/src/options/helpers/extractSourceCode.ts +++ b/src/options/helpers/extractSourceCode.ts @@ -1,68 +1,45 @@ import type { AllureTestItemMetadata, - AllureNestedTestStepMetadata, ExtractSourceCodeHelperResult, KeyedHelperCustomizer, - ExtractSourceCodeHelper, } from 'jest-allure2-reporter'; import { log } from '../../logger'; -import { compactArray, defaults, isEmpty } from '../../utils'; +import { defaults, isEmpty } from '../../utils'; import type { ReporterConfig } from '../types'; export const extractSourceCode: KeyedHelperCustomizer<'extractSourceCode'> = ({ reporterConfig, -}): ExtractSourceCodeHelper => { +}) => { const config = reporterConfig as ReporterConfig; - async function extractRecursively( - item: AllureTestItemMetadata, - ): Promise { - const steps = item.steps || []; - const before = steps.filter(isBefore); - const after = steps.filter(isAfter); - const data = [...before, item, ...after]; - const result = await Promise.all(data.map(extractSingle)); - return compactArray(result); - } - - async function extractSingle( + return async function extractSourceCodeHelper( item: AllureTestItemMetadata, + includeComments = false, ): Promise { let result: ExtractSourceCodeHelperResult = {}; - const context = { ...item.sourceLocation, transformedCode: item.transformedCode }; + const context = item.sourceLocation; const plugins = config.sourceCode ? Object.values(config.sourceCode.plugins) : []; - log.trace(context, 'Extracting source code'); + if (isEmpty(context)) { + return undefined; + } + for (const p of plugins) { try { - result = defaults(result, await p.extractSourceCode?.(context)); + result = defaults(result, await p.extractSourceCode?.(context, includeComments)); } catch (error: unknown) { log.warn( error, `Plugin "${p.name}" failed to extract source code for ${context.fileName}:${context.lineNumber}:${context.columnNumber}`, ); } - if (result.code) { + if (result?.code) { break; } } return isEmpty(result) ? undefined : result; - } - - function extractSourceCodeHelper(item: AllureTestItemMetadata, recursive?: boolean): any { - return recursive ? extractRecursively(item) : extractSingle(item); - } - - return extractSourceCodeHelper; + }; }; - -function isBefore(step: AllureNestedTestStepMetadata): boolean { - return step.hookType === 'beforeAll' || step.hookType === 'beforeEach'; -} - -function isAfter(step: AllureNestedTestStepMetadata): boolean { - return step.hookType === 'afterAll' || step.hookType === 'afterEach'; -} diff --git a/src/options/helpers/index.ts b/src/options/helpers/index.ts index a29ed68..eba9cbb 100644 --- a/src/options/helpers/index.ts +++ b/src/options/helpers/index.ts @@ -1,5 +1,4 @@ export { getFileNavigator } from './file-navigator'; -export { getExecutorInfo } from './executor'; -export { manifest } from './manifest'; export { extractSourceCode } from './extractSourceCode'; +export { manifest } from './manifest'; export { stripAnsi } from './strip-ansi'; diff --git a/src/options/index.ts b/src/options/index.ts index 11db2c3..0b602a4 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -1,23 +1,21 @@ import type { ReporterOptions } from 'jest-allure2-reporter'; -import { asArray, importCwd } from '../utils'; +import { asArray, importFrom } from '../utils'; import { testCaseSteps } from './custom'; import { defaultOptions } from './default'; import { extendOptions } from './extendOptions'; import type { ReporterConfig } from './types'; -import { defaultOverrides } from './override'; import { combineTestCaseAndSteps } from './combineTestCaseAndSteps'; -export async function resolveOptions(custom?: ReporterOptions | undefined) { - const extensions = custom ? await resolveExtendsChain(custom) : []; +export async function resolveOptions(rootDirectory: string, custom?: ReporterOptions | undefined) { + const extensions = custom ? await resolveExtendsChain(rootDirectory, custom) : []; let config: ReporterConfig = defaultOptions(); for (const extension of extensions) { config = extendOptions(config, extension); } - config = extendOptions(config, defaultOverrides()); config.testFile = combineTestCaseAndSteps( config.testFile, testCaseSteps(config.testStep, 'testFileMetadata'), @@ -35,13 +33,17 @@ export async function resolveOptions(custom?: ReporterOptions | undefined) { } export async function resolveExtendsChain( + rootDirectory: string, custom: ReporterOptions | undefined, ): Promise { if (custom) { const chain: ReporterOptions[] = [custom]; for (const reference of asArray(custom.extends)) { - const config = typeof reference === 'string' ? await importCwd(reference) : reference; - chain.unshift(...(await resolveExtendsChain(config))); + const resolution = + typeof reference === 'string' + ? await importFrom(reference, rootDirectory) + : { dirname: rootDirectory, exports: reference }; + chain.unshift(...(await resolveExtendsChain(resolution.dirname, resolution.exports))); } return chain; } diff --git a/src/options/override/index.ts b/src/options/override/index.ts deleted file mode 100644 index a5a1b08..0000000 --- a/src/options/override/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReporterOptions } from 'jest-allure2-reporter'; - -import { labels } from './labels'; - -export function defaultOverrides(): ReporterOptions { - return { - testRun: { - labels, - }, - testFile: { - labels, - }, - testCase: { - labels, - }, - }; -} diff --git a/src/options/override/labels.ts b/src/options/override/labels.ts deleted file mode 100644 index 362c08b..0000000 --- a/src/options/override/labels.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { LabelsCustomizer } from 'jest-allure2-reporter'; - -import { last } from '../common'; - -export const labels: LabelsCustomizer<{}> = { - owner: last, - package: last, - parentSuite: last, - severity: last, - subSuite: last, - suite: last, - testClass: last, - testMethod: last, - thread: last, -}; diff --git a/src/options/source-code/file/index.ts b/src/options/source-code/file/index.ts deleted file mode 100644 index 9879473..0000000 --- a/src/options/source-code/file/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { SourceCodePluginCustomizer } from 'jest-allure2-reporter'; - -import { merge } from '../../../utils'; - -export type FallbackSourceCodePluginOptions = { - languagePatterns?: Record>; -}; - -function compilePatterns(record: Record> | undefined) { - if (!record) return []; - - return Object.entries(record).map(([language, patterns]): [string, RegExp[]] => [ - language, - patterns.map((pattern) => (typeof pattern === 'string' ? new RegExp(pattern) : pattern)), - ]); -} - -export const file: SourceCodePluginCustomizer = ({ $, value = {} }) => { - const options = merge( - { - languagePatterns: { - javascript: [/\.[cm]?jsx?$/], - typescript: [/\.[cm]?tsx?$/], - }, - }, - value as FallbackSourceCodePluginOptions, - ); - - const patterns = compilePatterns(options.languagePatterns); - - function detectLanguage(fileName: string) { - return patterns.find(([, patterns]) => patterns.some((pattern) => pattern.test(fileName)))?.[0]; - } - - return { - name: 'file', - - extractSourceCode: ({ fileName, lineNumber }) => { - if (fileName && !lineNumber) { - return $.getFileNavigator(fileName).then((navigator) => { - const language = detectLanguage(fileName); - if (navigator) { - return { - code: navigator.getContent(), - language, - fileName, - }; - } - - if (language) { - return { language }; - } - - return; - }); - } - - return; - }, - }; -}; diff --git a/src/options/source-code/typescript/index.ts b/src/options/source-code/typescript/index.ts deleted file mode 100644 index 83a0a97..0000000 --- a/src/options/source-code/typescript/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { - SourceCodeExtractionContext, - SourceCodePluginCustomizer, -} from 'jest-allure2-reporter'; -import { extract, parseWithComments } from 'jest-docblock'; - -import { autoIndent, importCwd } from '../../../utils'; -import { detectJS } from '../common'; - -import { ASTHelper } from './ASTHelper'; - -export const typescript: SourceCodePluginCustomizer = async ({ $ }) => { - const ts = await importCwd('typescript').catch(() => null); - - function canProcess( - context: SourceCodeExtractionContext, - ): context is Required { - const { fileName, lineNumber, columnNumber } = context; - return Boolean(ts && fileName && lineNumber && columnNumber && detectJS(fileName)); - } - - const hast = new ASTHelper(ts); - return { - name: 'typescript', - - extractSourceCode(context) { - return canProcess(context) - ? $.getFileNavigator(context.fileName).then((navigator) => { - if (!navigator) return; - if (!navigator.jump(context.lineNumber)) return; - const lineNumber = context.lineNumber; - const columnNumber = Math.min(context.columnNumber, navigator.readLine().length); - - const ast = - hast.getAST(context.fileName) || - hast.parseAST(context.fileName, navigator.getContent()); - const expression = hast.findNodeInAST(ast, lineNumber, columnNumber); - const code = autoIndent(ast.text.slice(expression.getStart(), expression.getEnd())); - navigator.jumpToPosition(expression.getStart()); - const [startLine] = navigator.getPosition(); - navigator.jumpToPosition(expression.getEnd()); - const [endLine] = navigator.getPosition(); - - return { - code, - language: 'typescript', - fileName: context.fileName, - startLine, - endLine, - }; - }) - : undefined; - }, - - extractDocblock(context) { - return canProcess(context) - ? $.getFileNavigator(context.fileName).then((navigator) => { - if (!navigator) return; - if (!navigator.jump(context.lineNumber)) return; - const lineNumber = context.lineNumber; - const columnNumber = Math.min(context.columnNumber, navigator.readLine().length); - - const ast = - hast.getAST(context.fileName) || - hast.parseAST(context.fileName, navigator.getContent()); - const expression = hast.findNodeInAST(ast, lineNumber, columnNumber); - const fullStart = expression.getFullStart(); - const start = expression.getStart(); - const docblock = extract(ast.text.slice(fullStart, start).trim()); - return docblock ? parseWithComments(docblock) : undefined; - }) - : undefined; - }, - }; -}; diff --git a/src/options/types.ts b/src/options/types.ts index 4c3fec4..d3018ae 100644 --- a/src/options/types.ts +++ b/src/options/types.ts @@ -12,7 +12,6 @@ import type { TestRunExtractorContext, Primitive, PromisedProperties, - MarkdownProcessorOptions, MaybePromise, TestStepExtractorContext, SourceCodePluginCustomizer, @@ -32,7 +31,6 @@ export interface ReporterConfig { resultsDir: string; injectGlobals: boolean; attachments: Required; - markdown: Required; sourceCode: SourceCodeProcessorConfig; categories: CategoriesExtractor; environment: EnvironmentExtractor; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index 62b68f0..0d20028 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -27,20 +27,31 @@ import { type ReporterConfig, resolveOptions } from '../options'; import { AllureMetadataProxy, MetadataSquasher } from '../metadata'; import { compactArray, stringifyValues } from '../utils'; import { type AllureWriter, FileAllureWriter } from '../serialization'; +import { log, optimizeForTracing } from '../logger'; import * as fallbacks from './fallbacks'; -import { overwriteDirectory } from './overwriteDirectory'; 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, +})); + export class JestAllure2Reporter extends JestMetadataReporter { private readonly _globalConfig: Config.GlobalConfig; private readonly _options: ReporterOptions; - private _globalContext?: GlobalExtractorContext; - private _writer?: AllureWriter; - private _config?: ReporterConfig; - private _globalMetadataProxy?: AllureMetadataProxy; + private _globalContext: GlobalExtractorContext = NOT_INITIALIZED; + private _writer: AllureWriter = NOT_INITIALIZED; + private _config: ReporterConfig = NOT_INITIALIZED; + private _globalMetadataProxy: AllureMetadataProxy = NOT_INITIALIZED; constructor(globalConfig: Config.GlobalConfig, options: ReporterOptions) { super(globalConfig); @@ -50,11 +61,14 @@ export class JestAllure2Reporter extends JestMetadataReporter { } async #init() { - this._config = await resolveOptions(this._options); + this._config = await resolveOptions(this._globalConfig.rootDir, this._options); this._writer = new FileAllureWriter({ resultsDir: this._config.resultsDir, + overwrite: this._config.overwrite, }); + await this._writer.init?.(); + const testRunMetadata = JestAllure2Reporter.query.globalMetadata(); this._globalMetadataProxy = new AllureMetadataProxy(testRunMetadata); this._globalMetadataProxy.set('config', { @@ -65,15 +79,38 @@ export class JestAllure2Reporter extends JestMetadataReporter { }); } + 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 { await super.onRunStart(aggregatedResult, options); - await this.#init(); + const attemptInit = this.#attempt.bind(this, 'onRunStart#init()', this.#init); + const attemptRunStart = this.#attempt.bind(this, 'onRunStart()', this.#onRunStart); + + await log.trace.begin(__TID(), 'jest-allure2-reporter'); + await log.trace.complete(__TID(), 'init', attemptInit); + await log.trace.complete(__TID(), 'onRunStart', attemptRunStart); + } - const reporterConfig = this._config!; - const allureWriter = this._writer!; + async #onRunStart() { + const reporterConfig = this._config; + const allureWriter = this._writer; const globalContext = { $: {} as Helpers, @@ -100,29 +137,33 @@ export class JestAllure2Reporter extends JestMetadataReporter { this._globalContext = globalContext; - if (reporterConfig.overwrite) { - await overwriteDirectory(reporterConfig.resultsDir); - } - const environment = await reporterConfig.environment(globalContext); if (environment) { - allureWriter.writeEnvironmentInfo(stringifyValues(environment)); + await allureWriter.writeEnvironmentInfo(stringifyValues(environment)); } const executor = await reporterConfig.executor(globalContext); if (executor) { - allureWriter.writeExecutorInfo(executor); + await allureWriter.writeExecutorInfo(executor); } const categories = await reporterConfig.categories(globalContext); if (categories) { - allureWriter.writeCategories(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); + log.trace.complete(__TID(test), 'onTestFileStart', attempt); + } + + #onTestFileStart(test: Test) { const rawMetadata = JestAllure2Reporter.query.test(test); const testFileMetadata = new AllureMetadataProxy(rawMetadata); @@ -132,6 +173,12 @@ export class JestAllure2Reporter extends JestMetadataReporter { async onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { super.onTestCaseResult(test, testCaseResult); + const execute = this.#onTestCaseResult.bind(this, test, testCaseResult); + const attempt = this.#attempt.bind(this, 'onTestCaseResult()', execute); + log.trace.complete(__TID_NAME(test, testCaseResult), testCaseResult.title, attempt); + } + + #onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { const testCaseMetadata = new AllureMetadataProxy( JestAllure2Reporter.query.testCaseResult(testCaseResult).lastInvocation!, ); @@ -142,21 +189,28 @@ export class JestAllure2Reporter extends JestMetadataReporter { async onTestFileResult(test: Test, testResult: TestResult, aggregatedResult: AggregatedResult) { await 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(rawMetadata); - const globalMetadataProxy = this._globalMetadataProxy!; - const allureWriter = this._writer!; + const globalMetadataProxy = this._globalMetadataProxy; + const allureWriter = this._writer; fallbacks.onTestFileResult(test, testFileMetadata); - await postProcessMetadata(this._globalContext!, rawMetadata); + await postProcessMetadata(this._globalContext, rawMetadata); // --- - const config = this._config!; + const config = this._config; const squasher = new MetadataSquasher(); const testFileContext: PropertyExtractorContext = { - ...this._globalContext!, + ...this._globalContext, filePath: path.relative(this._globalConfig.rootDir, testResult.testFilePath).split(path.sep), result: {}, @@ -168,7 +222,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { const allureFileTest = await resolvePromisedTestCase(testFileContext, config.testFile); if (allureFileTest) { - writeTest({ + await writeTest({ resultsDir: config.resultsDir, containerName: `${testResult.testFilePath}`, writer: allureWriter, @@ -196,7 +250,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { const invocationIndex = allInvocations.indexOf(testInvocationMetadata); - writeTest({ + await writeTest({ resultsDir: config.resultsDir, containerName: `${testCaseResult.fullName} (${invocationIndex})`, writer: allureWriter, @@ -212,12 +266,19 @@ export class JestAllure2Reporter extends JestMetadataReporter { ): Promise { await super.onRunComplete(testContexts, aggregatedResult); - const globalMetadataProxy = this._globalMetadataProxy!; + 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 allureWriter = this._writer; + const config = this._config; + const globalContext = this._globalContext; const testRunContext: PropertyExtractorContext = { ...globalContext, aggregatedResult, @@ -228,12 +289,14 @@ export class JestAllure2Reporter extends JestMetadataReporter { const allureRunTest = await resolvePromisedTestCase(testRunContext, config.testRun); if (allureRunTest) { - writeTest({ + await writeTest({ resultsDir: config.resultsDir, containerName: `Test Run (${process.pid})`, writer: allureWriter, test: allureRunTest, }); } + + await allureWriter.cleanup?.(); } } diff --git a/src/reporter/allure-adapter/ensureUUID.ts b/src/reporter/allure-adapter/ensureUUID.ts new file mode 100644 index 0000000..5a7bc71 --- /dev/null +++ b/src/reporter/allure-adapter/ensureUUID.ts @@ -0,0 +1,18 @@ +import type { AllureTestCaseResult } from 'jest-allure2-reporter'; +import { v4, validate } from 'uuid'; + +import { log } from '../../logger'; + +export function ensureUUID(result: AllureTestCaseResult) { + const { uuid, fullName } = result; + if (uuid && !validate(uuid)) { + log.warn(`Detected invalid "uuid" (${uuid}) in test: ${fullName}`); + return v4(); + } + + if (!uuid) { + return v4(); + } + + return uuid; +} diff --git a/src/reporter/allure-adapter/hashHistoryId.ts b/src/reporter/allure-adapter/hashHistoryId.ts new file mode 100644 index 0000000..c48efe8 --- /dev/null +++ b/src/reporter/allure-adapter/hashHistoryId.ts @@ -0,0 +1,16 @@ +import { randomBytes } from 'node:crypto'; + +import type { AllureTestCaseResult } from 'jest-allure2-reporter'; + +import { log } from '../../logger'; +import { md5 } from '../../utils'; + +export function hashHistoryId(result: AllureTestCaseResult) { + const { fullName, historyId } = result; + if (!historyId) { + log.warn(`Detected empty "historyId" in test: ${fullName}`); + return md5(randomBytes(16)); + } + + return md5(String(historyId)); +} diff --git a/src/reporter/allure-adapter/index.ts b/src/reporter/allure-adapter/index.ts new file mode 100644 index 0000000..3a77578 --- /dev/null +++ b/src/reporter/allure-adapter/index.ts @@ -0,0 +1,2 @@ +export * from './toTestContainer'; +export * from './toTestResult'; diff --git a/src/reporter/allure-adapter/normalizeAttachments.ts b/src/reporter/allure-adapter/normalizeAttachments.ts new file mode 100644 index 0000000..ed5d78a --- /dev/null +++ b/src/reporter/allure-adapter/normalizeAttachments.ts @@ -0,0 +1,13 @@ +import path from 'node:path'; + +import type { AllureTestCaseResult, AllureTestStepResult } from 'jest-allure2-reporter'; + +export function normalizeAttachments( + rootDirectory: string, + result: AllureTestCaseResult | AllureTestStepResult, +) { + return result.attachments?.map((attachment) => { + const source = path.relative(rootDirectory, attachment.source); + return source.startsWith('..') ? attachment : { ...attachment, source }; + }); +} diff --git a/src/reporter/allure-adapter/normalizeLabels.ts b/src/reporter/allure-adapter/normalizeLabels.ts new file mode 100644 index 0000000..bffba46 --- /dev/null +++ b/src/reporter/allure-adapter/normalizeLabels.ts @@ -0,0 +1,32 @@ +import type { AllureTestCaseResult, Label } from 'jest-allure2-reporter'; + +const SINGLE_LABELS = new Set([ + 'owner', + 'package', + 'parentSuite', + 'severity', + 'subSuite', + 'suite', + 'testClass', + 'testMethod', + 'thread', +]); + +export function normalizeLabels(test: AllureTestCaseResult) { + if (test.labels) { + const accumulator: Record = {}; + + for (const label of test.labels) { + if (SINGLE_LABELS.has(label.name)) { + accumulator[label.name] = [label]; + } else { + accumulator[label.name] = accumulator[label.name] || []; + accumulator[label.name].push(label); + } + } + + return Object.values(accumulator).flat(); + } + + return; +} diff --git a/src/reporter/allure-adapter/normalizeParameters.ts b/src/reporter/allure-adapter/normalizeParameters.ts new file mode 100644 index 0000000..19978d1 --- /dev/null +++ b/src/reporter/allure-adapter/normalizeParameters.ts @@ -0,0 +1,9 @@ +import type { AllureTestCaseResult, AllureTestStepResult, Parameter } from 'jest-allure2-reporter'; + +export function normalizeParameters(result: AllureTestCaseResult | AllureTestStepResult) { + return result.parameters?.map(stringifyParameter); +} + +function stringifyParameter({ name, value }: Parameter) { + return { name, value: String(value) }; +} diff --git a/src/reporter/allure-adapter/toTestContainer.ts b/src/reporter/allure-adapter/toTestContainer.ts new file mode 100644 index 0000000..dd267dc --- /dev/null +++ b/src/reporter/allure-adapter/toTestContainer.ts @@ -0,0 +1,31 @@ +import type { AllureTestCaseResult } from 'jest-allure2-reporter'; +import { v5 } from 'uuid'; + +import type { AllureContainer } from '../../serialization'; + +import { toTestStep } from './toTestStep'; + +const NAMESPACE = v5('jest-allure2-reporter', '00000000-0000-0000-0000-000000000000'); + +export type TestContainerOptions = { + name: string; + rootDir: string; + testUUID: string; +}; + +export function toTestContainer( + test: AllureTestCaseResult, + options: TestContainerOptions, +): AllureContainer { + return { + uuid: v5(options.testUUID, NAMESPACE), + name: options.name, + children: [options.testUUID], + befores: test.steps + ?.filter((step) => step.hookType?.startsWith('before')) + .map((step) => toTestStep(options.rootDir, step)), + afters: test.steps + ?.filter((step) => step.hookType?.startsWith('after')) + .map((step) => toTestStep(options.rootDir, step)), + }; +} diff --git a/src/reporter/allure-adapter/toTestResult.ts b/src/reporter/allure-adapter/toTestResult.ts new file mode 100644 index 0000000..b2d3a98 --- /dev/null +++ b/src/reporter/allure-adapter/toTestResult.ts @@ -0,0 +1,33 @@ +import type { AllureTestCaseResult } from 'jest-allure2-reporter'; + +import type { AllureResult as Serialized } from '../../serialization'; + +import { ensureUUID } from './ensureUUID'; +import { hashHistoryId } from './hashHistoryId'; +import { normalizeLabels } from './normalizeLabels'; +import { normalizeAttachments } from './normalizeAttachments'; +import { normalizeParameters } from './normalizeParameters'; +import { toTestStep } from './toTestStep'; + +export function toTestResult(rootDirectory: string, test: AllureTestCaseResult): Serialized { + return { + uuid: ensureUUID(test), + historyId: hashHistoryId(test), + name: test.displayName, + fullName: test.fullName, + start: test.start, + stop: test.stop, + description: test.descriptionHtml ? undefined : test.description, + descriptionHtml: test.descriptionHtml, + stage: test.stage, + status: test.status, + statusDetails: test.statusDetails, + steps: test.steps + ?.filter((step) => !step.hookType) + .map((step) => toTestStep(rootDirectory, step)), + labels: normalizeLabels(test), + links: test.links, + attachments: normalizeAttachments(rootDirectory, test), + parameters: normalizeParameters(test), + }; +} diff --git a/src/reporter/allure-adapter/toTestStep.ts b/src/reporter/allure-adapter/toTestStep.ts new file mode 100644 index 0000000..f16d80a --- /dev/null +++ b/src/reporter/allure-adapter/toTestStep.ts @@ -0,0 +1,20 @@ +import type { AllureTestStepResult } from 'jest-allure2-reporter'; + +import type { AllureStep as Serialized } from '../../serialization'; + +import { normalizeAttachments } from './normalizeAttachments'; +import { normalizeParameters } from './normalizeParameters'; + +export function toTestStep(rootDirectory: string, step: AllureTestStepResult): Serialized { + return { + name: step.displayName, + start: step.start, + stop: step.stop, + stage: step.stage, + status: step.status, + statusDetails: step.statusDetails, + steps: step.steps?.map((inner) => toTestStep(rootDirectory, inner)), + attachments: normalizeAttachments(rootDirectory, step), + parameters: normalizeParameters(step), + }; +} diff --git a/src/reporter/allureCommons.ts b/src/reporter/allureCommons.ts index 58361f1..b71b8fe 100644 --- a/src/reporter/allureCommons.ts +++ b/src/reporter/allureCommons.ts @@ -1,12 +1,8 @@ -import { randomBytes } from 'node:crypto'; -import path from 'node:path'; +import type { AllureTestCaseResult } from 'jest-allure2-reporter'; -import type { AllureTestCaseResult, AllureTestStepResult } from 'jest-allure2-reporter'; -import { v4, v5, validate } from 'uuid'; +import type { AllureWriter } from '../serialization'; -import type { AllureTestResult, AllureTestResultContainer, AllureWriter } from '../serialization'; -import { log } from '../logger'; -import { md5 } from '../utils'; +import { toTestContainer, toTestResult } from './allure-adapter'; type CreateTestArguments = { resultsDir: string; @@ -16,118 +12,13 @@ type CreateTestArguments = { }; export async function writeTest({ resultsDir, writer, test, containerName }: CreateTestArguments) { - const testResult: AllureTestResult = { - uuid: test.uuid, - historyId: test.historyId, - displayName: test.displayName, - fullName: test.fullName, - start: test.start, - stop: test.stop, - description: test.description, - descriptionHtml: test.descriptionHtml, - stage: test.stage, - status: test.status, - statusDetails: test.statusDetails, - labels: test.labels, - links: test.links, - steps: [], - attachments: test.attachments, - parameters: test.parameters, - }; - - const testUUID = ensureUUID(testResult); - hashHistoryId(testResult); - normalizeDisplayName(testResult); - normalizeAttachments(resultsDir, testResult); - normalizeParameters(testResult); - normalizeDescription(testResult); - - const testContainer: AllureTestResultContainer = { - uuid: v5(testUUID, '6ba7b810-9dad-11d1-80b4-00c04fd430c8'), + const testResult = toTestResult(resultsDir, test); + const testContainer = toTestContainer(test, { + rootDir: resultsDir, name: containerName, - children: [testUUID], - befores: [], - afters: [], - }; - - if (test.steps && testResult.steps) { - for (const step of test.steps) { - normalizeDisplayName(step); - normalizeAttachments(resultsDir, step); - normalizeParameters(step); - - if (!step.hookType) { - testResult.steps.push(step); - } else if (step.hookType.startsWith('before')) { - testContainer.befores.push(step); - } else if (step.hookType.startsWith('after')) { - testContainer.afters.push(step); - } else { - log.warn({ data: step }, `Unknown hook type: ${step.hookType}`); - } - } - } + testUUID: testResult.uuid, + }); await writer.writeContainer(testContainer); await writer.writeResult(testResult); } - -function ensureUUID(result: AllureTestResult) { - const { uuid, fullName } = result; - if (uuid && !validate(uuid)) { - log.warn(`Detected invalid "uuid" (${uuid}) in test: ${fullName}`); - return (result.uuid = v4()); - } - - if (!uuid) { - return (result.uuid = v4()); - } - - return uuid; -} - -function hashHistoryId(result: AllureTestResult) { - const { fullName, historyId } = result; - if (!historyId) { - log.warn(`Detected empty "historyId" in test: ${fullName}`); - result.historyId = md5(randomBytes(16)); - } - - result.historyId = md5(String(historyId)); -} - -function normalizeParameters(result: AllureTestResult | AllureTestStepResult) { - if (result.parameters) { - for (const parameter of result.parameters) { - parameter.value = String(parameter.value); - } - } -} - -function normalizeAttachments( - rootDirectory: string, - result: AllureTestResult | AllureTestStepResult, -) { - if (result.attachments) { - for (const attachment of result.attachments) { - const source = path.relative(rootDirectory, attachment.source); - if (!source.startsWith('..')) { - attachment.source = source; - } - } - } -} - -function normalizeDescription(result: AllureTestResult) { - if (result.descriptionHtml) { - delete result.description; - } -} - -function normalizeDisplayName(result: AllureTestResult | AllureTestStepResult) { - if (result.displayName) { - const violation: any = result; - violation.name = result.displayName; - delete violation.displayName; - } -} diff --git a/src/reporter/overwriteDirectory.ts b/src/reporter/overwriteDirectory.ts deleted file mode 100644 index 5e9c64d..0000000 --- a/src/reporter/overwriteDirectory.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'node:fs/promises'; - -export async function overwriteDirectory(directoryPath: string) { - await fs.rm(directoryPath, { recursive: true }); - await fs.mkdir(directoryPath, { recursive: true }); -} diff --git a/src/reporter/postProcessMetadata.ts b/src/reporter/postProcessMetadata.ts index 58585da..8f01457 100644 --- a/src/reporter/postProcessMetadata.ts +++ b/src/reporter/postProcessMetadata.ts @@ -1,13 +1,14 @@ import type { AllureTestItemMetadata, GlobalExtractorContext, - SourceCodeExtractionContext, + DocblockExtractionContext, } from 'jest-allure2-reporter'; import type { TestFileMetadata } from 'jest-metadata'; import { AllureMetadataProxy } from '../metadata'; import type { ReporterConfig } from '../options'; import { log } from '../logger'; +import { compactObject, isEmpty } from '../utils'; export async function postProcessMetadata( globalContext: GlobalExtractorContext, @@ -35,22 +36,25 @@ export async function postProcessMetadata( await Promise.all( batch.map(async (metadata) => { const allureProxy = new AllureMetadataProxy(metadata); - const context: SourceCodeExtractionContext = { + const context: DocblockExtractionContext = compactObject({ ...allureProxy.get('sourceLocation'), transformedCode: allureProxy.get('transformedCode'), - }; - for (const p of config.sourceCode.plugins) { - try { - const docblock = await p.extractDocblock?.(context); - if (docblock) { - allureProxy.assign({ docblock }); - break; + }); + + if (!isEmpty(context)) { + for (const p of config.sourceCode.plugins) { + try { + const docblock = await p.extractDocblock?.(context); + if (docblock) { + allureProxy.assign({ docblock }); + break; + } + } catch (error: unknown) { + log.warn( + error, + `Plugin "${p.name}" failed to extract docblock for ${context.fileName}:${context.lineNumber}:${context.columnNumber}`, + ); } - } catch (error: unknown) { - log.warn( - error, - `Plugin "${p.name}" failed to extract docblock for ${context.fileName}:${context.lineNumber}:${context.columnNumber}`, - ); } } }), diff --git a/src/runtime/AllureRuntimeImplementation.ts b/src/runtime/AllureRuntimeImplementation.ts index 73d74a7..01552d8 100644 --- a/src/runtime/AllureRuntimeImplementation.ts +++ b/src/runtime/AllureRuntimeImplementation.ts @@ -137,6 +137,7 @@ export class AllureRuntimeImplementation implements AllureRuntime { attachment: AllureRuntime['attachment'] = (name, content, maybeOptions) => { typeAssertions.assertString(name, 'name'); + typeAssertions.assertAttachmentContent(content, 'content'); const options = typeof maybeOptions === 'string' ? { mimeType: maybeOptions } : maybeOptions; diff --git a/src/runtime/modules/AttachmentsModule.ts b/src/runtime/modules/AttachmentsModule.ts index 301a138..f30be75 100644 --- a/src/runtime/modules/AttachmentsModule.ts +++ b/src/runtime/modules/AttachmentsModule.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import util from 'node:util'; import type { MaybePromise } from 'jest-allure2-reporter'; @@ -185,7 +186,11 @@ export class ContentAttachmentsModule extends AttachmentsModule< } protected _createMimeContext(name: string, content: AttachmentContent) { - return { sourcePath: name, content }; + let value = content; + if (typeof content !== 'string' && !Buffer.isBuffer(content) && !ArrayBuffer.isView(content)) { + value = util.inspect(content); + } + return { sourcePath: name, content: value }; } protected _createAttachmentContext(context: AttachmentContext) { diff --git a/src/serialization/AllureWriter.ts b/src/serialization/AllureWriter.ts index 54a4a61..b0f93bf 100644 --- a/src/serialization/AllureWriter.ts +++ b/src/serialization/AllureWriter.ts @@ -1,8 +1,12 @@ import type { - AllureTestCaseResult, - AllureTestStepResult, + Attachment, Category, ExecutorInfo, + Label, + Link, + Stage, + Status, + StatusDetails, } from 'jest-allure2-reporter'; export interface AllureWriter { @@ -11,21 +15,57 @@ export interface AllureWriter { writeCategories(categories: Category[]): Promise; - writeEnvironmentInfo(info: Record): Promise; + writeEnvironmentInfo(info: Record): Promise; writeExecutorInfo(info: ExecutorInfo): Promise; - writeContainer(result: AllureTestResultContainer): Promise; + writeContainer(result: AllureContainer): Promise; - writeResult(result: AllureTestResult): Promise; + writeResult(result: AllureResult): Promise; } -export interface AllureTestResultContainer { +export interface AllureContainer { uuid: string; name?: string; children: string[]; - befores: AllureTestStepResult[]; - afters: AllureTestStepResult[]; + befores?: AllureStep[]; + afters?: AllureStep[]; } -export type AllureTestResult = Omit; +export interface AllureResult { + uuid: string; + historyId: string; + name: string; + fullName: string; + start: number; + stop: number; + description?: string; + descriptionHtml?: string; + stage: Stage; + status: Status; + statusDetails?: StatusDetails; + steps?: AllureStep[]; + labels?: Label[]; + links?: Link[]; + attachments?: Attachment[]; + parameters?: AllureParameter[]; +} + +export interface AllureStep { + name: string; + start: number; + stop: number; + stage: Stage; + status: Status; + statusDetails?: StatusDetails; + steps?: AllureStep[]; + attachments?: Attachment[]; + parameters?: AllureParameter[]; +} + +export interface AllureParameter { + name: string; + value: string; + excluded?: boolean; + mode?: 'hidden' | 'masked' | 'default'; +} diff --git a/src/serialization/FileAllureWriter.ts b/src/serialization/FileAllureWriter.ts index 82b2907..668a733 100644 --- a/src/serialization/FileAllureWriter.ts +++ b/src/serialization/FileAllureWriter.ts @@ -4,13 +4,14 @@ import path from 'node:path'; import { stringify } from 'properties'; import type { Category, ExecutorInfo } from 'jest-allure2-reporter'; -import type { AllureTestResult, AllureTestResultContainer, AllureWriter } from './AllureWriter'; +import type { AllureResult, AllureContainer, AllureWriter } from './AllureWriter'; async function writeJson(path: string, data: unknown) { await fs.writeFile(path, JSON.stringify(data) + '\n'); } export interface FileSystemAllureWriterConfig { + overwrite: boolean; resultsDir: string; } @@ -22,9 +23,16 @@ export class FileAllureWriter implements AllureWriter { } async init() { - await fs.mkdir(this.#config.resultsDir, { - recursive: true, - }); + const { resultsDir, overwrite } = this.#config; + const directoryExists = await fs.access(resultsDir).then( + () => true, + () => false, + ); + if (overwrite && directoryExists) { + await fs.rm(resultsDir, { recursive: true }); + } + + await fs.mkdir(resultsDir, { recursive: true }); } async writeCategories(categories: Category[]) { @@ -32,7 +40,7 @@ export class FileAllureWriter implements AllureWriter { await writeJson(path, categories); } - async writeContainer(result: AllureTestResultContainer) { + async writeContainer(result: AllureContainer) { const path = this.#buildPath(`${result.uuid}-container.json`); await writeJson(path, result); } @@ -49,7 +57,7 @@ export class FileAllureWriter implements AllureWriter { await writeJson(path, info); } - async writeResult(result: AllureTestResult) { + async writeResult(result: AllureResult) { const path = this.#buildPath(`${result.uuid}-result.json`); await writeJson(path, result); } diff --git a/src/options/source-code/common/detectJS.ts b/src/source-code/common/detectJS.ts similarity index 100% rename from src/options/source-code/common/detectJS.ts rename to src/source-code/common/detectJS.ts diff --git a/src/options/source-code/common/index.ts b/src/source-code/common/index.ts similarity index 100% rename from src/options/source-code/common/index.ts rename to src/source-code/common/index.ts diff --git a/src/options/source-code/index.ts b/src/source-code/index.ts similarity index 71% rename from src/options/source-code/index.ts rename to src/source-code/index.ts index 94afcc0..b0791a5 100644 --- a/src/options/source-code/index.ts +++ b/src/source-code/index.ts @@ -1,3 +1,2 @@ -export * from './file'; export * from './javascript'; export * from './typescript'; diff --git a/src/options/source-code/javascript/__snapshots__/extractDocblockAbove.test.ts.snap b/src/source-code/javascript/__snapshots__/extractDocblockAbove.test.ts.snap similarity index 100% rename from src/options/source-code/javascript/__snapshots__/extractDocblockAbove.test.ts.snap rename to src/source-code/javascript/__snapshots__/extractDocblockAbove.test.ts.snap diff --git a/src/options/source-code/javascript/extractDocblockAbove.test.ts b/src/source-code/javascript/extractDocblockAbove.test.ts similarity index 96% rename from src/options/source-code/javascript/extractDocblockAbove.test.ts rename to src/source-code/javascript/extractDocblockAbove.test.ts index 64c6a30..429226a 100644 --- a/src/options/source-code/javascript/extractDocblockAbove.test.ts +++ b/src/source-code/javascript/extractDocblockAbove.test.ts @@ -1,4 +1,4 @@ -import { FileNavigator } from '../../../utils'; +import { FileNavigator } from '../../utils'; import { extractDocblockAbove as extractJsDocument_ } from './extractDocblockAbove'; diff --git a/src/options/source-code/javascript/extractDocblockAbove.ts b/src/source-code/javascript/extractDocblockAbove.ts similarity index 100% rename from src/options/source-code/javascript/extractDocblockAbove.ts rename to src/source-code/javascript/extractDocblockAbove.ts diff --git a/src/options/source-code/javascript/index.ts b/src/source-code/javascript/index.ts similarity index 62% rename from src/options/source-code/javascript/index.ts rename to src/source-code/javascript/index.ts index 140a940..377886d 100644 --- a/src/options/source-code/javascript/index.ts +++ b/src/source-code/javascript/index.ts @@ -1,42 +1,30 @@ import type { SourceCodePluginCustomizer } from 'jest-allure2-reporter'; import { extract, parseWithComments } from 'jest-docblock'; -import { autoIndent } from '../../../utils'; import { detectJS } from '../common'; +import { AllureRuntimeError } from '../../errors'; import { extractDocblockAbove } from './extractDocblockAbove'; -export type JavaScriptSourceCodePluginOptions = { - extractTransformedCode?: boolean; -}; +export interface JavaScriptSourceCodePluginOptions { + docblockPosition?: 'inside' | 'outside'; +} export const javascript: SourceCodePluginCustomizer = ({ $, value = {} }) => { const options = value as JavaScriptSourceCodePluginOptions; + const extractDocblockFromLine = + options.docblockPosition === 'inside' ? notImplemented : extractDocblockAbove; return { name: 'javascript', - extractSourceCode: ({ fileName, transformedCode }) => { - if (options.extractTransformedCode && transformedCode) { - const code = autoIndent(transformedCode.trimStart()); - - return { - code, - language: 'javascript', - fileName, - }; - } - - return; - }, - extractDocblock: ({ fileName, lineNumber }) => { if (fileName && detectJS(fileName)) { return $.getFileNavigator(fileName).then((navigator) => { if (!navigator) return; let docblock = lineNumber - ? extractDocblockAbove(navigator, lineNumber) + ? extractDocblockFromLine(navigator, lineNumber) : extract(navigator.getContent()); docblock = docblock.trim(); @@ -48,3 +36,9 @@ export const javascript: SourceCodePluginCustomizer = ({ $, value = {} }) => { }, }; }; + +function notImplemented(): string { + throw new AllureRuntimeError( + 'Extracting docblock inside JavaScript source code is not implemented yet', + ); +} diff --git a/src/options/source-code/typescript/ASTHelper.ts b/src/source-code/typescript/ASTHelper.ts similarity index 100% rename from src/options/source-code/typescript/ASTHelper.ts rename to src/source-code/typescript/ASTHelper.ts diff --git a/src/source-code/typescript/index.ts b/src/source-code/typescript/index.ts new file mode 100644 index 0000000..f735477 --- /dev/null +++ b/src/source-code/typescript/index.ts @@ -0,0 +1,99 @@ +import type { + AllureTestItemSourceLocation, + SourceCodePluginCustomizer, +} from 'jest-allure2-reporter'; +import { extract, parseWithComments } from 'jest-docblock'; + +import { autoIndent, importFrom } from '../../utils'; +import { detectJS } from '../common'; + +import { ASTHelper } from './ASTHelper'; + +export interface TypescriptPluginOptions { + enabled: boolean; + extractDocblock: boolean; +} + +function resolveOptions(value: unknown): TypescriptPluginOptions { + if (typeof value === 'boolean') { + return { enabled: value, extractDocblock: value }; + } + + return { + enabled: true, + extractDocblock: false, + ...(value as TypescriptPluginOptions | undefined), + }; +} + +export const typescript: SourceCodePluginCustomizer = async ({ globalConfig, $, value }) => { + const options = resolveOptions(value); + const ts = options.enabled + ? await importFrom('typescript', globalConfig.rootDir).then( + (resolution) => resolution.exports, + () => null, + ) + : null; + + function canProcess( + location: AllureTestItemSourceLocation, + ): location is Required { + const { fileName, lineNumber, columnNumber } = location; + return Boolean(fileName && lineNumber && columnNumber && detectJS(fileName)); + } + + const helper = new ASTHelper(ts); + return { + name: 'typescript', + + extractSourceCode(location, includeComments) { + return ts && canProcess(location) + ? $.getFileNavigator(location.fileName).then((navigator) => { + if (!navigator) return; + if (!navigator.jump(location.lineNumber)) return; + const lineNumber = location.lineNumber; + const columnNumber = Math.min(location.columnNumber, navigator.readLine().length); + + const ast = + helper.getAST(location.fileName) || + helper.parseAST(location.fileName, navigator.getContent()); + const expression = helper.findNodeInAST(ast, lineNumber, columnNumber); + const codeStart = includeComments ? expression.getFullStart() : expression.getStart(); + const code = autoIndent(ast.text.slice(codeStart, expression.getEnd()).trim()); + navigator.jumpToPosition(codeStart); + const [startLine] = navigator.getPosition(); + navigator.jumpToPosition(expression.getEnd()); + const [endLine] = navigator.getPosition(); + + return { + code, + language: detectJS(location.fileName), + fileName: location.fileName, + startLine, + endLine, + }; + }) + : undefined; + }, + + extractDocblock(context) { + return ts && options.extractDocblock && canProcess(context) + ? $.getFileNavigator(context.fileName).then((navigator) => { + if (!navigator) return; + if (!navigator.jump(context.lineNumber)) return; + const lineNumber = context.lineNumber; + const columnNumber = Math.min(context.columnNumber, navigator.readLine().length); + + const ast = + helper.getAST(context.fileName) || + helper.parseAST(context.fileName, navigator.getContent()); + const expression = helper.findNodeInAST(ast, lineNumber, columnNumber); + const fullStart = expression.getFullStart(); + const start = expression.getStart(); + const docblock = extract(ast.text.slice(fullStart, start).trim()); + return docblock ? parseWithComments(docblock) : undefined; + }) + : undefined; + }, + }; +}; diff --git a/src/utils/autoIndent.ts b/src/utils/autoIndent.ts index 11b2068..5e1a316 100644 --- a/src/utils/autoIndent.ts +++ b/src/utils/autoIndent.ts @@ -10,7 +10,8 @@ export function autoIndent(text: string) { function detectIndent(lines: string[]) { const result = lines.reduce((min, line) => { - const indent = line.length - line.trimStart().length; + const trimmed = line.trimStart(); + const indent = trimmed === '' ? min : line.length - trimmed.length; return Math.min(indent, min); }, Number.POSITIVE_INFINITY); diff --git a/src/utils/importCwd.ts b/src/utils/importCwd.ts deleted file mode 100644 index 61a0eac..0000000 --- a/src/utils/importCwd.ts +++ /dev/null @@ -1,6 +0,0 @@ -const cwd = process.cwd(); - -export async function importCwd(module: string): Promise { - const resolved = require.resolve(module, { paths: [cwd] }); - return import(resolved).then((module) => module.default ?? module); -} diff --git a/src/utils/importFrom.ts b/src/utils/importFrom.ts new file mode 100644 index 0000000..ca5eadf --- /dev/null +++ b/src/utils/importFrom.ts @@ -0,0 +1,15 @@ +import path from 'node:path'; + +import { log } from '../logger'; + +import { asArray } from './asArray'; + +export async function importFrom(module: string, lookup: string | string[]) { + const filename = require.resolve(module, { paths: asArray(lookup) }); + const dirname = path.dirname(filename); + const exports = await import(filename).then((module) => module.default ?? module); + const result = { filename, dirname, exports }; + log.trace({ paths: lookup }, 'resolve module %j -> %j', module, filename); + + return result; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index dad5631..05fdab3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,7 +6,7 @@ export * from './compactObject'; export * from './FileNavigator'; export * from './getStatusDetails'; export * from './hijackFunction'; -export * from './importCwd'; +export * from './importFrom'; export * from './isNonNullish'; export * from './isJestAssertionError'; export * from './isLibraryPath'; diff --git a/src/utils/typeAssertions.ts b/src/utils/typeAssertions.ts index 5f1430c..e20f569 100644 --- a/src/utils/typeAssertions.ts +++ b/src/utils/typeAssertions.ts @@ -2,6 +2,8 @@ import util from 'node:util'; import type { Primitive, Stage, Status, Severity } from 'jest-allure2-reporter'; +import { isPromiseLike } from './isPromiseLike'; + const SEVERITY_VALUES = new Set(['blocker', 'critical', 'normal', 'minor', 'trivial']); const STATUS_VALUES = new Set(['failed', 'broken', 'passed', 'skipped', 'unknown']); const STAGE_VALUES = new Set(['scheduled', 'running', 'finished', 'pending', 'interrupted']); @@ -18,6 +20,18 @@ export function assertString(value: unknown, name = 'value'): asserts value is s } } +export function assertAttachmentContent(value: unknown, name = 'value'): asserts value is string { + if (isPromiseLike(value)) { + return; + } + + if (typeof value !== 'string' && !Buffer.isBuffer(value) && !ArrayBuffer.isView(value)) { + throw new TypeError( + `Expected a string or a buffer "${name}, got instead: ${util.inspect(value)}`, + ); + } +} + export function assertPrimitive(value: unknown, name = 'value'): asserts value is Primitive { if (typeof value === 'string') return; if (typeof value === 'number') return;