Skip to content

Commit

Permalink
restore code parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph committed Apr 8, 2024
1 parent c6c184f commit 290615b
Show file tree
Hide file tree
Showing 30 changed files with 1,121 additions and 41 deletions.
30 changes: 20 additions & 10 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,11 @@ declare module 'jest-allure2-reporter' {

// region Customizers

export type SourceCodePluginCustomizer = PropertyCustomizer<GlobalExtractorContext, SourceCodePlugin | undefined>;
export type SourceCodePluginCustomizer = PropertyExtractor<GlobalExtractorContext, unknown, MaybePromise<SourceCodePlugin>>;

export interface SourceCodePlugin<Context = {}> {
// TODO: use more interesting context type than `AllureTestItemSourceLocation`
prepareContext?(context: AllureTestItemSourceLocation): MaybePromise<Context | undefined>;
extractDocblock?(sourceCode: Context): MaybePromise<AllureTestItemDocblock | undefined>;
extractSourceCode?(sourceCode: Context): MaybePromise<AllureTestItemDocblock | undefined>;
export interface SourceCodePlugin {
extractDocblock?(location: AllureTestItemSourceLocation): MaybePromise<AllureTestItemDocblock | undefined>;
extractSourceCode?(location: AllureTestItemSourceLocation): MaybePromise<string | undefined>;
}

export interface TestCaseCustomizer<Context = {}> {
Expand Down Expand Up @@ -404,6 +402,7 @@ declare module 'jest-allure2-reporter' {
}

export interface Helpers extends HelpersAugmentation {
getFileNavigator(filePath: string): Promise<FileNavigator>;
/**
* 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.
Expand Down Expand Up @@ -440,13 +439,24 @@ declare module 'jest-allure2-reporter' {
*/
manifest: ManifestHelper;
markdown2html(markdown: string): Promise<string>;
source2markdown(sourceCode: Partial<ExtractorHelperSourceCode> | undefined): string;
source2markdown(sourceCode: Partial<ExtractSourceCodeHelperResult> | undefined): string;
stripAnsi: StripAnsiHelper;
}

export interface FileNavigator {
getContent(): string;
getLines(): string[];
getLineCount(): number;
getPosition(): [number, number, number];
jump(lineNumber: number): boolean;
moveUp(countOfLines?: number): boolean;
moveDown(countOfLines?: number): boolean;
readLine(lineNumber?: number): string;
}

export interface ExtractSourceCodeHelper {
(metadata: AllureTestItemMetadata): MaybePromise<ExtractorHelperSourceCode | undefined>;
(metadata: AllureTestItemMetadata, recursive: true): MaybePromise<ExtractorHelperSourceCode[]>;
(metadata: AllureTestItemMetadata, recursive?: never): Promise<ExtractSourceCodeHelperResult | undefined>;
(metadata: AllureTestItemMetadata, recursive: true): Promise<ExtractSourceCodeHelperResult[]>;
}

export interface GetExecutorInfoHelper {
Expand All @@ -462,7 +472,7 @@ declare module 'jest-allure2-reporter' {

export type ManifestHelperExtractor<T> = string | ((manifest: Record<string, any>) => T);

export interface ExtractorHelperSourceCode {
export interface ExtractSourceCodeHelperResult {
code: string;
language: string;
fileName: string;
Expand Down
2 changes: 1 addition & 1 deletion src/options/default/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function defaultOptions(): ReporterConfig {
fileHandler: 'ref',
},
sourceCode: {
enabled: false,
enabled: true,
plugins: [],
},
markdown: {
Expand Down
35 changes: 35 additions & 0 deletions src/options/helpers/file-navigator/getFileNavigator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from 'node:fs/promises';
import path from 'node:path';

import type { KeyedHelperCustomizer } from 'jest-allure2-reporter';

import { FileNavigator } from '../../../utils';

class FileNavigatorCache {
#cache = new Map<string, Promise<FileNavigator>>();

resolve(filePath: string): Promise<FileNavigator> {
const absolutePath = path.resolve(filePath);
if (!this.#cache.has(absolutePath)) {
this.#cache.set(absolutePath, this.#createNavigator(absolutePath));
}

return this.#cache.get(absolutePath)!;
}

#createNavigator = async (filePath: string) => {
const sourceCode = await fs.readFile(filePath, 'utf8').catch(() => '');
return new FileNavigator(sourceCode);
};

clear() {
this.#cache.clear();
}

static readonly instance = new FileNavigatorCache();
}

export const getFileNavigator: KeyedHelperCustomizer<'getFileNavigator'> = () => {
const cache = new FileNavigatorCache();
return (filePath) => cache.resolve(filePath);
};
1 change: 1 addition & 0 deletions src/options/helpers/file-navigator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getFileNavigator';
12 changes: 2 additions & 10 deletions src/options/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import type { ExtractSourceCodeHelper } from 'jest-allure2-reporter';

export { getFileNavigator } from './file-navigator';
export { getExecutorInfo } from './executor';
export { manifest } from './manifest';
export { markdown2html } from './markdown';
export { extractSourceCode, source2markdown } from './source-code';
export { stripAnsi } from './strip-ansi';

export const extractSourceCode =
(): ExtractSourceCodeHelper =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_: unknown, recursive?: true): any =>
recursive ? [] : void 0;

export const source2markdown = () => () => '';
65 changes: 65 additions & 0 deletions src/options/helpers/source-code/extractSourceCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type {
AllureTestItemMetadata,
AllureNestedTestStepMetadata,
ExtractSourceCodeHelperResult,
KeyedHelperCustomizer,
} from 'jest-allure2-reporter';

import { log } from '../../../logger';
import { compactArray } from '../../../utils';

export const extractSourceCode: KeyedHelperCustomizer<'extractSourceCode'> = ({ config }): any => {
async function extractRecursively(
item: AllureTestItemMetadata,
): Promise<ExtractSourceCodeHelperResult[]> {
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(
item: AllureTestItemMetadata,
): Promise<ExtractSourceCodeHelperResult | undefined> {
let code: string | undefined;
const sourceLocation = item.sourceLocation;
if (config.sourceCode?.enabled && sourceLocation) {
for (const p of config.sourceCode.plugins) {
try {
code = await p.extractSourceCode?.(sourceLocation);
} catch (error: unknown) {
log.warn(
error,
`Failed to extract source code for ${sourceLocation.fileName}:${sourceLocation.lineNumber}:${sourceLocation.columnNumber}`,
);
}
if (code) {
break;
}
}
}

return code && sourceLocation
? {
code,
language: 'typescript',
fileName: sourceLocation.fileName!,
lineNumber: sourceLocation.lineNumber!,
columnNumber: sourceLocation.columnNumber!,
}
: undefined;
}

return (item: AllureTestItemMetadata, recursive: boolean) =>
recursive ? extractRecursively(item) : extractSingle(item);
};

function isBefore(step: AllureNestedTestStepMetadata): boolean {
return step.hookType === 'beforeAll' || step.hookType === 'beforeEach';
}

function isAfter(step: AllureNestedTestStepMetadata): boolean {
return step.hookType === 'afterAll' || step.hookType === 'afterEach';
}
2 changes: 2 additions & 0 deletions src/options/helpers/source-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { extractSourceCode } from './extractSourceCode';
export { source2markdown } from './source2markdown';
11 changes: 11 additions & 0 deletions src/options/helpers/source-code/source2markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { KeyedHelperCustomizer } from 'jest-allure2-reporter';

const FENCE = '```';

export const source2markdown: KeyedHelperCustomizer<'source2markdown'> = () => (item) => {
if (!item || !item.code) return '';
const language = item.language || '';
const title = item.fileName ? ` title="${item.fileName}"` : '';
// this snippet also includes file name to work with Markdown plugin
return `${FENCE}${language}${title}\n${item.code}\n${FENCE}`;
};
7 changes: 7 additions & 0 deletions src/reporter/JestAllure2Reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {

import { type ReporterConfig, resolveOptions } from '../options';
import { AllureMetadataProxy, MetadataSquasher } from '../metadata';
import * as sourceCode from '../source-code';
import { stringifyValues } from '../utils';

import * as fallbacks from './fallbacks';
Expand Down Expand Up @@ -76,6 +77,12 @@ export class JestAllure2Reporter extends JestMetadataReporter {
};

globalContext.$ = await resolvePromisedItem(globalContext, config.helpers, '$');
if (config.sourceCode?.enabled) {
config.sourceCode.plugins.push(
await sourceCode.typescript(globalContext),
await sourceCode.javascript(globalContext),
);
}

this._globalContext = globalContext;

Expand Down
8 changes: 2 additions & 6 deletions src/reporter/postProcessMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ export async function postProcessMetadata($: Helpers, testFile: TestFileMetadata
await Promise.all(
batch.map(async (metadata) => {
const allureProxy = new AllureMetadataProxy<AllureTestItemMetadata>(metadata);
// TODO: do it for real
$.extractSourceCode(allureProxy.get('sourceLocation', {}));
// await this._callPlugins('postProcessMetadata', {
// $: this._$ as Helpers,
// metadata: allureProxy.assign({}).get(),
// });
// Cache source code for each test item
return $.extractSourceCode(allureProxy.get() ?? {});
}),
);
}
48 changes: 48 additions & 0 deletions src/source-code/coffeescript/extractCoffeeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { FileNavigator } from 'jest-allure2-reporter';

export function extractCoffeeBlock(
coffee: any,
navigator: FileNavigator,
testLineIndex: number,
): string {
if (testLineIndex > navigator.getLineCount()) return '';

let currentLineIndex = testLineIndex - 2;
if (currentLineIndex < 0) return '';

if (!navigator.jump(currentLineIndex + 1)) return '';
let currentLine = navigator.readLine();

const docblockEndIndex = currentLine.indexOf('###');
if (docblockEndIndex === -1) return '';

const docblockSignature = currentLine.slice(0, docblockEndIndex + 1);
if (docblockSignature.trimStart() !== '#') return '';

const commentBoundary = currentLine.slice(0, docblockEndIndex + 3);
const docblockStart = commentBoundary + '*';
const buffer = [];

buffer.unshift(commentBoundary.trimStart());
while (currentLineIndex >= 0) {
if (!navigator.moveUp()) break;
currentLineIndex--;
currentLine = navigator.readLine();

if (!currentLine.startsWith(docblockSignature)) {
break;
}

buffer.unshift(currentLine.trimStart());

if (currentLine.startsWith(docblockStart)) {
return coffee.compile(buffer.join('\n'), { bare: true });
}

if (currentLine.startsWith(commentBoundary)) {
break;
}
}

return '';
}
24 changes: 24 additions & 0 deletions src/source-code/coffeescript/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { SourceCodePluginCustomizer } from 'jest-allure2-reporter';
import { parseWithComments } from 'jest-docblock';

import { importDefault } from '../../utils';

import { extractCoffeeBlock } from './extractCoffeeBlock';

export const coffee: SourceCodePluginCustomizer = async ({ $ }) => {
const coffee = await importDefault('coffeescript').catch(() => null);
if (!coffee) return {};

return {
extractDocblock({ fileName, lineNumber }) {
if (fileName && lineNumber && fileName.endsWith('.coffee')) {
return $.getFileNavigator(fileName).then((navigator) => {
const docblock = extractCoffeeBlock(coffee, navigator, lineNumber);
return docblock ? parseWithComments(docblock) : undefined;
});
}

return;
},
};
};
3 changes: 3 additions & 0 deletions src/source-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './coffeescript';
export * from './javascript';
export * from './typescript';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`extractJsDoc should extract a broken docblock 1`] = `
"/**
* This is a broken docblock
* but it's still a docblock
*/"
`;

exports[`extractJsDoc should extract a multiline docblock 1`] = `
"/**
* This is a multiline docblock
*/"
`;

exports[`extractJsDoc should extract a single line docblock 1`] = `" /** This is a single line docblock */"`;

exports[`extractJsDoc should extract a weird two-line docblock 1`] = `
"/**
* This is a weird two-line docblock */"
`;

exports[`extractJsDoc should not extract a non-docblock 1`] = `""`;
41 changes: 41 additions & 0 deletions src/source-code/javascript/extractDocblockAbove.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { FileNavigator } from '../../utils';

import { extractDocblockAbove as extractJsDocument_ } from './extractDocblockAbove';

const FIXTURES = [
`\
/**
* This is a multiline docblock
*/`,
' /** This is a single line docblock */',
`/**
* This is a broken docblock
* but it's still a docblock
*/`,
`/**
* This is a weird two-line docblock */`,
`/*
* This is not a docblock
*/`,
].map((sourceCode) => sourceCode + '\n' + 'function test() {}\n');

describe('extractJsDoc', () => {
const extract = (index: number, line: number) =>
extractJsDocument_(new FileNavigator(FIXTURES[index]), line);

it('should extract a multiline docblock', () => expect(extract(0, 4)).toMatchSnapshot());

it('should extract a single line docblock', () => expect(extract(1, 2)).toMatchSnapshot());

it('should extract a broken docblock', () => expect(extract(2, 5)).toMatchSnapshot());

it('should extract a weird two-line docblock', () => expect(extract(3, 3)).toMatchSnapshot());

it('should not extract a non-docblock', () => expect(extract(4, 4)).toMatchSnapshot());

it('should ignore out of range line index', () => expect(extract(0, 5)).toBe(''));

it('should ignore zero line index', () => expect(extract(0, 0)).toBe(''));

it('should ignore the middle of a docblock', () => expect(extract(0, 2)).toBe(''));
});
Loading

0 comments on commit 290615b

Please sign in to comment.