diff --git a/__tests__/__outputs__/golang-json.md b/__tests__/__outputs__/golang-json.md new file mode 100644 index 0000000..aaa36ea --- /dev/null +++ b/__tests__/__outputs__/golang-json.md @@ -0,0 +1,33 @@ +![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%205%20failed%2C%201%20skipped-critical) +|Report|Passed|Failed|Skipped|Time| +|:---|---:|---:|---:|---:| +|fixtures/golang-json.json|3 ✅|5 ❌|1 ⚪|4s| +## ❌ fixtures/golang-json.json +**9** tests were completed in **4s** with **3** passed, **5** failed and **1** skipped. +|Test suite|Passed|Failed|Skipped|Time| +|:---|---:|---:|---:|---:| +|[_/home/james_t/git/test-reporter/reports/go](#user-content-r0s0)|3 ✅|5 ❌|1 ⚪|4s| +### ❌ _/home/james_t/git/test-reporter/reports/go +``` +✅ TestPassing +❌ TestFailing + calculator_test.go:19: expected 1+1 = 3, got 2 + +❌ TestPanicInsideFunction + calculator_test.go:71: caught panic: runtime error: integer divide by zero + +❌ TestPanicInsideTest + calculator_test.go:71: 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:62: expected 2 + 3 = 4, got 5 + +``` \ 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..f2e4b04 --- /dev/null +++ b/__tests__/__snapshots__/golang-json.test.ts.snap @@ -0,0 +1,108 @@ +// 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": 90, + }, + 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": 760, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:71: caught panic: runtime error: integer divide by zero +", + "message": "calculator_test.go:71: caught panic: runtime error: integer divide by zero +", + }, + "name": "TestPanicInsideFunction", + "result": "failed", + "time": 0, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:71: caught panic: bad stuff +", + "message": "calculator_test.go:71: 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": 110, + }, + TestCaseResult { + "error": { + "details": "", + "message": "", + }, + "name": "TestCases", + "result": "failed", + "time": 1620, + }, + ], + }, + TestGroupResult { + "name": "TestCases", + "tests": [ + TestCaseResult { + "error": undefined, + "name": "1_+_2_=_3", + "result": "success", + "time": 540, + }, + TestCaseResult { + "error": undefined, + "name": "4_+_7_=_11", + "result": "success", + "time": 540, + }, + TestCaseResult { + "error": { + "details": "calculator_test.go:62: expected 2 + 3 = 4, got 5 +", + "message": "calculator_test.go:62: expected 2 + 3 = 4, got 5 +", + }, + "name": "2_+_3_=_4", + "result": "failed", + "time": 530, + }, + ], + }, + ], + "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..c18d7ef --- /dev/null +++ b/__tests__/fixtures/golang-json.json @@ -0,0 +1,46 @@ +{"Time":"2025-04-21T14:45:43.899731191-05:00","Action":"start","Package":"_/home/james_t/git/test-reporter/reports/go"} +{"Time":"2025-04-21T14:45:43.907303273-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing"} +{"Time":"2025-04-21T14:45:43.907333465-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Output":"=== RUN TestPassing\n"} +{"Time":"2025-04-21T14:45:43.998551886-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-21T14:45:43.998572844-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Output":"--- PASS: TestPassing (0.09s)\n"} +{"Time":"2025-04-21T14:45:43.998576971-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPassing","Elapsed":0.09} +{"Time":"2025-04-21T14:45:43.998580617-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing"} +{"Time":"2025-04-21T14:45:43.998582298-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Output":"=== RUN TestFailing\n"} +{"Time":"2025-04-21T14:45:44.763532195-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-21T14:45:44.763556975-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Output":"--- FAIL: TestFailing (0.76s)\n"} +{"Time":"2025-04-21T14:45:44.763560434-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestFailing","Elapsed":0.76} +{"Time":"2025-04-21T14:45:44.763564534-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction"} +{"Time":"2025-04-21T14:45:44.763566339-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":"=== RUN TestPanicInsideFunction\n"} +{"Time":"2025-04-21T14:45:44.763568422-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":" calculator_test.go:71: caught panic: runtime error: integer divide by zero\n"} +{"Time":"2025-04-21T14:45:44.763571047-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Output":"--- FAIL: TestPanicInsideFunction (0.00s)\n"} +{"Time":"2025-04-21T14:45:44.763575434-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideFunction","Elapsed":0} +{"Time":"2025-04-21T14:45:44.763577511-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest"} +{"Time":"2025-04-21T14:45:44.763579734-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":"=== RUN TestPanicInsideTest\n"} +{"Time":"2025-04-21T14:45:44.763584346-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":" calculator_test.go:71: caught panic: bad stuff\n"} +{"Time":"2025-04-21T14:45:44.763592493-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Output":"--- FAIL: TestPanicInsideTest (0.00s)\n"} +{"Time":"2025-04-21T14:45:44.763595682-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestPanicInsideTest","Elapsed":0} +{"Time":"2025-04-21T14:45:44.763598032-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped"} +{"Time":"2025-04-21T14:45:44.763600787-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Output":"=== RUN TestSkipped\n"} +{"Time":"2025-04-21T14:45:44.875085305-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-21T14:45:44.875128112-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Output":"--- SKIP: TestSkipped (0.11s)\n"} +{"Time":"2025-04-21T14:45:44.875136234-05:00","Action":"skip","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestSkipped","Elapsed":0.11} +{"Time":"2025-04-21T14:45:44.87514445-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases"} +{"Time":"2025-04-21T14:45:44.87514908-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Output":"=== RUN TestCases\n"} +{"Time":"2025-04-21T14:45:44.875158116-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3"} +{"Time":"2025-04-21T14:45:44.875163067-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-21T14:45:45.413895814-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11"} +{"Time":"2025-04-21T14:45:45.413916191-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-21T14:45:45.957801848-05:00","Action":"run","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4"} +{"Time":"2025-04-21T14:45:45.957836789-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-21T14:45:46.492673381-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Output":" calculator_test.go:62: expected 2 + 3 = 4, got 5\n"} +{"Time":"2025-04-21T14:45:46.492759645-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Output":"--- FAIL: TestCases (1.62s)\n"} +{"Time":"2025-04-21T14:45:46.492779916-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.54s)\n"} +{"Time":"2025-04-21T14:45:46.492787539-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/1_+_2_=_3","Elapsed":0.54} +{"Time":"2025-04-21T14:45:46.492795891-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.54s)\n"} +{"Time":"2025-04-21T14:45:46.492801851-05:00","Action":"pass","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/4_+_7_=_11","Elapsed":0.54} +{"Time":"2025-04-21T14:45:46.492806634-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.53s)\n"} +{"Time":"2025-04-21T14:45:46.492811453-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases/2_+_3_=_4","Elapsed":0.53} +{"Time":"2025-04-21T14:45:46.49281562-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Test":"TestCases","Elapsed":1.62} +{"Time":"2025-04-21T14:45:46.492821605-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Output":"FAIL\n"} +{"Time":"2025-04-21T14:45:46.493310745-05:00","Action":"output","Package":"_/home/james_t/git/test-reporter/reports/go","Output":"FAIL\t_/home/james_t/git/test-reporter/reports/go\t2.593s\n"} +{"Time":"2025-04-21T14:45:46.493343801-05:00","Action":"fail","Package":"_/home/james_t/git/test-reporter/reports/go","Elapsed":2.594} diff --git a/__tests__/golang-json.test.ts b/__tests__/golang-json.test.ts new file mode 100644 index 0000000..37d45b3 --- /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/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..c0649fb --- /dev/null +++ b/reports/go/calculator_test.go @@ -0,0 +1,77 @@ +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 + a, b, c int + }{ + {"1 + 2 = 3", 1, 2, 3}, + {"4 + 7 = 11", 4, 7, 11}, + {"2 + 3 = 4", 2, 3, 4}, + } { + t.Run(tc.name, func(t *testing.T) { + randomSleep() + + c := CalculatorSum(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/parsers/golang-json/golang-json-parser.ts b/src/parsers/golang-json/golang-json-parser.ts new file mode 100644 index 0000000..b79e1f9 --- /dev/null +++ b/src/parsers/golang-json/golang-json-parser.ts @@ -0,0 +1,114 @@ +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 testName: string + [groupName, testName] = event.Test.split('/', 2) + 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 +}