diff --git a/README.md b/README.md index e5abd85..676d21e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This [Github Action](https://github.com/features/actions) displays test results - .NET / [dotnet test](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test#examples) ( [xUnit](https://xunit.net/) / [NUnit](https://nunit.org/) / [MSTest](https://github.com/Microsoft/testfx-docs) ) - Dart / [test](https://pub.dev/packages/test) - Flutter / [test](https://pub.dev/packages/test) +- Go / [go test](https://pkg.go.dev/testing) - Java / [JUnit](https://junit.org/) - JavaScript / [JEST](https://jestjs.io/) / [Mocha](https://mochajs.org/) - Swift / xUnit @@ -140,6 +141,7 @@ jobs: # dotnet-nunit # dotnet-trx # flutter-json + # golang-json # java-junit # jest-junit # mocha-json @@ -278,6 +280,13 @@ For more information see: +
+ golang-json + +You must use the `-json` flag and output the results to a file (ex: `go test -json > testresults.json`) + +
+
java-junit (Experimental) diff --git a/__tests__/__outputs__/golang-json.md b/__tests__/__outputs__/golang-json.md new file mode 100644 index 0000000..b3640d8 --- /dev/null +++ b/__tests__/__outputs__/golang-json.md @@ -0,0 +1,38 @@ +![Tests failed](https://img.shields.io/badge/tests-5%20passed%2C%206%20failed%2C%201%20skipped-critical) +|Report|Passed|Failed|Skipped|Time| +|:---|---:|---:|---:|---:| +|fixtures/golang-json.json|5 ✅|6 ❌|1 ⚪|6s| +## ❌ fixtures/golang-json.json +**12** tests were completed in **6s** with **5** passed, **6** failed and **1** skipped. +|Test suite|Passed|Failed|Skipped|Time| +|:---|---:|---:|---:|---:| +|[_/home/james_t/git/test-reporter/reports/go](#user-content-r0s0)|5 ✅|6 ❌|1 ⚪|6s| +### ❌ _/home/james_t/git/test-reporter/reports/go +``` +✅ TestPassing +❌ TestFailing + calculator_test.go:19: expected 1+1 = 3, got 2 + +❌ TestPanicInsideFunction + calculator_test.go:76: caught panic: runtime error: integer divide by zero + +❌ TestPanicInsideTest + calculator_test.go:76: caught panic: bad stuff + +⚪ TestSkipped + calculator_test.go:45: skipping test + +❌ TestCases + +TestCases + ✅ 1_+_2_=_3 + ✅ 4_+_7_=_11 + ❌ 2_+_3_=_4 + calculator_test.go:67: expected 2 + 3 = 4, got 5 + + ❌ 1_/_2_=_1 + calculator_test.go:67: expected 1 / 2 = 1, got 0 + + ✅ 9_/_3_=_3 + ✅ 14_/_7_=_2 +``` \ No newline at end of file diff --git a/__tests__/__snapshots__/golang-json.test.ts.snap b/__tests__/__snapshots__/golang-json.test.ts.snap new file mode 100644 index 0000000..75c6de1 --- /dev/null +++ b/__tests__/__snapshots__/golang-json.test.ts.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`golang-json tests report from ./reports/dotnet test results matches snapshot 1`] = ` +TestRunResult { + "path": "fixtures/golang-json.json", + "suites": [ + TestSuiteResult { + "groups": [ + TestGroupResult { + "name": null, + "tests": [ + TestCaseResult { + "error": undefined, + "name": "TestPassing", + "result": "success", + "time": 60, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:19: expected 1+1 = 3, got 2 +", + "message": "calculator_test.go:19: expected 1+1 = 3, got 2 +", + }, + "name": "TestFailing", + "result": "failed", + "time": 890, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:76: caught panic: runtime error: integer divide by zero +", + "message": "calculator_test.go:76: caught panic: runtime error: integer divide by zero +", + }, + "name": "TestPanicInsideFunction", + "result": "failed", + "time": 0, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:76: caught panic: bad stuff +", + "message": "calculator_test.go:76: caught panic: bad stuff +", + }, + "name": "TestPanicInsideTest", + "result": "failed", + "time": 0, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:45: skipping test +", + "message": "calculator_test.go:45: skipping test +", + }, + "name": "TestSkipped", + "result": "skipped", + "time": 940, + }, + TestCaseResult { + "error": { + "details": "", + "message": "", + }, + "name": "TestCases", + "result": "failed", + "time": 2250, + }, + ], + }, + TestGroupResult { + "name": "TestCases", + "tests": [ + TestCaseResult { + "error": undefined, + "name": "1_+_2_=_3", + "result": "success", + "time": 400, + }, + TestCaseResult { + "error": undefined, + "name": "4_+_7_=_11", + "result": "success", + "time": 460, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:67: expected 2 + 3 = 4, got 5 +", + "message": "calculator_test.go:67: expected 2 + 3 = 4, got 5 +", + }, + "name": "2_+_3_=_4", + "result": "failed", + "time": 90, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:67: expected 1 / 2 = 1, got 0 +", + "message": "calculator_test.go:67: expected 1 / 2 = 1, got 0 +", + }, + "name": "1_/_2_=_1", + "result": "failed", + "time": 920, + }, + TestCaseResult { + "error": undefined, + "name": "9_/_3_=_3", + "result": "success", + "time": 340, + }, + TestCaseResult { + "error": undefined, + "name": "14_/_7_=_2", + "result": "success", + "time": 40, + }, + ], + }, + ], + "name": "_/home/james_t/git/test-reporter/reports/go", + "totalTime": undefined, + }, + ], + "totalTime": undefined, +} +`; diff --git a/__tests__/fixtures/golang-json.json b/__tests__/fixtures/golang-json.json new file mode 100644 index 0000000..421a707 --- /dev/null +++ b/__tests__/fixtures/golang-json.json @@ -0,0 +1,59 @@ +{"Time":"2025-04-22T08:59:55.364618802-05:00","Action":"start","Package":"_/home/james_t/git/test-reporter/reports/go"} +{"Time":"2025-04-22T08:59:55.371779289-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing"} +{"Time":"2025-04-22T08:59:55.371805677-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Output":"=== RUN TestPassing\n"} +{"Time":"2025-04-22T08:59:55.428201983-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Output":" calculator_test.go:11: pass!\n"} +{"Time":"2025-04-22T08:59:55.428265529-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Output":"--- PASS: TestPassing (0.06s)\n"} +{"Time":"2025-04-22T08:59:55.428285649-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Elapsed":0.06} +{"Time":"2025-04-22T08:59:55.428299886-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing"} +{"Time":"2025-04-22T08:59:55.428309029-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Output":"=== RUN TestFailing\n"} +{"Time":"2025-04-22T08:59:56.317425091-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Output":" calculator_test.go:19: expected 1+1 = 3, got 2\n"} +{"Time":"2025-04-22T08:59:56.31748077-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Output":"--- FAIL: TestFailing (0.89s)\n"} +{"Time":"2025-04-22T08:59:56.317493452-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Elapsed":0.89} +{"Time":"2025-04-22T08:59:56.317506107-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction"} +{"Time":"2025-04-22T08:59:56.317514487-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":"=== RUN TestPanicInsideFunction\n"} +{"Time":"2025-04-22T08:59:56.317530448-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":" calculator_test.go:76: caught panic: runtime error: integer divide by zero\n"} +{"Time":"2025-04-22T08:59:56.317541866-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":"--- FAIL: TestPanicInsideFunction (0.00s)\n"} +{"Time":"2025-04-22T08:59:56.317552981-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Elapsed":0} +{"Time":"2025-04-22T08:59:56.317561057-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest"} +{"Time":"2025-04-22T08:59:56.317568742-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":"=== RUN TestPanicInsideTest\n"} +{"Time":"2025-04-22T08:59:56.317584113-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":" calculator_test.go:76: caught panic: bad stuff\n"} +{"Time":"2025-04-22T08:59:56.317598524-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":"--- FAIL: TestPanicInsideTest (0.00s)\n"} +{"Time":"2025-04-22T08:59:56.317608268-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Elapsed":0} +{"Time":"2025-04-22T08:59:56.317615472-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped"} +{"Time":"2025-04-22T08:59:56.317623959-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Output":"=== RUN TestSkipped\n"} +{"Time":"2025-04-22T08:59:57.256475698-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Output":" calculator_test.go:45: skipping test\n"} +{"Time":"2025-04-22T08:59:57.256536372-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Output":"--- SKIP: TestSkipped (0.94s)\n"} +{"Time":"2025-04-22T08:59:57.256549142-05:00","Action":"skip","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Elapsed":0.94} +{"Time":"2025-04-22T08:59:57.256562053-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases"} +{"Time":"2025-04-22T08:59:57.256569388-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Output":"=== RUN TestCases\n"} +{"Time":"2025-04-22T08:59:57.256580104-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3"} +{"Time":"2025-04-22T08:59:57.256587408-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3","Output":"=== RUN TestCases/1_+_2_=_3\n"} +{"Time":"2025-04-22T08:59:57.653005399-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11"} +{"Time":"2025-04-22T08:59:57.653036336-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11","Output":"=== RUN TestCases/4_+_7_=_11\n"} +{"Time":"2025-04-22T08:59:58.112825221-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4"} +{"Time":"2025-04-22T08:59:58.112858016-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Output":"=== RUN TestCases/2_+_3_=_4\n"} +{"Time":"2025-04-22T08:59:58.201204209-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Output":" calculator_test.go:67: expected 2 + 3 = 4, got 5\n"} +{"Time":"2025-04-22T08:59:58.201245827-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_/_2_=_1"} +{"Time":"2025-04-22T08:59:58.201255566-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_/_2_=_1","Output":"=== RUN TestCases/1_/_2_=_1\n"} +{"Time":"2025-04-22T08:59:59.119852965-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_/_2_=_1","Output":" calculator_test.go:67: expected 1 / 2 = 1, got 0\n"} +{"Time":"2025-04-22T08:59:59.119877603-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/9_/_3_=_3"} +{"Time":"2025-04-22T08:59:59.119879955-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/9_/_3_=_3","Output":"=== RUN TestCases/9_/_3_=_3\n"} +{"Time":"2025-04-22T08:59:59.460576385-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/14_/_7_=_2"} +{"Time":"2025-04-22T08:59:59.460607599-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/14_/_7_=_2","Output":"=== RUN TestCases/14_/_7_=_2\n"} +{"Time":"2025-04-22T08:59:59.504952672-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Output":"--- FAIL: TestCases (2.25s)\n"} +{"Time":"2025-04-22T08:59:59.504995938-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3","Output":" --- PASS: TestCases/1_+_2_=_3 (0.40s)\n"} +{"Time":"2025-04-22T08:59:59.505006062-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3","Elapsed":0.4} +{"Time":"2025-04-22T08:59:59.505017551-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11","Output":" --- PASS: TestCases/4_+_7_=_11 (0.46s)\n"} +{"Time":"2025-04-22T08:59:59.505026099-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11","Elapsed":0.46} +{"Time":"2025-04-22T08:59:59.505033963-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Output":" --- FAIL: TestCases/2_+_3_=_4 (0.09s)\n"} +{"Time":"2025-04-22T08:59:59.505042238-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Elapsed":0.09} +{"Time":"2025-04-22T08:59:59.505050917-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_/_2_=_1","Output":" --- FAIL: TestCases/1_/_2_=_1 (0.92s)\n"} +{"Time":"2025-04-22T08:59:59.505059901-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_/_2_=_1","Elapsed":0.92} +{"Time":"2025-04-22T08:59:59.505068125-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/9_/_3_=_3","Output":" --- PASS: TestCases/9_/_3_=_3 (0.34s)\n"} +{"Time":"2025-04-22T08:59:59.505076976-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/9_/_3_=_3","Elapsed":0.34} +{"Time":"2025-04-22T08:59:59.5050845-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/14_/_7_=_2","Output":" --- PASS: TestCases/14_/_7_=_2 (0.04s)\n"} +{"Time":"2025-04-22T08:59:59.505091554-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/14_/_7_=_2","Elapsed":0.04} +{"Time":"2025-04-22T08:59:59.505098998-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Elapsed":2.25} +{"Time":"2025-04-22T08:59:59.505107502-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Output":"FAIL\n"} +{"Time":"2025-04-22T08:59:59.505552861-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Output":"FAIL\t_/home/james_t/git/test-reporter/reports/go\t4.141s\n"} +{"Time":"2025-04-22T08:59:59.505584529-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Elapsed":4.141} diff --git a/__tests__/golang-json.test.ts b/__tests__/golang-json.test.ts new file mode 100644 index 0000000..0bfdd86 --- /dev/null +++ b/__tests__/golang-json.test.ts @@ -0,0 +1,29 @@ +import * as fs from 'fs' +import * as path from 'path' + +import {GolangJsonParser} from '../src/parsers/golang-json/golang-json-parser' +import {ParseOptions} from '../src/test-parser' +import {getReport} from '../src/report/get-report' +import {normalizeFilePath} from '../src/utils/path-utils' + +describe('golang-json tests', () => { + it('report from ./reports/dotnet test results matches snapshot', async () => { + const fixturePath = path.join(__dirname, 'fixtures', 'golang-json.json') + const outputPath = path.join(__dirname, '__outputs__', 'golang-json.md') + const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) + const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) + + const opts: ParseOptions = { + parseErrors: true, + trackedFiles: ['calculator.go', 'calculator_test.go'] + } + + const parser = new GolangJsonParser(opts) + const result = await parser.parse(filePath, fileContent) + expect(result).toMatchSnapshot() + + const report = getReport([result]) + fs.mkdirSync(path.dirname(outputPath), {recursive: true}) + fs.writeFileSync(outputPath, report) + }) +}) diff --git a/dist/index.js b/dist/index.js index 26cc77d..94ba8b0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -273,6 +273,7 @@ const get_report_1 = __nccwpck_require__(7070); const dart_json_parser_1 = __nccwpck_require__(1254); const dotnet_nunit_parser_1 = __nccwpck_require__(6394); const dotnet_trx_parser_1 = __nccwpck_require__(1658); +const golang_json_parser_1 = __nccwpck_require__(5162); const java_junit_parser_1 = __nccwpck_require__(8342); const jest_junit_parser_1 = __nccwpck_require__(1042); const mocha_json_parser_1 = __nccwpck_require__(5402); @@ -475,6 +476,8 @@ class TestReporter { return new dotnet_nunit_parser_1.DotnetNunitParser(options); case 'dotnet-trx': return new dotnet_trx_parser_1.DotnetTrxParser(options); + case 'golang-json': + return new golang_json_parser_1.GolangJsonParser(options); case 'flutter-json': return new dart_json_parser_1.DartJsonParser(options, 'flutter'); case 'java-junit': @@ -1066,6 +1069,106 @@ class DotnetTrxParser { exports.DotnetTrxParser = DotnetTrxParser; +/***/ }), + +/***/ 5162: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.GolangJsonParser = void 0; +const test_results_1 = __nccwpck_require__(613); +class GolangJsonParser { + options; + assumedWorkDir; + constructor(options) { + this.options = options; + } + async parse(path, content) { + const events = await this.getGolangTestEvents(path, content); + return this.getTestRunResult(path, events); + } + async getGolangTestEvents(path, content) { + return content.trim().split('\n').map((line, index) => { + try { + return JSON.parse(line); + } + catch (e) { + throw new Error(`Invalid JSON at ${path} line ${index + 1}\n\n${e}`); + } + }); + } + getTestRunResult(path, events) { + const eventGroups = new Map(); + for (const event of events) { + if (!event.Test) { + continue; + } + const k = `${event.Package}/${event.Test}`; + let g = eventGroups.get(k); + if (!g) { + g = []; + eventGroups.set(k, g); + } + g.push(event); + } + const suites = []; + for (const eventGroup of eventGroups.values()) { + const event = eventGroup[0]; + let suite = suites.find(s => s.name === event.Package); + if (!suite) { + suite = new test_results_1.TestSuiteResult(event.Package, []); + suites.push(suite); + } + if (!event.Test) { + continue; + } + let groupName; + let rest; + [groupName, ...rest] = event.Test.split('/'); + let testName = rest.join('/'); + if (!testName) { + testName = groupName; + groupName = null; + } + let group = suite.groups.find(g => g.name === groupName); + if (!group) { + group = new test_results_1.TestGroupResult(groupName, []); + suite.groups.push(group); + } + const lastEvent = eventGroup.at(-1); + const result = lastEvent.Action === 'pass' ? 'success' + : lastEvent.Action === 'skip' ? 'skipped' + : 'failed'; + if (lastEvent.Elapsed === undefined) { + throw new Error('missing elapsed on final test event'); + } + const time = lastEvent.Elapsed * 1000; + let error = undefined; + if (result !== 'success') { + const outputEvents = eventGroup + .filter(e => e.Action === 'output') + .map(e => e.Output ?? '') + // Go output prepends indentation to help group tests - remove it + .map(o => o.replace(/^ /, '')); + // First and last lines will be generic "test started" and "test finished" lines - remove them + outputEvents.splice(0, 1); + outputEvents.splice(-1, 1); + const details = outputEvents.join(''); + error = { + message: details, + details: details + }; + } + group.tests.push(new test_results_1.TestCaseResult(testName, result, time, error)); + } + return new test_results_1.TestRunResult(path, suites); + } +} +exports.GolangJsonParser = GolangJsonParser; + + /***/ }), /***/ 8342: diff --git a/package.json b/package.json index 96a951e..6c8cdc0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "dart-fixture": "cd \"reports/dart\" && dart test --file-reporter=\"json:../../__tests__/fixtures/dart-json.json\"", "dotnet-fixture": "dotnet test reports/dotnet/DotnetTests.XUnitTests --logger \"trx;LogFileName=../../../../__tests__/fixtures/dotnet-trx.trx\"", "dotnet-nunit-fixture": "nunit.exe reports/dotnet/DotnetTests.NUnitV3Tests/bin/Debug/netcoreapp3.1/DotnetTests.NUnitV3Tests.dll --result=__tests__/fixtures/dotnet-nunit.xml", + "dotnet-nunit-legacy-fixture": "nunit-console.exe reports/dotnet-nunit-legacy/NUnitLegacy.sln --result=__tests__/fixtures/dotnet-nunit-legacy.xml", + "golang-json-fixture": "go test -v -json -timeout 5s ./reports/go | tee __tests__/fixtures/golang-json.json", "jest-fixture": "cd \"reports/jest\" && npm test", "mocha-fixture": "cd \"reports/mocha\" && npm test" }, diff --git a/reports/go/calculator.go b/reports/go/calculator.go new file mode 100644 index 0000000..c863ae0 --- /dev/null +++ b/reports/go/calculator.go @@ -0,0 +1,20 @@ +package main + +import "errors" + +func CalculatorSum(a, b int) int { + return a + b +} + +func CalculatorDivide(a, b int) int { + return a / b +} + +var ErrDivideByZero = errors.New("divide by zero") + +func CalculatorSafeDivide(a, b int) (int, error) { + if b == 0 { + return 0, ErrDivideByZero + } + return a / b, nil +} diff --git a/reports/go/calculator_test.go b/reports/go/calculator_test.go new file mode 100644 index 0000000..d9f6128 --- /dev/null +++ b/reports/go/calculator_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "math/rand" + "testing" + "time" +) + +func TestPassing(t *testing.T) { + randomSleep() + t.Log("pass!") +} + +func TestFailing(t *testing.T) { + randomSleep() + expected := 3 + actual := CalculatorSum(1, 1) + if actual != expected { + t.Fatalf("expected 1+1 = %d, got %d", expected, actual) + } +} + +func TestPanicInsideFunction(t *testing.T) { + defer catchPanics(t) + + expected := 0 + actual := CalculatorDivide(1, 0) + if actual != expected { + t.Fatalf("expected 1/1 = %d, got %d", expected, actual) + } +} + +func TestPanicInsideTest(t *testing.T) { + defer catchPanics(t) + panic("bad stuff") +} + +// Timeouts cause the entire test process to end - so we can't get good output for these +// func TestTimeout(t *testing.T) { +// time.Sleep(time.Second * 5) +// } + +func TestSkipped(t *testing.T) { + randomSleep() + t.Skipf("skipping test") +} + +func TestCases(t *testing.T) { + for _, tc := range []struct { + name string + fn func(int, int) int + a, b, c int + }{ + {"1 + 2 = 3", CalculatorSum, 1, 2, 3}, + {"4 + 7 = 11", CalculatorSum, 4, 7, 11}, + {"2 + 3 = 4", CalculatorSum, 2, 3, 4}, + + {"1 / 2 = 1", CalculatorDivide, 1, 2, 1}, + {"9 / 3 = 3", CalculatorDivide, 9, 3, 3}, + {"14 / 7 = 2", CalculatorDivide, 14, 7, 2}, + } { + t.Run(tc.name, func(t *testing.T) { + randomSleep() + + c := tc.fn(tc.a, tc.b) + if c != tc.c { + t.Fatalf("expected %s, got %d", tc.name, c) + } + }) + } +} + +func catchPanics(t *testing.T) { + err := recover() + if err != nil { + t.Fatalf("caught panic: %v", err) + } +} + +func randomSleep() { + time.Sleep(time.Duration(rand.Int63n(int64(time.Second)))) +} diff --git a/reports/go/go.mod b/reports/go/go.mod new file mode 100644 index 0000000..4b3a577 --- /dev/null +++ b/reports/go/go.mod @@ -0,0 +1,3 @@ +module test_reporter_example + +go 1.24.2 diff --git a/src/main.ts b/src/main.ts index 109d75b..b018390 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import {getReport} from './report/get-report' import {DartJsonParser} from './parsers/dart-json/dart-json-parser' import {DotnetNunitParser} from './parsers/dotnet-nunit/dotnet-nunit-parser' import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser' +import {GolangJsonParser} from './parsers/golang-json/golang-json-parser' import {JavaJunitParser} from './parsers/java-junit/java-junit-parser' import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser' import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser' @@ -248,6 +249,8 @@ class TestReporter { return new DotnetNunitParser(options) case 'dotnet-trx': return new DotnetTrxParser(options) + case 'golang-json': + return new GolangJsonParser(options) case 'flutter-json': return new DartJsonParser(options, 'flutter') case 'java-junit': diff --git a/src/parsers/golang-json/golang-json-parser.ts b/src/parsers/golang-json/golang-json-parser.ts new file mode 100644 index 0000000..bc20821 --- /dev/null +++ b/src/parsers/golang-json/golang-json-parser.ts @@ -0,0 +1,115 @@ +import { ParseOptions, TestParser } from '../../test-parser' + +import { GoTestEvent } from './golang-json-types' +import { getExceptionSource } from '../../utils/node-utils' +import { getBasePath, normalizeFilePath } from '../../utils/path-utils' + +import { + TestExecutionResult, + TestRunResult, + TestSuiteResult, + TestGroupResult, + TestCaseResult, + TestCaseError +} from '../../test-results' + +export class GolangJsonParser implements TestParser { + assumedWorkDir: string | undefined + + constructor(readonly options: ParseOptions) { } + + async parse(path: string, content: string): Promise { + const events = await this.getGolangTestEvents(path, content) + return this.getTestRunResult(path, events) + } + + private async getGolangTestEvents(path: string, content: string): Promise { + return content.trim().split('\n').map((line, index) => { + try { + return JSON.parse(line) as GoTestEvent + } catch (e) { + throw new Error(`Invalid JSON at ${path} line ${index + 1}\n\n${e}`) + } + }) + } + + private getTestRunResult(path: string, events: GoTestEvent[]): TestRunResult { + const eventGroups = new Map() + for (const event of events) { + if (!event.Test) { + continue + } + const k = `${event.Package}/${event.Test}` + let g = eventGroups.get(k) + if (!g) { + g = [] + eventGroups.set(k, g) + } + g.push(event) + } + + const suites: TestSuiteResult[] = [] + + for (const eventGroup of eventGroups.values()) { + const event = eventGroup[0] + + let suite = suites.find(s => s.name === event.Package) + if (!suite) { + suite = new TestSuiteResult(event.Package, []) + suites.push(suite) + } + + if (!event.Test) { + continue + } + + let groupName: string | null + let rest: string[] + [groupName, ...rest] = event.Test.split('/') + let testName = rest.join('/') + if (!testName) { + testName = groupName + groupName = null + } + + let group = suite.groups.find(g => g.name === groupName) + if (!group) { + group = new TestGroupResult(groupName, []) + suite.groups.push(group) + } + + const lastEvent = eventGroup.at(-1)! + + const result: TestExecutionResult = lastEvent.Action === 'pass' ? 'success' + : lastEvent.Action === 'skip' ? 'skipped' + : 'failed' + if (lastEvent.Elapsed === undefined) { + throw new Error('missing elapsed on final test event') + } + const time: number = lastEvent.Elapsed * 1000 + + let error: TestCaseError | undefined = undefined + if (result !== 'success') { + const outputEvents = eventGroup + .filter(e => e.Action === 'output') + .map(e => e.Output ?? '') + // Go output prepends indentation to help group tests - remove it + .map(o => o.replace(/^ /, '')) + + // First and last lines will be generic "test started" and "test finished" lines - remove them + outputEvents.splice(0, 1) + outputEvents.splice(-1, 1) + + const details = outputEvents.join('') + error = { + message: details, + details: details + } + } + + group.tests.push(new TestCaseResult(testName, result, time, error)) + } + + return new TestRunResult(path, suites) + } +} diff --git a/src/parsers/golang-json/golang-json-types.ts b/src/parsers/golang-json/golang-json-types.ts new file mode 100644 index 0000000..f27fa86 --- /dev/null +++ b/src/parsers/golang-json/golang-json-types.ts @@ -0,0 +1,19 @@ +export type GoTestAction = 'start' + | 'run' + | 'pause' + | 'cont' + | 'pass' + | 'bench' + | 'fail' + | 'output' + | 'skip' + +export type GoTestEvent = { + Time: string + Action: GoTestAction + Package: string + Test?: string + Elapsed?: number + Output?: string + FailedBuild?: string +}