mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-16 14:27:10 +01:00
commit
adab07ff74
16 changed files with 1113 additions and 177 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -19,12 +19,10 @@ jobs:
|
|||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
continue-on-error: true
|
||||
- name: 'Evaluate test results'
|
||||
if: always()
|
||||
uses: ./
|
||||
with:
|
||||
name: 'JEST Tests'
|
||||
path: '__tests__/__results__/jest-junit.xml'
|
||||
reporter: 'jest-junit'
|
||||
- name: 'Sanity check' # re-run tests in case this action failed to detect failing test
|
||||
run: npm test
|
||||
|
|
|
|||
92
__tests__/__snapshots__/dart-json.test.ts.snap
Normal file
92
__tests__/__snapshots__/dart-json.test.ts.snap
Normal 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 ❌",
|
||||
}
|
||||
`;
|
||||
|
|
@ -76,8 +76,8 @@ Received: false
|
|||
"summary": "**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed.
|
||||
| Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ |
|
||||
| :---: | :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
| ❌ | [__tests__\\\\main.test.js](#ts-0-tests-main-test-js) | 4 | 0.486s | 1 | 3 | 0 |
|
||||
| ❌ | [__tests__\\\\second.test.js](#ts-1-tests-second-test-js) | 2 | 0.082s | 0 | 1 | 1 |
|
||||
| ❌ | [__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 | 82ms | 0 | 1 | 1 |
|
||||
# Test Suites
|
||||
|
||||
## <a id=\\"user-content-ts-0-tests-main-test-js\\" href=\\"#ts-0-tests-main-test-js\\">__tests__\\\\main.test.js</a> ❌
|
||||
|
|
|
|||
26
__tests__/dart-json.test.ts
Normal file
26
__tests__/dart-json.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,33 +1,31 @@
|
|||
{"protocolVersion":"0.1.1","runnerVersion":"1.15.4","pid":21728,"type":"start","time":0}
|
||||
{"suite":{"id":0,"platform":"vm","path":"test\\main_test.dart"},"type":"suite","time":1}
|
||||
{"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":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}
|
||||
{"suite":{"id":2,"platform":"vm","path":"test\\second_test.dart"},"type":"suite","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":12}
|
||||
{"count":2,"type":"allSuites","time":13}
|
||||
{"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3761}
|
||||
{"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}
|
||||
{"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}
|
||||
{"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3784}
|
||||
{"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":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}
|
||||
{"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}
|
||||
{"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,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3804}
|
||||
{"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}
|
||||
{"testID":9,"messageType":"skip","message":"Skip: skipped test","type":"print","time":3807}
|
||||
{"testID":9,"result":"success","skipped":true,"hidden":false,"type":"testDone","time":3808}
|
||||
{"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3815}
|
||||
{"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}
|
||||
{"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}
|
||||
{"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,"result":"failure","skipped":false,"hidden":false,"type":"testDone","time":3840}
|
||||
{"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}
|
||||
{"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,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3853}
|
||||
{"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}
|
||||
{"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}
|
||||
{"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,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3869}
|
||||
{"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}
|
||||
{"testID":15,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3882}
|
||||
{"success":false,"type":"done","time":3886}
|
||||
{"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":11}
|
||||
{"count":2,"type":"allSuites","time":11}
|
||||
{"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":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":3655}
|
||||
{"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":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":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":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":3692}
|
||||
{"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":3693}
|
||||
{"testID":9,"messageType":"skip","message":"Skip: skipped test","type":"print","time":3706}
|
||||
{"testID":9,"result":"success","skipped":true,"hidden":false,"type":"testDone","time":3707}
|
||||
{"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":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":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":3736}
|
||||
{"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":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":3743}
|
||||
{"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":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":3744}
|
||||
{"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":3756}
|
||||
{"success":false,"type":"done","time":3760}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as fs from 'fs'
|
|||
import * as path from 'path'
|
||||
|
||||
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 outputPath = __dirname + '/__outputs__/jest-junit.md'
|
||||
|
|
@ -10,6 +10,7 @@ const outputPath = __dirname + '/__outputs__/jest-junit.md'
|
|||
describe('jest-junit tests', () => {
|
||||
it('matches report snapshot', async () => {
|
||||
const opts: ParseOptions = {
|
||||
name: 'jest tests',
|
||||
annotations: true,
|
||||
trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'],
|
||||
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/'
|
||||
|
|
|
|||
478
dist/index.js
generated
vendored
478
dist/index.js
generated
vendored
|
|
@ -30,6 +30,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|||
const core = __importStar(__webpack_require__(2186));
|
||||
const github = __importStar(__webpack_require__(5438));
|
||||
const jest_junit_parser_1 = __webpack_require__(1113);
|
||||
const dart_json_parser_1 = __webpack_require__(4528);
|
||||
const file_utils_1 = __webpack_require__(2711);
|
||||
const git_1 = __webpack_require__(9844);
|
||||
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
|
||||
const trackedFiles = annotations ? await git_1.listFiles() : [];
|
||||
const opts = {
|
||||
name,
|
||||
annotations,
|
||||
trackedFiles,
|
||||
workDir
|
||||
|
|
@ -81,10 +83,12 @@ async function main() {
|
|||
}
|
||||
function getParser(reporter) {
|
||||
switch (reporter) {
|
||||
case 'dart-json':
|
||||
return dart_json_parser_1.parseDartJson;
|
||||
case 'dotnet-trx':
|
||||
throw new Error('Not implemented yet!');
|
||||
case 'flutter-machine':
|
||||
throw new Error('Not implemented yet!');
|
||||
return dart_json_parser_1.parseDartJson;
|
||||
case 'jest-junit':
|
||||
return jest_junit_parser_1.parseJestJunit;
|
||||
default:
|
||||
|
|
@ -96,18 +100,248 @@ run();
|
|||
|
||||
/***/ }),
|
||||
|
||||
/***/ 1113:
|
||||
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
||||
/***/ 4528:
|
||||
/***/ (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 }));
|
||||
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 }));
|
||||
exports.exceptionThrowSource = exports.parseJestJunit = void 0;
|
||||
const xml2js_1 = __webpack_require__(6189);
|
||||
const markdown_utils_1 = __webpack_require__(6482);
|
||||
const file_utils_1 = __webpack_require__(2711);
|
||||
const slugger_1 = __webpack_require__(3328);
|
||||
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) {
|
||||
var _a, _b;
|
||||
const junit = (await xml2js_1.parseStringPromise(content, {
|
||||
|
|
@ -119,44 +353,25 @@ async function parseJestJunit(content, options) {
|
|||
return {
|
||||
success,
|
||||
output: {
|
||||
title: `${junit.testsuites.$.name.trim()} ${icon}`,
|
||||
summary: getSummary(success, junit),
|
||||
title: `${options.name.trim()} ${icon}`,
|
||||
summary: getSummary(junit),
|
||||
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.parseJestJunit = parseJestJunit;
|
||||
function getSummary(success, junit) {
|
||||
var _a, _b;
|
||||
const stats = junit.testsuites.$;
|
||||
const time = `${stats.time.toFixed(3)}s`;
|
||||
const skipped = getSkippedCount(junit.testsuites);
|
||||
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 ? 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];
|
||||
function getSummary(junit) {
|
||||
const suites = junit.testsuites.testsuite.map(ts => {
|
||||
const name = ts.$.name.trim();
|
||||
const time = ts.$.time * 1000;
|
||||
const sr = new test_results_1.TestSuiteResult(name, getGroups(ts), time);
|
||||
return sr;
|
||||
});
|
||||
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 = (_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 suitesSection = `# Test Suites\n\n${suites}`;
|
||||
return `${headingLine}\n${summary}\n${suitesSection}`;
|
||||
const time = junit.testsuites.$.time * 1000;
|
||||
const tr = new test_results_1.TestRunResult(suites, time);
|
||||
return get_report_1.default(tr);
|
||||
}
|
||||
function getSkippedCount(suites) {
|
||||
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;
|
||||
function getGroups(suite) {
|
||||
const groups = [];
|
||||
for (const tc of suite.testcase) {
|
||||
let grp = groups.find(g => g.describe === tc.$.classname);
|
||||
|
|
@ -166,33 +381,22 @@ function getSuiteSummary(suite, index) {
|
|||
}
|
||||
grp.tests.push(tc);
|
||||
}
|
||||
const content = groups
|
||||
.map(grp => {
|
||||
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 => {
|
||||
return groups.map(grp => {
|
||||
const tests = 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');
|
||||
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}`;
|
||||
const result = getTestCaseResult(tc);
|
||||
const time = tc.$.time * 1000;
|
||||
return new test_results_1.TestCaseResult(name, result, time);
|
||||
});
|
||||
return new test_results_1.TestGroupResult(grp.describe, tests);
|
||||
});
|
||||
}
|
||||
function getTestCaseIcon(test) {
|
||||
function getTestCaseResult(test) {
|
||||
if (test.failure)
|
||||
return markdown_utils_1.Icon.fail;
|
||||
return 'failed';
|
||||
if (test.skipped)
|
||||
return markdown_utils_1.Icon.skip;
|
||||
return markdown_utils_1.Icon.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}`);
|
||||
return 'skipped';
|
||||
return 'success';
|
||||
}
|
||||
function getAnnotations(junit, workDir, trackedFiles) {
|
||||
const annotations = [];
|
||||
|
|
@ -240,6 +444,164 @@ function exceptionThrowSource(ex, workDir, trackedFiles) {
|
|||
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:
|
||||
|
|
|
|||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -7867,9 +7867,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.memoize": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
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 {listFiles} from './utils/git'
|
||||
import {getCheckRunSha} from './utils/github-utils'
|
||||
|
|
@ -35,6 +36,7 @@ async function main(): Promise<void> {
|
|||
const trackedFiles = annotations ? await listFiles() : []
|
||||
|
||||
const opts: ParseOptions = {
|
||||
name,
|
||||
annotations,
|
||||
trackedFiles,
|
||||
workDir
|
||||
|
|
@ -62,10 +64,12 @@ async function main(): Promise<void> {
|
|||
|
||||
function getParser(reporter: string): ParseTestResult {
|
||||
switch (reporter) {
|
||||
case 'dart-json':
|
||||
return parseDartJson
|
||||
case 'dotnet-trx':
|
||||
throw new Error('Not implemented yet!')
|
||||
case 'flutter-machine':
|
||||
throw new Error('Not implemented yet!')
|
||||
return parseDartJson
|
||||
case 'jest-junit':
|
||||
return parseJestJunit
|
||||
default:
|
||||
|
|
|
|||
215
src/parsers/dart-json/dart-json-parser.ts
Normal file
215
src/parsers/dart-json/dart-json-parser.ts
Normal 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
|
||||
}
|
||||
129
src/parsers/dart-json/dart-json-types.ts
Normal file
129
src/parsers/dart-json/dart-json-types.ts
Normal 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'
|
||||
}
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
import {Annotation, ParseOptions, TestResult} from '../test-parser'
|
||||
import {Annotation, ParseOptions, TestResult} from '../parser-types'
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {JunitReport, TestCase, TestSuite, TestSuites} from './jest-junit-types'
|
||||
import {Align, Icon, link, table} from '../../utils/markdown-utils'
|
||||
import {JunitReport, TestCase, TestSuite} from './jest-junit-types'
|
||||
import {Icon} from '../../utils/markdown-utils'
|
||||
import {normalizeFilePath} from '../../utils/file-utils'
|
||||
import {slug} from '../../utils/slugger'
|
||||
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> {
|
||||
const junit = (await parseStringPromise(content, {
|
||||
attrValueProcessors: [parseAttribute]
|
||||
|
|
@ -18,56 +26,27 @@ export async function parseJestJunit(content: string, options: ParseOptions): Pr
|
|||
return {
|
||||
success,
|
||||
output: {
|
||||
title: `${junit.testsuites.$.name.trim()} ${icon}`,
|
||||
summary: getSummary(success, junit),
|
||||
title: `${options.name.trim()} ${icon}`,
|
||||
summary: getSummary(junit),
|
||||
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSummary(success: boolean, junit: JunitReport): string {
|
||||
const stats = junit.testsuites.$
|
||||
|
||||
const time = `${stats.time.toFixed(3)}s`
|
||||
|
||||
const skipped = getSkippedCount(junit.testsuites)
|
||||
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]
|
||||
function getSummary(junit: JunitReport): string {
|
||||
const suites = junit.testsuites.testsuite.map(ts => {
|
||||
const name = ts.$.name.trim()
|
||||
const time = ts.$.time * 1000
|
||||
const sr = new TestSuiteResult(name, getGroups(ts), time)
|
||||
return sr
|
||||
})
|
||||
|
||||
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 = junit.testsuites?.testsuite?.map((ts, i) => getSuiteSummary(ts, i)).join('\n')
|
||||
const suitesSection = `# Test Suites\n\n${suites}`
|
||||
|
||||
return `${headingLine}\n${summary}\n${suitesSection}`
|
||||
const time = junit.testsuites.$.time * 1000
|
||||
const tr = new TestRunResult(suites, time)
|
||||
return getReport(tr)
|
||||
}
|
||||
|
||||
function getSkippedCount(suites: TestSuites): number {
|
||||
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
|
||||
|
||||
function getGroups(suite: TestSuite): TestGroupResult[] {
|
||||
const groups: {describe: string; tests: TestCase[]}[] = []
|
||||
for (const tc of suite.testcase) {
|
||||
let grp = groups.find(g => g.describe === tc.$.classname)
|
||||
|
|
@ -78,39 +57,21 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
|
|||
grp.tests.push(tc)
|
||||
}
|
||||
|
||||
const content = groups
|
||||
.map(grp => {
|
||||
const header = grp.describe !== '' ? `### ${grp.describe.trim()}\n\n` : ''
|
||||
const tests = table(
|
||||
['Result', 'Test', 'Time'],
|
||||
[Align.Center, Align.Left, Align.Right],
|
||||
...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`
|
||||
return groups.map(grp => {
|
||||
const tests = grp.tests.map(tc => {
|
||||
const name = tc.$.name.trim()
|
||||
const result = getTestCaseResult(tc)
|
||||
const time = tc.$.time * 1000
|
||||
return new TestCaseResult(name, result, time)
|
||||
})
|
||||
.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}`
|
||||
return new TestGroupResult(grp.describe, tests)
|
||||
})
|
||||
}
|
||||
|
||||
function getTestCaseIcon(test: TestCase): string {
|
||||
if (test.failure) return Icon.fail
|
||||
if (test.skipped) return Icon.skip
|
||||
return Icon.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 getTestCaseResult(test: TestCase): TestExecutionResult {
|
||||
if (test.failure) return 'failed'
|
||||
if (test.skipped) return 'skipped'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function getAnnotations(junit: JunitReport, workDir: string, trackedFiles: string[]): Annotation[] {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type Annotation = {
|
|||
export type ParseTestResult = (content: string, options: ParseOptions) => Promise<TestResult>
|
||||
|
||||
export interface ParseOptions {
|
||||
name: string
|
||||
annotations: boolean
|
||||
workDir: string
|
||||
trackedFiles: string[]
|
||||
72
src/report/get-report.ts
Normal file
72
src/report/get-report.ts
Normal 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 ''
|
||||
}
|
||||
}
|
||||
77
src/report/test-results.ts
Normal file
77
src/report/test-results.ts
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue