Skip to content

Commit

Permalink
fix: duplicate calls for multiple jest-metadata reporters (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph authored Sep 30, 2023
1 parent e8e57e0 commit 47af38f
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 75 deletions.
6 changes: 4 additions & 2 deletions packages/library/src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { realm } from './realms';
import { ParentProcessRealm, realm } from './realms';

export { aggregateLogs } from './utils';

Expand All @@ -7,7 +7,9 @@ export function isEnabled() {
}

export function isFallback() {
return realm.fallbackAPI.enabled;
return realm.type === 'parent_process'
? (realm as ParentProcessRealm).fallbackAPI.enabled
: undefined;
}

export const events = realm.events;
14 changes: 0 additions & 14 deletions packages/library/src/jest-reporter/AssociateMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'path';

import type { TestCaseResult } from '@jest/reporters';

import { JestMetadataError } from '../errors';
Expand All @@ -8,24 +6,12 @@ import type { GlobalMetadata, Metadata, TestFileMetadata, TestEntryMetadata } fr
export class AssociateMetadata {
private readonly _map = new Map<unknown, Metadata>();

constructor(private readonly _cwd: string) {}

filePath(value: string, metadata: TestFileMetadata): void {
if (!value) {
throw new JestMetadataError('Cannot associate metadata with an empty file path');
}

this._map.set(value, metadata);

if (path.isAbsolute(value)) {
this._map.set(path.relative(this._cwd, value), metadata);
} else {
this._map.set(path.resolve(value), metadata);
}
}

testCaseName(nameIdentifier: string[], metadata: TestEntryMetadata): void {
this._map.set(nameIdentifier.join('\u001F'), metadata);
}

testCaseResult(testCaseResult: TestCaseResult, metadata: TestEntryMetadata): void {
Expand Down
32 changes: 21 additions & 11 deletions packages/library/src/jest-reporter/FallbackAPI.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { TestCaseResult, TestResult } from '@jest/reporters';
import { JestMetadataError } from '../errors';
import type { GlobalMetadata, MetadataEventEmitter, TestEntryMetadata } from '../metadata';
import { memoizeLast, Rotator } from '../utils';
import type {
GlobalMetadata,
MetadataEventEmitter,
TestDoneEvent,
TestEntryMetadata,
TestSkipEvent,
} from '../metadata';
import { Rotator } from '../utils';

export class FallbackAPI {
private _fallbackMode: boolean | undefined = undefined;
Expand All @@ -10,10 +16,7 @@ export class FallbackAPI {
constructor(
private readonly globalMetadata: GlobalMetadata,
private readonly eventEmitter: MetadataEventEmitter,
) {
this.reportTestFile = memoizeLast(this.reportTestFile.bind(this));
this.reportTestCase = memoizeLast(this.reportTestCase.bind(this));
}
) {}

public get enabled() {
return this._fallbackMode ?? true;
Expand All @@ -24,16 +27,18 @@ export class FallbackAPI {
type: 'add_test_file',
testFilePath,
});

return this.globalMetadata.getTestFileMetadata(testFilePath);
}

reportTestCase(testFilePath: string, testCaseResult: TestCaseResult) {
reportTestCase(testFilePath: string, testCaseResult: TestCaseResult): TestEntryMetadata {
const file = this.globalMetadata.getTestFileMetadata(testFilePath);
if (this._fallbackMode === undefined) {
this._fallbackMode = !file.rootDescribeBlock;
}

if (!this._fallbackMode) {
return;
return file.lastTestEntry!;
}

if (!file.rootDescribeBlock) {
Expand Down Expand Up @@ -88,7 +93,9 @@ export class FallbackAPI {
type: this._getCompletionEventType(testCaseResult),
testFilePath,
testId,
});
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);

return lastChild;
} else {
const tests = this._cache.get(nameIdentifier)!;
const info = tests.find((t) => t.testCaseResult.status === 'failed')!;
Expand All @@ -104,7 +111,9 @@ export class FallbackAPI {
type: this._getCompletionEventType(testCaseResult),
testFilePath: info.testFilePath,
testId: info.testId,
});
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);

return info.testEntryMetadata;
}
}

Expand Down Expand Up @@ -148,7 +157,7 @@ export class FallbackAPI {
type: this._getCompletionEventType(testCaseResult),
testFilePath,
testId,
});
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);
}

result.push(info ? info.testEntryMetadata : file.lastTestEntry!);
Expand Down Expand Up @@ -188,5 +197,6 @@ type TestEntryInfo = {
testId: string;
testFilePath: string;
testEntryMetadata: TestEntryMetadata;
/** Only or the last invocation */
testCaseResult: TestCaseResult;
};
3 changes: 1 addition & 2 deletions packages/library/src/jest-reporter/QueryMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { JestMetadataError } from '../errors';
import type {
GlobalMetadata,
MetadataChecker,
InstanceOfMetadataChecker,
TestFileMetadata,
TestEntryMetadata,
} from '../metadata';
Expand All @@ -18,7 +17,7 @@ export class QueryMetadata {
private readonly [_associate]: AssociateMetadata;
private readonly [_checker]: MetadataChecker;

constructor(associate: AssociateMetadata, checker: InstanceOfMetadataChecker) {
constructor(associate: AssociateMetadata, checker: MetadataChecker) {
this[_associate] = associate;
this[_checker] = checker;
}
Expand Down
84 changes: 84 additions & 0 deletions packages/library/src/jest-reporter/ReporterServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* eslint-disable @typescript-eslint/no-empty-function,unicorn/no-for-loop */
import type { Test, TestCaseResult, TestResult } from '@jest/reporters';
import memoize from 'lodash.memoize';
import type { IPCServer } from '../ipc';
import { logger } from '../utils';
import type { AssociateMetadata } from './AssociateMetadata';
import type { FallbackAPI } from './FallbackAPI';

export type ReporterServerConfig = {
ipc: IPCServer;
fallbackAPI: FallbackAPI;
associate: AssociateMetadata;
};

/**
* @implements {import('@jest/reporters').Reporter}
*/
export class ReporterServer {
#log = logger.child({ cat: 'reporter', tid: 'reporter' });
#associate: AssociateMetadata;
#fallbackAPI: FallbackAPI;
#ipc: IPCServer;

constructor(config: ReporterServerConfig) {
this.#associate = config.associate;
this.#fallbackAPI = config.fallbackAPI;
this.#ipc = config.ipc;

// We are memoizing all methods because there might be
// multiple reporters based on jest-metadata, so we need to
// make sure that we are calling every method only once per
// a given test case result.
//
// Unfortunately, we can't use a quicker `memoizeLast` because
// of obscure race conditions in Jest's ReporterDispatcher.
// This might be a good candidate for a future optimization.

this.onRunStart = memoize(this.onRunStart.bind(this));
this.onTestFileStart = memoize(this.onTestFileStart.bind(this));
this.onTestCaseStart = memoize(this.onTestCaseStart.bind(this));
this.onTestCaseResult = memoize(this.onTestCaseResult.bind(this));
this.onTestFileResult = memoize(this.onTestFileResult.bind(this));
this.onRunComplete = memoize(this.onRunComplete.bind(this));
}

async onRunStart(): Promise<void> {
await this.#ipc.start();
}

onTestFileStart(testPath: string): void {
this.#log.debug.begin({ tid: ['reporter', testPath] }, testPath);
const testFileMetadata = this.#fallbackAPI.reportTestFile(testPath);
this.#associate.filePath(testPath, testFileMetadata);
}

onTestCaseStart(test: Test): void {
this.#log.debug({ tid: ['reporter', test.path] }, 'onTestCaseStart');
// We cannot use the fallback API here because `testCaseStartInfo`
// does not contain information, whether this is a retry or not.
// That's why we might end up with multiple test entries for the same test,
// so better to ignore this event, rather than distort the data.
}

onTestCaseResult(test: Test, testCaseResult: TestCaseResult): void {
this.#log.debug({ tid: ['reporter', test.path] }, 'onTestCaseResult');

const lastTestEntry = this.#fallbackAPI.reportTestCase(test.path, testCaseResult);
this.#associate.testCaseResult(testCaseResult, lastTestEntry);
}

onTestFileResult(test: Test, testResult: TestResult): void {
const allTestEntries = this.#fallbackAPI.reportTestFileResult(testResult);
const testResults = testResult.testResults;
for (let i = 0; i < testResults.length; i++) {
this.#associate.testCaseResult(testResults[i], allTestEntries[i]);
}

this.#log.debug.end({ tid: ['reporter', test.path] });
}

async onRunComplete(): Promise<void> {
await this.#ipc.stop();
}
}
1 change: 1 addition & 0 deletions packages/library/src/jest-reporter/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { AssociateMetadata } from './AssociateMetadata';
export { FallbackAPI } from './FallbackAPI';
export { QueryMetadata } from './QueryMetadata';
export { ReporterServer } from './ReporterServer';
13 changes: 6 additions & 7 deletions packages/library/src/realms/BaseRealm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EnvironmentEventHandler } from '../jest-environment';

import { AssociateMetadata, FallbackAPI, QueryMetadata } from '../jest-reporter';
import { AssociateMetadata, QueryMetadata } from '../jest-reporter';
import {
GlobalMetadataRegistry,
MetadataDSL,
Expand All @@ -15,13 +15,13 @@ import {
import { AggregatedEmitter, SerialSyncEmitter } from '../utils';

export abstract class BaseRealm {
readonly coreEmitter: MetadataEventEmitter = new SerialSyncEmitter<MetadataEvent>('core').on(
readonly coreEmitter = new SerialSyncEmitter<MetadataEvent>('core').on(
'*',
(event: MetadataEvent) => {
this.metadataHandler.handle(event);
},
);
readonly setEmitter: SetMetadataEventEmitter = new SerialSyncEmitter<SetMetadataEvent>('set');
) as MetadataEventEmitter;
readonly setEmitter = new SerialSyncEmitter<SetMetadataEvent>('set') as SetMetadataEventEmitter;
readonly events = new AggregatedEmitter<MetadataEvent>('events').add(this.coreEmitter);

readonly metadataRegistry = new GlobalMetadataRegistry();
Expand All @@ -34,11 +34,10 @@ export abstract class BaseRealm {
globalMetadata: this.globalMetadata,
metadataRegistry: this.metadataRegistry,
});
readonly associate = new AssociateMetadata(process.cwd());
readonly query = new QueryMetadata(this.associate, this.metadataFactory.checker);
readonly fallbackAPI = new FallbackAPI(this.globalMetadata, this.coreEmitter);
readonly metadataDSL = new MetadataDSL(
this.coreEmitter,
() => this.globalMetadata.currentMetadata,
);
readonly associate = new AssociateMetadata();
readonly query = new QueryMetadata(this.associate, this.metadataFactory.checker);
}
2 changes: 1 addition & 1 deletion packages/library/src/realms/ChildProcessRealm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BaseRealm } from './BaseRealm';
import { getClientId, getServerId } from './detect';

export class ChildProcessRealm extends BaseRealm {
readonly type = 'child_process';
readonly type = 'child_process' as const;

readonly environmentHandler: EnvironmentEventHandler = new EnvironmentEventHandler({
emitter: this.coreEmitter,
Expand Down
11 changes: 10 additions & 1 deletion packages/library/src/realms/ParentProcessRealm.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { IPCServer } from '../ipc';
import { FallbackAPI, ReporterServer } from '../jest-reporter';
import { getVersion } from '../utils';

import { BaseRealm } from './BaseRealm';
import { registerServerId } from './detect';

export class ParentProcessRealm extends BaseRealm {
readonly type = 'parent_process';
readonly type = 'parent_process' as const;

readonly ipc = new IPCServer({
appspace: `jest-metadata@${getVersion()}-`,
serverId: `${process.pid}`,
emitter: this.coreEmitter,
});

readonly fallbackAPI = new FallbackAPI(this.globalMetadata, this.coreEmitter);

readonly reporterServer = new ReporterServer({
associate: this.associate,
fallbackAPI: this.fallbackAPI,
ipc: this.ipc,
});

constructor() {
super();

Expand Down
2 changes: 2 additions & 0 deletions packages/library/src/realms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { injectRealmIntoSandbox } from './detect';
export { default as realm } from './realm';
export type { ParentProcessRealm } from './ParentProcessRealm';
export type { ChildProcessRealm } from './ChildProcessRealm';
Loading

0 comments on commit 47af38f

Please sign in to comment.