@travetto/test
Version:
Declarative test framework
121 lines (107 loc) • 4.28 kB
text/typescript
import type { TestConsumerShape } from '../types.ts';
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
import { DelegatingConsumer } from './delegating.ts';
import type { SuiteCore } from '../../model/common.ts';
import { TestModelUtil } from '../../model/util.ts';
type ClassId = string;
type ImportName = string;
type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
tests: Record<string, CumulativeTestResult>;
};
/**
* Cumulative Summary consumer
*/
export class CumulativeSummaryConsumer extends DelegatingConsumer {
/**
* Total state of all tests run so far
*/
#state: Record<ImportName, Record<ClassId, CumulativeSuiteResult>> = {};
constructor(target: TestConsumerShape) {
super([target]);
}
getSuite(core: Pick<SuiteCore, 'import' | 'classId'>): CumulativeSuiteResult {
return this.#state[core.import]?.[core.classId];
}
getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
}
onTestBefore(config: TestConfig): TestConfig {
const suite = this.getSuite(config);
suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
return config;
}
onTestAfter(result: TestResult): TestResult {
const test = this.getSuite(result).tests[result.methodName];
Object.assign(test, { status: result.status, duration: result.duration });
return result;
}
onSuiteBefore(config: SuiteConfig): SuiteConfig {
const suite = this.getOrCreateSuite(config);
suite.sourceHash = config.sourceHash;
return config;
}
onSuiteAfter(result: SuiteResult): SuiteResult {
// Reset counts
const suite = this.getSuite(result);
const totals: Counts & { duration: number } = { passed: 0, failed: 0, skipped: 0, unknown: 0, total: 0, duration: 0 };
for (const test of Object.values(suite.tests)) {
totals[test.status] += 1;
totals.total += 1;
totals.duration += test.duration ?? 0;
}
return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
}
removeTest(importName: string, classId?: string, methodName?: string): void {
if (methodName && classId && importName) {
delete this.getSuite({ import: importName, classId }).tests[methodName];
} else if (classId && importName) {
delete this.#state[importName][classId];
} else if (importName) {
delete this.#state[importName];
}
}
transformRemove(event: TestRemoveEvent): TestRemoveEvent {
this.removeTest(event.import, event.classId, event.methodName);
return event;
}
/**
* Handle cumulative events, and emit a summarized summary
*/
transform(event: TestEvent): TestEvent | undefined {
try {
if (event.type === 'suite') {
if (event.phase === 'before') {
return { ...event, suite: this.onSuiteBefore(event.suite) };
} else if (event.phase === 'after') {
return { ...event, suite: this.onSuiteAfter(event.suite) };
}
} else if (event.type === 'test') {
if (event.phase === 'before') {
return { ...event, test: this.onTestBefore(event.test) };
} else if (event.phase === 'after') {
return { ...event, test: this.onTestAfter(event.test) };
}
}
return event;
} catch (error) {
console.warn('Summarization Error', { error });
}
}
/**
* Produce diff source for import file
*/
produceDiffSource(importName: string): TestDiffSource {
const output: TestDiffSource = {};
for (const [clsId, suite] of Object.entries(this.#state[importName] || {})) {
const methods: TestDiffSource[string]['methods'] = {};
for (const [methodName, test] of Object.entries(suite.tests)) {
methods[methodName] = test.sourceHash!;
}
output[clsId] = { sourceHash: suite.sourceHash!, methods };
}
return output;
}
}