@reporters/github
Version:
A github actions reporter for `node:test`
160 lines (142 loc) • 5.24 kB
JavaScript
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import util from 'node:util';
import { EOL } from 'node:os';
import { summary } from '@actions/core';
import StackUtils from 'stack-utils';
import { Command, toCommandProperties } from './gh_core.js';
const WORKSPACE = process.env.GITHUB_WORKSPACE ?? '';
const stack = new StackUtils({ cwd: WORKSPACE, internals: StackUtils.nodeInternals() });
const isFile = (name) => name?.startsWith(WORKSPACE);
const getRelativeFilePath = (name) => (isFile(name) ? path.relative(WORKSPACE, name) : null);
function getFilePath(fileName) {
if (fileName.startsWith('file://')) {
return getRelativeFilePath(fileURLToPath(fileName));
}
if (!path.isAbsolute(fileName)) {
return getRelativeFilePath(path.resolve(fileName) ?? '');
}
return getRelativeFilePath(fileName);
}
const parseStack = (error, file) => {
const err = error?.code === 'ERR_TEST_FAILURE' ? error?.cause : error;
const stackLines = (err?.stack ?? '').split(/\r?\n/);
const line = stackLines.find((l) => l.includes(file)) ?? stackLines[0];
return line ? stack.parseLine(line) : null;
};
const DIAGNOSTIC_KEYS = {
tests: 'Total Tests',
suites: 'Suites 📂',
pass: 'Passed ✅',
fail: 'Failed ❌',
cancelled: 'Canceled 🚫',
skipped: 'Skipped ⏭️',
todo: 'Todo 📝',
duration_ms: 'Duration 🕐',
};
export const DIAGNOSTIC_VALUES = {
duration_ms: (value) => `${Number(value).toFixed(3)}ms`,
};
function extractLocation(data) {
let { line, column, file } = data;
const error = data.details?.error;
file = getFilePath(file);
if (error) {
const errorLocation = parseStack(error, file);
file = getFilePath(errorLocation?.file ?? file) ?? file;
line = errorLocation?.line ?? line;
column = errorLocation?.column ?? column;
}
return { file, startLine: line, startColumn: column };
}
const counter = { pass: 0, fail: 0 };
const diagnostics = [];
export function isTopLevelDiagnostic(data) {
return (data.file === undefined
|| data.line === undefined
|| data.column === undefined
|| (data.line === 1 && data.column === 1));
}
export function transformEvent(event) {
switch (event.type) {
case 'test:start':
return new Command('debug', {}, `starting to run ${event.data.name}`).toString();
case 'test:pass':
counter.pass += 1;
return new Command('debug', {}, `completed running ${event.data.name}`).toString();
case 'test:fail': {
const error = event.data.details?.error;
if (!error || (error.code === 'ERR_TEST_FAILURE' && error.failureType === 'subtestsFailed')) {
break;
}
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
counter.fail += 1;
return new Command('error', toCommandProperties({
...extractLocation(event.data),
title: event.data.name,
}), util.inspect(err, { colors: true, breakLength: Infinity })).toString();
} case 'test:diagnostic':
if (isTopLevelDiagnostic(event.data)) {
diagnostics.push(event.data.message);
} else if (process.env.GITHUB_ACTIONS_REPORTER_VERBOSE) {
return new Command('notice', toCommandProperties(extractLocation(event.data)), `${event.data.message}`).toString();
}
break;
/* c8 ignore start */
case 'test:interrupted': {
const { tests } = event.data;
let res = '';
for (let i = 0; i < tests.length; i += 1) {
const test = tests[i];
const file = test.file ? getFilePath(test.file) : undefined;
let msg = `Interrupted while running: ${test.name}`;
if (file) {
msg += ` at ${file}:${test.line}:${test.column}`;
}
res += new Command('warning', toCommandProperties({
file,
startLine: test.line,
startColumn: test.column,
title: `Interrupted: ${test.name}`,
}), msg).toString();
}
return res;
}
/* c8 ignore stop */
default:
break;
}
return '';
}
export async function getSummary() {
const formattedDiagnostics = diagnostics.map((d) => {
const [key, ...rest] = d.split(' ');
const value = rest.join(' ');
return [
DIAGNOSTIC_KEYS[key] ?? key,
DIAGNOSTIC_VALUES[key] ? DIAGNOSTIC_VALUES[key](value) : value,
];
});
let res = '';
res += new Command('group', {}, `Test results (${formattedDiagnostics.find(([key]) => key === DIAGNOSTIC_KEYS.pass)?.[1] ?? counter.pass} passed, ${formattedDiagnostics.find(([key]) => key === DIAGNOSTIC_KEYS.fail)?.[1] ?? counter.fail} failed)`).toString();
res += new Command('notice', {}, formattedDiagnostics.map((d) => d.join(': ')).join(EOL)).toString();
res += new Command('endgroup').toString();
if (process.env.GITHUB_STEP_SUMMARY) {
await summary
.addHeading('Test Results')
.addTable(formattedDiagnostics)
.write();
}
return res;
}
export default async function* githubReporter(source) {
if (!process.env.GITHUB_ACTIONS) {
// eslint-disable-next-line no-unused-vars
for await (const _ of source);
return;
}
for await (const event of source) {
yield transformEvent(event);
}
yield await getSummary();
}