mirror of
https://github.com/game-ci/unity-test-runner.git
synced 2026-01-29 06:20:07 +08:00
Feature/create check (#97)
This commit is contained in:
13
src/index.js
13
src/index.js
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core';
|
||||
import { Action, Docker, Input, ImageTag, Output } from './model';
|
||||
import { Action, Docker, Input, ImageTag, Output, ResultsCheck } from './model';
|
||||
|
||||
async function action() {
|
||||
Action.checkCompatibility();
|
||||
@@ -12,6 +12,9 @@ async function action() {
|
||||
testMode,
|
||||
artifactsPath,
|
||||
useHostNetwork,
|
||||
createCheck,
|
||||
checkName,
|
||||
githubToken,
|
||||
customParameters,
|
||||
} = Input.getFromUser();
|
||||
const baseImage = ImageTag.createForBase({ version: unityVersion, customImage });
|
||||
@@ -28,12 +31,20 @@ async function action() {
|
||||
testMode,
|
||||
artifactsPath,
|
||||
useHostNetwork,
|
||||
createCheck,
|
||||
customParameters,
|
||||
});
|
||||
} finally {
|
||||
// Set output
|
||||
await Output.setArtifactsPath(artifactsPath);
|
||||
}
|
||||
|
||||
if (createCheck) {
|
||||
const failedTestCount = await ResultsCheck.createCheck(artifactsPath, checkName, githubToken);
|
||||
if (failedTestCount >= 1) {
|
||||
core.setFailed(`Test(s) Failed! Check '${checkName}' for details.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action().catch(error => {
|
||||
|
||||
@@ -25,6 +25,7 @@ class Docker {
|
||||
testMode,
|
||||
artifactsPath,
|
||||
useHostNetwork,
|
||||
createCheck,
|
||||
customParameters,
|
||||
} = parameters;
|
||||
|
||||
@@ -62,6 +63,7 @@ class Docker {
|
||||
--volume "/home/runner/work/_temp/_github_workflow":"/github/workflow" \
|
||||
--volume "${workspace}":"/github/workspace" \
|
||||
${useHostNetwork ? '--net=host' : ''} \
|
||||
${createCheck ? '--env USE_EXIT_CODE=false' : '--env USE_EXIT_CODE=true'} \
|
||||
${image}`;
|
||||
|
||||
await exec(command, undefined, { silent });
|
||||
|
||||
@@ -3,5 +3,6 @@ import Docker from './docker';
|
||||
import Input from './input';
|
||||
import ImageTag from './image-tag';
|
||||
import Output from './output';
|
||||
import ResultsCheck from './results-check';
|
||||
|
||||
export { Action, Docker, Input, ImageTag, Output };
|
||||
export { Action, Docker, Input, ImageTag, Output, ResultsCheck };
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as Index from '.';
|
||||
|
||||
describe('Index', () => {
|
||||
test.each(['Action', 'Docker', 'ImageTag', 'Input', 'Output'])('exports %s', exportedModule => {
|
||||
expect(typeof Index[exportedModule]).toStrictEqual('function');
|
||||
});
|
||||
test.each(['Action', 'Docker', 'ImageTag', 'Input', 'Output', 'ResultsCheck'])(
|
||||
'exports %s',
|
||||
exportedModule => {
|
||||
expect(typeof Index[exportedModule]).toStrictEqual('function');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,9 @@ class Input {
|
||||
const rawProjectPath = getInput('projectPath') || '.';
|
||||
const rawArtifactsPath = getInput('artifactsPath') || 'artifacts';
|
||||
const rawUseHostNetwork = getInput('useHostNetwork') || 'false';
|
||||
const rawCreateCheck = getInput('createCheck') || 'false';
|
||||
const checkName = getInput('checkName') || 'Test Results';
|
||||
const githubToken = getInput('githubToken') || '';
|
||||
const customParameters = getInput('customParameters') || '';
|
||||
|
||||
// Validate input
|
||||
@@ -44,6 +47,7 @@ class Input {
|
||||
const projectPath = rawProjectPath.replace(/\/$/, '');
|
||||
const artifactsPath = rawArtifactsPath.replace(/\/$/, '');
|
||||
const useHostNetwork = rawUseHostNetwork === 'true';
|
||||
const createCheck = rawCreateCheck === 'true';
|
||||
const unityVersion =
|
||||
rawUnityVersion === 'auto' ? UnityVersionParser.read(projectPath) : rawUnityVersion;
|
||||
|
||||
@@ -55,6 +59,9 @@ class Input {
|
||||
testMode,
|
||||
artifactsPath,
|
||||
useHostNetwork,
|
||||
createCheck,
|
||||
checkName,
|
||||
githubToken,
|
||||
customParameters,
|
||||
};
|
||||
}
|
||||
|
||||
114
src/model/results-check.js
Normal file
114
src/model/results-check.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import Handlebars from 'handlebars';
|
||||
import ResultsParser from './results-parser';
|
||||
import { RunMeta } from './ts/results-meta.ts';
|
||||
|
||||
class ResultsCheck {
|
||||
static async createCheck(artifactsPath, checkName, githubToken) {
|
||||
// Validate input
|
||||
if (!artifactsPath || !checkName || !githubToken) {
|
||||
throw new Error(
|
||||
`Missing input! {"artifactsPath": "${artifactsPath}", "checkName": "${checkName}", "githubToken": "${githubToken}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse all results files
|
||||
const runs = [];
|
||||
const files = fs.readdirSync(artifactsPath);
|
||||
await Promise.all(
|
||||
files.map(async filepath => {
|
||||
if (!filepath.endsWith('.xml')) return;
|
||||
core.info(`Processing file ${filepath}...`);
|
||||
const fileData = await ResultsParser.parseResults(path.join(artifactsPath, filepath));
|
||||
core.info(fileData.summary);
|
||||
runs.push(fileData);
|
||||
}),
|
||||
);
|
||||
|
||||
// Combine all results into a single run summary
|
||||
const runSummary = new RunMeta(checkName);
|
||||
runs.forEach(run => {
|
||||
runSummary.total += run.total;
|
||||
runSummary.passed += run.passed;
|
||||
runSummary.skipped += run.skipped;
|
||||
runSummary.failed += run.failed;
|
||||
runSummary.duration += run.duration;
|
||||
run.suites.forEach(suite => {
|
||||
runSummary.addTests(suite.tests);
|
||||
});
|
||||
});
|
||||
|
||||
// Log
|
||||
core.info('=================');
|
||||
core.info('Analyze result:');
|
||||
core.info(runSummary.summary);
|
||||
|
||||
// Call GitHub API
|
||||
await ResultsCheck.requestGitHubCheck(checkName, githubToken, runs, runSummary);
|
||||
return runSummary.failed;
|
||||
}
|
||||
|
||||
static async requestGitHubCheck(checkName, githubToken, runs, runSummary) {
|
||||
const pullRequest = github.context.payload.pull_request;
|
||||
const headSha = (pullRequest && pullRequest.head.sha) || github.context.sha;
|
||||
|
||||
const title = runSummary.summary;
|
||||
const summary = await ResultsCheck.renderSummary(runs);
|
||||
const details = await ResultsCheck.renderDetails(runs);
|
||||
const rawAnnotations = runSummary.extractAnnotations();
|
||||
const annotations = rawAnnotations.map(rawAnnotation => {
|
||||
const annotation = rawAnnotation;
|
||||
annotation.path = rawAnnotation.path.replace('/github/workspace/', '');
|
||||
return annotation;
|
||||
});
|
||||
|
||||
core.info(`Posting results for ${headSha}`);
|
||||
const createCheckRequest = {
|
||||
...github.context.repo,
|
||||
name: checkName,
|
||||
head_sha: headSha,
|
||||
status: 'completed',
|
||||
conclusion: 'neutral',
|
||||
output: {
|
||||
title,
|
||||
summary,
|
||||
text: details,
|
||||
annotations: annotations.slice(0, 50),
|
||||
},
|
||||
};
|
||||
|
||||
const octokit = github.getOctokit(githubToken);
|
||||
await octokit.checks.create(createCheckRequest);
|
||||
}
|
||||
|
||||
static async renderSummary(runMetas) {
|
||||
return ResultsCheck.render(`${__dirname}/../views/results-check-summary.hbs`, runMetas);
|
||||
}
|
||||
|
||||
static async renderDetails(runMetas) {
|
||||
return ResultsCheck.render(`${__dirname}/../views/results-check-details.hbs`, runMetas);
|
||||
}
|
||||
|
||||
static async render(viewPath, runMetas) {
|
||||
Handlebars.registerHelper('indent', toIndent =>
|
||||
toIndent
|
||||
.split('\n')
|
||||
.map(s => ` ${s.replace('/github/workspace/', '')}`)
|
||||
.join('\n'),
|
||||
);
|
||||
const source = await fs.promises.readFile(viewPath, 'utf8');
|
||||
const template = Handlebars.compile(source);
|
||||
return template(
|
||||
{ runs: runMetas },
|
||||
{
|
||||
allowProtoMethodsByDefault: true,
|
||||
allowProtoPropertiesByDefault: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResultsCheck;
|
||||
9
src/model/results-check.test.js
Normal file
9
src/model/results-check.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import ResultsCheck from './results-check';
|
||||
|
||||
describe('ResultsCheck', () => {
|
||||
describe('createCheck', () => {
|
||||
it('throws for missing input', () => {
|
||||
expect(() => ResultsCheck.createCheck('', '', '')).rejects.toEqual(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
121
src/model/results-parser.js
Normal file
121
src/model/results-parser.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as xmljs from 'xml-js';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { RunMeta, TestMeta } from './ts/results-meta.ts';
|
||||
|
||||
class ResultsParser {
|
||||
static async parseResults(filepath) {
|
||||
if (!fs.existsSync(filepath)) {
|
||||
throw new Error(`Missing file! {"filepath": "${filepath}"}`);
|
||||
}
|
||||
|
||||
core.info(`Trying to open ${filepath}`);
|
||||
const file = await fs.promises.readFile(filepath, 'utf8');
|
||||
const results = xmljs.xml2js(file, { compact: true });
|
||||
core.info(`File ${filepath} parsed...`);
|
||||
|
||||
return ResultsParser.convertResults(path.basename(filepath), results);
|
||||
}
|
||||
|
||||
static convertResults(filename, filedata) {
|
||||
core.info(`Start analyzing results: ${filename}`);
|
||||
|
||||
const run = filedata['test-run'];
|
||||
const runMeta = new RunMeta(filename);
|
||||
|
||||
runMeta.total = Number(run._attributes.total);
|
||||
runMeta.failed = Number(run._attributes.failed);
|
||||
runMeta.skipped = Number(run._attributes.skipped);
|
||||
runMeta.passed = Number(run._attributes.passed);
|
||||
runMeta.duration = Number(run._attributes.duration);
|
||||
runMeta.addTests(ResultsParser.convertSuite(run['test-suite']));
|
||||
|
||||
return runMeta;
|
||||
}
|
||||
|
||||
static convertSuite(suites) {
|
||||
if (Array.isArray(suites)) {
|
||||
const innerResult = [];
|
||||
suites.forEach(suite => {
|
||||
innerResult.push(ResultsParser.convertSuite(suite));
|
||||
});
|
||||
return innerResult;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
const innerSuite = suites['test-suite'];
|
||||
if (innerSuite) {
|
||||
result.push(...ResultsParser.convertSuite(innerSuite));
|
||||
}
|
||||
|
||||
const tests = suites['test-case'];
|
||||
if (tests) {
|
||||
result.push(...ResultsParser.convertTests(suites._attributes.fullname, tests));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static convertTests(suite, tests) {
|
||||
if (Array.isArray(tests)) {
|
||||
const result = [];
|
||||
tests.forEach(test => {
|
||||
result.push(ResultsParser.convertTestCase(suite, test));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return [ResultsParser.convertTestCase(suite, tests)];
|
||||
}
|
||||
|
||||
static convertTestCase(suite, testCase) {
|
||||
const { _attributes, failure } = testCase;
|
||||
const { name, fullname, result, duration } = _attributes;
|
||||
const testMeta = new TestMeta(suite, name);
|
||||
testMeta.result = result;
|
||||
testMeta.duration = Number(duration);
|
||||
|
||||
if (!failure) {
|
||||
core.debug(`Skip test ${fullname} without failure data`);
|
||||
return testMeta;
|
||||
}
|
||||
|
||||
core.debug(`Convert data for test ${fullname}`);
|
||||
if (failure['stack-trace'] === undefined) {
|
||||
core.warning(`No stack trace for test case: ${fullname}`);
|
||||
return testMeta;
|
||||
}
|
||||
|
||||
const trace = failure['stack-trace']._cdata;
|
||||
const point = ResultsParser.findAnnotationPoint(trace);
|
||||
if (!point.path || !point.line) {
|
||||
core.warning(`Not able to find annotation point for failed test! Test trace: ${trace}`);
|
||||
return testMeta;
|
||||
}
|
||||
|
||||
testMeta.annotation = {
|
||||
path: point.path,
|
||||
start_line: point.line,
|
||||
end_line: point.line,
|
||||
annotation_level: 'failure',
|
||||
title: fullname,
|
||||
message: failure.message._cdata,
|
||||
raw_details: trace,
|
||||
};
|
||||
core.info(
|
||||
`- ${testMeta.annotation.path}:${testMeta.annotation.start_line} - ${testMeta.annotation.title}`,
|
||||
);
|
||||
return testMeta;
|
||||
}
|
||||
|
||||
static findAnnotationPoint(trace) {
|
||||
const match = trace.match(/at .* in ((?<path>[^:]+):(?<line>\d+))/);
|
||||
return {
|
||||
path: match ? match.groups.path : '',
|
||||
line: match ? Number(match.groups.line) : 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ResultsParser;
|
||||
181
src/model/results-parser.test.js
Normal file
181
src/model/results-parser.test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import ResultsParser from './results-parser';
|
||||
|
||||
describe('ResultsParser', () => {
|
||||
describe('parseResults', () => {
|
||||
it('throws for missing file', () => {
|
||||
expect(() => ResultsParser.parseResults('')).rejects.toEqual(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertSuite', () => {
|
||||
test('convert single', () => {
|
||||
const targetSuite = {
|
||||
_attributes: {
|
||||
fullname: 'Suite Full Name',
|
||||
},
|
||||
'test-case': [{ _attributes: { name: 'testA' } }, { _attributes: { name: 'testB' } }],
|
||||
'test-suite': [
|
||||
{
|
||||
_attributes: {
|
||||
fullname: 'Inner Suite Full Name',
|
||||
},
|
||||
'test-case': { _attributes: { name: 'testC' } },
|
||||
'test-suite': [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = ResultsParser.convertSuite(targetSuite);
|
||||
|
||||
expect(result).toMatchObject([
|
||||
[
|
||||
{
|
||||
annotation: undefined,
|
||||
duration: Number.NaN,
|
||||
result: undefined,
|
||||
suite: 'Inner Suite Full Name',
|
||||
title: 'testC',
|
||||
},
|
||||
],
|
||||
{
|
||||
annotation: undefined,
|
||||
duration: Number.NaN,
|
||||
result: undefined,
|
||||
suite: 'Suite Full Name',
|
||||
title: 'testA',
|
||||
},
|
||||
{
|
||||
annotation: undefined,
|
||||
duration: Number.NaN,
|
||||
result: undefined,
|
||||
suite: 'Suite Full Name',
|
||||
title: 'testB',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertTests', () => {
|
||||
test('convert array', () => {
|
||||
const testA = { _attributes: { name: 'testA' } };
|
||||
const testB = { _attributes: { name: 'testB' } };
|
||||
const testResult = [testA, testB];
|
||||
const result = ResultsParser.convertTests('Test Suite', testResult);
|
||||
|
||||
expect(result).toMatchObject([
|
||||
{ suite: 'Test Suite', title: 'testA' },
|
||||
{ suite: 'Test Suite', title: 'testB' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('convert single', () => {
|
||||
const testA = { _attributes: { name: 'testA' } };
|
||||
const result = ResultsParser.convertTests('Test Suite', testA);
|
||||
|
||||
expect(result).toMatchObject([{ suite: 'Test Suite', title: 'testA' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertTestCase', () => {
|
||||
test('not failed', () => {
|
||||
const result = ResultsParser.convertTestCase('Test Suite', {
|
||||
_attributes: {
|
||||
name: 'Test Name',
|
||||
duration: '3.14',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.suite).toBe('Test Suite');
|
||||
expect(result.title).toBe('Test Name');
|
||||
expect(result.duration).toBe(3.14);
|
||||
expect(result.annotation).toBeUndefined();
|
||||
});
|
||||
|
||||
test('no stack trace', () => {
|
||||
const result = ResultsParser.convertTestCase('Test Suite', {
|
||||
_attributes: {
|
||||
name: 'Test Name',
|
||||
duration: '3.14',
|
||||
},
|
||||
failure: {
|
||||
message: { _cdata: 'Message CDATA' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.suite).toBe('Test Suite');
|
||||
expect(result.title).toBe('Test Name');
|
||||
expect(result.duration).toBe(3.14);
|
||||
expect(result.annotation).toBeUndefined();
|
||||
});
|
||||
|
||||
test('no annotation path', () => {
|
||||
const result = ResultsParser.convertTestCase('Test Suite', {
|
||||
_attributes: {
|
||||
name: 'Test Name',
|
||||
duration: '3.14',
|
||||
},
|
||||
failure: {
|
||||
message: { _cdata: 'Message CDATA' },
|
||||
'stack-trace': { _cdata: 'Test CDATA' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.suite).toBe('Test Suite');
|
||||
expect(result.title).toBe('Test Name');
|
||||
expect(result.duration).toBe(3.14);
|
||||
expect(result.annotation).toBeUndefined();
|
||||
});
|
||||
|
||||
test('prepare annotation', () => {
|
||||
const result = ResultsParser.convertTestCase('Test Suite', {
|
||||
_attributes: {
|
||||
name: 'Test Name',
|
||||
fullname: 'Test Full Name',
|
||||
duration: '3.14',
|
||||
},
|
||||
failure: {
|
||||
message: { _cdata: 'Message CDATA' },
|
||||
'stack-trace': {
|
||||
_cdata:
|
||||
'at Tests.SetupFailedTest.SetUp () [0x00000] in /github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs:10',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.suite).toBe('Test Suite');
|
||||
expect(result.title).toBe('Test Name');
|
||||
expect(result.duration).toBe(3.14);
|
||||
expect(result.annotation).toMatchObject({
|
||||
annotation_level: 'failure',
|
||||
end_line: 10,
|
||||
message: 'Message CDATA',
|
||||
path: '/github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs',
|
||||
raw_details:
|
||||
'at Tests.SetupFailedTest.SetUp () [0x00000] in /github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs:10',
|
||||
start_line: 10,
|
||||
title: 'Test Full Name',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAnnotationPoint', () => {
|
||||
test('keep working if not matching', () => {
|
||||
const result = ResultsParser.findAnnotationPoint('');
|
||||
expect(result.path).toBe('');
|
||||
expect(result.line).toBe(0);
|
||||
});
|
||||
|
||||
test('simple annotation point', () => {
|
||||
const result = ResultsParser.findAnnotationPoint(`at Tests.PlayModeTest+<FailedUnityTest>d__5.MoveNext () [0x0002e] in /github/workspace/unity-project/Assets/Tests/PlayModeTest.cs:39
|
||||
at UnityEngine.TestTools.TestEnumerator+<Execute>d__6.MoveNext () [0x00038] in /github/workspace/unity-project/Library/PackageCache/com.unity.test-framework@1.1.19/UnityEngine.TestRunner/NUnitExtensions/Attributes/TestEnumerator.cs:36`);
|
||||
expect(result.path).toBe('/github/workspace/unity-project/Assets/Tests/PlayModeTest.cs');
|
||||
expect(result.line).toBe(39);
|
||||
});
|
||||
|
||||
test('setup annotation point', () => {
|
||||
const result = ResultsParser.findAnnotationPoint(`SetUp
|
||||
at Tests.SetupFailedTest.SetUp () [0x00000] in /github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs:10`);
|
||||
expect(result.path).toBe('/github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs');
|
||||
expect(result.line).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
118
src/model/ts/results-meta.ts
Normal file
118
src/model/ts/results-meta.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { components } from '@octokit/openapi-types/dist-types/generated/types';
|
||||
|
||||
export function timeHelper(seconds: number): string {
|
||||
return `${seconds.toFixed(3)}s`;
|
||||
}
|
||||
|
||||
export abstract class Meta {
|
||||
title: string;
|
||||
duration = 0;
|
||||
|
||||
constructor(title: string) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
abstract get summary(): string;
|
||||
|
||||
abstract get mark(): string;
|
||||
}
|
||||
|
||||
export type Annotation = components['schemas']['check-annotation'];
|
||||
|
||||
export class RunMeta extends Meta {
|
||||
total = 0;
|
||||
passed = 0;
|
||||
skipped = 0;
|
||||
failed = 0;
|
||||
|
||||
tests: TestMeta[] = [];
|
||||
suites: RunMeta[] = [];
|
||||
|
||||
extractAnnotations(): Annotation[] {
|
||||
const result = [] as Annotation[];
|
||||
for (const suite of this.suites) {
|
||||
result.push(...suite.extractAnnotations());
|
||||
}
|
||||
for (const test of this.tests) {
|
||||
if (test.annotation !== undefined) {
|
||||
result.push(test.annotation);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
addTests(testsToAdd: TestMeta[]): void {
|
||||
testsToAdd.forEach(test => {
|
||||
this.addTest(test);
|
||||
});
|
||||
}
|
||||
|
||||
addTest(test: TestMeta): void {
|
||||
if (test.suite === undefined) {
|
||||
return;
|
||||
}
|
||||
if (test.suite === this.title) {
|
||||
this.total++;
|
||||
this.duration += test.duration;
|
||||
this.tests.push(test);
|
||||
if (test.result === 'Passed') this.passed++;
|
||||
else if (test.result === 'Failed') this.failed++;
|
||||
else this.skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
let target = this.suites.find(s => s.title === test.suite);
|
||||
if (target === undefined) {
|
||||
target = new RunMeta(test.suite);
|
||||
this.suites.push(target);
|
||||
}
|
||||
|
||||
target.addTest(test);
|
||||
}
|
||||
|
||||
get summary(): string {
|
||||
const result = this.failed > 0 ? 'Failed' : 'Passed';
|
||||
const sPart = this.skipped > 0 ? `, skipped: ${this.skipped}` : '';
|
||||
const fPart = this.failed > 0 ? `, failed: ${this.failed}` : '';
|
||||
const dPart = ` in ${timeHelper(this.duration)}`;
|
||||
return `${this.mark} ${this.title} - ${this.passed}/${this.total}${sPart}${fPart} - ${result}${dPart}`;
|
||||
}
|
||||
|
||||
get mark(): string {
|
||||
if (this.failed > 0) return '❌️';
|
||||
else if (this.skipped === 0) return '✅';
|
||||
return '⚠️';
|
||||
}
|
||||
}
|
||||
|
||||
export class TestMeta extends Meta {
|
||||
suite: string;
|
||||
result: string | undefined;
|
||||
annotation: Annotation | undefined;
|
||||
|
||||
constructor(suite: string, title: string) {
|
||||
super(title);
|
||||
this.suite = suite;
|
||||
}
|
||||
|
||||
isSkipped(): boolean {
|
||||
return this.result === 'Skipped';
|
||||
}
|
||||
|
||||
isFailed(): boolean {
|
||||
return this.result === 'Failed';
|
||||
}
|
||||
|
||||
get summary(): string {
|
||||
const dPart = this.isSkipped()
|
||||
? ''
|
||||
: ` in ${timeHelper(this.duration)}`;
|
||||
return `${this.mark} **${this.title}** - ${this.result}${dPart}`;
|
||||
}
|
||||
|
||||
get mark(): string {
|
||||
if (this.isFailed()) return '❌️';
|
||||
else if (this.isSkipped()) return '⚠️';
|
||||
return '✅';
|
||||
}
|
||||
}
|
||||
58
src/model/ts/results-report.ts
Normal file
58
src/model/ts/results-report.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
interface CommonAttributes {
|
||||
id: string;
|
||||
result: string;
|
||||
asserts: string;
|
||||
|
||||
'start-time': string;
|
||||
'end-time': string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
interface CommonSuiteAttributes extends CommonAttributes {
|
||||
total: string;
|
||||
passed: string;
|
||||
failed: string;
|
||||
skipped: string;
|
||||
}
|
||||
|
||||
export interface TestRun {
|
||||
_attributes: TestRunAttributes;
|
||||
'test-suite': TestSuite | TestSuite[];
|
||||
}
|
||||
|
||||
export interface TestRunAttributes extends CommonSuiteAttributes {
|
||||
testcasecount: string;
|
||||
'engine-version': string;
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
_attributes: TestSuiteAttributes;
|
||||
'test-suite': TestSuite | TestSuite[];
|
||||
'test-case': TestCase | TestCase[];
|
||||
failure?: FailureMessage;
|
||||
}
|
||||
|
||||
export interface TestSuiteAttributes extends CommonSuiteAttributes {
|
||||
type: string;
|
||||
name: string;
|
||||
fullname: string;
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
_attributes: TestCaseAttributes;
|
||||
failure?: FailureMessage;
|
||||
}
|
||||
|
||||
export interface TestCaseAttributes extends CommonAttributes {
|
||||
name: string;
|
||||
fullname: string;
|
||||
methodname: string;
|
||||
classname: string;
|
||||
runstate: string;
|
||||
seed: string;
|
||||
}
|
||||
|
||||
export interface FailureMessage {
|
||||
message: { _cdata: string };
|
||||
'stack-trace'?: { _cdata: string };
|
||||
}
|
||||
18
src/views/results-check-details.hbs
Normal file
18
src/views/results-check-details.hbs
Normal file
@@ -0,0 +1,18 @@
|
||||
{{#runs}}
|
||||
|
||||
<details><summary>{{summary}}</summary>
|
||||
|
||||
{{#suites}}
|
||||
* {{summary}}
|
||||
{{#tests}}
|
||||
* {{summary}}
|
||||
{{#if annotation}}
|
||||
{{indent annotation.message}}
|
||||
{{indent annotation.raw_details}}
|
||||
{{/if}}
|
||||
{{/tests}}
|
||||
{{/suites}}
|
||||
|
||||
</details>
|
||||
|
||||
{{/runs}}
|
||||
3
src/views/results-check-summary.hbs
Normal file
3
src/views/results-check-summary.hbs
Normal file
@@ -0,0 +1,3 @@
|
||||
{{#runs}}
|
||||
### {{summary}}
|
||||
{{/runs}}
|
||||
Reference in New Issue
Block a user