Merge pull request #32 from dorny/dart-json

Support dart-json
This commit is contained in:
Michal Dorner 2021-01-10 18:44:35 +01:00 committed by GitHub
commit adab07ff74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1113 additions and 177 deletions

View file

@ -19,12 +19,10 @@ jobs:
- run: npm run format-check - run: npm run format-check
- run: npm run lint - run: npm run lint
- run: npm test - run: npm test
continue-on-error: true
- name: 'Evaluate test results' - name: 'Evaluate test results'
if: always()
uses: ./ uses: ./
with: with:
name: 'JEST Tests' name: 'JEST Tests'
path: '__tests__/__results__/jest-junit.xml' path: '__tests__/__results__/jest-junit.xml'
reporter: 'jest-junit' reporter: 'jest-junit'
- name: 'Sanity check' # re-run tests in case this action failed to detect failing test
run: npm test

View file

@ -0,0 +1,92 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`dart-json tests matches report snapshot 1`] = `
Object {
"annotations": Array [
Object {
"annotation_level": "failure",
"end_line": 13,
"message": "Expected: <2>
Actual: <1>
package:test_api expect
test\\\\main_test.dart 13:9 main.<fn>.<fn>.<fn>
",
"path": "test/main_test.dart",
"start_line": 13,
"title": "[test\\\\main_test.dart] Test 1 Test 1.1 Failing test",
},
Object {
"annotation_level": "failure",
"end_line": 2,
"message": "Exception: Some error
package:darttest/main.dart 2:3 throwError
test\\\\main_test.dart 17:9 main.<fn>.<fn>.<fn>
",
"path": "lib/main.dart",
"start_line": 2,
"title": "[test\\\\main_test.dart] Test 1 Test 1.1 Exception in target unit",
},
Object {
"annotation_level": "failure",
"end_line": 24,
"message": "Exception: Some error
test\\\\main_test.dart 24:7 main.<fn>.<fn>
",
"path": "test/main_test.dart",
"start_line": 24,
"title": "[test\\\\main_test.dart] Test 2 Exception in test",
},
Object {
"annotation_level": "failure",
"end_line": 5,
"message": "TimeoutException after 0:00:00.000001: Test timed out after 0 seconds.
dart:isolate _RawReceivePortImpl._handleMessage
",
"path": "test/second_test.dart",
"start_line": 5,
"title": "[test\\\\second_test.dart] Timeout test",
},
],
"summary": "**6** tests were completed in **3.760s** with **1** passed, **1** skipped and **4** failed.
| Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ |
| :---: | :--- | ---: | ---: | ---: | ---: | ---: |
| ❌ | [test\\\\main_test.dart](#ts-0-test-maintest-dart) | 4 | 74ms | 1 | 3 | 0 |
| ❌ | [test\\\\second_test.dart](#ts-1-test-secondtest-dart) | 2 | 51ms | 0 | 1 | 1 |
# Test Suites
## <a id=\\"user-content-ts-0-test-maintest-dart\\" href=\\"#ts-0-test-maintest-dart\\">test\\\\main_test.dart</a> ❌
### Test 1
| Result | Test | Time |
| :---: | :--- | ---: |
| ✔️ | Test 1 Passing test | 36ms |
### Test 1 Test 1.1
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Test 1 Test 1.1 Failing test | 20ms |
| ❌ | Test 1 Test 1.1 Exception in target unit | 6ms |
### Test 2
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Test 2 Exception in test | 12ms |
## <a id=\\"user-content-ts-1-test-secondtest-dart\\" href=\\"#ts-1-test-secondtest-dart\\">test\\\\second_test.dart</a> ❌
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Timeout test | 37ms |
| ✖️ | Skipped test | 14ms |
",
"title": "Dart tests ❌",
}
`;

View file

@ -76,8 +76,8 @@ Received: false
"summary": "**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed. "summary": "**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed.
| Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ | | Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ |
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | | :---: | :--- | ---: | ---: | ---: | ---: | ---: |
| ❌ | [__tests__\\\\main.test.js](#ts-0-tests-main-test-js) | 4 | 0.486s | 1 | 3 | 0 | | ❌ | [__tests__\\\\main.test.js](#ts-0-tests-main-test-js) | 4 | 486ms | 1 | 3 | 0 |
| ❌ | [__tests__\\\\second.test.js](#ts-1-tests-second-test-js) | 2 | 0.082s | 0 | 1 | 1 | | ❌ | [__tests__\\\\second.test.js](#ts-1-tests-second-test-js) | 2 | 82ms | 0 | 1 | 1 |
# Test Suites # Test Suites
## <a id=\\"user-content-ts-0-tests-main-test-js\\" href=\\"#ts-0-tests-main-test-js\\">__tests__\\\\main.test.js</a> ❌ ## <a id=\\"user-content-ts-0-tests-main-test-js\\" href=\\"#ts-0-tests-main-test-js\\">__tests__\\\\main.test.js</a> ❌

View file

@ -0,0 +1,26 @@
import * as fs from 'fs'
import * as path from 'path'
import {parseDartJson} from '../src/parsers/dart-json/dart-json-parser'
import {ParseOptions} from '../src/parsers/parser-types'
const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'dart-json.json'), {encoding: 'utf8'})
const outputPath = __dirname + '/__outputs__/dart-json.md'
describe('dart-json tests', () => {
it('matches report snapshot', async () => {
const opts: ParseOptions = {
name: 'Dart tests',
annotations: true,
trackedFiles: ['lib/main.dart', 'test/main_test.dart', 'test/second_test.dart'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dart/'
}
const result = await parseDartJson(xmlFixture, opts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, result?.output?.summary ?? '')
expect(result.success).toBeFalsy()
expect(result?.output).toMatchSnapshot()
})
})

View file

@ -1,33 +1,31 @@
{"protocolVersion":"0.1.1","runnerVersion":"1.15.4","pid":21728,"type":"start","time":0} {"protocolVersion":"0.1.1","runnerVersion":"1.15.4","pid":7504,"type":"start","time":0}
{"suite":{"id":0,"platform":"vm","path":"test\\main_test.dart"},"type":"suite","time":1} {"suite":{"id":0,"platform":"vm","path":"test\\main_test.dart"},"type":"suite","time":0}
{"test":{"id":1,"name":"loading test\\main_test.dart","suiteID":0,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":1} {"test":{"id":1,"name":"loading test\\main_test.dart","suiteID":0,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":1}
{"suite":{"id":2,"platform":"vm","path":"test\\second_test.dart"},"type":"suite","time":12} {"suite":{"id":2,"platform":"vm","path":"test\\second_test.dart"},"type":"suite","time":11}
{"test":{"id":3,"name":"loading test\\second_test.dart","suiteID":2,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":12} {"test":{"id":3,"name":"loading test\\second_test.dart","suiteID":2,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":11}
{"count":2,"type":"allSuites","time":13} {"count":2,"type":"allSuites","time":11}
{"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3761} {"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3649}
{"group":{"id":4,"suiteID":2,"parentID":null,"name":null,"metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":3766} {"group":{"id":4,"suiteID":2,"parentID":null,"name":null,"metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":3654}
{"test":{"id":5,"name":"Timeout test","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":5,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/second_test.dart"},"type":"testStart","time":3767} {"test":{"id":5,"name":"Timeout test","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":5,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/second_test.dart"},"type":"testStart","time":3655}
{"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3784} {"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3672}
{"group":{"id":6,"suiteID":0,"parentID":null,"name":null,"metadata":{"skip":false,"skipReason":null},"testCount":5,"line":null,"column":null,"url":null},"type":"group","time":3784} {"group":{"id":6,"suiteID":0,"parentID":null,"name":null,"metadata":{"skip":false,"skipReason":null},"testCount":4,"line":null,"column":null,"url":null},"type":"group","time":3672}
{"group":{"id":7,"suiteID":0,"parentID":6,"name":"Test 1","metadata":{"skip":false,"skipReason":null},"testCount":3,"line":6,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3785} {"group":{"id":7,"suiteID":0,"parentID":6,"name":"Test 1","metadata":{"skip":false,"skipReason":null},"testCount":3,"line":6,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3672}
{"test":{"id":8,"name":"Test 1 Passing test","suiteID":0,"groupIDs":[6,7],"metadata":{"skip":false,"skipReason":null},"line":7,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3785} {"test":{"id":8,"name":"Test 1 Passing test","suiteID":0,"groupIDs":[6,7],"metadata":{"skip":false,"skipReason":null},"line":7,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3672}
{"testID":5,"error":"TimeoutException after 0:00:00.000001: Test timed out after 0 seconds.","stackTrace":"dart:isolate _RawReceivePortImpl._handleMessage\n","isFailure":false,"type":"error","time":3804} {"testID":5,"error":"TimeoutException after 0:00:00.000001: Test timed out after 0 seconds.","stackTrace":"dart:isolate _RawReceivePortImpl._handleMessage\n","isFailure":false,"type":"error","time":3692}
{"testID":5,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3804} {"testID":5,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3692}
{"test":{"id":9,"name":"Skipped test","suiteID":2,"groupIDs":[4],"metadata":{"skip":true,"skipReason":"skipped test"},"line":9,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/second_test.dart"},"type":"testStart","time":3805} {"test":{"id":9,"name":"Skipped test","suiteID":2,"groupIDs":[4],"metadata":{"skip":true,"skipReason":"skipped test"},"line":9,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/second_test.dart"},"type":"testStart","time":3693}
{"testID":9,"messageType":"skip","message":"Skip: skipped test","type":"print","time":3807} {"testID":9,"messageType":"skip","message":"Skip: skipped test","type":"print","time":3706}
{"testID":9,"result":"success","skipped":true,"hidden":false,"type":"testDone","time":3808} {"testID":9,"result":"success","skipped":true,"hidden":false,"type":"testDone","time":3707}
{"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3815} {"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3708}
{"group":{"id":10,"suiteID":0,"parentID":7,"name":"Test 1 Test 1.1","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":11,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3817} {"group":{"id":10,"suiteID":0,"parentID":7,"name":"Test 1 Test 1.1","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":11,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3715}
{"test":{"id":11,"name":"Test 1 Test 1.1 Failing test","suiteID":0,"groupIDs":[6,7,10],"metadata":{"skip":false,"skipReason":null},"line":12,"column":7,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3817} {"test":{"id":11,"name":"Test 1 Test 1.1 Failing test","suiteID":0,"groupIDs":[6,7,10],"metadata":{"skip":false,"skipReason":null},"line":12,"column":7,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3716}
{"testID":11,"error":"Expected: <2>\n Actual: <1>\n","stackTrace":"package:test_api expect\ntest\\main_test.dart 13:9 main.<fn>.<fn>.<fn>\n","isFailure":true,"type":"error","time":3840} {"testID":11,"error":"Expected: <2>\n Actual: <1>\n","stackTrace":"package:test_api expect\ntest\\main_test.dart 13:9 main.<fn>.<fn>.<fn>\n","isFailure":true,"type":"error","time":3736}
{"testID":11,"result":"failure","skipped":false,"hidden":false,"type":"testDone","time":3840} {"testID":11,"result":"failure","skipped":false,"hidden":false,"type":"testDone","time":3736}
{"test":{"id":12,"name":"Test 1 Test 1.1 Exception in target unit","suiteID":0,"groupIDs":[6,7,10],"metadata":{"skip":false,"skipReason":null},"line":16,"column":7,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3845} {"test":{"id":12,"name":"Test 1 Test 1.1 Exception in target unit","suiteID":0,"groupIDs":[6,7,10],"metadata":{"skip":false,"skipReason":null},"line":16,"column":7,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3737}
{"testID":12,"error":"Exception: Some error","stackTrace":"package:darttest/main.dart 2:3 throwError\ntest\\main_test.dart 17:9 main.<fn>.<fn>.<fn>\n","isFailure":false,"type":"error","time":3852} {"testID":12,"error":"Exception: Some error","stackTrace":"package:darttest/main.dart 2:3 throwError\ntest\\main_test.dart 17:9 main.<fn>.<fn>.<fn>\n","isFailure":false,"type":"error","time":3743}
{"testID":12,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3853} {"testID":12,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3743}
{"group":{"id":13,"suiteID":0,"parentID":6,"name":"Test 2","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":22,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3854} {"group":{"id":13,"suiteID":0,"parentID":6,"name":"Test 2","metadata":{"skip":false,"skipReason":null},"testCount":1,"line":22,"column":3,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"group","time":3744}
{"test":{"id":14,"name":"Test 2 Exception in test","suiteID":0,"groupIDs":[6,13],"metadata":{"skip":false,"skipReason":null},"line":23,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3854} {"test":{"id":14,"name":"Test 2 Exception in test","suiteID":0,"groupIDs":[6,13],"metadata":{"skip":false,"skipReason":null},"line":23,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3744}
{"testID":14,"error":"Exception: Some error","stackTrace":"test\\main_test.dart 24:7 main.<fn>.<fn>\n","isFailure":false,"type":"error","time":3869} {"testID":14,"error":"Exception: Some error","stackTrace":"test\\main_test.dart 24:7 main.<fn>.<fn>\n","isFailure":false,"type":"error","time":3756}
{"testID":14,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3869} {"testID":14,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3756}
{"test":{"id":15,"name":"Test 2 Timeout test","suiteID":0,"groupIDs":[6,13],"metadata":{"skip":false,"skipReason":null},"line":27,"column":5,"url":"file:///C:/Users/Michal/Workspace/dorny/test-check/reports/dart/test/main_test.dart"},"type":"testStart","time":3870} {"success":false,"type":"done","time":3760}
{"testID":15,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3882}
{"success":false,"type":"done","time":3886}

View file

@ -2,7 +2,7 @@ import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {parseJestJunit} from '../src/parsers/jest-junit/jest-junit-parser' import {parseJestJunit} from '../src/parsers/jest-junit/jest-junit-parser'
import {ParseOptions} from '../src/parsers/test-parser' import {ParseOptions} from '../src/parsers/parser-types'
const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'jest-junit.xml'), {encoding: 'utf8'}) const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'jest-junit.xml'), {encoding: 'utf8'})
const outputPath = __dirname + '/__outputs__/jest-junit.md' const outputPath = __dirname + '/__outputs__/jest-junit.md'
@ -10,6 +10,7 @@ const outputPath = __dirname + '/__outputs__/jest-junit.md'
describe('jest-junit tests', () => { describe('jest-junit tests', () => {
it('matches report snapshot', async () => { it('matches report snapshot', async () => {
const opts: ParseOptions = { const opts: ParseOptions = {
name: 'jest tests',
annotations: true, annotations: true,
trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'], trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/' workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/'

478
dist/index.js generated vendored
View file

@ -30,6 +30,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__webpack_require__(2186)); const core = __importStar(__webpack_require__(2186));
const github = __importStar(__webpack_require__(5438)); const github = __importStar(__webpack_require__(5438));
const jest_junit_parser_1 = __webpack_require__(1113); const jest_junit_parser_1 = __webpack_require__(1113);
const dart_json_parser_1 = __webpack_require__(4528);
const file_utils_1 = __webpack_require__(2711); const file_utils_1 = __webpack_require__(2711);
const git_1 = __webpack_require__(9844); const git_1 = __webpack_require__(9844);
const github_utils_1 = __webpack_require__(3522); const github_utils_1 = __webpack_require__(3522);
@ -58,6 +59,7 @@ async function main() {
// We won't need tracked files if we are not going to create annotations // We won't need tracked files if we are not going to create annotations
const trackedFiles = annotations ? await git_1.listFiles() : []; const trackedFiles = annotations ? await git_1.listFiles() : [];
const opts = { const opts = {
name,
annotations, annotations,
trackedFiles, trackedFiles,
workDir workDir
@ -81,10 +83,12 @@ async function main() {
} }
function getParser(reporter) { function getParser(reporter) {
switch (reporter) { switch (reporter) {
case 'dart-json':
return dart_json_parser_1.parseDartJson;
case 'dotnet-trx': case 'dotnet-trx':
throw new Error('Not implemented yet!'); throw new Error('Not implemented yet!');
case 'flutter-machine': case 'flutter-machine':
throw new Error('Not implemented yet!'); return dart_json_parser_1.parseDartJson;
case 'jest-junit': case 'jest-junit':
return jest_junit_parser_1.parseJestJunit; return jest_junit_parser_1.parseJestJunit;
default: default:
@ -96,18 +100,248 @@ run();
/***/ }), /***/ }),
/***/ 1113: /***/ 4528:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => { /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict"; "use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.parseDartJson = void 0;
const get_report_1 = __importDefault(__webpack_require__(3737));
const file_utils_1 = __webpack_require__(2711);
const markdown_utils_1 = __webpack_require__(6482);
const dart_json_types_1 = __webpack_require__(7887);
const test_results_1 = __webpack_require__(8407);
class TestRun {
constructor(suites, success, time) {
this.suites = suites;
this.success = success;
this.time = time;
}
}
class TestSuite {
constructor(suite) {
this.suite = suite;
this.groups = {};
}
}
class TestGroup {
constructor(group) {
this.group = group;
this.tests = [];
}
}
class TestCase {
constructor(testStart) {
this.testStart = testStart;
this.groupId = testStart.test.groupIDs[testStart.test.groupIDs.length - 1];
}
get result() {
var _a, _b, _c, _d;
if ((_a = this.testDone) === null || _a === void 0 ? void 0 : _a.skipped) {
return 'skipped';
}
if (((_b = this.testDone) === null || _b === void 0 ? void 0 : _b.result) === 'success') {
return 'success';
}
if (((_c = this.testDone) === null || _c === void 0 ? void 0 : _c.result) === 'error' || ((_d = this.testDone) === null || _d === void 0 ? void 0 : _d.result) === 'failure') {
return 'failed';
}
return undefined;
}
get time() {
return this.testDone !== undefined ? this.testDone.time - this.testStart.time : 0;
}
}
async function parseDartJson(content, options) {
const testRun = getTestRun(content);
const icon = testRun.success ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.fail;
return {
success: testRun.success,
output: {
title: `${options.name.trim()} ${icon}`,
summary: get_report_1.default(getTestRunResult(testRun)),
annotations: options.annotations ? getAnnotations(testRun, options.workDir, options.trackedFiles) : undefined
}
};
}
exports.parseDartJson = parseDartJson;
function getTestRun(content) {
const lines = content.split(/\n\r?/g).filter(line => line !== '');
const events = lines.map(str => JSON.parse(str));
let success = false;
let totalTime = 0;
const suites = {};
const tests = {};
for (const evt of events) {
if (dart_json_types_1.isSuiteEvent(evt)) {
suites[evt.suite.id] = new TestSuite(evt.suite);
}
else if (dart_json_types_1.isGroupEvent(evt)) {
suites[evt.group.suiteID].groups[evt.group.id] = new TestGroup(evt.group);
}
else if (dart_json_types_1.isTestStartEvent(evt) && evt.test.url !== null) {
const test = new TestCase(evt);
const suite = suites[evt.test.suiteID];
const group = suite.groups[evt.test.groupIDs[evt.test.groupIDs.length - 1]];
group.tests.push(test);
tests[evt.test.id] = test;
}
else if (dart_json_types_1.isTestDoneEvent(evt) && !evt.hidden) {
tests[evt.testID].testDone = evt;
}
else if (dart_json_types_1.isErrorEvent(evt)) {
tests[evt.testID].error = evt;
}
else if (dart_json_types_1.isDoneEvent(evt)) {
success = evt.success;
totalTime = evt.time;
}
}
return new TestRun(Object.values(suites), success, totalTime);
}
function getTestRunResult(tr) {
const suites = tr.suites.map(s => {
return new test_results_1.TestSuiteResult(s.suite.path, getGroups(s));
});
return new test_results_1.TestRunResult(suites, tr.time);
}
function getGroups(suite) {
const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0);
groups.sort((a, b) => { var _a, _b; return ((_a = a.group.line) !== null && _a !== void 0 ? _a : 0) - ((_b = b.group.line) !== null && _b !== void 0 ? _b : 0); });
return groups.map(group => {
group.tests.sort((a, b) => { var _a, _b; return ((_a = a.testStart.test.line) !== null && _a !== void 0 ? _a : 0) - ((_b = b.testStart.test.line) !== null && _b !== void 0 ? _b : 0); });
const tests = group.tests.map(t => new test_results_1.TestCaseResult(t.testStart.test.name, t.result, t.time));
return new test_results_1.TestGroupResult(group.group.name, tests);
});
}
function getAnnotations(tr, workDir, trackedFiles) {
const annotations = [];
for (const suite of tr.suites) {
for (const group of Object.values(suite.groups)) {
for (const test of group.tests) {
if (test.error) {
const err = getAnnotation(test, suite, workDir, trackedFiles);
if (err !== null) {
annotations.push(err);
}
}
}
}
}
return annotations;
}
function getAnnotation(test, testSuite, workDir, trackedFiles) {
var _a, _b, _c, _d, _e, _f;
const stack = (_b = (_a = test.error) === null || _a === void 0 ? void 0 : _a.stackTrace) !== null && _b !== void 0 ? _b : '';
let src = exceptionThrowSource(stack, trackedFiles);
if (src === null) {
const file = getRelativePathFromUrl((_c = test.testStart.test.url) !== null && _c !== void 0 ? _c : '', workDir);
if (!trackedFiles.includes(file)) {
return null;
}
src = {
file,
line: (_d = test.testStart.test.line) !== null && _d !== void 0 ? _d : 0
};
}
return {
annotation_level: 'failure',
start_line: src.line,
end_line: src.line,
path: src.file,
message: `${(_e = test.error) === null || _e === void 0 ? void 0 : _e.error}\n\n${(_f = test.error) === null || _f === void 0 ? void 0 : _f.stackTrace}`,
title: `[${testSuite.suite.path}] ${test.testStart.test.name}`
};
}
function exceptionThrowSource(ex, trackedFiles) {
// imports from package which is tested are listed in stack traces as 'package:xyz/' which maps to relative path 'lib/'
const packageRe = /^package:[a-zA-z0-9_$]+\//;
const lines = ex.split(/\r?\n/).map(str => str.replace(packageRe, 'lib/'));
// regexp to extract file path and line number from stack trace
const re = /^(.*)\s+(\d+):\d+\s+/;
for (const str of lines) {
const match = str.match(re);
if (match !== null) {
const [_, fileStr, lineStr] = match;
const file = file_utils_1.normalizeFilePath(fileStr);
if (trackedFiles.includes(file)) {
const line = parseInt(lineStr);
return { file, line };
}
}
}
return null;
}
function getRelativePathFromUrl(file, workdir) {
const prefix = 'file:///';
if (file.startsWith(prefix)) {
file = file.substr(prefix.length);
}
if (file.startsWith(workdir)) {
file = file.substr(workdir.length);
}
return file;
}
/***/ }),
/***/ 7887:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
/// reflects documentation at https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isDoneEvent = exports.isErrorEvent = exports.isTestDoneEvent = exports.isTestStartEvent = exports.isGroupEvent = exports.isSuiteEvent = void 0;
function isSuiteEvent(event) {
return event.type === 'suite';
}
exports.isSuiteEvent = isSuiteEvent;
function isGroupEvent(event) {
return event.type === 'group';
}
exports.isGroupEvent = isGroupEvent;
function isTestStartEvent(event) {
return event.type === 'testStart';
}
exports.isTestStartEvent = isTestStartEvent;
function isTestDoneEvent(event) {
return event.type === 'testDone';
}
exports.isTestDoneEvent = isTestDoneEvent;
function isErrorEvent(event) {
return event.type === 'error';
}
exports.isErrorEvent = isErrorEvent;
function isDoneEvent(event) {
return event.type === 'done';
}
exports.isDoneEvent = isDoneEvent;
/***/ }),
/***/ 1113:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.exceptionThrowSource = exports.parseJestJunit = void 0; exports.exceptionThrowSource = exports.parseJestJunit = void 0;
const xml2js_1 = __webpack_require__(6189); const xml2js_1 = __webpack_require__(6189);
const markdown_utils_1 = __webpack_require__(6482); const markdown_utils_1 = __webpack_require__(6482);
const file_utils_1 = __webpack_require__(2711); const file_utils_1 = __webpack_require__(2711);
const slugger_1 = __webpack_require__(3328);
const xml_utils_1 = __webpack_require__(8653); const xml_utils_1 = __webpack_require__(8653);
const test_results_1 = __webpack_require__(8407);
const get_report_1 = __importDefault(__webpack_require__(3737));
async function parseJestJunit(content, options) { async function parseJestJunit(content, options) {
var _a, _b; var _a, _b;
const junit = (await xml2js_1.parseStringPromise(content, { const junit = (await xml2js_1.parseStringPromise(content, {
@ -119,44 +353,25 @@ async function parseJestJunit(content, options) {
return { return {
success, success,
output: { output: {
title: `${junit.testsuites.$.name.trim()} ${icon}`, title: `${options.name.trim()} ${icon}`,
summary: getSummary(success, junit), summary: getSummary(junit),
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
} }
}; };
} }
exports.parseJestJunit = parseJestJunit; exports.parseJestJunit = parseJestJunit;
function getSummary(success, junit) { function getSummary(junit) {
var _a, _b; const suites = junit.testsuites.testsuite.map(ts => {
const stats = junit.testsuites.$; const name = ts.$.name.trim();
const time = `${stats.time.toFixed(3)}s`; const time = ts.$.time * 1000;
const skipped = getSkippedCount(junit.testsuites); const sr = new test_results_1.TestSuiteResult(name, getGroups(ts), time);
const failed = stats.errors + stats.failures; return sr;
const passed = stats.tests - failed - skipped;
const headingLine = `**${stats.tests}** tests were completed in **${time}** with **${passed}** passed, **${skipped}** skipped and **${failed}** failed.`;
const suitesSummary = junit.testsuites.testsuite.map((ts, i) => {
const skip = ts.$.skipped;
const fail = ts.$.errors + ts.$.failures;
const pass = ts.$.tests - fail - skip;
const tm = `${ts.$.time.toFixed(3)}s`;
const result = success ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.fail;
const tsName = ts.$.name.trim();
const tsAddr = makeSuiteSlug(i, tsName).link;
const tsNameLink = markdown_utils_1.link(tsName, tsAddr);
return [result, tsNameLink, ts.$.tests, tm, pass, fail, skip];
}); });
const summary = markdown_utils_1.table(['Result', 'Suite', 'Tests', 'Time', `Passed ${markdown_utils_1.Icon.success}`, `Failed ${markdown_utils_1.Icon.fail}`, `Skipped ${markdown_utils_1.Icon.skip}`], [markdown_utils_1.Align.Center, markdown_utils_1.Align.Left, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right], ...suitesSummary); const time = junit.testsuites.$.time * 1000;
const suites = (_b = (_a = junit.testsuites) === null || _a === void 0 ? void 0 : _a.testsuite) === null || _b === void 0 ? void 0 : _b.map((ts, i) => getSuiteSummary(ts, i)).join('\n'); const tr = new test_results_1.TestRunResult(suites, time);
const suitesSection = `# Test Suites\n\n${suites}`; return get_report_1.default(tr);
return `${headingLine}\n${summary}\n${suitesSection}`;
} }
function getSkippedCount(suites) { function getGroups(suite) {
return suites.testsuite.reduce((sum, suite) => sum + suite.$.skipped, 0);
}
function getSuiteSummary(suite, index) {
var _a, _b;
const success = !(((_a = suite.$) === null || _a === void 0 ? void 0 : _a.failures) > 0 || ((_b = suite.$) === null || _b === void 0 ? void 0 : _b.errors) > 0);
const icon = success ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.fail;
const groups = []; const groups = [];
for (const tc of suite.testcase) { for (const tc of suite.testcase) {
let grp = groups.find(g => g.describe === tc.$.classname); let grp = groups.find(g => g.describe === tc.$.classname);
@ -166,33 +381,22 @@ function getSuiteSummary(suite, index) {
} }
grp.tests.push(tc); grp.tests.push(tc);
} }
const content = groups return groups.map(grp => {
.map(grp => { const tests = grp.tests.map(tc => {
const header = grp.describe !== '' ? `### ${grp.describe.trim()}\n\n` : '';
const tests = markdown_utils_1.table(['Result', 'Test', 'Time'], [markdown_utils_1.Align.Center, markdown_utils_1.Align.Left, markdown_utils_1.Align.Right], ...grp.tests.map(tc => {
const name = tc.$.name.trim(); const name = tc.$.name.trim();
const time = `${Math.round(tc.$.time * 1000)}ms`; const result = getTestCaseResult(tc);
const result = getTestCaseIcon(tc); const time = tc.$.time * 1000;
return [result, name, time]; return new test_results_1.TestCaseResult(name, result, time);
})); });
return `${header}${tests}\n`; return new test_results_1.TestGroupResult(grp.describe, tests);
}) });
.join('\n');
const tsName = suite.$.name.trim();
const tsSlug = makeSuiteSlug(index, tsName);
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`;
return `## ${tsNameLink} ${icon}\n\n${content}`;
} }
function getTestCaseIcon(test) { function getTestCaseResult(test) {
if (test.failure) if (test.failure)
return markdown_utils_1.Icon.fail; return 'failed';
if (test.skipped) if (test.skipped)
return markdown_utils_1.Icon.skip; return 'skipped';
return markdown_utils_1.Icon.success; return 'success';
}
function makeSuiteSlug(index, name) {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
return slugger_1.slug(`ts-${index}-${name}`);
} }
function getAnnotations(junit, workDir, trackedFiles) { function getAnnotations(junit, workDir, trackedFiles) {
const annotations = []; const annotations = [];
@ -240,6 +444,164 @@ function exceptionThrowSource(ex, workDir, trackedFiles) {
exports.exceptionThrowSource = exceptionThrowSource; exports.exceptionThrowSource = exceptionThrowSource;
/***/ }),
/***/ 3737:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
const markdown_utils_1 = __webpack_require__(6482);
const slugger_1 = __webpack_require__(3328);
function getReport(tr) {
const time = `${(tr.time / 1000).toFixed(3)}s`;
const headingLine = `**${tr.tests}** tests were completed in **${time}** with **${tr.passed}** passed, **${tr.skipped}** skipped and **${tr.failed}** failed.`;
const suitesSummary = tr.suites.map((s, i) => {
const icon = getResultIcon(s.result);
const tsTime = `${s.time}ms`;
const tsName = s.name;
const tsAddr = makeSuiteSlug(i, tsName).link;
const tsNameLink = markdown_utils_1.link(tsName, tsAddr);
return [icon, tsNameLink, s.tests, tsTime, s.passed, s.failed, s.skipped];
});
const summary = markdown_utils_1.table(['Result', 'Suite', 'Tests', 'Time', `Passed ${markdown_utils_1.Icon.success}`, `Failed ${markdown_utils_1.Icon.fail}`, `Skipped ${markdown_utils_1.Icon.skip}`], [markdown_utils_1.Align.Center, markdown_utils_1.Align.Left, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right, markdown_utils_1.Align.Right], ...suitesSummary);
const suites = tr.suites.map((ts, i) => getSuiteSummary(ts, i)).join('\n');
const suitesSection = `# Test Suites\n\n${suites}`;
return `${headingLine}\n${summary}\n${suitesSection}`;
}
exports.default = getReport;
function getSuiteSummary(ts, index) {
const icon = getResultIcon(ts.result);
const content = ts.groups
.map(grp => {
const header = grp.name ? `### ${grp.name}\n\n` : '';
const tests = markdown_utils_1.table(['Result', 'Test', 'Time'], [markdown_utils_1.Align.Center, markdown_utils_1.Align.Left, markdown_utils_1.Align.Right], ...grp.tests.map(tc => {
const name = tc.name;
const time = `${tc.time}ms`;
const result = getResultIcon(tc.result);
return [result, name, time];
}));
return `${header}${tests}\n`;
})
.join('\n');
const tsName = ts.name;
const tsSlug = makeSuiteSlug(index, tsName);
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`;
return `## ${tsNameLink} ${icon}\n\n${content}`;
}
function makeSuiteSlug(index, name) {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
return slugger_1.slug(`ts-${index}-${name}`);
}
function getResultIcon(result) {
switch (result) {
case 'success':
return markdown_utils_1.Icon.success;
case 'skipped':
return markdown_utils_1.Icon.skip;
case 'failed':
return markdown_utils_1.Icon.fail;
default:
return '';
}
}
/***/ }),
/***/ 8407:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.TestCaseResult = exports.TestGroupResult = exports.TestSuiteResult = exports.TestRunResult = void 0;
class TestRunResult {
constructor(suites, totalTime) {
this.suites = suites;
this.totalTime = totalTime;
}
get tests() {
return this.suites.reduce((sum, g) => sum + g.tests, 0);
}
get passed() {
return this.suites.reduce((sum, g) => sum + g.passed, 0);
}
get failed() {
return this.suites.reduce((sum, g) => sum + g.failed, 0);
}
get skipped() {
return this.suites.reduce((sum, g) => sum + g.skipped, 0);
}
get time() {
var _a;
return (_a = this.totalTime) !== null && _a !== void 0 ? _a : this.suites.reduce((sum, g) => sum + g.time, 0);
}
get result() {
return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success';
}
}
exports.TestRunResult = TestRunResult;
class TestSuiteResult {
constructor(name, groups, totalTime) {
this.name = name;
this.groups = groups;
this.totalTime = totalTime;
}
get tests() {
return this.groups.reduce((sum, g) => sum + g.tests.length, 0);
}
get passed() {
return this.groups.reduce((sum, g) => sum + g.passed, 0);
}
get failed() {
return this.groups.reduce((sum, g) => sum + g.failed, 0);
}
get skipped() {
return this.groups.reduce((sum, g) => sum + g.skipped, 0);
}
get time() {
var _a;
return (_a = this.totalTime) !== null && _a !== void 0 ? _a : this.groups.reduce((sum, g) => sum + g.time, 0);
}
get result() {
return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success';
}
}
exports.TestSuiteResult = TestSuiteResult;
class TestGroupResult {
constructor(name, tests) {
this.name = name;
this.tests = tests;
}
get passed() {
return this.tests.reduce((sum, t) => (t.result === 'success' ? sum + 1 : sum), 0);
}
get failed() {
return this.tests.reduce((sum, t) => (t.result === 'failed' ? sum + 1 : sum), 0);
}
get skipped() {
return this.tests.reduce((sum, t) => (t.result === 'skipped' ? sum + 1 : sum), 0);
}
get time() {
return this.tests.reduce((sum, t) => sum + t.time, 0);
}
get result() {
return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success';
}
}
exports.TestGroupResult = TestGroupResult;
class TestCaseResult {
constructor(name, result, time) {
this.name = name;
this.result = result;
this.time = time;
}
}
exports.TestCaseResult = TestCaseResult;
/***/ }), /***/ }),
/***/ 6069: /***/ 6069:

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

6
package-lock.json generated
View file

@ -7867,9 +7867,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.19", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true "dev": true
}, },
"lodash.memoize": { "lodash.memoize": {

View file

@ -1,7 +1,8 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser' import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser'
import {ParseOptions, ParseTestResult} from './parsers/test-parser' import {parseDartJson} from './parsers/dart-json/dart-json-parser'
import {ParseOptions, ParseTestResult} from './parsers/parser-types'
import {getFileContent, normalizeDirPath} from './utils/file-utils' import {getFileContent, normalizeDirPath} from './utils/file-utils'
import {listFiles} from './utils/git' import {listFiles} from './utils/git'
import {getCheckRunSha} from './utils/github-utils' import {getCheckRunSha} from './utils/github-utils'
@ -35,6 +36,7 @@ async function main(): Promise<void> {
const trackedFiles = annotations ? await listFiles() : [] const trackedFiles = annotations ? await listFiles() : []
const opts: ParseOptions = { const opts: ParseOptions = {
name,
annotations, annotations,
trackedFiles, trackedFiles,
workDir workDir
@ -62,10 +64,12 @@ async function main(): Promise<void> {
function getParser(reporter: string): ParseTestResult { function getParser(reporter: string): ParseTestResult {
switch (reporter) { switch (reporter) {
case 'dart-json':
return parseDartJson
case 'dotnet-trx': case 'dotnet-trx':
throw new Error('Not implemented yet!') throw new Error('Not implemented yet!')
case 'flutter-machine': case 'flutter-machine':
throw new Error('Not implemented yet!') return parseDartJson
case 'jest-junit': case 'jest-junit':
return parseJestJunit return parseJestJunit
default: default:

View file

@ -0,0 +1,215 @@
import {Annotation, ParseOptions, TestResult} from '../parser-types'
import getReport from '../../report/get-report'
import {normalizeFilePath} from '../../utils/file-utils'
import {Icon} from '../../utils/markdown-utils'
import {
ReportEvent,
Suite,
Group,
TestStartEvent,
TestDoneEvent,
ErrorEvent,
isSuiteEvent,
isGroupEvent,
isTestStartEvent,
isTestDoneEvent,
isErrorEvent,
isDoneEvent
} from './dart-json-types'
import {
TestExecutionResult,
TestRunResult,
TestSuiteResult,
TestGroupResult,
TestCaseResult
} from '../../report/test-results'
class TestRun {
constructor(readonly suites: TestSuite[], readonly success: boolean, readonly time: number) {}
}
class TestSuite {
constructor(readonly suite: Suite) {}
readonly groups: {[id: number]: TestGroup} = {}
}
class TestGroup {
constructor(readonly group: Group) {}
readonly tests: TestCase[] = []
}
class TestCase {
constructor(readonly testStart: TestStartEvent) {
this.groupId = testStart.test.groupIDs[testStart.test.groupIDs.length - 1]
}
readonly groupId: number
testDone?: TestDoneEvent
error?: ErrorEvent
get result(): TestExecutionResult {
if (this.testDone?.skipped) {
return 'skipped'
}
if (this.testDone?.result === 'success') {
return 'success'
}
if (this.testDone?.result === 'error' || this.testDone?.result === 'failure') {
return 'failed'
}
return undefined
}
get time(): number {
return this.testDone !== undefined ? this.testDone.time - this.testStart.time : 0
}
}
export async function parseDartJson(content: string, options: ParseOptions): Promise<TestResult> {
const testRun = getTestRun(content)
const icon = testRun.success ? Icon.success : Icon.fail
return {
success: testRun.success,
output: {
title: `${options.name.trim()} ${icon}`,
summary: getReport(getTestRunResult(testRun)),
annotations: options.annotations ? getAnnotations(testRun, options.workDir, options.trackedFiles) : undefined
}
}
}
function getTestRun(content: string): TestRun {
const lines = content.split(/\n\r?/g).filter(line => line !== '')
const events = lines.map(str => JSON.parse(str)) as ReportEvent[]
let success = false
let totalTime = 0
const suites: {[id: number]: TestSuite} = {}
const tests: {[id: number]: TestCase} = {}
for (const evt of events) {
if (isSuiteEvent(evt)) {
suites[evt.suite.id] = new TestSuite(evt.suite)
} else if (isGroupEvent(evt)) {
suites[evt.group.suiteID].groups[evt.group.id] = new TestGroup(evt.group)
} else if (isTestStartEvent(evt) && evt.test.url !== null) {
const test: TestCase = new TestCase(evt)
const suite = suites[evt.test.suiteID]
const group = suite.groups[evt.test.groupIDs[evt.test.groupIDs.length - 1]]
group.tests.push(test)
tests[evt.test.id] = test
} else if (isTestDoneEvent(evt) && !evt.hidden) {
tests[evt.testID].testDone = evt
} else if (isErrorEvent(evt)) {
tests[evt.testID].error = evt
} else if (isDoneEvent(evt)) {
success = evt.success
totalTime = evt.time
}
}
return new TestRun(Object.values(suites), success, totalTime)
}
function getTestRunResult(tr: TestRun): TestRunResult {
const suites = tr.suites.map(s => {
return new TestSuiteResult(s.suite.path, getGroups(s))
})
return new TestRunResult(suites, tr.time)
}
function getGroups(suite: TestSuite): TestGroupResult[] {
const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0)
groups.sort((a, b) => (a.group.line ?? 0) - (b.group.line ?? 0))
return groups.map(group => {
group.tests.sort((a, b) => (a.testStart.test.line ?? 0) - (b.testStart.test.line ?? 0))
const tests = group.tests.map(t => new TestCaseResult(t.testStart.test.name, t.result, t.time))
return new TestGroupResult(group.group.name, tests)
})
}
function getAnnotations(tr: TestRun, workDir: string, trackedFiles: string[]): Annotation[] {
const annotations: Annotation[] = []
for (const suite of tr.suites) {
for (const group of Object.values(suite.groups)) {
for (const test of group.tests) {
if (test.error) {
const err = getAnnotation(test, suite, workDir, trackedFiles)
if (err !== null) {
annotations.push(err)
}
}
}
}
}
return annotations
}
function getAnnotation(
test: TestCase,
testSuite: TestSuite,
workDir: string,
trackedFiles: string[]
): Annotation | null {
const stack = test.error?.stackTrace ?? ''
let src = exceptionThrowSource(stack, trackedFiles)
if (src === null) {
const file = getRelativePathFromUrl(test.testStart.test.url ?? '', workDir)
if (!trackedFiles.includes(file)) {
return null
}
src = {
file,
line: test.testStart.test.line ?? 0
}
}
return {
annotation_level: 'failure',
start_line: src.line,
end_line: src.line,
path: src.file,
message: `${test.error?.error}\n\n${test.error?.stackTrace}`,
title: `[${testSuite.suite.path}] ${test.testStart.test.name}`
}
}
function exceptionThrowSource(ex: string, trackedFiles: string[]): {file: string; line: number} | null {
// imports from package which is tested are listed in stack traces as 'package:xyz/' which maps to relative path 'lib/'
const packageRe = /^package:[a-zA-z0-9_$]+\//
const lines = ex.split(/\r?\n/).map(str => str.replace(packageRe, 'lib/'))
// regexp to extract file path and line number from stack trace
const re = /^(.*)\s+(\d+):\d+\s+/
for (const str of lines) {
const match = str.match(re)
if (match !== null) {
const [_, fileStr, lineStr] = match
const file = normalizeFilePath(fileStr)
if (trackedFiles.includes(file)) {
const line = parseInt(lineStr)
return {file, line}
}
}
}
return null
}
function getRelativePathFromUrl(file: string, workdir: string): string {
const prefix = 'file:///'
if (file.startsWith(prefix)) {
file = file.substr(prefix.length)
}
if (file.startsWith(workdir)) {
file = file.substr(workdir.length)
}
return file
}

View file

@ -0,0 +1,129 @@
/// reflects documentation at https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
export type ReportEvent =
| StartEvent
| AllSuitesEvent
| SuiteEvent
| DebugEvent
| GroupEvent
| TestStartEvent
| TestDoneEvent
| DoneEvent
| MessageEvent
| ErrorEvent
export interface Event {
type: 'start' | 'allSuites' | 'suite' | 'debug' | 'group' | 'testStart' | 'print' | 'error' | 'testDone' | 'done'
time: number
}
export interface StartEvent extends Event {
type: 'start'
protocolVersion: string
runnerVersion: string
pid: number
}
export interface AllSuitesEvent extends Event {
type: 'allSuites'
count: number // The total number of suites that will be loaded.
}
export interface SuiteEvent extends Event {
type: 'suite'
suite: Suite
}
export interface GroupEvent extends Event {
type: 'group'
group: Group
}
export interface TestStartEvent extends Event {
type: 'testStart'
test: Test
}
export interface TestDoneEvent extends Event {
type: 'testDone'
testID: number
result: 'success' | 'failure' | 'error'
hidden: boolean
skipped: boolean
}
export interface DoneEvent extends Event {
type: 'done'
success: boolean
}
export interface ErrorEvent extends Event {
type: 'error'
testID: number
error: string
stackTrace: string
isFailure: boolean
}
export interface DebugEvent extends Event {
type: 'debug'
suiteID: number
observatory: string
remoteDebugger: string
}
export interface MessageEvent extends Event {
type: 'print'
testID: number
messageType: string
message: string
}
export interface Suite {
id: number
platform?: string
path: string
}
export interface Group {
id: number
name?: string
suiteID: number
parentID?: number
testCount: number
line: number | null // The (1-based) line on which the group was defined, or `null`.
column: number | null // The (1-based) column on which the group was defined, or `null`.
url: string | null
}
export interface Test {
id: number
name: string
suiteID: number
groupIDs: number[] // The IDs of groups containing this test, in order from outermost to innermost.
line: number | null // The (1-based) line on which the test was defined, or `null`.
column: number | null // The (1-based) column on which the test was defined, or `null`.
url: string | null
root_line?: number
root_column?: number
root_url: string | undefined
}
export function isSuiteEvent(event: Event): event is SuiteEvent {
return event.type === 'suite'
}
export function isGroupEvent(event: Event): event is GroupEvent {
return event.type === 'group'
}
export function isTestStartEvent(event: Event): event is TestStartEvent {
return event.type === 'testStart'
}
export function isTestDoneEvent(event: Event): event is TestDoneEvent {
return event.type === 'testDone'
}
export function isErrorEvent(event: Event): event is ErrorEvent {
return event.type === 'error'
}
export function isDoneEvent(event: Event): event is DoneEvent {
return event.type === 'done'
}

View file

@ -1,12 +1,20 @@
import {Annotation, ParseOptions, TestResult} from '../test-parser' import {Annotation, ParseOptions, TestResult} from '../parser-types'
import {parseStringPromise} from 'xml2js' import {parseStringPromise} from 'xml2js'
import {JunitReport, TestCase, TestSuite, TestSuites} from './jest-junit-types' import {JunitReport, TestCase, TestSuite} from './jest-junit-types'
import {Align, Icon, link, table} from '../../utils/markdown-utils' import {Icon} from '../../utils/markdown-utils'
import {normalizeFilePath} from '../../utils/file-utils' import {normalizeFilePath} from '../../utils/file-utils'
import {slug} from '../../utils/slugger'
import {parseAttribute} from '../../utils/xml-utils' import {parseAttribute} from '../../utils/xml-utils'
import {
TestExecutionResult,
TestRunResult,
TestSuiteResult,
TestGroupResult,
TestCaseResult
} from '../../report/test-results'
import getReport from '../../report/get-report'
export async function parseJestJunit(content: string, options: ParseOptions): Promise<TestResult> { export async function parseJestJunit(content: string, options: ParseOptions): Promise<TestResult> {
const junit = (await parseStringPromise(content, { const junit = (await parseStringPromise(content, {
attrValueProcessors: [parseAttribute] attrValueProcessors: [parseAttribute]
@ -18,56 +26,27 @@ export async function parseJestJunit(content: string, options: ParseOptions): Pr
return { return {
success, success,
output: { output: {
title: `${junit.testsuites.$.name.trim()} ${icon}`, title: `${options.name.trim()} ${icon}`,
summary: getSummary(success, junit), summary: getSummary(junit),
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
} }
} }
} }
function getSummary(success: boolean, junit: JunitReport): string { function getSummary(junit: JunitReport): string {
const stats = junit.testsuites.$ const suites = junit.testsuites.testsuite.map(ts => {
const name = ts.$.name.trim()
const time = `${stats.time.toFixed(3)}s` const time = ts.$.time * 1000
const sr = new TestSuiteResult(name, getGroups(ts), time)
const skipped = getSkippedCount(junit.testsuites) return sr
const failed = stats.errors + stats.failures
const passed = stats.tests - failed - skipped
const headingLine = `**${stats.tests}** tests were completed in **${time}** with **${passed}** passed, **${skipped}** skipped and **${failed}** failed.`
const suitesSummary = junit.testsuites.testsuite.map((ts, i) => {
const skip = ts.$.skipped
const fail = ts.$.errors + ts.$.failures
const pass = ts.$.tests - fail - skip
const tm = `${ts.$.time.toFixed(3)}s`
const result = success ? Icon.success : Icon.fail
const tsName = ts.$.name.trim()
const tsAddr = makeSuiteSlug(i, tsName).link
const tsNameLink = link(tsName, tsAddr)
return [result, tsNameLink, ts.$.tests, tm, pass, fail, skip]
}) })
const summary = table( const time = junit.testsuites.$.time * 1000
['Result', 'Suite', 'Tests', 'Time', `Passed ${Icon.success}`, `Failed ${Icon.fail}`, `Skipped ${Icon.skip}`], const tr = new TestRunResult(suites, time)
[Align.Center, Align.Left, Align.Right, Align.Right, Align.Right, Align.Right, Align.Right], return getReport(tr)
...suitesSummary
)
const suites = junit.testsuites?.testsuite?.map((ts, i) => getSuiteSummary(ts, i)).join('\n')
const suitesSection = `# Test Suites\n\n${suites}`
return `${headingLine}\n${summary}\n${suitesSection}`
} }
function getSkippedCount(suites: TestSuites): number { function getGroups(suite: TestSuite): TestGroupResult[] {
return suites.testsuite.reduce((sum, suite) => sum + suite.$.skipped, 0)
}
function getSuiteSummary(suite: TestSuite, index: number): string {
const success = !(suite.$?.failures > 0 || suite.$?.errors > 0)
const icon = success ? Icon.success : Icon.fail
const groups: {describe: string; tests: TestCase[]}[] = [] const groups: {describe: string; tests: TestCase[]}[] = []
for (const tc of suite.testcase) { for (const tc of suite.testcase) {
let grp = groups.find(g => g.describe === tc.$.classname) let grp = groups.find(g => g.describe === tc.$.classname)
@ -78,39 +57,21 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
grp.tests.push(tc) grp.tests.push(tc)
} }
const content = groups return groups.map(grp => {
.map(grp => { const tests = grp.tests.map(tc => {
const header = grp.describe !== '' ? `### ${grp.describe.trim()}\n\n` : '' const name = tc.$.name.trim()
const tests = table( const result = getTestCaseResult(tc)
['Result', 'Test', 'Time'], const time = tc.$.time * 1000
[Align.Center, Align.Left, Align.Right], return new TestCaseResult(name, result, time)
...grp.tests.map(tc => {
const name = tc.$.name.trim()
const time = `${Math.round(tc.$.time * 1000)}ms`
const result = getTestCaseIcon(tc)
return [result, name, time]
})
)
return `${header}${tests}\n`
}) })
.join('\n') return new TestGroupResult(grp.describe, tests)
})
const tsName = suite.$.name.trim()
const tsSlug = makeSuiteSlug(index, tsName)
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
return `## ${tsNameLink} ${icon}\n\n${content}`
} }
function getTestCaseIcon(test: TestCase): string { function getTestCaseResult(test: TestCase): TestExecutionResult {
if (test.failure) return Icon.fail if (test.failure) return 'failed'
if (test.skipped) return Icon.skip if (test.skipped) return 'skipped'
return Icon.success return 'success'
}
function makeSuiteSlug(index: number, name: string): {id: string; link: string} {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
return slug(`ts-${index}-${name}`)
} }
function getAnnotations(junit: JunitReport, workDir: string, trackedFiles: string[]): Annotation[] { function getAnnotations(junit: JunitReport, workDir: string, trackedFiles: string[]): Annotation[] {

View file

@ -16,6 +16,7 @@ export type Annotation = {
export type ParseTestResult = (content: string, options: ParseOptions) => Promise<TestResult> export type ParseTestResult = (content: string, options: ParseOptions) => Promise<TestResult>
export interface ParseOptions { export interface ParseOptions {
name: string
annotations: boolean annotations: boolean
workDir: string workDir: string
trackedFiles: string[] trackedFiles: string[]

72
src/report/get-report.ts Normal file
View file

@ -0,0 +1,72 @@
import {TestExecutionResult, TestRunResult, TestSuiteResult} from './test-results'
import {Align, Icon, link, table} from '../utils/markdown-utils'
import {slug} from '../utils/slugger'
export default function getReport(tr: TestRunResult): string {
const time = `${(tr.time / 1000).toFixed(3)}s`
const headingLine = `**${tr.tests}** tests were completed in **${time}** with **${tr.passed}** passed, **${tr.skipped}** skipped and **${tr.failed}** failed.`
const suitesSummary = tr.suites.map((s, i) => {
const icon = getResultIcon(s.result)
const tsTime = `${s.time}ms`
const tsName = s.name
const tsAddr = makeSuiteSlug(i, tsName).link
const tsNameLink = link(tsName, tsAddr)
return [icon, tsNameLink, s.tests, tsTime, s.passed, s.failed, s.skipped]
})
const summary = table(
['Result', 'Suite', 'Tests', 'Time', `Passed ${Icon.success}`, `Failed ${Icon.fail}`, `Skipped ${Icon.skip}`],
[Align.Center, Align.Left, Align.Right, Align.Right, Align.Right, Align.Right, Align.Right],
...suitesSummary
)
const suites = tr.suites.map((ts, i) => getSuiteSummary(ts, i)).join('\n')
const suitesSection = `# Test Suites\n\n${suites}`
return `${headingLine}\n${summary}\n${suitesSection}`
}
function getSuiteSummary(ts: TestSuiteResult, index: number): string {
const icon = getResultIcon(ts.result)
const content = ts.groups
.map(grp => {
const header = grp.name ? `### ${grp.name}\n\n` : ''
const tests = table(
['Result', 'Test', 'Time'],
[Align.Center, Align.Left, Align.Right],
...grp.tests.map(tc => {
const name = tc.name
const time = `${tc.time}ms`
const result = getResultIcon(tc.result)
return [result, name, time]
})
)
return `${header}${tests}\n`
})
.join('\n')
const tsName = ts.name
const tsSlug = makeSuiteSlug(index, tsName)
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
return `## ${tsNameLink} ${icon}\n\n${content}`
}
function makeSuiteSlug(index: number, name: string): {id: string; link: string} {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
return slug(`ts-${index}-${name}`)
}
function getResultIcon(result: TestExecutionResult): string {
switch (result) {
case 'success':
return Icon.success
case 'skipped':
return Icon.skip
case 'failed':
return Icon.fail
default:
return ''
}
}

View file

@ -0,0 +1,77 @@
export class TestRunResult {
constructor(readonly suites: TestSuiteResult[], private totalTime?: number) {}
get tests(): number {
return this.suites.reduce((sum, g) => sum + g.tests, 0)
}
get passed(): number {
return this.suites.reduce((sum, g) => sum + g.passed, 0)
}
get failed(): number {
return this.suites.reduce((sum, g) => sum + g.failed, 0)
}
get skipped(): number {
return this.suites.reduce((sum, g) => sum + g.skipped, 0)
}
get time(): number {
return this.totalTime ?? this.suites.reduce((sum, g) => sum + g.time, 0)
}
get result(): TestExecutionResult {
return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success'
}
}
export class TestSuiteResult {
constructor(readonly name: string, readonly groups: TestGroupResult[], private totalTime?: number) {}
get tests(): number {
return this.groups.reduce((sum, g) => sum + g.tests.length, 0)
}
get passed(): number {
return this.groups.reduce((sum, g) => sum + g.passed, 0)
}
get failed(): number {
return this.groups.reduce((sum, g) => sum + g.failed, 0)
}
get skipped(): number {
return this.groups.reduce((sum, g) => sum + g.skipped, 0)
}
get time(): number {
return this.totalTime ?? this.groups.reduce((sum, g) => sum + g.time, 0)
}
get result(): TestExecutionResult {
return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success'
}
}
export class TestGroupResult {
constructor(readonly name: string | undefined, readonly tests: TestCaseResult[]) {}
get passed(): number {
return this.tests.reduce((sum, t) => (t.result === 'success' ? sum + 1 : sum), 0)
}
get failed(): number {
return this.tests.reduce((sum, t) => (t.result === 'failed' ? sum + 1 : sum), 0)
}
get skipped(): number {
return this.tests.reduce((sum, t) => (t.result === 'skipped' ? sum + 1 : sum), 0)
}
get time(): number {
return this.tests.reduce((sum, t) => sum + t.time, 0)
}
get result(): TestExecutionResult {
return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success'
}
}
export class TestCaseResult {
constructor(readonly name: string, readonly result: TestExecutionResult, readonly time: number) {}
}
export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined