@travetto/test
Version:
Declarative test framework
121 lines (105 loc) • 3.2 kB
text/typescript
import type { Writable } from 'node:stream';
import { stringify } from 'yaml';
import { RuntimeIndex } from '@travetto/runtime';
import type { TestEvent } from '../../model/event.ts';
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
import { TestConsumer } from '../decorator.ts';
/**
* Xunit consumer, compatible with JUnit formatters
*/
()
export class XunitEmitter implements TestConsumerShape {
#tests: string[] = [];
#suites: string[] = [];
#stream: Writable;
constructor(stream: Writable = process.stdout) {
this.#stream = stream;
}
/**
* Process metadata information (e.g. logs)
*/
buildMeta(meta: Record<string, unknown>): string {
if (!meta) {
return '';
}
for (const key of Object.keys(meta)) {
if (!meta[key]) {
delete meta[key];
}
}
if (Object.keys(meta).length) {
let body = stringify(meta);
body = body.split('\n').map(line => ` ${line}`).join('\n');
return `<![CDATA[\n${body}\n]]>`;
} else {
return '';
}
}
/**
* Handle each test event
*/
onEvent(event: TestEvent): void {
if (event.type === 'test' && event.phase === 'after') {
const { test } = event;
let name = `${test.methodName}`;
if (test.description) {
name += `: ${test.description}`;
}
let body = '';
if (test.error) {
const assertion = test.assertions.find(item => !!item.error)!;
body = `<failure type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></failure>`;
}
const groupedByLevel: Record<string, string[]> = {};
for (const log of test.output) {
(groupedByLevel[log.level] ??= []).push(log.message);
}
this.#tests.push(`
<testcase
name="${name}"
time="${test.duration}"
classname="${test.classId}"
>
${body}
<system-out>${this.buildMeta({ log: groupedByLevel.log, info: groupedByLevel.info, debug: groupedByLevel.debug })}</system-out>
<system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
</testcase>`
);
} else if (event.type === 'suite' && event.phase === 'after') {
const { suite } = event;
const testBodies = this.#tests.slice(0);
this.#tests = [];
const out = `
<testsuite
name="${suite.classId}"
time="${suite.duration}"
tests="${suite.total}"
failures="${suite.failed}"
errors="${suite.failed}"
skipped="${suite.skipped}"
file="${RuntimeIndex.getFromImport(suite.import)!.sourceFile}"
>
${testBodies.join('\n')}
</testsuite>
`;
this.#suites.push(out);
}
}
/**
* Summarize all results
*/
onSummary(summary: SuitesSummary): void {
this.#stream.write(`
<?xml version="1.0" encoding="UTF-8"?>
<testsuites
name="${summary.suites.length ? RuntimeIndex.getFromImport(summary.suites[0].import)?.sourceFile : 'nameless'}"
time="${summary.duration}"
tests="${summary.total}"
failures="${summary.failed}"
errors="${summary.failed}"
>
${this.#suites.join('\n')}
</testsuites>
`);
}
}