From aed3b29c84313193f711dc9c33b5e5f6112e81e6 Mon Sep 17 00:00:00 2001 From: Michal Dorner Date: Sun, 10 Jan 2021 11:21:16 +0100 Subject: [PATCH] Add annotations support to dart-json parser --- .../__snapshots__/dart-json.test.ts.snap | 51 ++++++++++- __tests__/dart-json.test.ts | 2 +- src/parsers/dart-json/dart-json-parser.ts | 86 ++++++++++++++++++- 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/__tests__/__snapshots__/dart-json.test.ts.snap b/__tests__/__snapshots__/dart-json.test.ts.snap index a97ccc5..194aadd 100644 --- a/__tests__/__snapshots__/dart-json.test.ts.snap +++ b/__tests__/__snapshots__/dart-json.test.ts.snap @@ -2,7 +2,56 @@ exports[`dart-json tests matches report snapshot 1`] = ` Object { - "annotations": Array [], + "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... +", + "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... +", + "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.. +", + "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 ✖️ | | :---: | :--- | ---: | ---: | ---: | ---: | ---: | diff --git a/__tests__/dart-json.test.ts b/__tests__/dart-json.test.ts index 7d460ca..05e771f 100644 --- a/__tests__/dart-json.test.ts +++ b/__tests__/dart-json.test.ts @@ -12,7 +12,7 @@ describe('dart-json tests', () => { const opts: ParseOptions = { name: 'Dart tests', annotations: true, - trackedFiles: ['test/main_test.dart', 'test/second_test.dart'], + trackedFiles: ['lib/main.dart', 'test/main_test.dart', 'test/second_test.dart'], workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dart/' } diff --git a/src/parsers/dart-json/dart-json-parser.ts b/src/parsers/dart-json/dart-json-parser.ts index a731fc3..7de56f7 100644 --- a/src/parsers/dart-json/dart-json-parser.ts +++ b/src/parsers/dart-json/dart-json-parser.ts @@ -1,4 +1,6 @@ -import {ParseOptions, TestResult} from '../test-parser' +import {Annotation, ParseOptions, TestResult} from '../test-parser' + +import {normalizeFilePath} from '../../utils/file-utils' import {Align, Icon, link, table} from '../../utils/markdown-utils' import {slug} from '../../utils/slugger' @@ -103,7 +105,7 @@ export async function parseDartJson(content: string, options: ParseOptions): Pro output: { title: `${options.name.trim()} ${icon}`, summary: getSummary(testRun), - annotations: options.annotations ? [] : undefined + annotations: options.annotations ? getAnnotations(testRun, options.workDir, options.trackedFiles) : undefined } } } @@ -208,3 +210,83 @@ function getTestCaseIcon(test: TestCase): string { if (test.isSkipped) return Icon.skip return Icon.success } + +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 +}