├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── dirty-laundry.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __outputs__ │ ├── dart-json.md │ ├── dotnet-trx-only-failed.md │ ├── dotnet-trx.md │ ├── fluent-validation-test-results.md │ ├── jest-junit-eslint.md │ ├── jest-junit.md │ ├── jest-test-errors-results.md │ ├── jest-test-results.md │ ├── mocha-json.md │ ├── mocha-test-results.md │ ├── mochawesome-json.md │ ├── playwright-test-errors-results.md │ ├── provider-test-results.md │ ├── pulsar-test-results-no-merge.md │ ├── pulsar-test-results.md │ └── silent-notes-test-results.md ├── __snapshots__ │ ├── dart-json.test.ts.snap │ ├── dotnet-trx.test.ts.snap │ ├── java-junit.test.ts.snap │ ├── jest-junit.test.ts.snap │ ├── mocha-json.test.ts.snap │ └── mochawesome-json.test.ts.snap ├── dart-json.test.ts ├── dotnet-trx.test.ts ├── fixtures │ ├── assets │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ ├── app.css │ │ ├── app.css.map │ │ ├── app.js │ │ ├── app.js.LICENSE.txt │ │ ├── app.js.map │ │ ├── roboto-light-webfont.woff │ │ ├── roboto-light-webfont.woff2 │ │ ├── roboto-medium-webfont.woff │ │ ├── roboto-medium-webfont.woff2 │ │ ├── roboto-regular-webfont.woff │ │ └── roboto-regular-webfont.woff2 │ ├── dart-json.json │ ├── dotnet-trx.trx │ ├── empty │ │ ├── dart-json.json │ │ ├── dotnet-trx.trx │ │ ├── java-junit.xml │ │ ├── jest-junit.xml │ │ ├── mocha-json.json │ │ └── mochawesome-json.json │ ├── external │ │ ├── FluentValidation.Tests.trx │ │ ├── SilentNotes.trx │ │ ├── flutter │ │ │ ├── files.txt │ │ │ └── provider-test-results.json │ │ ├── java │ │ │ ├── TEST-org.apache.pulsar.AddMissingPatchVersionTest.xml │ │ │ ├── files.txt │ │ │ └── pulsar-test-report.xml │ │ ├── jest │ │ │ ├── files.txt │ │ │ └── jest-test-results.xml │ │ └── mocha │ │ │ ├── files.txt │ │ │ └── mocha-test-results.json │ ├── jest-junit-eslint.xml │ ├── jest-junit.xml │ ├── mocha-json.json │ ├── mochawesome-json.html │ ├── mochawesome-json.json │ ├── mochawesome1-json.json │ └── test-errors │ │ ├── jest │ │ ├── files.txt │ │ └── jest-test-results.xml │ │ └── playwright │ │ ├── files.txt │ │ └── test-results.xml ├── java-junit.test.ts ├── jest-junit.test.ts ├── mocha-json.test.ts ├── mochawesome-json.test.ts └── utils │ └── parse-utils.test.ts ├── action.yml ├── assets ├── fluent-validation-report.png ├── mocha-groups.png ├── provider-error-details.png └── provider-error-summary.png ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── jest.config.js ├── package-lock.json ├── package.json ├── reports ├── dart │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib │ │ └── main.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ │ ├── main_test.dart │ │ └── second_test.dart ├── dotnet │ ├── .gitignore │ ├── DotnetTests.Unit │ │ ├── Calculator.cs │ │ └── DotnetTests.Unit.csproj │ ├── DotnetTests.XUnitTests │ │ ├── CalculatorTests.cs │ │ └── DotnetTests.XUnitTests.csproj │ └── DotnetTests.sln ├── jest │ ├── __tests__ │ │ ├── main.test.js │ │ └── second.test.js │ ├── lib │ │ └── main.js │ ├── package-lock.json │ └── package.json ├── mocha │ ├── lib │ │ └── main.js │ ├── package-lock.json │ ├── package.json │ └── test │ │ ├── main.test.js │ │ └── second.test.js └── mochawesome │ ├── lib │ └── main.js │ ├── package-lock.json │ ├── package.json │ └── test │ ├── main.test.js │ └── second.test.js ├── src ├── input-providers │ ├── artifact-provider.ts │ ├── input-provider.ts │ └── local-file-provider.ts ├── main.ts ├── parsers │ ├── dart-json │ │ ├── dart-json-parser.ts │ │ └── dart-json-types.ts │ ├── dotnet-trx │ │ ├── dotnet-trx-parser.ts │ │ └── dotnet-trx-types.ts │ ├── java-junit │ │ ├── java-junit-parser.ts │ │ └── java-junit-types.ts │ ├── jest-junit │ │ ├── jest-junit-parser.ts │ │ └── jest-junit-types.ts │ ├── mocha-json │ │ ├── mocha-json-parser.ts │ │ └── mocha-json-types.ts │ └── mochawesome-json │ │ ├── mochawesome-json-parser.ts │ │ └── mochawesome-json-types.ts ├── report │ ├── get-annotations.ts │ └── get-report.ts ├── test-parser.ts ├── test-results.ts └── utils │ ├── constants.ts │ ├── exec.ts │ ├── git.ts │ ├── github-utils.ts │ ├── markdown-utils.ts │ ├── node-utils.ts │ ├── parse-utils.ts │ ├── path-utils.ts │ └── slugger.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | 9 | [*.cs] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "camelcase": "off", 12 | "eslint-comments/no-use": "off", 13 | "i18n-text/no-en": "off", 14 | "import/no-namespace": "off", 15 | "no-shadow": "off", 16 | "no-unused-vars": "off", 17 | "prefer-template": "off", 18 | "semi": [ "error", "never"], 19 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 20 | "@typescript-eslint/array-type": "error", 21 | "@typescript-eslint/await-thenable": "error", 22 | "@typescript-eslint/ban-ts-comment": "error", 23 | "@typescript-eslint/consistent-type-assertions": "error", 24 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 25 | "@typescript-eslint/func-call-spacing": ["error", "never"], 26 | "@typescript-eslint/no-array-constructor": "error", 27 | "@typescript-eslint/no-empty-interface": "error", 28 | "@typescript-eslint/no-explicit-any": "off", 29 | "@typescript-eslint/no-extraneous-class": "error", 30 | "@typescript-eslint/no-for-in-array": "error", 31 | "@typescript-eslint/no-inferrable-types": "error", 32 | "@typescript-eslint/no-misused-new": "error", 33 | "@typescript-eslint/no-namespace": "error", 34 | "@typescript-eslint/no-require-imports": "error", 35 | "@typescript-eslint/no-shadow": "error", 36 | "@typescript-eslint/no-non-null-assertion": "warn", 37 | "@typescript-eslint/no-unnecessary-qualifier": "error", 38 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 39 | "@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}], 40 | "@typescript-eslint/no-useless-constructor": "error", 41 | "@typescript-eslint/no-var-requires": "error", 42 | "@typescript-eslint/prefer-for-of": "warn", 43 | "@typescript-eslint/prefer-function-type": "warn", 44 | "@typescript-eslint/prefer-includes": "error", 45 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 46 | "@typescript-eslint/promise-function-async": "error", 47 | "@typescript-eslint/require-array-sort-compare": "error", 48 | "@typescript-eslint/restrict-plus-operands": "error", 49 | "@typescript-eslint/semi": ["error", "never"], 50 | "@typescript-eslint/type-annotation-spacing": "error", 51 | "@typescript-eslint/unbound-method": "error" 52 | }, 53 | "env": { 54 | "node": true, 55 | "es6": true, 56 | "jest/globals": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | dist/** -diff linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | on: 3 | pull_request: 4 | paths-ignore: ['**.md'] 5 | push: 6 | paths-ignore: ['**.md'] 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | permissions: 12 | actions: read 13 | checks: write 14 | 15 | jobs: 16 | build-test: 17 | name: Build & Test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - run: npm ci 22 | - run: npm run build 23 | - run: npm run format-check 24 | - run: npm run lint 25 | - run: npm test 26 | 27 | - name: Upload test results 28 | if: success() || failure() 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: test-results 32 | path: __tests__/__results__/*.xml 33 | 34 | - name: Create test report 35 | uses: ./ 36 | with: 37 | artifact: test-results 38 | name: Workflow Report 39 | path: '*.xml' 40 | reporter: jest-junit 41 | 42 | - name: Create mochawesome report 43 | uses: ./ 44 | if: success() || failure() 45 | with: 46 | name: Mochawesome Tests 47 | path: __tests__/fixtures/mochawesome-json.json 48 | reporter: mochawesome-json 49 | fail-on-error: false 50 | -------------------------------------------------------------------------------- /.github/workflows/dirty-laundry.yml: -------------------------------------------------------------------------------- 1 | name: 'Dirty Laundry Test' 2 | on: 3 | pull_request: 4 | paths-ignore: ['**.md'] 5 | push: 6 | paths-ignore: ['**.md'] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-test: 11 | name: Build & Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - run: npm ci 16 | - run: npm run build 17 | - run: npm run format-check 18 | - run: npm run lint 19 | 20 | - name: Create mochawesome report 21 | uses: ./ 22 | id: test-report 23 | if: success() || failure() 24 | with: 25 | name: Mochawesome Tests 26 | path: __tests__/fixtures/mochawesome1-json.json 27 | reporter: mochawesome-json 28 | fail-on-error: false 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # Project specific 102 | __tests__/__results__ 103 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | __tests__/__outputs__ 5 | __tests__/__snapshots__ 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest All", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["--runInBand"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Jest Current File", 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "args": [ 23 | "${fileBasenameNoExtension}", 24 | "--config", 25 | "jest.config.js" 26 | ], 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen", 29 | "disableOptimisticBPs": true, 30 | "windows": { 31 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.5.0 4 | - [Add option to convert backslashes in path pattern to forward slashes](https://github.com/phoenix-actions/test-reporting/pull/128) 5 | - [Add option to generate only the summary from processed test results files](https://github.com/phoenix-actions/test-reporting/pull/123) 6 | 7 | ## v1.4.3 8 | - [Patch java-junit to handle missing time field](https://github.com/phoenix-actions/test-reporting/pull/115) 9 | - [Fix dart-json parsing broken by print message](https://github.com/phoenix-actions/test-reporting/pull/114) 10 | 11 | ## v1.4.2 12 | - [Fix dotnet-trx parsing of passed tests with non-empty error info](https://github.com/phoenix-actions/test-reporting/commit/43d89d5ee509bcef7bd0287aacc0c4a4fb9c1657) 13 | 14 | ## v1.4.1 15 | - [Fix dotnet-trx parsing of tests with custom display names](https://github.com/phoenix-actions/test-reporting/pull/105) 16 | 17 | ## v1.4.0 18 | - [Add support for mocha-json](https://github.com/phoenix-actions/test-reporting/pull/90) 19 | - [Use full URL to fix navigation from summary to suite details](https://github.com/phoenix-actions/test-reporting/pull/89) 20 | - [New report rendering with code blocks instead of tables](https://github.com/phoenix-actions/test-reporting/pull/88) 21 | - [Improve test error messages from flutter](https://github.com/phoenix-actions/test-reporting/pull/87) 22 | 23 | ## v1.3.1 24 | - [Fix: parsing of .NET duration string without milliseconds](https://github.com/phoenix-actions/test-reporting/pull/84) 25 | - [Fix: dart-json - remove group name from test case names](https://github.com/phoenix-actions/test-reporting/pull/85) 26 | - [Fix: net-trx parser crashing on missing duration attribute](https://github.com/phoenix-actions/test-reporting/pull/86) 27 | 28 | ## v1.3.0 29 | - [Add support for java-junit](https://github.com/phoenix-actions/test-reporting/pull/80) 30 | - [Fix: Handle test reports with no test cases](https://github.com/phoenix-actions/test-reporting/pull/70) 31 | - [Fix: Reduce number of API calls to get list of files tracked by GitHub](https://github.com/phoenix-actions/test-reporting/pull/69) 32 | 33 | ## v1.2.0 34 | - [Set `listTests` and `listSuites` to lower detail if report is too big](https://github.com/phoenix-actions/test-reporting/pull/60) 35 | 36 | ## v1.1.0 37 | - [Support public repo PR workflow](https://github.com/phoenix-actions/test-reporting/pull/56) 38 | 39 | ## v1.0.0 40 | Supported languages / frameworks: 41 | - .NET / xUnit / NUnit / MSTest 42 | - Dart / test 43 | - Flutter / test 44 | - JavaScript / JEST 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Michal Dorner and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /__tests__/__outputs__/dart-json.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/dart-json.json 3 | **6** tests were completed in **4s** with **1** passed, **4** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[test/main_test.dart](#r0s0)|1✔️|3❌||74ms| 7 | |[test/second_test.dart](#r0s1)||1❌|1✖️|51ms| 8 | ### ❌ test/main_test.dart 9 | ``` 10 | Test 1 11 | ✔️ Passing test 12 | Test 1 Test 1.1 13 | ❌ Failing test 14 | Expected: <2> 15 | Actual: <1> 16 | 17 | ❌ Exception in target unit 18 | Exception: Some error 19 | Test 2 20 | ❌ Exception in test 21 | Exception: Some error 22 | ``` 23 | ### ❌ test/second_test.dart 24 | ``` 25 | ❌ Timeout test 26 | TimeoutException after 0:00:00.000001: Test timed out after 0 seconds. 27 | ✖️ Skipped test 28 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/dotnet-trx-only-failed.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-5%20passed%2C%205%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/dotnet-trx.trx 3 | **11** tests were completed in **1s** with **5** passed, **5** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[DotnetTests.XUnitTests.CalculatorTests](#r0s0)|5✔️|5❌|1✖️|118ms| 7 | ### ❌ DotnetTests.XUnitTests.CalculatorTests 8 | ``` 9 | ❌ Exception_In_TargetTest 10 | System.DivideByZeroException : Attempted to divide by zero. 11 | ❌ Exception_In_Test 12 | System.Exception : Test 13 | ❌ Failing_Test 14 | Assert.Equal() Failure 15 | Expected: 3 16 | Actual: 2 17 | ❌ Is_Even_Number(i: 3) 18 | Assert.True() Failure 19 | Expected: True 20 | Actual: False 21 | ❌ Should be even number(i: 3) 22 | Assert.True() Failure 23 | Expected: True 24 | Actual: False 25 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/dotnet-trx.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-5%20passed%2C%205%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/dotnet-trx.trx 3 | **11** tests were completed in **1s** with **5** passed, **5** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[DotnetTests.XUnitTests.CalculatorTests](#r0s0)|5✔️|5❌|1✖️|118ms| 7 | ### ❌ DotnetTests.XUnitTests.CalculatorTests 8 | ``` 9 | ✔️ Custom Name 10 | ❌ Exception_In_TargetTest 11 | System.DivideByZeroException : Attempted to divide by zero. 12 | ❌ Exception_In_Test 13 | System.Exception : Test 14 | ❌ Failing_Test 15 | Assert.Equal() Failure 16 | Expected: 3 17 | Actual: 2 18 | ✔️ Is_Even_Number(i: 2) 19 | ❌ Is_Even_Number(i: 3) 20 | Assert.True() Failure 21 | Expected: True 22 | Actual: False 23 | ✔️ Passing_Test 24 | ✔️ Should be even number(i: 2) 25 | ❌ Should be even number(i: 3) 26 | Assert.True() Failure 27 | Expected: True 28 | Actual: False 29 | ✖️ Skipped_Test 30 | ✔️ Timeout_Test 31 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/jest-junit-eslint.md: -------------------------------------------------------------------------------- 1 | ![Tests passed successfully](https://img.shields.io/badge/tests-1%20passed-success) 2 | ## ✔️ fixtures/jest-junit-eslint.xml 3 | **1** tests were completed in **0ms** with **1** passed, **0** failed and **0** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[test.jsx](#r0s0)|1✔️|||0ms| 7 | ### ✔️ test.jsx 8 | ``` 9 | test 10 | ✔️ test.jsx 11 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/jest-junit.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/jest-junit.xml 3 | **6** tests were completed in **1s** with **1** passed, **4** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[__tests__\main.test.js](#r0s0)|1✔️|3❌||486ms| 7 | |[__tests__\second.test.js](#r0s1)||1❌|1✖️|82ms| 8 | ### ❌ __tests__\main.test.js 9 | ``` 10 | Test 1 11 | ✔️ Passing test 12 | Test 1 › Test 1.1 13 | ❌ Failing test 14 | Error: expect(received).toBeTruthy() 15 | ❌ Exception in target unit 16 | Error: Some error 17 | Test 2 18 | ❌ Exception in test 19 | Error: Some error 20 | ``` 21 | ### ❌ __tests__\second.test.js 22 | ``` 23 | ❌ Timeout test 24 | : Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Error: 25 | ✖️ Skipped test 26 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/jest-test-errors-results.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-2%20failed-critical) 2 | ## ❌ fixtures/test-errors/jest/jest-test-results.xml 3 | **2** tests were completed in **646ms** with **0** passed, **2** failed and **0** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[libs/bar.spec.ts](#r0s0)||1❌||0ms| 7 | |[libs/foo.spec.ts](#r0s1)||1❌||0ms| 8 | ### ❌ libs/bar.spec.ts 9 | ``` 10 | Test suite failed to run 11 | ❌ libs/bar.spec.ts 12 | ● Test suite failed to run 13 | ``` 14 | ### ❌ libs/foo.spec.ts 15 | ``` 16 | Test suite failed to run 17 | ❌ libs/foo.spec.ts 18 | ● Test suite failed to run 19 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/mocha-json.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/mocha-json.json 3 | **6** tests were completed in **11ms** with **1** passed, **4** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[test/main.test.js](#r0s0)|1✔️|3❌||3ms| 7 | |[test/second.test.js](#r0s1)||1❌|1✖️|2ms| 8 | ### ❌ test/main.test.js 9 | ``` 10 | Test 1 11 | ✔️ Passing test 12 | Test 1 Test 1.1 13 | ❌ Exception in target unit 14 | Some error 15 | ❌ Failing test 16 | Expected values to be strictly equal: 17 | 18 | false !== true 19 | 20 | Test 2 21 | ❌ Exception in test 22 | Some error 23 | ``` 24 | ### ❌ test/second.test.js 25 | ``` 26 | ✖️ Skipped test 27 | ❌ Timeout test 28 | Timeout of 1ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js) 29 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/mocha-test-results.md: -------------------------------------------------------------------------------- 1 | ![Tests passed successfully](https://img.shields.io/badge/tests-833%20passed%2C%206%20skipped-success) 2 | ## ✔️ fixtures/external/mocha/mocha-test-results.json 3 | **839** tests were completed in **6s** with **833** passed, **0** failed and **6** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |test/node-unit/buffered-worker-pool.spec.js|14✔️|||8ms| 7 | |test/node-unit/cli/config.spec.js|10✔️|||8ms| 8 | |test/node-unit/cli/node-flags.spec.js|105✔️|||9ms| 9 | |test/node-unit/cli/options.spec.js|36✔️|||250ms| 10 | |test/node-unit/cli/run-helpers.spec.js|9✔️|||8ms| 11 | |test/node-unit/cli/run.spec.js|40✔️|||4ms| 12 | |test/node-unit/mocha.spec.js|24✔️|||33ms| 13 | |test/node-unit/parallel-buffered-runner.spec.js|19✔️|||23ms| 14 | |test/node-unit/reporters/parallel-buffered.spec.js|6✔️|||16ms| 15 | |test/node-unit/serializer.spec.js|40✔️|||31ms| 16 | |test/node-unit/stack-trace-filter.spec.js|2✔️||4✖️|1ms| 17 | |test/node-unit/utils.spec.js|5✔️|||1ms| 18 | |test/node-unit/worker.spec.js|15✔️|||92ms| 19 | |test/unit/context.spec.js|8✔️|||5ms| 20 | |test/unit/duration.spec.js|3✔️|||166ms| 21 | |test/unit/errors.spec.js|13✔️|||5ms| 22 | |test/unit/globals.spec.js|4✔️|||0ms| 23 | |test/unit/grep.spec.js|8✔️|||2ms| 24 | |test/unit/hook-async.spec.js|3✔️|||1ms| 25 | |test/unit/hook-sync-nested.spec.js|4✔️|||1ms| 26 | |test/unit/hook-sync.spec.js|3✔️|||0ms| 27 | |test/unit/hook-timeout.spec.js|1✔️|||0ms| 28 | |test/unit/hook.spec.js|4✔️|||0ms| 29 | |test/unit/mocha.spec.js|115✔️||1✖️|128ms| 30 | |test/unit/overspecified-async.spec.js|1✔️|||3ms| 31 | |test/unit/parse-query.spec.js|2✔️|||1ms| 32 | |test/unit/plugin-loader.spec.js|41✔️||1✖️|16ms| 33 | |test/unit/required-tokens.spec.js|1✔️|||0ms| 34 | |test/unit/root.spec.js|1✔️|||0ms| 35 | |test/unit/runnable.spec.js|55✔️|||122ms| 36 | |test/unit/runner.spec.js|77✔️|||43ms| 37 | |test/unit/suite.spec.js|57✔️|||14ms| 38 | |test/unit/test.spec.js|15✔️|||0ms| 39 | |test/unit/throw.spec.js|9✔️|||9ms| 40 | |test/unit/timeout.spec.js|8✔️|||109ms| 41 | |test/unit/utils.spec.js|75✔️|||24ms| -------------------------------------------------------------------------------- /__tests__/__outputs__/mochawesome-json.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed-critical) 2 | ## ❌ fixtures/mochawesome-json.json 3 | **5** tests were completed in **14ms** with **1** passed, **4** failed and **0** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[nofile](#r0s0)|1✔️|4❌||5ms| 7 | ### ❌ nofile 8 | ``` 9 | ❌ Timeout test 10 | Error: Timeout of 1ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/second.test.js) 11 | Test 1 12 | ✔️ Passing test 13 | Test 1 Test 1.1 14 | ❌ Exception in target unit 15 | Error: Some error 16 | ❌ Failing test 17 | AssertionError: Expected values to be strictly equal: 18 | 19 | false !== true 20 | 21 | Test 2 22 | ❌ Exception in test 23 | Error: Some error 24 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/playwright-test-errors-results.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20failed-critical) 2 | ## ❌ fixtures/test-errors/playwright/test-results.xml 3 | **1** tests were completed in **849ms** with **0** passed, **1** failed and **0** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[libs/foo.spec.ts](#r0s0)||1❌||144s| 7 | ### ❌ libs/foo.spec.ts 8 | ``` 9 | libs/foo.spec.ts 10 | ❌ Test suite failure 11 | [LRS:Chromium] › libs/foo.spec.ts:38:5 › Test suite failure 12 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/pulsar-test-results-no-merge.md: -------------------------------------------------------------------------------- 1 | ![Tests failed](https://img.shields.io/badge/tests-1%20failed%2C%201%20skipped-critical) 2 | ## ❌ fixtures/external/java/TEST-org.apache.pulsar.AddMissingPatchVersionTest.xml 3 | **2** tests were completed in **116ms** with **0** passed, **1** failed and **1** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[org.apache.pulsar.AddMissingPatchVersionTest](#r0s0)||1❌|1✖️|116ms| 7 | ### ❌ org.apache.pulsar.AddMissingPatchVersionTest 8 | ``` 9 | ✖️ testVersionStrings 10 | ❌ testVersionStrings 11 | java.lang.AssertionError: expected [1.2.1] but found [1.2.0] 12 | ``` -------------------------------------------------------------------------------- /__tests__/__outputs__/silent-notes-test-results.md: -------------------------------------------------------------------------------- 1 | ![Tests passed successfully](https://img.shields.io/badge/tests-67%20passed%2C%2012%20skipped-success) 2 | ## ✔️ fixtures/external/SilentNotes.trx 3 | **79** tests were completed in **1s** with **67** passed, **0** failed and **12** skipped. 4 | |Test suite|Passed|Failed|Skipped|Time| 5 | |:---|---:|---:|---:|---:| 6 | |[VanillaCloudStorageClientTest.CloudStorageCredentialsTest](#r0s0)|6✔️|||30ms| 7 | |[VanillaCloudStorageClientTest.CloudStorageProviders.DropboxCloudStorageClientTest](#r0s1)|2✔️||3✖️|101ms| 8 | |[VanillaCloudStorageClientTest.CloudStorageProviders.FtpCloudStorageClientTest](#r0s2)|4✔️||3✖️|166ms| 9 | |[VanillaCloudStorageClientTest.CloudStorageProviders.GmxCloudStorageClientTest](#r0s3)|2✔️|||7ms| 10 | |[VanillaCloudStorageClientTest.CloudStorageProviders.GoogleCloudStorageClientTest](#r0s4)|1✔️||3✖️|40ms| 11 | |[VanillaCloudStorageClientTest.CloudStorageProviders.OnedriveCloudStorageClientTest](#r0s5)|1✔️||3✖️|15ms| 12 | |[VanillaCloudStorageClientTest.CloudStorageProviders.WebdavCloudStorageClientTest](#r0s6)|5✔️|||16ms| 13 | |[VanillaCloudStorageClientTest.CloudStorageTokenTest](#r0s7)|9✔️|||0ms| 14 | |[VanillaCloudStorageClientTest.OAuth2.AuthorizationResponseErrorTest](#r0s8)|3✔️|||3ms| 15 | |[VanillaCloudStorageClientTest.OAuth2.OAuth2UtilsTest](#r0s9)|9✔️|||12ms| 16 | |[VanillaCloudStorageClientTest.OAuth2CloudStorageClientTest](#r0s10)|5✔️|||13ms| 17 | |[VanillaCloudStorageClientTest.SecureStringExtensionsTest](#r0s11)|7✔️|||0ms| 18 | |[VanillaCloudStorageClientTest.SerializeableCloudStorageCredentialsTest](#r0s12)|13✔️|||43ms| 19 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageCredentialsTest 20 | ``` 21 | ✔️ AreEqualWorksWithDifferentPassword 22 | ✔️ AreEqualWorksWithSameContent 23 | ✔️ CorrectlyConvertsSecureStringToString 24 | ✔️ CorrectlyConvertsStringToSecureString 25 | ✔️ ValidateAcceptsValidCredentials 26 | ✔️ ValidateRejectsInvalidCredentials 27 | ``` 28 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.DropboxCloudStorageClientTest 29 | ``` 30 | ✔️ FileLifecycleWorks 31 | ✖️ ReallyDoFetchToken 32 | ✖️ ReallyDoOpenAuthorizationPageInBrowser 33 | ✖️ ReallyDoRefreshToken 34 | ✔️ ThrowsAccessDeniedExceptionWithInvalidToken 35 | ``` 36 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.FtpCloudStorageClientTest 37 | ``` 38 | ✔️ FileLifecycleWorks 39 | ✔️ SanitizeCredentials_ChangesInvalidPrefix 40 | ✔️ SecureSslConnectionWorks 41 | ✔️ ThrowsWithHttpInsteadOfFtp 42 | ✖️ ThrowsWithInvalidPassword 43 | ✖️ ThrowsWithInvalidUrl 44 | ✖️ ThrowsWithInvalidUsername 45 | ``` 46 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.GmxCloudStorageClientTest 47 | ``` 48 | ✔️ ChoosesCorrectUrlForGmxComEmail 49 | ✔️ ChoosesCorrectUrlForGmxNetEmail 50 | ``` 51 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.GoogleCloudStorageClientTest 52 | ``` 53 | ✔️ FileLifecycleWorks 54 | ✖️ ReallyDoFetchToken 55 | ✖️ ReallyDoOpenAuthorizationPageInBrowser 56 | ✖️ ReallyDoRefreshToken 57 | ``` 58 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.OnedriveCloudStorageClientTest 59 | ``` 60 | ✔️ FileLifecycleWorks 61 | ✖️ ReallyDoFetchToken 62 | ✖️ ReallyDoOpenAuthorizationPageInBrowser 63 | ✖️ ReallyDoRefreshToken 64 | ``` 65 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageProviders.WebdavCloudStorageClientTest 66 | ``` 67 | ✔️ FileLifecycleWorks 68 | ✔️ ParseGmxWebdavResponseCorrectly 69 | ✔️ ParseStratoWebdavResponseCorrectly 70 | ✔️ ThrowsWithInvalidPath 71 | ✔️ ThrowsWithInvalidUsername 72 | ``` 73 | ### ✔️ VanillaCloudStorageClientTest.CloudStorageTokenTest 74 | ``` 75 | ✔️ AreEqualWorksWithNullDate 76 | ✔️ AreEqualWorksWithSameContent 77 | ✔️ NeedsRefreshReturnsFalseForTokenFlow 78 | ✔️ NeedsRefreshReturnsFalseIfNotExpired 79 | ✔️ NeedsRefreshReturnsTrueIfExpired 80 | ✔️ NeedsRefreshReturnsTrueIfNoExpirationDate 81 | ✔️ SetExpiryDateBySecondsWorks 82 | ✔️ SetExpiryDateBySecondsWorksWithNull 83 | ✔️ SetExpiryDateBySecondsWorksWithVeryShortPeriod 84 | ``` 85 | ### ✔️ VanillaCloudStorageClientTest.OAuth2.AuthorizationResponseErrorTest 86 | ``` 87 | ✔️ ParsesAllErrorCodesCorrectly 88 | ✔️ ParsesNullErrorCodeCorrectly 89 | ✔️ ParsesUnknownErrorCodeCorrectly 90 | ``` 91 | ### ✔️ VanillaCloudStorageClientTest.OAuth2.OAuth2UtilsTest 92 | ``` 93 | ✔️ BuildAuthorizationRequestUrlEscapesParameters 94 | ✔️ BuildAuthorizationRequestUrlLeavesOutOptionalParameters 95 | ✔️ BuildAuthorizationRequestUrlThrowsWithMissingRedirectUrlForTokenFlow 96 | ✔️ BuildAuthorizationRequestUrlUsesAllParameters 97 | ✔️ BuildAuthorizationRequestUrlUsesCodeVerifier 98 | ✔️ ParseRealWorldDropboxRejectResponse 99 | ✔️ ParseRealWorldDropboxSuccessResponse 100 | ✔️ ParseRealWorldGoogleRejectResponse 101 | ✔️ ParseRealWorldGoogleSuccessResponse 102 | ``` 103 | ### ✔️ VanillaCloudStorageClientTest.OAuth2CloudStorageClientTest 104 | ``` 105 | ✔️ BuildOAuth2AuthorizationRequestUrlWorks 106 | ✔️ FetchTokenCanInterpretGoogleResponse 107 | ✔️ FetchTokenReturnsNullForDeniedAccess 108 | ✔️ FetchTokenThrowsWithWrongState 109 | ✔️ RefreshTokenCanInterpretGoogleResponse 110 | ``` 111 | ### ✔️ VanillaCloudStorageClientTest.SecureStringExtensionsTest 112 | ``` 113 | ✔️ AreEqualsWorksCorrectly 114 | ✔️ CorrectlyConvertsSecureStringToString 115 | ✔️ CorrectlyConvertsSecureStringToUnicodeBytes 116 | ✔️ CorrectlyConvertsSecureStringToUtf8Bytes 117 | ✔️ CorrectlyConvertsStringToSecureString 118 | ✔️ CorrectlyConvertsUnicodeBytesToSecureString 119 | ✔️ CorrectlyConvertsUtf8BytesToSecureString 120 | ``` 121 | ### ✔️ VanillaCloudStorageClientTest.SerializeableCloudStorageCredentialsTest 122 | ``` 123 | ✔️ DecryptAfterDesrializationCanReadAllPropertiesBack 124 | ✔️ DecryptAfterDesrializationRespectsNullProperties 125 | ✔️ EncryptBeforeSerializationProtectsAllNecessaryProperties 126 | ✔️ EncryptBeforeSerializationRespectsNullProperties 127 | ✔️ SerializedDatacontractCanBeReadBack 128 | ✔️ SerializedDatacontractDoesNotContainNullProperties 129 | ✔️ SerializedDatacontractDoesNotContainPlaintextData 130 | ✔️ SerializedJsonCanBeReadBack 131 | ✔️ SerializedJsonDoesNotContainNullProperties 132 | ✔️ SerializedJsonDoesNotContainPlaintextData 133 | ✔️ SerializedXmlCanBeReadBack 134 | ✔️ SerializedXmlDoesNotContainNullProperties 135 | ✔️ SerializedXmlDoesNotContainPlaintextData 136 | ``` -------------------------------------------------------------------------------- /__tests__/__snapshots__/mochawesome-json.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mochawesome-json tests report from ./reports/mochawesome-json test results matches snapshot 1`] = ` 4 | TestRunResult { 5 | "path": "fixtures/mochawesome-json.json", 6 | "suites": [ 7 | TestSuiteResult { 8 | "groups": [ 9 | TestGroupResult { 10 | "name": null, 11 | "tests": [ 12 | TestCaseResult { 13 | "error": { 14 | "details": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/second.test.js) 15 | at listOnTimeout (node:internal/timers:557:17) 16 | at processTimers (node:internal/timers:500:7)", 17 | "line": undefined, 18 | "message": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/second.test.js)", 19 | "path": undefined, 20 | }, 21 | "name": "Timeout test", 22 | "result": "failed", 23 | "time": 3, 24 | }, 25 | ], 26 | }, 27 | TestGroupResult { 28 | "name": "Test 1", 29 | "tests": [ 30 | TestCaseResult { 31 | "error": undefined, 32 | "name": "Passing test", 33 | "result": "success", 34 | "time": 0, 35 | }, 36 | ], 37 | }, 38 | TestGroupResult { 39 | "name": "Test 1 Test 1.1", 40 | "tests": [ 41 | TestCaseResult { 42 | "error": { 43 | "details": "Error: Some error 44 | at Object.throwError (lib/main.js:2:9) 45 | at Context. (test/main.test.js:15:11) 46 | at processImmediate (node:internal/timers:464:21)", 47 | "line": 2, 48 | "message": "Error: Some error", 49 | "path": "lib/main.js", 50 | }, 51 | "name": "Exception in target unit", 52 | "result": "failed", 53 | "time": 0, 54 | }, 55 | TestCaseResult { 56 | "error": { 57 | "details": "AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 58 | 59 | false !== true 60 | 61 | at Context. (test/main.test.js:11:14) 62 | at processImmediate (node:internal/timers:464:21)", 63 | "line": 11, 64 | "message": "AssertionError: Expected values to be strictly equal: 65 | 66 | false !== true 67 | ", 68 | "path": "test/main.test.js", 69 | }, 70 | "name": "Failing test", 71 | "result": "failed", 72 | "time": 2, 73 | }, 74 | ], 75 | }, 76 | TestGroupResult { 77 | "name": "Test 2", 78 | "tests": [ 79 | TestCaseResult { 80 | "error": { 81 | "details": "Error: Some error 82 | at Context. (test/main.test.js:22:11) 83 | at processImmediate (node:internal/timers:464:21)", 84 | "line": 22, 85 | "message": "Error: Some error", 86 | "path": "test/main.test.js", 87 | }, 88 | "name": "Exception in test", 89 | "result": "failed", 90 | "time": 0, 91 | }, 92 | ], 93 | }, 94 | ], 95 | "name": "nofile", 96 | "totalTime": undefined, 97 | }, 98 | ], 99 | "totalTime": 14, 100 | } 101 | `; 102 | -------------------------------------------------------------------------------- /__tests__/dart-json.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {DartJsonParser} from '../src/parsers/dart-json/dart-json-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('dart-json tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'dart-json.json') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new DartJsonParser(opts, 'dart') 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('matches report snapshot', async () => { 27 | const opts: ParseOptions = { 28 | parseErrors: true, 29 | trackedFiles: ['lib/main.dart', 'test/main_test.dart', 'test/second_test.dart'] 30 | //workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dart/' 31 | } 32 | 33 | const fixturePath = path.join(__dirname, 'fixtures', 'dart-json.json') 34 | const outputPath = path.join(__dirname, '__outputs__', 'dart-json.md') 35 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 36 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 37 | 38 | const parser = new DartJsonParser(opts, 'dart') 39 | const result = await parser.parse(filePath, fileContent) 40 | expect(result).toMatchSnapshot() 41 | 42 | const report = getReport([result]) 43 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 44 | fs.writeFileSync(outputPath, report) 45 | }) 46 | 47 | it('report from rrousselGit/provider test results matches snapshot', async () => { 48 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'flutter', 'provider-test-results.json') 49 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'flutter', 'files.txt') 50 | const outputPath = path.join(__dirname, '__outputs__', 'provider-test-results.md') 51 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 52 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 53 | 54 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 55 | const opts: ParseOptions = { 56 | trackedFiles, 57 | parseErrors: true 58 | //workDir: '/__w/provider/provider/' 59 | } 60 | 61 | const parser = new DartJsonParser(opts, 'flutter') 62 | const result = await parser.parse(filePath, fileContent) 63 | expect(result).toMatchSnapshot() 64 | 65 | const report = getReport([result]) 66 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 67 | fs.writeFileSync(outputPath, report) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /__tests__/dotnet-trx.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {DotnetTrxParser} from '../src/parsers/dotnet-trx/dotnet-trx-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport, ReportOptions} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('dotnet-trx tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'dotnet-trx.trx') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new DotnetTrxParser(opts) 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('matches report snapshot', async () => { 27 | const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-trx.trx') 28 | const outputPath = path.join(__dirname, '__outputs__', 'dotnet-trx.md') 29 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 30 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 31 | 32 | const opts: ParseOptions = { 33 | parseErrors: true, 34 | trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.XUnitTests/CalculatorTests.cs'] 35 | //workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dotnet/' 36 | } 37 | 38 | const parser = new DotnetTrxParser(opts) 39 | const result = await parser.parse(filePath, fileContent) 40 | expect(result).toMatchSnapshot() 41 | 42 | const report = getReport([result]) 43 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 44 | fs.writeFileSync(outputPath, report) 45 | }) 46 | 47 | it('matches report snapshot (only failed tests)', async () => { 48 | const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-trx.trx') 49 | const outputPath = path.join(__dirname, '__outputs__', 'dotnet-trx-only-failed.md') 50 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 51 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 52 | 53 | const opts: ParseOptions = { 54 | parseErrors: true, 55 | trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.XUnitTests/CalculatorTests.cs'] 56 | //workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dotnet/' 57 | } 58 | 59 | const parser = new DotnetTrxParser(opts) 60 | const result = await parser.parse(filePath, fileContent) 61 | expect(result).toMatchSnapshot() 62 | 63 | const reportOptions: ReportOptions = { 64 | listSuites: 'all', 65 | listTests: 'failed', 66 | onlySummary: false, 67 | slugPrefix: '', 68 | baseUrl: '' 69 | } 70 | const report = getReport([result], reportOptions) 71 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 72 | fs.writeFileSync(outputPath, report) 73 | }) 74 | 75 | it('report from FluentValidation test results matches snapshot', async () => { 76 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'FluentValidation.Tests.trx') 77 | const outputPath = path.join(__dirname, '__outputs__', 'fluent-validation-test-results.md') 78 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 79 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 80 | 81 | const opts: ParseOptions = { 82 | trackedFiles: [], 83 | parseErrors: true 84 | } 85 | 86 | const parser = new DotnetTrxParser(opts) 87 | const result = await parser.parse(filePath, fileContent) 88 | expect(result).toMatchSnapshot() 89 | 90 | const report = getReport([result]) 91 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 92 | fs.writeFileSync(outputPath, report) 93 | }) 94 | 95 | it('report from SilentNotes test results matches snapshot', async () => { 96 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'SilentNotes.trx') 97 | const outputPath = path.join(__dirname, '__outputs__', 'silent-notes-test-results.md') 98 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 99 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 100 | 101 | const opts: ParseOptions = { 102 | trackedFiles: [], 103 | parseErrors: true 104 | } 105 | 106 | const parser = new DotnetTrxParser(opts) 107 | const result = await parser.parse(filePath, fileContent) 108 | expect(result).toMatchSnapshot() 109 | 110 | const report = getReport([result]) 111 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 112 | fs.writeFileSync(outputPath, report) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /__tests__/fixtures/assets/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /__tests__/fixtures/assets/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /__tests__/fixtures/assets/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! ***************************************************************************** 14 | Copyright (c) Microsoft Corporation. All rights reserved. 15 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 16 | this file except in compliance with the License. You may obtain a copy of the 17 | License at http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 21 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 22 | MERCHANTABLITY OR NON-INFRINGEMENT. 23 | 24 | See the Apache Version 2.0 License for specific language governing permissions 25 | and limitations under the License. 26 | ***************************************************************************** */ 27 | 28 | /*! mochawesome-report-generator 6.0.1 | https://github.com/adamgruber/mochawesome-report-generator */ 29 | 30 | /** @license React v0.19.1 31 | * scheduler.production.min.js 32 | * 33 | * Copyright (c) Facebook, Inc. and its affiliates. 34 | * 35 | * This source code is licensed under the MIT license found in the 36 | * LICENSE file in the root directory of this source tree. 37 | */ 38 | 39 | /** @license React v16.13.1 40 | * react-dom.production.min.js 41 | * 42 | * Copyright (c) Facebook, Inc. and its affiliates. 43 | * 44 | * This source code is licensed under the MIT license found in the 45 | * LICENSE file in the root directory of this source tree. 46 | */ 47 | 48 | /** @license React v16.13.1 49 | * react.production.min.js 50 | * 51 | * Copyright (c) Facebook, Inc. and its affiliates. 52 | * 53 | * This source code is licensed under the MIT license found in the 54 | * LICENSE file in the root directory of this source tree. 55 | */ 56 | -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-light-webfont.woff -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-light-webfont.woff2 -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-medium-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-medium-webfont.woff -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-medium-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-medium-webfont.woff2 -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-regular-webfont.woff -------------------------------------------------------------------------------- /__tests__/fixtures/assets/roboto-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/__tests__/fixtures/assets/roboto-regular-webfont.woff2 -------------------------------------------------------------------------------- /__tests__/fixtures/dart-json.json: -------------------------------------------------------------------------------- 1 | {"protocolVersion":"0.1.1","runnerVersion":"1.15.4","pid":7504,"type":"start","time":0} 2 | {"suite":{"id":0,"platform":"vm","path":"test\\main_test.dart"},"type":"suite","time":0} 3 | {"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} 4 | {"suite":{"id":2,"platform":"vm","path":"test\\second_test.dart"},"type":"suite","time":11} 5 | {"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} 6 | {"count":2,"type":"allSuites","time":11} 7 | {"testID":1,"messageType":"print","message":"Hello from the test","type":"print","time":3828} 8 | {"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3649} 9 | {"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} 10 | {"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} 11 | {"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":3672} 12 | {"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} 13 | {"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} 14 | {"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} 15 | {"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} 16 | {"testID":5,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3692} 17 | {"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} 18 | {"testID":9,"messageType":"skip","message":"Skip: skipped test","type":"print","time":3706} 19 | {"testID":9,"result":"success","skipped":true,"hidden":false,"type":"testDone","time":3707} 20 | {"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":3708} 21 | {"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} 22 | {"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} 23 | {"testID":11,"error":"Expected: <2>\n Actual: <1>\n","stackTrace":"package:test_api expect\ntest\\main_test.dart 13:9 main...\n","isFailure":true,"type":"error","time":3736} 24 | {"testID":11,"result":"failure","skipped":false,"hidden":false,"type":"testDone","time":3736} 25 | {"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} 26 | {"testID":12,"error":"Exception: Some error","stackTrace":"package:darttest/main.dart 2:3 throwError\ntest\\main_test.dart 17:9 main...\n","isFailure":false,"type":"error","time":3743} 27 | {"testID":12,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3743} 28 | {"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} 29 | {"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} 30 | {"testID":14,"error":"Exception: Some error","stackTrace":"test\\main_test.dart 24:7 main..\n","isFailure":false,"type":"error","time":3756} 31 | {"testID":14,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":3756} 32 | {"success":false,"type":"done","time":3760} 33 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/dart-json.json: -------------------------------------------------------------------------------- 1 | {"protocolVersion":"0.1.1","runnerVersion":"1.25.3","pid":7103,"type":"start","time":0} 2 | {"suite":{"id":0,"platform":"vm","path":"test/second_test.dart"},"type":"suite","time":0} 3 | {"test":{"id":1,"name":"loading test/second_test.dart","suiteID":0,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":0} 4 | {"suite":{"id":2,"platform":"vm","path":"test/main_test.dart"},"type":"suite","time":4} 5 | {"test":{"id":3,"name":"loading test/main_test.dart","suiteID":2,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":4} 6 | {"count":2,"time":5,"type":"allSuites"} 7 | {"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":294} 8 | {"testID":3,"messageType":"print","message":"Hello from the test","type":"print","time":297} 9 | {"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":321} 10 | {"group":{"id":4,"suiteID":2,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":0,"line":null,"column":null,"url":null},"type":"group","time":322} 11 | {"test":{"id":5,"name":"(setUpAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":6,"column":3,"url":"file:///Users/user/test-reporter/reports/dart/test/main_test.dart"},"type":"testStart","time":322} 12 | {"testID":5,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":330} 13 | {"test":{"id":6,"name":"(tearDownAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":7,"column":3,"url":"file:///Users/user/test-reporter/reports/dart/test/main_test.dart"},"type":"testStart","time":330} 14 | {"testID":6,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":331} 15 | {"success":true,"type":"done","time":333} 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/dotnet-trx.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | No test is available in (...). Make sure that test discoverer & executors are registered and platform & framework version settings are appropriate and try again. 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/java-junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/jest-junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/mocha-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "suites": 0, 4 | "tests": 0, 5 | "passes": 0, 6 | "pending": 0, 7 | "failures": 0, 8 | "start": "2021-03-08T20:01:44.391Z", 9 | "end": "2021-03-08T20:01:44.391Z", 10 | "duration": 0 11 | }, 12 | "tests": [], 13 | "pending": [], 14 | "failures": [], 15 | "passes": [] 16 | } -------------------------------------------------------------------------------- /__tests__/fixtures/empty/mochawesome-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "suites": 0, 4 | "tests": 0, 5 | "passes": 0, 6 | "pending": 0, 7 | "failures": 0, 8 | "start": "2022-02-17T18:05:44.524Z", 9 | "end": "2022-02-17T18:05:54.535Z", 10 | "duration": 0, 11 | "testsRegistered": 0, 12 | "passPercent": 0, 13 | "pendingPercent": 0, 14 | "other": 0, 15 | "hasOther": false, 16 | "skipped": 0, 17 | "hasSkipped": true 18 | }, 19 | "results": [], 20 | "meta": {} 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/fixtures/external/flutter/files.txt: -------------------------------------------------------------------------------- 1 | .github/ISSUE_TEMPLATE/bug_report.md 2 | .github/ISSUE_TEMPLATE/config.yml 3 | .github/ISSUE_TEMPLATE/feature_request.md 4 | .github/workflows/build.yml 5 | .gitignore 6 | CHANGELOG.md 7 | LICENSE 8 | README.md 9 | all_lint_rules.yaml 10 | analysis_options.yaml 11 | dart_test.yaml 12 | example/.gitignore 13 | example/.metadata 14 | example/lib/main.dart 15 | example/pubspec.yaml 16 | example/test/widget_test.dart 17 | example/test_driver/app.dart 18 | example/test_driver/app_test.dart 19 | lib/provider.dart 20 | lib/single_child_widget.dart 21 | lib/src/async_provider.dart 22 | lib/src/change_notifier_provider.dart 23 | lib/src/consumer.dart 24 | lib/src/deferred_inherited_provider.dart 25 | lib/src/inherited_provider.dart 26 | lib/src/listenable_provider.dart 27 | lib/src/provider.dart 28 | lib/src/proxy_provider.dart 29 | lib/src/reassemble_handler.dart 30 | lib/src/selector.dart 31 | lib/src/value_listenable_provider.dart 32 | pubspec.yaml 33 | resources/devtools_providers.jpg 34 | resources/expanded_devtools.jpg 35 | resources/flutter_favorite.png 36 | resources/translations/es_MX/README.md 37 | resources/translations/pt_br/README.md 38 | resources/translations/zh-CN/README.md 39 | scripts/flutter_test.sh 40 | test/builder_test.dart 41 | test/change_notifier_provider_test.dart 42 | test/common.dart 43 | test/consumer_test.dart 44 | test/context_test.dart 45 | test/dart_test.yaml 46 | test/future_provider_test.dart 47 | test/inherited_provider_test.dart 48 | test/listenable_provider_test.dart 49 | test/listenable_proxy_provider_test.dart 50 | test/multi_provider_test.dart 51 | test/provider_test.dart 52 | test/proxy_provider_test.dart 53 | test/reassemble_test.dart 54 | test/selector_test.dart 55 | test/stateful_provider_test.dart 56 | test/stream_provider_test.dart 57 | test/value_listenable_provider_test.dart 58 | -------------------------------------------------------------------------------- /__tests__/fixtures/external/java/TEST-org.apache.pulsar.AddMissingPatchVersionTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /__tests__/fixtures/jest-junit-eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/jest-junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Error: expect(received).toBeTruthy() 8 | 9 | Received: false 10 | at Object.<anonymous> (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\__tests__\main.test.js:10:21) 11 | at Object.asyncJestTest (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmineAsyncInstall.js:106:37) 12 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:45:12 13 | at new Promise (<anonymous>) 14 | at mapper (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:28:19) 15 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:75:41 16 | at processTicksAndRejections (internal/process/task_queues.js:97:5) 17 | 18 | 19 | Error: Some error 20 | at Object.throwError (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\lib\main.js:2:9) 21 | at Object.<anonymous> (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\__tests__\main.test.js:14:11) 22 | at Object.asyncJestTest (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmineAsyncInstall.js:106:37) 23 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:45:12 24 | at new Promise (<anonymous>) 25 | at mapper (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:28:19) 26 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:75:41 27 | at processTicksAndRejections (internal/process/task_queues.js:97:5) 28 | 29 | 30 | Error: Some error 31 | at Object.<anonymous> (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\__tests__\main.test.js:21:11) 32 | at Object.asyncJestTest (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmineAsyncInstall.js:106:37) 33 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:45:12 34 | at new Promise (<anonymous>) 35 | at mapper (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:28:19) 36 | at C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\queueRunner.js:75:41 37 | at processTicksAndRejections (internal/process/task_queues.js:97:5) 38 | 39 | 40 | 41 | 42 | : Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Error: 43 | at new Spec (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmine\Spec.js:116:22) 44 | at new Spec (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\setup_jest_globals.js:78:9) 45 | at specFactory (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmine\Env.js:523:24) 46 | at Env.it (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmine\Env.js:592:24) 47 | at Env.it (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmineAsyncInstall.js:134:23) 48 | at it (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\jasmine\jasmineLight.js:100:21) 49 | at Object.<anonymous> (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\__tests__\second.test.js:1:34) 50 | at Runtime._execModule (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-runtime\build\index.js:1245:24) 51 | at Runtime._loadModule (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-runtime\build\index.js:844:12) 52 | at Runtime.requireModule (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-runtime\build\index.js:694:10) 53 | at jasmine2 (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-jasmine2\build\index.js:230:13) 54 | at runTestInternal (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-runner\build\runTest.js:380:22) 55 | at runTest (C:\Users\Michal\Workspace\dorny\test-check\reports\jest\node_modules\jest-runner\build\runTest.js:472:34) 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /__tests__/fixtures/mocha-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "suites": 3, 4 | "tests": 6, 5 | "passes": 1, 6 | "pending": 1, 7 | "failures": 4, 8 | "start": "2022-02-18T14:30:07.077Z", 9 | "end": "2022-02-18T14:30:07.088Z", 10 | "duration": 11 11 | }, 12 | "tests": [ 13 | { 14 | "title": "Timeout test", 15 | "fullTitle": "Timeout test", 16 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js", 17 | "duration": 2, 18 | "currentRetry": 0, 19 | "err": { 20 | "stack": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js)\n at listOnTimeout (node:internal/timers:557:17)\n at processTimers (node:internal/timers:500:7)", 21 | "message": "Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js)", 22 | "code": "ERR_MOCHA_TIMEOUT", 23 | "timeout": 1, 24 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js" 25 | } 26 | }, 27 | { 28 | "title": "Skipped test", 29 | "fullTitle": "Skipped test", 30 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js", 31 | "currentRetry": 0, 32 | "err": {} 33 | }, 34 | { 35 | "title": "Passing test", 36 | "fullTitle": "Test 1 Passing test", 37 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 38 | "duration": 0, 39 | "currentRetry": 0, 40 | "speed": "fast", 41 | "err": {} 42 | }, 43 | { 44 | "title": "Failing test", 45 | "fullTitle": "Test 1 Test 1.1 Failing test", 46 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 47 | "duration": 3, 48 | "currentRetry": 0, 49 | "err": { 50 | "stack": "AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:\n\nfalse !== true\n\n at Context. (test/main.test.js:11:14)\n at processImmediate (node:internal/timers:464:21)", 51 | "message": "Expected values to be strictly equal:\n\nfalse !== true\n", 52 | "generatedMessage": true, 53 | "name": "AssertionError", 54 | "code": "ERR_ASSERTION", 55 | "actual": "false", 56 | "expected": "true", 57 | "operator": "strictEqual" 58 | } 59 | }, 60 | { 61 | "title": "Exception in target unit", 62 | "fullTitle": "Test 1 Test 1.1 Exception in target unit", 63 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 64 | "duration": 0, 65 | "currentRetry": 0, 66 | "err": { 67 | "stack": "Error: Some error\n at Object.throwError (lib/main.js:2:9)\n at Context. (test/main.test.js:15:11)\n at processImmediate (node:internal/timers:464:21)", 68 | "message": "Some error" 69 | } 70 | }, 71 | { 72 | "title": "Exception in test", 73 | "fullTitle": "Test 2 Exception in test", 74 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 75 | "duration": 0, 76 | "currentRetry": 0, 77 | "err": { 78 | "stack": "Error: Some error\n at Context. (test/main.test.js:22:11)\n at processImmediate (node:internal/timers:464:21)", 79 | "message": "Some error" 80 | } 81 | } 82 | ], 83 | "pending": [ 84 | { 85 | "title": "Skipped test", 86 | "fullTitle": "Skipped test", 87 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js", 88 | "currentRetry": 0, 89 | "err": {} 90 | } 91 | ], 92 | "failures": [ 93 | { 94 | "title": "Timeout test", 95 | "fullTitle": "Timeout test", 96 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js", 97 | "duration": 2, 98 | "currentRetry": 0, 99 | "err": { 100 | "stack": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js)\n at listOnTimeout (node:internal/timers:557:17)\n at processTimers (node:internal/timers:500:7)", 101 | "message": "Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js)", 102 | "code": "ERR_MOCHA_TIMEOUT", 103 | "timeout": 1, 104 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/second.test.js" 105 | } 106 | }, 107 | { 108 | "title": "Failing test", 109 | "fullTitle": "Test 1 Test 1.1 Failing test", 110 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 111 | "duration": 3, 112 | "currentRetry": 0, 113 | "err": { 114 | "stack": "AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:\n\nfalse !== true\n\n at Context. (test/main.test.js:11:14)\n at processImmediate (node:internal/timers:464:21)", 115 | "message": "Expected values to be strictly equal:\n\nfalse !== true\n", 116 | "generatedMessage": true, 117 | "name": "AssertionError", 118 | "code": "ERR_ASSERTION", 119 | "actual": "false", 120 | "expected": "true", 121 | "operator": "strictEqual" 122 | } 123 | }, 124 | { 125 | "title": "Exception in target unit", 126 | "fullTitle": "Test 1 Test 1.1 Exception in target unit", 127 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 128 | "duration": 0, 129 | "currentRetry": 0, 130 | "err": { 131 | "stack": "Error: Some error\n at Object.throwError (lib/main.js:2:9)\n at Context. (test/main.test.js:15:11)\n at processImmediate (node:internal/timers:464:21)", 132 | "message": "Some error" 133 | } 134 | }, 135 | { 136 | "title": "Exception in test", 137 | "fullTitle": "Test 2 Exception in test", 138 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 139 | "duration": 0, 140 | "currentRetry": 0, 141 | "err": { 142 | "stack": "Error: Some error\n at Context. (test/main.test.js:22:11)\n at processImmediate (node:internal/timers:464:21)", 143 | "message": "Some error" 144 | } 145 | } 146 | ], 147 | "passes": [ 148 | { 149 | "title": "Passing test", 150 | "fullTitle": "Test 1 Passing test", 151 | "file": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mocha/test/main.test.js", 152 | "duration": 0, 153 | "currentRetry": 0, 154 | "speed": "fast", 155 | "err": {} 156 | } 157 | ] 158 | } 159 | -------------------------------------------------------------------------------- /__tests__/fixtures/mochawesome-json.html: -------------------------------------------------------------------------------- 1 | 2 | Mochawesome Report
-------------------------------------------------------------------------------- /__tests__/fixtures/mochawesome-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "stats": { 3 | "suites": 3, 4 | "tests": 6, 5 | "passes": 1, 6 | "pending": 1, 7 | "failures": 4, 8 | "start": "2022-02-18T14:32:42.133Z", 9 | "end": "2022-02-18T14:32:42.147Z", 10 | "duration": 14, 11 | "testsRegistered": 6, 12 | "passPercent": 20, 13 | "pendingPercent": 16.666666666666664, 14 | "other": 0, 15 | "hasOther": false, 16 | "skipped": 0, 17 | "hasSkipped": false 18 | }, 19 | "results": [ 20 | { 21 | "uuid": "e74145e8-4f10-432c-8fe1-79fab8a0fe12", 22 | "title": "In-line test with no file", 23 | "fullFile": "nofile", 24 | "file": "nofile", 25 | "beforeHooks": [], 26 | "afterHooks": [], 27 | "tests": [ 28 | { 29 | "title": "Timeout test", 30 | "fullTitle": "Timeout test", 31 | "timedOut": true, 32 | "duration": 3, 33 | "state": "failed", 34 | "speed": null, 35 | "pass": false, 36 | "fail": true, 37 | "pending": false, 38 | "context": null, 39 | "code": "this.timeout(1);\nsetTimeout(done, 1000);", 40 | "err": { 41 | "message": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/second.test.js)", 42 | "estack": "Error: Timeout of 1ms exceeded. For async tests and hooks, ensure \"done()\" is called; if returning a Promise, ensure it resolves. (/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/second.test.js)\n at listOnTimeout (node:internal/timers:557:17)\n at processTimers (node:internal/timers:500:7)", 43 | "diff": null 44 | }, 45 | "uuid": "3ad24933-108e-426f-9cc3-0d71ab9f35c2", 46 | "parentUUID": "e74145e8-4f10-432c-8fe1-79fab8a0fe12", 47 | "isHook": false, 48 | "skipped": false 49 | }, 50 | { 51 | "title": "Skipped test", 52 | "fullTitle": "Skipped test", 53 | "timedOut": false, 54 | "duration": 0, 55 | "state": "pending", 56 | "speed": null, 57 | "pass": false, 58 | "fail": false, 59 | "pending": true, 60 | "context": null, 61 | "code": "", 62 | "err": {}, 63 | "uuid": "f1b2a440-4080-44da-9346-bb5df85581e0", 64 | "parentUUID": "e74145e8-4f10-432c-8fe1-79fab8a0fe12", 65 | "isHook": false, 66 | "skipped": false 67 | } 68 | ], 69 | "suites": [ 70 | { 71 | "uuid": "36bafd85-ff6a-4a77-920a-090deb0631c3", 72 | "title": "Test 1", 73 | "fullFile": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/main.test.js", 74 | "file": "/test/main.test.js", 75 | "beforeHooks": [], 76 | "afterHooks": [], 77 | "tests": [ 78 | { 79 | "title": "Passing test", 80 | "fullTitle": "Test 1 Passing test", 81 | "timedOut": false, 82 | "duration": 0, 83 | "state": "passed", 84 | "speed": "fast", 85 | "pass": true, 86 | "fail": false, 87 | "pending": false, 88 | "context": null, 89 | "code": "assert.equal(true, true)", 90 | "err": {}, 91 | "uuid": "3ebb7382-adde-438c-9d31-694714656ac3", 92 | "parentUUID": "36bafd85-ff6a-4a77-920a-090deb0631c3", 93 | "isHook": false, 94 | "skipped": false 95 | } 96 | ], 97 | "suites": [ 98 | { 99 | "uuid": "bd89b790-970b-41f8-965d-e67eae577576", 100 | "title": "Test 1.1", 101 | "fullFile": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/main.test.js", 102 | "file": "/test/main.test.js", 103 | "beforeHooks": [], 104 | "afterHooks": [], 105 | "tests": [ 106 | { 107 | "title": "Failing test", 108 | "fullTitle": "Test 1 Test 1.1 Failing test", 109 | "timedOut": false, 110 | "duration": 2, 111 | "state": "failed", 112 | "speed": null, 113 | "pass": false, 114 | "fail": true, 115 | "pending": false, 116 | "context": null, 117 | "code": "assert.equal(false, true)", 118 | "err": { 119 | "message": "AssertionError: Expected values to be strictly equal:\n\nfalse !== true\n", 120 | "estack": "AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:\n\nfalse !== true\n\n at Context. (test/main.test.js:11:14)\n at processImmediate (node:internal/timers:464:21)", 121 | "diff": "- false\n+ true\n" 122 | }, 123 | "uuid": "e154e320-a501-49bd-b0dc-fcdf8fb6460a", 124 | "parentUUID": "bd89b790-970b-41f8-965d-e67eae577576", 125 | "isHook": false, 126 | "skipped": false 127 | }, 128 | { 129 | "title": "Exception in target unit", 130 | "fullTitle": "Test 1 Test 1.1 Exception in target unit", 131 | "timedOut": false, 132 | "duration": 0, 133 | "state": "failed", 134 | "speed": null, 135 | "pass": false, 136 | "fail": true, 137 | "pending": false, 138 | "context": null, 139 | "code": "lib.throwError();", 140 | "err": { 141 | "message": "Error: Some error", 142 | "estack": "Error: Some error\n at Object.throwError (lib/main.js:2:9)\n at Context. (test/main.test.js:15:11)\n at processImmediate (node:internal/timers:464:21)", 143 | "diff": null 144 | }, 145 | "uuid": "bee0d977-da09-43a9-9683-6e2676952f60", 146 | "parentUUID": "bd89b790-970b-41f8-965d-e67eae577576", 147 | "isHook": false, 148 | "skipped": false 149 | } 150 | ], 151 | "suites": [], 152 | "passes": [], 153 | "failures": ["e154e320-a501-49bd-b0dc-fcdf8fb6460a", "bee0d977-da09-43a9-9683-6e2676952f60"], 154 | "pending": [], 155 | "skipped": [], 156 | "duration": 2, 157 | "root": false, 158 | "rootEmpty": false, 159 | "_timeout": 2000 160 | } 161 | ], 162 | "passes": ["3ebb7382-adde-438c-9d31-694714656ac3"], 163 | "failures": [], 164 | "pending": [], 165 | "skipped": [], 166 | "duration": 0, 167 | "root": false, 168 | "rootEmpty": false, 169 | "_timeout": 2000 170 | }, 171 | { 172 | "uuid": "930f5258-1a08-44d1-8343-5782cb1e0f39", 173 | "title": "Test 2", 174 | "fullFile": "/Users/work/Source/Repos/thirdparty/phoenix-test-reporter/reports/mochawesome/test/main.test.js", 175 | "file": "/test/main.test.js", 176 | "beforeHooks": [], 177 | "afterHooks": [], 178 | "tests": [ 179 | { 180 | "title": "Exception in test", 181 | "fullTitle": "Test 2 Exception in test", 182 | "timedOut": false, 183 | "duration": 0, 184 | "state": "failed", 185 | "speed": null, 186 | "pass": false, 187 | "fail": true, 188 | "pending": false, 189 | "context": null, 190 | "code": "throw new Error('Some error');", 191 | "err": { 192 | "message": "Error: Some error", 193 | "estack": "Error: Some error\n at Context. (test/main.test.js:22:11)\n at processImmediate (node:internal/timers:464:21)", 194 | "diff": null 195 | }, 196 | "uuid": "add99db1-a040-4ab8-86cf-91051665fb47", 197 | "parentUUID": "930f5258-1a08-44d1-8343-5782cb1e0f39", 198 | "isHook": false, 199 | "skipped": false 200 | } 201 | ], 202 | "suites": [], 203 | "passes": [], 204 | "failures": ["add99db1-a040-4ab8-86cf-91051665fb47"], 205 | "pending": [], 206 | "skipped": [], 207 | "duration": 0, 208 | "root": false, 209 | "rootEmpty": false, 210 | "_timeout": 2000 211 | } 212 | ], 213 | "passes": [], 214 | "failures": ["3ad24933-108e-426f-9cc3-0d71ab9f35c2"], 215 | "pending": ["f1b2a440-4080-44da-9346-bb5df85581e0"], 216 | "skipped": [], 217 | "duration": 3, 218 | "root": true, 219 | "rootEmpty": false, 220 | "_timeout": 2000 221 | } 222 | ], 223 | "meta": { 224 | "mocha": { 225 | "version": "9.2.0" 226 | }, 227 | "mochawesome": { 228 | "options": { 229 | "quiet": false, 230 | "reportFilename": "mochawesome-json.json", 231 | "saveHtml": true, 232 | "saveJson": true, 233 | "consoleReporter": "spec", 234 | "useInlineDiffs": false, 235 | "code": true 236 | }, 237 | "version": "7.0.1" 238 | }, 239 | "marge": { 240 | "options": { 241 | "reportDir": "../../__tests__/fixtures/", 242 | "reportFilename": "mochawesome-json.json" 243 | }, 244 | "version": "6.0.1" 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-errors/jest/files.txt: -------------------------------------------------------------------------------- 1 | libs/bar.spec.ts 2 | libs/foo.spec.ts 3 | tsconfig.json 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-errors/jest/jest-test-results.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ● Test suite failed to run 6 | 7 | tsconfig.json:13:3 - error TS6258: 'typeRoots' should be set inside the 'compilerOptions' object of the config json file 8 | 9 | 13 "typeRoots": ["./src/lib/types", "./node_modules/@types"], 10 | ~~~~~~~~~~~ 11 | 12 | 13 | 14 | 15 | 16 | ● Test suite failed to run 17 | 18 | tsconfig.json:13:3 - error TS6258: 'typeRoots' should be set inside the 'compilerOptions' object of the config json file 19 | 20 | 13 "typeRoots": ["./src/lib/types", "./node_modules/@types"], 21 | ~~~~~~~~~~~ 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-errors/playwright/files.txt: -------------------------------------------------------------------------------- 1 | libs/foo.spec.ts 2 | tsconfig.json 3 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-errors/playwright/test-results.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Email aka getByTestId('first') 9 | 2) aka getByTestId('second') 10 | ]]> 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /__tests__/java-junit.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {JavaJunitParser} from '../src/parsers/java-junit/java-junit-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('java-junit tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'java-junit.xml') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new JavaJunitParser(opts) 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('report from apache/pulsar single suite test results matches snapshot', async () => { 27 | const fixturePath = path.join( 28 | __dirname, 29 | 'fixtures', 30 | 'external', 31 | 'java', 32 | 'TEST-org.apache.pulsar.AddMissingPatchVersionTest.xml' 33 | ) 34 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'java', 'files.txt') 35 | const outputPath = path.join(__dirname, '__outputs__', 'pulsar-test-results-no-merge.md') 36 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 37 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 38 | 39 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 40 | const opts: ParseOptions = { 41 | parseErrors: true, 42 | trackedFiles 43 | } 44 | 45 | const parser = new JavaJunitParser(opts) 46 | const result = await parser.parse(filePath, fileContent) 47 | expect(result).toMatchSnapshot() 48 | 49 | const report = getReport([result]) 50 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 51 | fs.writeFileSync(outputPath, report) 52 | }) 53 | 54 | it('report from apache/pulsar test results matches snapshot', async () => { 55 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'java', 'pulsar-test-report.xml') 56 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'java', 'files.txt') 57 | const outputPath = path.join(__dirname, '__outputs__', 'pulsar-test-results.md') 58 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 59 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 60 | 61 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 62 | const opts: ParseOptions = { 63 | parseErrors: true, 64 | trackedFiles 65 | } 66 | 67 | const parser = new JavaJunitParser(opts) 68 | const result = await parser.parse(filePath, fileContent) 69 | expect(result).toMatchSnapshot() 70 | 71 | const report = getReport([result]) 72 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 73 | fs.writeFileSync(outputPath, report) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /__tests__/jest-junit.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {JestJunitParser} from '../src/parsers/jest-junit/jest-junit-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('jest-junit tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'jest-junit.xml') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new JestJunitParser(opts) 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('report from ./reports/jest test results matches snapshot', async () => { 27 | const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit.xml') 28 | const outputPath = path.join(__dirname, '__outputs__', 'jest-junit.md') 29 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 30 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 31 | 32 | const opts: ParseOptions = { 33 | parseErrors: true, 34 | trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'] 35 | //workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/' 36 | } 37 | 38 | const parser = new JestJunitParser(opts) 39 | const result = await parser.parse(filePath, fileContent) 40 | expect(result).toMatchSnapshot() 41 | 42 | const report = getReport([result]) 43 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 44 | fs.writeFileSync(outputPath, report) 45 | }) 46 | 47 | it('report from facebook/jest test results matches snapshot', async () => { 48 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'jest', 'jest-test-results.xml') 49 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'jest', 'files.txt') 50 | const outputPath = path.join(__dirname, '__outputs__', 'jest-test-results.md') 51 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 52 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 53 | 54 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 55 | const opts: ParseOptions = { 56 | parseErrors: true, 57 | trackedFiles 58 | //workDir: '/home/dorny/dorny/jest/' 59 | } 60 | 61 | const parser = new JestJunitParser(opts) 62 | const result = await parser.parse(filePath, fileContent) 63 | expect(result).toMatchSnapshot() 64 | 65 | const report = getReport([result]) 66 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 67 | fs.writeFileSync(outputPath, report) 68 | }) 69 | 70 | it('parsing ESLint report without timing information works', async () => { 71 | const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit-eslint.xml') 72 | const outputPath = path.join(__dirname, '__outputs__', 'jest-junit-eslint.md') 73 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 74 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 75 | 76 | const opts: ParseOptions = { 77 | parseErrors: true, 78 | trackedFiles: ['test.js'] 79 | } 80 | 81 | const parser = new JestJunitParser(opts) 82 | const result = await parser.parse(filePath, fileContent) 83 | expect(result).toMatchSnapshot() 84 | 85 | const report = getReport([result]) 86 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 87 | fs.writeFileSync(outputPath, report) 88 | }) 89 | 90 | it('jest testsuite errors example test results matches snapshot', async () => { 91 | const fixturePath = path.join(__dirname, 'fixtures', 'test-errors', 'jest', 'jest-test-results.xml') 92 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'test-errors', 'jest', 'files.txt') 93 | const outputPath = path.join(__dirname, '__outputs__', 'jest-test-errors-results.md') 94 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 95 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 96 | 97 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 98 | const opts: ParseOptions = { 99 | parseErrors: true, 100 | trackedFiles 101 | //workDir: '/home/dorny/dorny/jest/' 102 | } 103 | 104 | const parser = new JestJunitParser(opts) 105 | const result = await parser.parse(filePath, fileContent) 106 | expect(result).toMatchSnapshot() 107 | 108 | const report = getReport([result]) 109 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 110 | fs.writeFileSync(outputPath, report) 111 | }) 112 | 113 | it('playwright testsuite errors example test results matches snapshot', async () => { 114 | const fixturePath = path.join(__dirname, 'fixtures', 'test-errors', 'playwright', 'test-results.xml') 115 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'test-errors', 'playwright', 'files.txt') 116 | const outputPath = path.join(__dirname, '__outputs__', 'playwright-test-errors-results.md') 117 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 118 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 119 | 120 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 121 | const opts: ParseOptions = { 122 | parseErrors: true, 123 | trackedFiles 124 | } 125 | 126 | const parser = new JestJunitParser(opts) 127 | const result = await parser.parse(filePath, fileContent) 128 | expect(result).toMatchSnapshot() 129 | 130 | const report = getReport([result]) 131 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 132 | fs.writeFileSync(outputPath, report) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /__tests__/mocha-json.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {MochaJsonParser} from '../src/parsers/mocha-json/mocha-json-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('mocha-json tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'mocha-json.json') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new MochaJsonParser(opts) 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('report from ./reports/mocha-json test results matches snapshot', async () => { 27 | const fixturePath = path.join(__dirname, 'fixtures', 'mocha-json.json') 28 | const outputPath = path.join(__dirname, '__outputs__', 'mocha-json.md') 29 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 30 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 31 | 32 | const opts: ParseOptions = { 33 | parseErrors: true, 34 | trackedFiles: ['test/main.test.js', 'test/second.test.js', 'lib/main.js'] 35 | } 36 | 37 | const parser = new MochaJsonParser(opts) 38 | const result = await parser.parse(filePath, fileContent) 39 | expect(result).toMatchSnapshot() 40 | 41 | const report = getReport([result]) 42 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 43 | fs.writeFileSync(outputPath, report) 44 | }) 45 | 46 | it('report from mochajs/mocha test results matches snapshot', async () => { 47 | const fixturePath = path.join(__dirname, 'fixtures', 'external', 'mocha', 'mocha-test-results.json') 48 | const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'mocha', 'files.txt') 49 | const outputPath = path.join(__dirname, '__outputs__', 'mocha-test-results.md') 50 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 51 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 52 | 53 | const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 54 | const opts: ParseOptions = { 55 | parseErrors: true, 56 | trackedFiles 57 | } 58 | 59 | const parser = new MochaJsonParser(opts) 60 | const result = await parser.parse(filePath, fileContent) 61 | expect(result).toMatchSnapshot() 62 | 63 | const report = getReport([result]) 64 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 65 | fs.writeFileSync(outputPath, report) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /__tests__/mochawesome-json.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | import {MochawesomeJsonParser} from '../src/parsers/mochawesome-json/mochawesome-json-parser' 5 | import {ParseOptions} from '../src/test-parser' 6 | import {getReport} from '../src/report/get-report' 7 | import {normalizeFilePath} from '../src/utils/path-utils' 8 | 9 | describe('mochawesome-json tests', () => { 10 | it('produces empty test run result when there are no test cases', async () => { 11 | const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'mochawesome-json.json') 12 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 13 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 14 | 15 | const opts: ParseOptions = { 16 | parseErrors: true, 17 | trackedFiles: [] 18 | } 19 | 20 | const parser = new MochawesomeJsonParser(opts) 21 | const result = await parser.parse(filePath, fileContent) 22 | expect(result.tests).toBe(0) 23 | expect(result.result).toBe('success') 24 | }) 25 | 26 | it('report from ./reports/mochawesome-json test results matches snapshot', async () => { 27 | const fixturePath = path.join(__dirname, 'fixtures', 'mochawesome-json.json') 28 | const outputPath = path.join(__dirname, '__outputs__', 'mochawesome-json.md') 29 | const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 30 | const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 31 | 32 | const opts: ParseOptions = { 33 | parseErrors: true, 34 | trackedFiles: ['test/main.test.js', 'test/second.test.js', 'lib/main.js'] 35 | } 36 | 37 | const parser = new MochawesomeJsonParser(opts) 38 | const result = await parser.parse(filePath, fileContent) 39 | expect(result).toMatchSnapshot() 40 | 41 | const report = getReport([result]) 42 | fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 43 | fs.writeFileSync(outputPath, report) 44 | }) 45 | 46 | // TODO - External 47 | // it('report from mochawesomejs/mochawesome test results matches snapshot', async () => { 48 | // const fixturePath = path.join(__dirname, 'fixtures', 'external', 'mochawesome', 'mochawesome-test-results.json') 49 | // const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'mochawesome', 'files.txt') 50 | // const outputPath = path.join(__dirname, '__outputs__', 'mochawesome-test-results.md') 51 | // const filePath = normalizeFilePath(path.relative(__dirname, fixturePath)) 52 | // const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'}) 53 | 54 | // const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g) 55 | // const opts: ParseOptions = { 56 | // parseErrors: true, 57 | // trackedFiles 58 | // } 59 | 60 | // const parser = new MochawesomeJsonParser(opts) 61 | // const result = await parser.parse(filePath, fileContent) 62 | // expect(result).toMatchSnapshot() 63 | 64 | // const report = getReport([result]) 65 | // fs.mkdirSync(path.dirname(outputPath), {recursive: true}) 66 | // fs.writeFileSync(outputPath, report) 67 | // }) 68 | }) 69 | -------------------------------------------------------------------------------- /__tests__/utils/parse-utils.test.ts: -------------------------------------------------------------------------------- 1 | import {parseNetDuration} from '../../src/utils/parse-utils' 2 | 3 | describe('parseNetDuration', () => { 4 | it('returns 0 for 00:00:00', () => { 5 | const ms = parseNetDuration('00:00:00') 6 | expect(ms).toBe(0) 7 | }) 8 | 9 | it('returns 0 for 00:00:00.0000000', () => { 10 | const ms = parseNetDuration('00:00:00.0000000') 11 | expect(ms).toBe(0) 12 | }) 13 | 14 | it('returns 123 for 00:00:00.123', () => { 15 | const ms = parseNetDuration('00:00:00.123') 16 | expect(ms).toBe(123) 17 | }) 18 | 19 | it('returns 12 * 1000 for 00:00:12', () => { 20 | const ms = parseNetDuration('00:00:12') 21 | expect(ms).toBe(12 * 1000) 22 | }) 23 | 24 | it('returns 12 * 60 * 1000 for 00:12:00', () => { 25 | const ms = parseNetDuration('00:12:00') 26 | expect(ms).toBe(12 * 60 * 1000) 27 | }) 28 | 29 | it('returns 12 * 60 * 60 * 1000 for 12:00:00', () => { 30 | const ms = parseNetDuration('12:00:00') 31 | expect(ms).toBe(12 * 60 * 60 * 1000) 32 | }) 33 | 34 | it('throws when string has invalid format', () => { 35 | expect(() => parseNetDuration('12:34:56 not a duration')).toThrowError(/^Invalid format/) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Test Reporting 2 | description: | 3 | Shows test results in GitHub UI: .NET (xUnit, NUnit, MSTest), Dart, Flutter, Java (JUnit), JavaScript (JEST, Mocha) 4 | author: Ian Moroney 5 | inputs: 6 | artifact: 7 | description: Name or regex of artifact containing test results 8 | required: false 9 | name: 10 | description: Name of the check run 11 | required: true 12 | path: 13 | description: | 14 | Coma separated list of paths to test results 15 | Supports wildcards via [fast-glob](https://github.com/mrmlnc/fast-glob) 16 | All matched result files must be of same format 17 | required: true 18 | path-replace-backslashes: 19 | description: | 20 | The fast-glob library that is internally used interprets backslashes as escape characters. 21 | If enabled, all backslashes in provided path will be replaced by forward slashes and act as directory separators. 22 | It might be useful when path input variable is composed dynamically from existing directory paths on Windows. 23 | default: 'false' 24 | required: false 25 | reporter: 26 | description: | 27 | Format of test results. Supported options: 28 | - dart-json 29 | - dotnet-trx 30 | - flutter-json 31 | - java-junit 32 | - jest-junit 33 | - mocha-json 34 | - mochawesome-json 35 | required: true 36 | list-suites: 37 | description: | 38 | Limits which test suites are listed. Supported options: 39 | - all 40 | - failed 41 | required: true 42 | default: 'all' 43 | list-tests: 44 | description: | 45 | Limits which test cases are listed. Supported options: 46 | - all 47 | - failed 48 | - none 49 | required: true 50 | default: 'all' 51 | max-annotations: 52 | description: | 53 | Limits number of created annotations with error message and stack trace captured during test execution. 54 | Must be less or equal to 50. 55 | required: true 56 | default: '10' 57 | fail-on-error: 58 | description: Set this action as failed if test report contain any failed test 59 | required: true 60 | default: 'true' 61 | working-directory: 62 | description: Relative path under $GITHUB_WORKSPACE where the repository was checked out 63 | required: false 64 | only-summary: 65 | description: | 66 | Allows you to generate only the summary. 67 | If enabled, the report will contain a table listing each test results file and the number of passed, failed, and skipped tests. 68 | Detailed listing of test suites and test cases will be skipped. 69 | default: 'false' 70 | required: false 71 | output-to: 72 | description: | 73 | The location to write the report to. Supported options: 74 | - checks 75 | - step-summary 76 | default: 'checks' 77 | required: false 78 | token: 79 | description: GitHub Access Token 80 | required: false 81 | default: ${{ github.token }} 82 | outputs: 83 | conclusion: 84 | description: | 85 | Final conclusion of the created check run: 86 | - 'success' if no failed tests was found 87 | - 'failure' if any failed test was found 88 | passed: 89 | description: Count of passed tests 90 | failed: 91 | description: Count of failed tests 92 | skipped: 93 | description: Count of skipped tests 94 | time: 95 | description: Test execution time [ms] 96 | runHtmlUrl: 97 | description: Exposes the URL for the HTML version of the run report 98 | runs: 99 | using: 'node20' 100 | main: 'dist/index.js' 101 | branding: 102 | color: blue 103 | icon: file-text 104 | -------------------------------------------------------------------------------- /assets/fluent-validation-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/assets/fluent-validation-report.png -------------------------------------------------------------------------------- /assets/mocha-groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/assets/mocha-groups.png -------------------------------------------------------------------------------- /assets/provider-error-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/assets/provider-error-details.png -------------------------------------------------------------------------------- /assets/provider-error-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-actions/test-reporting/f957cd93fc2d848d556fa0d03c57bc79127b6b5e/assets/provider-error-summary.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-check", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Presents test results from popular testing frameworks as Github check run", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "package": "ncc build --source-map --license licenses.txt", 13 | "test": "jest --ci --reporters=default --reporters=jest-junit", 14 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test", 15 | "dart-fixture": "cd \"reports/dart\" && dart test --file-reporter=\"json:../../__tests__/fixtures/dart-json.json\"", 16 | "dotnet-fixture": "dotnet test reports/dotnet/DotnetTests.XUnitTests --logger \"trx;LogFileName=../../../../__tests__/fixtures/dotnet-trx.trx\"", 17 | "jest-fixture": "cd \"reports/jest\" && npm test", 18 | "mocha-fixture": "cd \"reports/mocha\" && npm test", 19 | "mochawesome-fixture": "cd \"reports/mochawesome\" && npm test" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/dorny/test-check.git" 24 | }, 25 | "keywords": [ 26 | "actions", 27 | "node", 28 | "test", 29 | "report" 30 | ], 31 | "author": "Michal Dorner ", 32 | "license": "MIT", 33 | "dependencies": { 34 | "@actions/core": "^1.10.1", 35 | "@actions/exec": "^1.1.1", 36 | "@actions/github": "^6.0.0", 37 | "adm-zip": "^0.5.3", 38 | "fast-glob": "^3.2.5", 39 | "got": "^11.8.2", 40 | "picomatch": "^2.2.2", 41 | "xml2js": "^0.4.23" 42 | }, 43 | "devDependencies": { 44 | "@octokit/types": "^12.4.0", 45 | "@octokit/webhooks": "^12.0.11", 46 | "@octokit/webhooks-types": "^7.3.1", 47 | "@types/adm-zip": "^0.5.5", 48 | "@types/github-slugger": "^1.3.0", 49 | "@types/jest": "^29.5.12", 50 | "@types/node": "^18.19.28", 51 | "@types/picomatch": "^2.3.3", 52 | "@types/xml2js": "^0.4.14", 53 | "@typescript-eslint/eslint-plugin": "^7.8.0", 54 | "@typescript-eslint/parser": "^7.8.0", 55 | "@vercel/ncc": "^0.38.1", 56 | "eol-converter-cli": "^1.0.8", 57 | "eslint": "^8.56.0", 58 | "eslint-import-resolver-typescript": "^3.6.1", 59 | "eslint-plugin-github": "^4.10.2", 60 | "eslint-plugin-import": "^2.29.1", 61 | "eslint-plugin-jest": "^27.9.0", 62 | "eslint-plugin-prettier": "^5.1.3", 63 | "jest": "^29.7.0", 64 | "jest-circus": "^29.7.0", 65 | "jest-junit": "^16.0.0", 66 | "js-yaml": "^4.1.0", 67 | "prettier": "3.2.5", 68 | "ts-jest": "^29.1.2", 69 | "typescript": "^5.4.3" 70 | }, 71 | "jest-junit": { 72 | "suiteName": "jest tests", 73 | "outputDirectory": "__tests__/__results__", 74 | "outputName": "jest-junit.xml", 75 | "ancestorSeparator": " › ", 76 | "uniqueOutputName": "false", 77 | "suiteNameTemplate": "{filepath}", 78 | "classNameTemplate": "{classname}", 79 | "titleTemplate": "{title}" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /reports/dart/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | doc/api/ 10 | -------------------------------------------------------------------------------- /reports/dart/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /reports/dart/lib/main.dart: -------------------------------------------------------------------------------- 1 | void throwError() { 2 | throw Exception('Some error'); 3 | } 4 | -------------------------------------------------------------------------------- /reports/dart/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: darttest 2 | description: A simple command-line application. 3 | 4 | environment: 5 | sdk: '>=2.12.0 <3.0.0' 6 | 7 | dev_dependencies: 8 | pedantic: ^1.9.0 9 | test: ^1.15.4 10 | -------------------------------------------------------------------------------- /reports/dart/test/main_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:darttest/main.dart'; 2 | import 'package:test/test.dart'; 3 | import 'dart:io'; 4 | 5 | void main() { 6 | group('Test 1', () { 7 | test('Passing test', () { 8 | expect(1, equals(1)); 9 | }); 10 | 11 | group('Test 1.1', () { 12 | test('Failing test', () { 13 | expect(1, equals(2)); 14 | }); 15 | 16 | test('Exception in target unit', () { 17 | throwError(); 18 | }); 19 | }); 20 | }); 21 | 22 | group('Test 2', () { 23 | test('Exception in test', () { 24 | throw Exception('Some error'); 25 | }); 26 | }); 27 | 28 | print('Hello from the test'); 29 | } 30 | -------------------------------------------------------------------------------- /reports/dart/test/second_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Timeout test', () async { 6 | await Future.delayed(const Duration(seconds: 1)); 7 | }, timeout: Timeout(Duration(microseconds: 1))); 8 | 9 | test('Skipped test', () { 10 | // do nothing 11 | }, skip: 'skipped test'); 12 | } 13 | -------------------------------------------------------------------------------- /reports/dotnet/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | -------------------------------------------------------------------------------- /reports/dotnet/DotnetTests.Unit/Calculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetTests.Unit 4 | { 5 | public class Calculator 6 | { 7 | public int Sum(int a, int b) => a + b; 8 | 9 | public int Div(int a, int b) => a / b; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /reports/dotnet/DotnetTests.Unit/DotnetTests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /reports/dotnet/DotnetTests.XUnitTests/CalculatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using DotnetTests.Unit; 4 | using Xunit; 5 | 6 | namespace DotnetTests.XUnitTests 7 | { 8 | public class CalculatorTests 9 | { 10 | private readonly Calculator _calculator = new Calculator(); 11 | 12 | [Fact] 13 | public void Passing_Test() 14 | { 15 | Assert.Equal(2, _calculator.Sum(1,1)); 16 | } 17 | 18 | [Fact(DisplayName = "Custom Name")] 19 | public void Passing_Test_With_Name() 20 | { 21 | Assert.Equal(2, _calculator.Sum(1, 1)); 22 | } 23 | 24 | [Fact] 25 | public void Failing_Test() 26 | { 27 | Assert.Equal(3, _calculator.Sum(1, 1)); 28 | } 29 | 30 | [Fact] 31 | public void Exception_In_TargetTest() 32 | { 33 | _calculator.Div(1, 0); 34 | } 35 | 36 | [Fact] 37 | public void Exception_In_Test() 38 | { 39 | throw new Exception("Test"); 40 | } 41 | 42 | [Fact(Timeout = 1)] 43 | public void Timeout_Test() 44 | { 45 | Thread.Sleep(100); 46 | } 47 | 48 | [Fact(Skip = "Skipped test")] 49 | public void Skipped_Test() 50 | { 51 | throw new Exception("Test"); 52 | } 53 | 54 | [Theory] 55 | [InlineData(2)] 56 | [InlineData(3)] 57 | public void Is_Even_Number(int i) 58 | { 59 | Assert.True(i % 2 == 0); 60 | } 61 | 62 | [Theory(DisplayName = "Should be even number")] 63 | [InlineData(2)] 64 | [InlineData(3)] 65 | public void Theory_With_Custom_Name(int i) 66 | { 67 | Assert.True(i % 2 == 0); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /reports/dotnet/DotnetTests.XUnitTests/DotnetTests.XUnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /reports/dotnet/DotnetTests.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30320.27 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetTests.Unit", "DotnetTests.Unit\DotnetTests.Unit.csproj", "{A47249E3-01A3-4E0A-9601-A3B2F7437C46}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BCAC3B31-ADB1-4221-9D5B-182EE868648C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetTests.XUnitTests", "DotnetTests.XUnitTests\DotnetTests.XUnitTests.csproj", "{F8607EDB-D25D-47AA-8132-38ACA242E845}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {A47249E3-01A3-4E0A-9601-A3B2F7437C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {A47249E3-01A3-4E0A-9601-A3B2F7437C46}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {A47249E3-01A3-4E0A-9601-A3B2F7437C46}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {A47249E3-01A3-4E0A-9601-A3B2F7437C46}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {F8607EDB-D25D-47AA-8132-38ACA242E845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F8607EDB-D25D-47AA-8132-38ACA242E845}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F8607EDB-D25D-47AA-8132-38ACA242E845}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F8607EDB-D25D-47AA-8132-38ACA242E845}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {F8607EDB-D25D-47AA-8132-38ACA242E845} = {BCAC3B31-ADB1-4221-9D5B-182EE868648C} 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {6ED5543C-74AA-4B21-8050-943550F3F66E} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /reports/jest/__tests__/main.test.js: -------------------------------------------------------------------------------- 1 | const lib = require('../lib/main') 2 | 3 | describe('Test 1', () => { 4 | test('Passing test', () => { 5 | expect(true).toBeTruthy() 6 | }); 7 | 8 | describe('Test 1.1', () => { 9 | test('Failing test', () => { 10 | expect(false).toBeTruthy() 11 | }); 12 | 13 | test('Exception in target unit', () => { 14 | lib.throwError(); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('Test 2', () => { 20 | test('Exception in test', () => { 21 | throw new Error('Some error'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /reports/jest/__tests__/second.test.js: -------------------------------------------------------------------------------- 1 | test('Timeout test', async () => { 2 | await new Promise(resolve => setTimeout(resolve, 1000)); 3 | }, 1); 4 | 5 | test.skip('Skipped test', () => { 6 | // do nothing 7 | }); 8 | -------------------------------------------------------------------------------- /reports/jest/lib/main.js: -------------------------------------------------------------------------------- 1 | function throwError() { 2 | throw new Error('Some error') 3 | } 4 | 5 | exports.throwError = throwError 6 | -------------------------------------------------------------------------------- /reports/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-fixture", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Generates test fixtures for test-check action", 6 | "scripts": { 7 | "test": "jest --ci --reporters=default --reporters=jest-junit" 8 | }, 9 | "author": "Michal Dorner ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "jest": "^26.5.3", 13 | "jest-junit": "^12.0.0" 14 | }, 15 | "jest-junit": { 16 | "suiteName": "jest tests", 17 | "outputDirectory": "../../__tests__/fixtures", 18 | "outputName": "jest-junit.xml", 19 | "ancestorSeparator": " › ", 20 | "uniqueOutputName": "false", 21 | "suiteNameTemplate": "{filepath}", 22 | "classNameTemplate": "{classname}", 23 | "titleTemplate": "{title}" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /reports/mocha/lib/main.js: -------------------------------------------------------------------------------- 1 | function throwError() { 2 | throw new Error('Some error') 3 | } 4 | 5 | exports.throwError = throwError 6 | -------------------------------------------------------------------------------- /reports/mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-fixture", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Generates test fixtures for test-reporting action", 6 | "scripts": { 7 | "test": "mocha --reporter json > ../../__tests__/fixtures/mocha-json.json" 8 | }, 9 | "author": "Michal Dorner ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "mocha": "^8.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /reports/mocha/test/main.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const lib = require('../lib/main') 3 | 4 | describe('Test 1', () => { 5 | it('Passing test', () => { 6 | assert.equal(true, true) 7 | }); 8 | 9 | describe('Test 1.1', () => { 10 | it('Failing test', () => { 11 | assert.equal(false, true) 12 | }); 13 | 14 | it('Exception in target unit', () => { 15 | lib.throwError(); 16 | }); 17 | }); 18 | }); 19 | 20 | describe('Test 2', () => { 21 | it('Exception in test', () => { 22 | throw new Error('Some error'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /reports/mocha/test/second.test.js: -------------------------------------------------------------------------------- 1 | it('Timeout test', async function(done) { 2 | this.timeout(1); 3 | setTimeout(done, 1000); 4 | }); 5 | 6 | it.skip('Skipped test', () => { 7 | // do nothing 8 | }); 9 | -------------------------------------------------------------------------------- /reports/mochawesome/lib/main.js: -------------------------------------------------------------------------------- 1 | function throwError() { 2 | throw new Error('Some error') 3 | } 4 | 5 | exports.throwError = throwError 6 | -------------------------------------------------------------------------------- /reports/mochawesome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mochawesome-fixture", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Generates test fixtures for test-reporting action", 6 | "scripts": { 7 | "test": "mocha --reporter mochawesome --reporter-options reportDir=../../__tests__/fixtures/,reportFilename=mochawesome-json.json" 8 | }, 9 | "author": "Ian Moroney ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "cypress": "^9.5.0", 13 | "mochawesome": "^7.0.1", 14 | "mocha": "^9.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /reports/mochawesome/test/main.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const lib = require('../lib/main') 3 | 4 | describe('Test 1', () => { 5 | it('Passing test', () => { 6 | assert.equal(true, true) 7 | }); 8 | 9 | describe('Test 1.1', () => { 10 | it('Failing test', () => { 11 | assert.equal(false, true) 12 | }); 13 | 14 | it('Exception in target unit', () => { 15 | lib.throwError(); 16 | }); 17 | }); 18 | }); 19 | 20 | describe('Test 2', () => { 21 | it('Exception in test', () => { 22 | throw new Error('Some error'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /reports/mochawesome/test/second.test.js: -------------------------------------------------------------------------------- 1 | it('Timeout test', async function(done) { 2 | this.timeout(1); 3 | setTimeout(done, 1000); 4 | }); 5 | 6 | it.skip('Skipped test', () => { 7 | // do nothing 8 | }); 9 | -------------------------------------------------------------------------------- /src/input-providers/artifact-provider.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import {GitHub} from '@actions/github/lib/utils' 4 | 5 | import Zip from 'adm-zip' 6 | import picomatch from 'picomatch' 7 | 8 | import {FileContent, InputProvider, ReportInput} from './input-provider' 9 | import {downloadArtifact, listFiles} from '../utils/github-utils' 10 | 11 | export class ArtifactProvider implements InputProvider { 12 | private readonly artifactNameMatch: (name: string) => boolean 13 | private readonly fileNameMatch: (name: string) => boolean 14 | private readonly getReportName: (name: string) => string 15 | 16 | constructor( 17 | readonly octokit: InstanceType, 18 | readonly artifact: string, 19 | readonly name: string, 20 | readonly pattern: string[], 21 | readonly sha: string, 22 | readonly runId: number, 23 | readonly token: string 24 | ) { 25 | if (this.artifact.startsWith('/')) { 26 | const endIndex = this.artifact.lastIndexOf('/') 27 | const rePattern = this.artifact.substring(1, endIndex) 28 | const reOpts = this.artifact.substring(endIndex + 1) 29 | const re = new RegExp(rePattern, reOpts) 30 | this.artifactNameMatch = (str: string) => re.test(str) 31 | this.getReportName = (str: string) => { 32 | const match = str.match(re) 33 | if (match === null) { 34 | throw new Error(`Artifact name '${str}' does not match regex ${this.artifact}`) 35 | } 36 | let reportName = this.name 37 | for (let i = 1; i < match.length; i++) { 38 | reportName = reportName.replace(new RegExp(`\\$${i}`, 'g'), match[i]) 39 | } 40 | return reportName 41 | } 42 | } else { 43 | this.artifactNameMatch = (str: string) => str === this.artifact 44 | this.getReportName = () => this.name 45 | } 46 | 47 | this.fileNameMatch = picomatch(pattern) 48 | } 49 | 50 | async load(): Promise { 51 | const result: ReportInput = {} 52 | 53 | const resp = await this.octokit.rest.actions.listWorkflowRunArtifacts({ 54 | ...github.context.repo, 55 | run_id: this.runId 56 | }) 57 | 58 | if (resp.data.artifacts.length === 0) { 59 | core.warning(`No artifacts found in run ${this.runId}`) 60 | return {} 61 | } 62 | 63 | const artifacts = resp.data.artifacts.filter(a => this.artifactNameMatch(a.name)) 64 | if (artifacts.length === 0) { 65 | core.warning(`No artifact matches ${this.artifact}`) 66 | return {} 67 | } 68 | 69 | for (const art of artifacts) { 70 | const fileName = `${art.name}.zip` 71 | await downloadArtifact(this.octokit, art.id, fileName, this.token) 72 | core.startGroup(`Reading archive ${fileName}`) 73 | try { 74 | const reportName = this.getReportName(art.name) 75 | core.info(`Report name: ${reportName}`) 76 | const files: FileContent[] = [] 77 | const zip = new Zip(fileName) 78 | for (const entry of zip.getEntries()) { 79 | const file = entry.entryName 80 | if (entry.isDirectory) { 81 | core.info(`Skipping ${file}: entry is a directory`) 82 | continue 83 | } 84 | if (!this.fileNameMatch(file)) { 85 | core.info(`Skipping ${file}: filename does not match pattern`) 86 | continue 87 | } 88 | const content = zip.readAsText(entry) 89 | files.push({file, content}) 90 | core.info(`Read ${file}: ${content.length} chars`) 91 | } 92 | if (result[reportName]) { 93 | result[reportName].push(...files) 94 | } else { 95 | result[reportName] = files 96 | } 97 | } finally { 98 | core.endGroup() 99 | } 100 | } 101 | 102 | return result 103 | } 104 | 105 | async listTrackedFiles(): Promise { 106 | return listFiles(this.octokit, this.sha) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/input-providers/input-provider.ts: -------------------------------------------------------------------------------- 1 | export interface ReportInput { 2 | [reportName: string]: FileContent[] 3 | } 4 | 5 | export interface FileContent { 6 | file: string 7 | content: string 8 | } 9 | 10 | export interface InputProvider { 11 | load(): Promise 12 | listTrackedFiles(): Promise 13 | } 14 | -------------------------------------------------------------------------------- /src/input-providers/local-file-provider.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import glob from 'fast-glob' 3 | import {FileContent, InputProvider, ReportInput} from './input-provider' 4 | import {listFiles} from '../utils/git' 5 | 6 | export class LocalFileProvider implements InputProvider { 7 | constructor( 8 | readonly name: string, 9 | readonly pattern: string[] 10 | ) {} 11 | 12 | async load(): Promise { 13 | const result: FileContent[] = [] 14 | for (const pat of this.pattern) { 15 | const paths = await glob(pat, {dot: true}) 16 | for (const file of paths) { 17 | const content = await fs.promises.readFile(file, {encoding: 'utf8'}) 18 | result.push({file, content}) 19 | } 20 | } 21 | 22 | return {[this.name]: result} 23 | } 24 | 25 | async listTrackedFiles(): Promise { 26 | return listFiles() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/parsers/dart-json/dart-json-parser.ts: -------------------------------------------------------------------------------- 1 | import {ParseOptions, TestParser} from '../../test-parser' 2 | 3 | import {getBasePath, normalizeFilePath} from '../../utils/path-utils' 4 | 5 | import { 6 | ReportEvent, 7 | Suite, 8 | Group, 9 | TestStartEvent, 10 | TestDoneEvent, 11 | ErrorEvent, 12 | isSuiteEvent, 13 | isGroupEvent, 14 | isTestStartEvent, 15 | isTestDoneEvent, 16 | isErrorEvent, 17 | isDoneEvent, 18 | isMessageEvent, 19 | MessageEvent 20 | } from './dart-json-types' 21 | 22 | import { 23 | TestExecutionResult, 24 | TestRunResult, 25 | TestSuiteResult, 26 | TestGroupResult, 27 | TestCaseResult, 28 | TestCaseError 29 | } from '../../test-results' 30 | 31 | class TestRun { 32 | constructor( 33 | readonly path: string, 34 | readonly suites: TestSuite[], 35 | readonly success: boolean, 36 | readonly time: number 37 | ) {} 38 | } 39 | 40 | class TestSuite { 41 | constructor(readonly suite: Suite) {} 42 | readonly groups: {[id: number]: TestGroup} = {} 43 | } 44 | 45 | class TestGroup { 46 | constructor(readonly group: Group) {} 47 | readonly tests: TestCase[] = [] 48 | } 49 | 50 | class TestCase { 51 | constructor(readonly testStart: TestStartEvent) { 52 | this.groupId = testStart.test.groupIDs[testStart.test.groupIDs.length - 1] 53 | } 54 | readonly groupId: number 55 | readonly print: MessageEvent[] = [] 56 | testDone?: TestDoneEvent 57 | error?: ErrorEvent 58 | 59 | get result(): TestExecutionResult { 60 | if (this.testDone?.skipped) { 61 | return 'skipped' 62 | } 63 | if (this.testDone?.result === 'success') { 64 | return 'success' 65 | } 66 | 67 | if (this.testDone?.result === 'error' || this.testDone?.result === 'failure') { 68 | return 'failed' 69 | } 70 | 71 | return undefined 72 | } 73 | 74 | get time(): number { 75 | return this.testDone !== undefined ? this.testDone.time - this.testStart.time : 0 76 | } 77 | } 78 | 79 | export class DartJsonParser implements TestParser { 80 | assumedWorkDir: string | undefined 81 | 82 | constructor( 83 | readonly options: ParseOptions, 84 | readonly sdk: 'dart' | 'flutter' 85 | ) {} 86 | 87 | async parse(path: string, content: string): Promise { 88 | const tr = this.getTestRun(path, content) 89 | const result = this.getTestRunResult(tr) 90 | return Promise.resolve(result) 91 | } 92 | 93 | private getTestRun(path: string, content: string): TestRun { 94 | const lines = content.split(/\n\r?/g) 95 | const events = lines 96 | .map((str, i) => { 97 | if (str.trim() === '') { 98 | return null 99 | } 100 | try { 101 | return JSON.parse(str) 102 | } catch (e: any) { 103 | const col = e.columnNumber !== undefined ? `:${e.columnNumber}` : '' 104 | throw new Error(`Invalid JSON at ${path}:${i + 1}${col}\n\n${e}`) 105 | } 106 | }) 107 | .filter(evt => evt != null) as ReportEvent[] 108 | 109 | let success = false 110 | let totalTime = 0 111 | const suites: {[id: number]: TestSuite} = {} 112 | const tests: {[id: number]: TestCase} = {} 113 | 114 | for (const evt of events) { 115 | if (isSuiteEvent(evt)) { 116 | suites[evt.suite.id] = new TestSuite(evt.suite) 117 | } else if (isGroupEvent(evt)) { 118 | suites[evt.group.suiteID].groups[evt.group.id] = new TestGroup(evt.group) 119 | } else if (isTestStartEvent(evt) && evt.test.url !== null) { 120 | const test: TestCase = new TestCase(evt) 121 | const suite = suites[evt.test.suiteID] 122 | const group = suite.groups[evt.test.groupIDs[evt.test.groupIDs.length - 1]] 123 | group.tests.push(test) 124 | tests[evt.test.id] = test 125 | } else if (isTestDoneEvent(evt) && tests[evt.testID]) { 126 | tests[evt.testID].testDone = evt 127 | } else if (isErrorEvent(evt) && tests[evt.testID]) { 128 | tests[evt.testID].error = evt 129 | } else if (isMessageEvent(evt) && tests[evt.testID]) { 130 | tests[evt.testID].print.push(evt) 131 | } else if (isDoneEvent(evt)) { 132 | success = evt.success 133 | totalTime = evt.time 134 | } 135 | } 136 | 137 | return new TestRun(path, Object.values(suites), success, totalTime) 138 | } 139 | 140 | private getTestRunResult(tr: TestRun): TestRunResult { 141 | const suites = tr.suites.map(s => { 142 | return new TestSuiteResult(this.getRelativePath(s.suite.path), this.getGroups(s)) 143 | }) 144 | 145 | return new TestRunResult(tr.path, suites, tr.time) 146 | } 147 | 148 | private getGroups(suite: TestSuite): TestGroupResult[] { 149 | const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0) 150 | groups.sort((a, b) => (a.group.line ?? 0) - (b.group.line ?? 0)) 151 | 152 | return groups.map(group => { 153 | group.tests.sort((a, b) => (a.testStart.test.line ?? 0) - (b.testStart.test.line ?? 0)) 154 | const tests = group.tests 155 | .filter(tc => !tc.testDone?.hidden) 156 | .map(tc => { 157 | const error = this.getError(suite, tc) 158 | const testName = 159 | group.group.name !== undefined && tc.testStart.test.name.startsWith(group.group.name) 160 | ? tc.testStart.test.name.slice(group.group.name.length).trim() 161 | : tc.testStart.test.name.trim() 162 | return new TestCaseResult(testName, tc.result, tc.time, error) 163 | }) 164 | return new TestGroupResult(group.group.name, tests) 165 | }) 166 | } 167 | 168 | private getError(testSuite: TestSuite, test: TestCase): TestCaseError | undefined { 169 | if (!this.options.parseErrors || !test.error) { 170 | return undefined 171 | } 172 | 173 | const {trackedFiles} = this.options 174 | const stackTrace = test.error?.stackTrace ?? '' 175 | const print = test.print 176 | .filter(p => p.messageType === 'print') 177 | .map(p => p.message) 178 | .join('\n') 179 | const details = [print, stackTrace].filter(str => str !== '').join('\n') 180 | const src = this.exceptionThrowSource(details, trackedFiles) 181 | const message = this.getErrorMessage(test.error?.error ?? '', print) 182 | let path 183 | let line 184 | 185 | if (src !== undefined) { 186 | path = src.path 187 | line = src.line 188 | } else { 189 | const testStartPath = this.getRelativePath(testSuite.suite.path) 190 | if (trackedFiles.includes(testStartPath)) { 191 | path = testStartPath 192 | line = test.testStart.test.root_line ?? test.testStart.test.line ?? undefined 193 | } 194 | } 195 | 196 | return { 197 | path, 198 | line, 199 | message, 200 | details 201 | } 202 | } 203 | 204 | private getErrorMessage(message: string, print: string): string { 205 | if (this.sdk === 'flutter') { 206 | const uselessMessageRe = /^Test failed\. See exception logs above\.\nThe test description was:/m 207 | const flutterPrintRe = 208 | /^══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═+\s+(.*)\s+When the exception was thrown, this was the stack:/ms 209 | if (uselessMessageRe.test(message)) { 210 | const match = print.match(flutterPrintRe) 211 | if (match !== null) { 212 | return match[1] 213 | } 214 | } 215 | } 216 | 217 | return message || print 218 | } 219 | 220 | private exceptionThrowSource(ex: string, trackedFiles: string[]): {path: string; line: number} | undefined { 221 | const lines = ex.split(/\r?\n/g) 222 | 223 | // regexp to extract file path and line number from stack trace 224 | const dartRe = /^(?!package:)(.*)\s+(\d+):\d+\s+/ 225 | const flutterRe = /^#\d+\s+.*\((?!package:)(.*):(\d+):\d+\)$/ 226 | const re = this.sdk === 'dart' ? dartRe : flutterRe 227 | 228 | for (const str of lines) { 229 | const match = str.match(re) 230 | if (match !== null) { 231 | const [_, pathStr, lineStr] = match 232 | const path = normalizeFilePath(this.getRelativePath(pathStr)) 233 | if (trackedFiles.includes(path)) { 234 | const line = parseInt(lineStr) 235 | return {path, line} 236 | } 237 | } 238 | } 239 | } 240 | 241 | private getRelativePath(path: string): string { 242 | const prefix = 'file://' 243 | if (path.startsWith(prefix)) { 244 | path = path.substr(prefix.length) 245 | } 246 | 247 | path = normalizeFilePath(path) 248 | const workDir = this.getWorkDir(path) 249 | if (workDir !== undefined && path.startsWith(workDir)) { 250 | path = path.substr(workDir.length) 251 | } 252 | return path 253 | } 254 | 255 | private getWorkDir(path: string): string | undefined { 256 | return ( 257 | this.options.workDir ?? 258 | this.assumedWorkDir ?? 259 | (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) 260 | ) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/parsers/dart-json/dart-json-types.ts: -------------------------------------------------------------------------------- 1 | /// reflects documentation at https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md 2 | 3 | export type ReportEvent = 4 | | StartEvent 5 | | AllSuitesEvent 6 | | SuiteEvent 7 | | DebugEvent 8 | | GroupEvent 9 | | TestStartEvent 10 | | TestDoneEvent 11 | | DoneEvent 12 | | MessageEvent 13 | | ErrorEvent 14 | 15 | export interface Event { 16 | type: 'start' | 'allSuites' | 'suite' | 'debug' | 'group' | 'testStart' | 'print' | 'error' | 'testDone' | 'done' 17 | time: number 18 | } 19 | 20 | export interface StartEvent extends Event { 21 | type: 'start' 22 | protocolVersion: string 23 | runnerVersion: string 24 | pid: number 25 | } 26 | 27 | export interface AllSuitesEvent extends Event { 28 | type: 'allSuites' 29 | count: number // The total number of suites that will be loaded. 30 | } 31 | 32 | export interface SuiteEvent extends Event { 33 | type: 'suite' 34 | suite: Suite 35 | } 36 | 37 | export interface GroupEvent extends Event { 38 | type: 'group' 39 | group: Group 40 | } 41 | 42 | export interface TestStartEvent extends Event { 43 | type: 'testStart' 44 | test: Test 45 | } 46 | 47 | export interface TestDoneEvent extends Event { 48 | type: 'testDone' 49 | testID: number 50 | result: 'success' | 'failure' | 'error' 51 | hidden: boolean 52 | skipped: boolean 53 | } 54 | 55 | export interface DoneEvent extends Event { 56 | type: 'done' 57 | success: boolean 58 | } 59 | 60 | export interface ErrorEvent extends Event { 61 | type: 'error' 62 | testID: number 63 | error: string 64 | stackTrace: string 65 | isFailure: boolean 66 | } 67 | 68 | export interface DebugEvent extends Event { 69 | type: 'debug' 70 | suiteID: number 71 | observatory: string 72 | remoteDebugger: string 73 | } 74 | 75 | export interface MessageEvent extends Event { 76 | type: 'print' 77 | testID: number 78 | messageType: 'print' | 'skip' 79 | message: string 80 | } 81 | 82 | export interface Suite { 83 | id: number 84 | platform?: string 85 | path: string 86 | } 87 | 88 | export interface Group { 89 | id: number 90 | name?: string 91 | suiteID: number 92 | parentID?: number 93 | testCount: number 94 | line: number | null // The (1-based) line on which the group was defined, or `null`. 95 | column: number | null // The (1-based) column on which the group was defined, or `null`. 96 | url: string | null 97 | } 98 | 99 | export interface Test { 100 | id: number 101 | name: string 102 | suiteID: number 103 | groupIDs: number[] // The IDs of groups containing this test, in order from outermost to innermost. 104 | line: number | null // The (1-based) line on which the test was defined, or `null`. 105 | column: number | null // The (1-based) column on which the test was defined, or `null`. 106 | url: string | null 107 | root_line?: number 108 | root_column?: number 109 | root_url: string | undefined 110 | } 111 | 112 | export function isSuiteEvent(event: Event): event is SuiteEvent { 113 | return event.type === 'suite' 114 | } 115 | export function isGroupEvent(event: Event): event is GroupEvent { 116 | return event.type === 'group' 117 | } 118 | export function isTestStartEvent(event: Event): event is TestStartEvent { 119 | return event.type === 'testStart' 120 | } 121 | export function isTestDoneEvent(event: Event): event is TestDoneEvent { 122 | return event.type === 'testDone' 123 | } 124 | export function isErrorEvent(event: Event): event is ErrorEvent { 125 | return event.type === 'error' 126 | } 127 | export function isDoneEvent(event: Event): event is DoneEvent { 128 | return event.type === 'done' 129 | } 130 | export function isMessageEvent(event: Event): event is MessageEvent { 131 | return event.type === 'print' 132 | } 133 | -------------------------------------------------------------------------------- /src/parsers/dotnet-trx/dotnet-trx-parser.ts: -------------------------------------------------------------------------------- 1 | import {parseStringPromise} from 'xml2js' 2 | 3 | import {ErrorInfo, Outcome, TrxReport, UnitTest, UnitTestResult} from './dotnet-trx-types' 4 | import {ParseOptions, TestParser} from '../../test-parser' 5 | 6 | import {getBasePath, normalizeFilePath} from '../../utils/path-utils' 7 | import {parseIsoDate, parseNetDuration} from '../../utils/parse-utils' 8 | 9 | import { 10 | TestExecutionResult, 11 | TestRunResult, 12 | TestSuiteResult, 13 | TestGroupResult, 14 | TestCaseResult, 15 | TestCaseError 16 | } from '../../test-results' 17 | 18 | class TestClass { 19 | constructor(readonly name: string) {} 20 | readonly tests: Test[] = [] 21 | } 22 | 23 | class Test { 24 | constructor( 25 | readonly name: string, 26 | readonly outcome: Outcome, 27 | readonly duration: number, 28 | readonly error?: ErrorInfo 29 | ) {} 30 | 31 | get result(): TestExecutionResult | undefined { 32 | switch (this.outcome) { 33 | case 'Passed': 34 | return 'success' 35 | case 'NotExecuted': 36 | return 'skipped' 37 | case 'Failed': 38 | return 'failed' 39 | } 40 | } 41 | } 42 | 43 | export class DotnetTrxParser implements TestParser { 44 | assumedWorkDir: string | undefined 45 | 46 | constructor(readonly options: ParseOptions) {} 47 | 48 | async parse(path: string, content: string): Promise { 49 | const trx = await this.getTrxReport(path, content) 50 | const tc = this.getTestClasses(trx) 51 | const tr = this.getTestRunResult(path, trx, tc) 52 | tr.sort(true) 53 | return tr 54 | } 55 | 56 | private async getTrxReport(path: string, content: string): Promise { 57 | try { 58 | return (await parseStringPromise(content)) as TrxReport 59 | } catch (e) { 60 | throw new Error(`Invalid XML at ${path}\n\n${e}`) 61 | } 62 | } 63 | 64 | private getTestClasses(trx: TrxReport): TestClass[] { 65 | if (trx.TestRun.TestDefinitions === undefined || trx.TestRun.Results === undefined) { 66 | return [] 67 | } 68 | 69 | const unitTests: {[id: string]: UnitTest} = {} 70 | for (const td of trx.TestRun.TestDefinitions) { 71 | for (const ut of td.UnitTest) { 72 | unitTests[ut.$.id] = ut 73 | } 74 | } 75 | 76 | const unitTestsResults = trx.TestRun.Results.flatMap(r => r.UnitTestResult).flatMap(result => ({ 77 | result, 78 | test: unitTests[result.$.testId] 79 | })) 80 | 81 | const testClasses: {[name: string]: TestClass} = {} 82 | for (const r of unitTestsResults) { 83 | const className = r.test.TestMethod[0].$.className 84 | let tc = testClasses[className] 85 | if (tc === undefined) { 86 | tc = new TestClass(className) 87 | testClasses[tc.name] = tc 88 | } 89 | const error = this.getErrorInfo(r.result) 90 | const durationAttr = r.result.$.duration 91 | const duration = durationAttr ? parseNetDuration(durationAttr) : 0 92 | 93 | const resultTestName = r.result.$.testName 94 | const testName = 95 | resultTestName.startsWith(className) && resultTestName[className.length] === '.' 96 | ? resultTestName.substr(className.length + 1) 97 | : resultTestName 98 | 99 | const test = new Test(testName, r.result.$.outcome, duration, error) 100 | tc.tests.push(test) 101 | } 102 | 103 | const result = Object.values(testClasses) 104 | return result 105 | } 106 | 107 | private getTestRunResult(path: string, trx: TrxReport, testClasses: TestClass[]): TestRunResult { 108 | const times = trx.TestRun.Times[0].$ 109 | const totalTime = parseIsoDate(times.finish).getTime() - parseIsoDate(times.start).getTime() 110 | 111 | const suites = testClasses.map(testClass => { 112 | const tests = testClass.tests.map(test => { 113 | const error = this.getError(test) 114 | return new TestCaseResult(test.name, test.result, test.duration, error) 115 | }) 116 | const group = new TestGroupResult(null, tests) 117 | return new TestSuiteResult(testClass.name, [group]) 118 | }) 119 | 120 | return new TestRunResult(path, suites, totalTime) 121 | } 122 | 123 | private getErrorInfo(testResult: UnitTestResult): ErrorInfo | undefined { 124 | if (testResult.$.outcome !== 'Failed') { 125 | return undefined 126 | } 127 | 128 | const output = testResult.Output 129 | const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined 130 | return error 131 | } 132 | 133 | private getError(test: Test): TestCaseError | undefined { 134 | if (!this.options.parseErrors || !test.error) { 135 | return undefined 136 | } 137 | 138 | const error = test.error 139 | if ( 140 | !Array.isArray(error.Message) || 141 | error.Message.length === 0 || 142 | !Array.isArray(error.StackTrace) || 143 | error.StackTrace.length === 0 144 | ) { 145 | return undefined 146 | } 147 | 148 | const message = test.error.Message[0] 149 | const stackTrace = test.error.StackTrace[0] 150 | let path 151 | let line 152 | 153 | const src = this.exceptionThrowSource(stackTrace) 154 | if (src) { 155 | path = src.path 156 | line = src.line 157 | } 158 | 159 | return { 160 | path, 161 | line, 162 | message, 163 | details: `${message}\n${stackTrace}` 164 | } 165 | } 166 | 167 | private exceptionThrowSource(stackTrace: string): {path: string; line: number} | undefined { 168 | const lines = stackTrace.split(/\r*\n/) 169 | const re = / in (.+):line (\d+)$/ 170 | const {trackedFiles} = this.options 171 | 172 | for (const str of lines) { 173 | const match = str.match(re) 174 | if (match !== null) { 175 | const [_, fileStr, lineStr] = match 176 | const filePath = normalizeFilePath(fileStr) 177 | const workDir = this.getWorkDir(filePath) 178 | if (workDir) { 179 | const file = filePath.substr(workDir.length) 180 | if (trackedFiles.includes(file)) { 181 | const line = parseInt(lineStr) 182 | return {path: file, line} 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | private getWorkDir(path: string): string | undefined { 190 | return ( 191 | this.options.workDir ?? 192 | this.assumedWorkDir ?? 193 | (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) 194 | ) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/parsers/dotnet-trx/dotnet-trx-types.ts: -------------------------------------------------------------------------------- 1 | export interface TrxReport { 2 | TestRun: TestRun 3 | } 4 | 5 | export interface TestRun { 6 | Times: Times[] 7 | Results?: Results[] 8 | TestDefinitions?: TestDefinitions[] 9 | } 10 | 11 | export interface Times { 12 | $: { 13 | creation: string 14 | queuing: string 15 | start: string 16 | finish: string 17 | } 18 | } 19 | 20 | export interface TestDefinitions { 21 | UnitTest: UnitTest[] 22 | } 23 | 24 | export interface UnitTest { 25 | $: { 26 | id: string 27 | } 28 | TestMethod: TestMethod[] 29 | } 30 | 31 | export interface TestMethod { 32 | $: { 33 | className: string 34 | name: string 35 | } 36 | } 37 | 38 | export interface Results { 39 | UnitTestResult: UnitTestResult[] 40 | } 41 | 42 | export interface UnitTestResult { 43 | $: { 44 | testId: string 45 | testName: string 46 | duration?: string 47 | outcome: Outcome 48 | } 49 | Output: Output[] 50 | } 51 | 52 | export interface Output { 53 | ErrorInfo: ErrorInfo[] 54 | } 55 | export interface ErrorInfo { 56 | Message: string[] 57 | StackTrace: string[] 58 | } 59 | 60 | export type Outcome = 'Passed' | 'NotExecuted' | 'Failed' 61 | -------------------------------------------------------------------------------- /src/parsers/java-junit/java-junit-parser.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import {ParseOptions, TestParser} from '../../test-parser' 3 | import {parseStringPromise} from 'xml2js' 4 | 5 | import {JunitReport, SingleSuiteReport, TestCase, TestSuite} from './java-junit-types' 6 | import {normalizeFilePath} from '../../utils/path-utils' 7 | 8 | import { 9 | TestExecutionResult, 10 | TestRunResult, 11 | TestSuiteResult, 12 | TestGroupResult, 13 | TestCaseResult, 14 | TestCaseError 15 | } from '../../test-results' 16 | 17 | export class JavaJunitParser implements TestParser { 18 | readonly trackedFiles: {[fileName: string]: string[]} 19 | 20 | constructor(readonly options: ParseOptions) { 21 | // Map to efficient lookup of all paths with given file name 22 | this.trackedFiles = {} 23 | for (const filePath of options.trackedFiles) { 24 | const fileName = path.basename(filePath) 25 | const files = this.trackedFiles[fileName] ?? (this.trackedFiles[fileName] = []) 26 | files.push(normalizeFilePath(filePath)) 27 | } 28 | } 29 | 30 | async parse(filePath: string, content: string): Promise { 31 | const reportOrSuite = await this.getJunitReport(filePath, content) 32 | const isReport = (reportOrSuite as JunitReport).testsuites !== undefined 33 | 34 | // XML might contain: 35 | // - multiple suites under root node 36 | // - single as root node 37 | let ju: JunitReport 38 | if (isReport) { 39 | ju = reportOrSuite as JunitReport 40 | } else { 41 | // Make it behave the same way as if suite was inside root node 42 | const suite = (reportOrSuite as SingleSuiteReport).testsuite 43 | ju = { 44 | testsuites: { 45 | $: {time: suite.$.time}, 46 | testsuite: [suite] 47 | } 48 | } 49 | } 50 | 51 | return this.getTestRunResult(filePath, ju) 52 | } 53 | 54 | private async getJunitReport(filePath: string, content: string): Promise { 55 | try { 56 | return await parseStringPromise(content) 57 | } catch (e) { 58 | throw new Error(`Invalid XML at ${filePath}\n\n${e}`) 59 | } 60 | } 61 | 62 | private getTestRunResult(filePath: string, junit: JunitReport): TestRunResult { 63 | const suites = 64 | junit.testsuites.testsuite === undefined 65 | ? [] 66 | : junit.testsuites.testsuite.map(ts => { 67 | const name = ts.$.name.trim() 68 | const time = parseFloat(ts.$.time) * 1000 69 | return new TestSuiteResult(name, this.getGroups(ts), time) 70 | }) 71 | 72 | const seconds = parseFloat(junit.testsuites.$?.time) 73 | const time = isNaN(seconds) ? undefined : seconds * 1000 74 | return new TestRunResult(filePath, suites, time) 75 | } 76 | 77 | private getGroups(suite: TestSuite): TestGroupResult[] { 78 | if (suite.testcase === undefined) { 79 | return [] 80 | } 81 | 82 | const groups: {name: string; tests: TestCase[]}[] = [] 83 | for (const tc of suite.testcase) { 84 | // Normally classname is same as suite name - both refer to same Java class 85 | // Therefore it doesn't make sense to process it as a group 86 | // and tests will be added to default group with empty name 87 | const className = tc.$.classname === suite.$.name ? '' : tc.$.classname 88 | let grp = groups.find(g => g.name === className) 89 | if (grp === undefined) { 90 | grp = {name: className, tests: []} 91 | groups.push(grp) 92 | } 93 | grp.tests.push(tc) 94 | } 95 | 96 | return groups.map(grp => { 97 | const tests = grp.tests.map(tc => { 98 | const name = tc.$.name.trim() 99 | const result = this.getTestCaseResult(tc) 100 | const time = parseFloat(tc.$.time) * 1000 101 | const error = this.getTestCaseError(tc) 102 | return new TestCaseResult(name, result, time, error) 103 | }) 104 | return new TestGroupResult(grp.name, tests) 105 | }) 106 | } 107 | 108 | private getTestCaseResult(test: TestCase): TestExecutionResult { 109 | if (test.failure || test.error) return 'failed' 110 | if (test.skipped) return 'skipped' 111 | return 'success' 112 | } 113 | 114 | private getTestCaseError(tc: TestCase): TestCaseError | undefined { 115 | if (!this.options.parseErrors) { 116 | return undefined 117 | } 118 | 119 | // We process and the same way 120 | const failures = tc.failure ?? tc.error 121 | if (!failures) { 122 | return undefined 123 | } 124 | 125 | const failure = failures[0] 126 | const details = typeof failure === 'object' ? failure._ ?? '' : failure 127 | let filePath 128 | let line 129 | 130 | const src = this.exceptionThrowSource(details) 131 | if (src) { 132 | filePath = src.filePath 133 | line = src.line 134 | } 135 | 136 | let message 137 | if (typeof failure === 'object') { 138 | message = failure.$.message 139 | if (failure.$?.type) { 140 | message = failure.$.type + ': ' + message 141 | } 142 | } 143 | return { 144 | path: filePath, 145 | line, 146 | details, 147 | message 148 | } 149 | } 150 | 151 | private exceptionThrowSource(stackTrace = ''): {filePath: string; line: number} | undefined { 152 | const lines = stackTrace.split(/\r?\n/) 153 | const re = /^at (.*)\((.*):(\d+)\)$/ 154 | 155 | for (const str of lines) { 156 | const match = str.match(re) 157 | if (match !== null) { 158 | const [_, tracePath, fileName, lineStr] = match 159 | const filePath = this.getFilePath(tracePath, fileName) 160 | if (filePath !== undefined) { 161 | const line = parseInt(lineStr) 162 | return {filePath, line} 163 | } 164 | } 165 | } 166 | } 167 | 168 | // Stacktrace in Java doesn't contain full paths to source file. 169 | // There are only package, file name and line. 170 | // Assuming folder structure matches package name (as it should in Java), 171 | // we can try to match tracked file. 172 | private getFilePath(tracePath: string, fileName: string): string | undefined { 173 | // Check if there is any tracked file with given name 174 | const files = this.trackedFiles[fileName] 175 | if (files === undefined) { 176 | return undefined 177 | } 178 | 179 | // Remove class name and method name from trace. 180 | // Take parts until first item with capital letter - package names are lowercase while class name is CamelCase. 181 | const packageParts = tracePath.split(/\./g) 182 | const packageIndex = packageParts.findIndex(part => part[0] <= 'Z') 183 | if (packageIndex !== -1) { 184 | packageParts.splice(packageIndex, packageParts.length - packageIndex) 185 | } 186 | 187 | if (packageParts.length === 0) { 188 | return undefined 189 | } 190 | 191 | // Get right file 192 | // - file name matches 193 | // - parent folders structure must reflect the package name 194 | for (const filePath of files) { 195 | const dirs = path.dirname(filePath).split(/\//g) 196 | if (packageParts.length > dirs.length) { 197 | continue 198 | } 199 | // get only N parent folders, where N = length of package name parts 200 | if (dirs.length > packageParts.length) { 201 | dirs.splice(0, dirs.length - packageParts.length) 202 | } 203 | // check if parent folder structure matches package name 204 | const isMatch = packageParts.every((part, i) => part === dirs[i]) 205 | if (isMatch) { 206 | return filePath 207 | } 208 | } 209 | 210 | return undefined 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/parsers/java-junit/java-junit-types.ts: -------------------------------------------------------------------------------- 1 | export interface JunitReport { 2 | testsuites: TestSuites 3 | } 4 | 5 | export interface SingleSuiteReport { 6 | testsuite: TestSuite 7 | } 8 | 9 | export interface TestSuites { 10 | $: { 11 | time: string 12 | } 13 | testsuite?: TestSuite[] 14 | } 15 | 16 | export interface TestSuite { 17 | $: { 18 | name: string 19 | tests: string 20 | errors: string 21 | failures: string 22 | skipped: string 23 | time: string 24 | timestamp?: Date 25 | } 26 | testcase: TestCase[] 27 | } 28 | 29 | export interface TestCase { 30 | $: { 31 | classname: string 32 | file?: string 33 | name: string 34 | time: string 35 | } 36 | failure?: string | Failure[] 37 | error?: string | Failure[] 38 | skipped?: string[] 39 | } 40 | 41 | export interface Failure { 42 | _?: string 43 | $: { 44 | type?: string 45 | message: string 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/parsers/jest-junit/jest-junit-parser.ts: -------------------------------------------------------------------------------- 1 | import {ParseOptions, TestParser} from '../../test-parser' 2 | import {parseStringPromise} from 'xml2js' 3 | 4 | import {JunitReport, TestCase, TestSuite} from './jest-junit-types' 5 | import {getExceptionSource} from '../../utils/node-utils' 6 | import {getBasePath, normalizeFilePath} from '../../utils/path-utils' 7 | 8 | import { 9 | TestExecutionResult, 10 | TestRunResult, 11 | TestSuiteResult, 12 | TestGroupResult, 13 | TestCaseResult, 14 | TestCaseError 15 | } from '../../test-results' 16 | 17 | export class JestJunitParser implements TestParser { 18 | assumedWorkDir: string | undefined 19 | 20 | constructor(readonly options: ParseOptions) {} 21 | 22 | async parse(path: string, content: string): Promise { 23 | const ju = await this.getJunitReport(path, content) 24 | return this.getTestRunResult(path, ju) 25 | } 26 | 27 | private async getJunitReport(path: string, content: string): Promise { 28 | try { 29 | return (await parseStringPromise(content)) as JunitReport 30 | } catch (e) { 31 | throw new Error(`Invalid XML at ${path}\n\n${e}`) 32 | } 33 | } 34 | 35 | private getTestRunResult(path: string, junit: JunitReport): TestRunResult { 36 | const suites = 37 | junit.testsuites.testsuite === undefined 38 | ? [] 39 | : junit.testsuites.testsuite.map(ts => { 40 | const name = ts.$.name.trim() 41 | const time = parseFloat(ts.$.time) * 1000 42 | const sr = new TestSuiteResult(name, this.getGroups(ts), time) 43 | return sr 44 | }) 45 | 46 | const time = junit.testsuites.$ && parseFloat(junit.testsuites.$.time) * 1000 47 | return new TestRunResult(path, suites, time) 48 | } 49 | 50 | private getGroups(suite: TestSuite): TestGroupResult[] { 51 | const groups: {describe: string; tests: TestCase[]}[] = [] 52 | for (const tc of suite.testcase) { 53 | let grp = groups.find(g => g.describe === tc.$.classname) 54 | if (grp === undefined) { 55 | grp = {describe: tc.$.classname, tests: []} 56 | groups.push(grp) 57 | } 58 | grp.tests.push(tc) 59 | } 60 | 61 | return groups.map(grp => { 62 | const tests = grp.tests.map(tc => { 63 | const name = tc.$.name.trim() 64 | const result = this.getTestCaseResult(tc) 65 | const time = parseFloat(tc.$.time) * 1000 66 | const error = this.getTestCaseError(tc) 67 | return new TestCaseResult(name, result, time, error) 68 | }) 69 | return new TestGroupResult(grp.describe, tests) 70 | }) 71 | } 72 | 73 | private getTestCaseResult(test: TestCase): TestExecutionResult { 74 | if (test.failure || test.error) return 'failed' 75 | if (test.skipped) return 'skipped' 76 | return 'success' 77 | } 78 | 79 | private getTestCaseError(tc: TestCase): TestCaseError | undefined { 80 | if (!this.options.parseErrors || !(tc.failure || tc.error)) { 81 | return undefined 82 | } 83 | 84 | const details = tc.failure 85 | ? typeof tc.failure[0] === 'string' 86 | ? tc.failure[0] 87 | : tc.failure[0]._ 88 | : tc.error 89 | ? tc.error[0] 90 | : 'unknown failure' 91 | let path 92 | let line 93 | 94 | const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file)) 95 | if (src) { 96 | path = src.path 97 | line = src.line 98 | } 99 | 100 | return { 101 | path, 102 | line, 103 | details 104 | } 105 | } 106 | 107 | private getRelativePath(path: string): string { 108 | path = normalizeFilePath(path) 109 | const workDir = this.getWorkDir(path) 110 | if (workDir !== undefined && path.startsWith(workDir)) { 111 | path = path.substr(workDir.length) 112 | } 113 | return path 114 | } 115 | 116 | private getWorkDir(path: string): string | undefined { 117 | return ( 118 | this.options.workDir ?? 119 | this.assumedWorkDir ?? 120 | (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) 121 | ) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/parsers/jest-junit/jest-junit-types.ts: -------------------------------------------------------------------------------- 1 | export interface JunitReport { 2 | testsuites: TestSuites 3 | } 4 | 5 | export interface TestSuites { 6 | $: { 7 | time: string 8 | } 9 | testsuite?: TestSuite[] 10 | } 11 | 12 | export interface TestSuite { 13 | $: { 14 | name: string 15 | tests: string 16 | errors: string 17 | failures: string 18 | skipped: string 19 | time: string 20 | timestamp?: Date 21 | } 22 | testcase: TestCase[] 23 | } 24 | 25 | export interface TestCase { 26 | $: { 27 | classname: string 28 | file?: string 29 | name: string 30 | time: string 31 | } 32 | failure?: TestFailure[] 33 | skipped?: string[] 34 | error?: string[] 35 | } 36 | 37 | export type TestFailure = 38 | | string 39 | | { 40 | $: { 41 | type: string 42 | message: string 43 | } 44 | _: string 45 | } 46 | -------------------------------------------------------------------------------- /src/parsers/mocha-json/mocha-json-parser.ts: -------------------------------------------------------------------------------- 1 | import {ParseOptions, TestParser} from '../../test-parser' 2 | import { 3 | TestCaseError, 4 | TestCaseResult, 5 | TestExecutionResult, 6 | TestGroupResult, 7 | TestRunResult, 8 | TestSuiteResult 9 | } from '../../test-results' 10 | import {getExceptionSource} from '../../utils/node-utils' 11 | import {getBasePath, normalizeFilePath} from '../../utils/path-utils' 12 | import {MochaJson, MochaJsonTest} from './mocha-json-types' 13 | 14 | export class MochaJsonParser implements TestParser { 15 | assumedWorkDir: string | undefined 16 | 17 | constructor(readonly options: ParseOptions) {} 18 | 19 | async parse(path: string, content: string): Promise { 20 | const mocha = this.getMochaJson(path, content) 21 | const result = this.getTestRunResult(path, mocha) 22 | result.sort(true) 23 | return Promise.resolve(result) 24 | } 25 | 26 | private getMochaJson(path: string, content: string): MochaJson { 27 | try { 28 | return JSON.parse(content) 29 | } catch (e) { 30 | throw new Error(`Invalid JSON at ${path}\n\n${e}`) 31 | } 32 | } 33 | 34 | private getTestRunResult(resultsPath: string, mocha: MochaJson): TestRunResult { 35 | const suitesMap: {[path: string]: TestSuiteResult} = {} 36 | 37 | const getSuite = (test: MochaJsonTest): TestSuiteResult => { 38 | const path = this.getRelativePath(test.file) 39 | return suitesMap[path] ?? (suitesMap[path] = new TestSuiteResult(path, [])) 40 | } 41 | 42 | for (const test of mocha.passes) { 43 | const suite = getSuite(test) 44 | this.processTest(suite, test, 'success') 45 | } 46 | 47 | for (const test of mocha.failures) { 48 | const suite = getSuite(test) 49 | this.processTest(suite, test, 'failed') 50 | } 51 | 52 | for (const test of mocha.pending) { 53 | const suite = getSuite(test) 54 | this.processTest(suite, test, 'skipped') 55 | } 56 | 57 | const suites = Object.values(suitesMap) 58 | return new TestRunResult(resultsPath, suites, mocha.stats.duration) 59 | } 60 | 61 | private processTest(suite: TestSuiteResult, test: MochaJsonTest, result: TestExecutionResult): void { 62 | const groupName = 63 | test.fullTitle !== test.title 64 | ? test.fullTitle.substr(0, test.fullTitle.length - test.title.length).trimEnd() 65 | : null 66 | 67 | let group = suite.groups.find(grp => grp.name === groupName) 68 | if (group === undefined) { 69 | group = new TestGroupResult(groupName, []) 70 | suite.groups.push(group) 71 | } 72 | 73 | const error = this.getTestCaseError(test) 74 | const testCase = new TestCaseResult(test.title, result, test.duration ?? 0, error) 75 | group.tests.push(testCase) 76 | } 77 | 78 | private getTestCaseError(test: MochaJsonTest): TestCaseError | undefined { 79 | const details = test.err.stack 80 | const message = test.err.message 81 | if (details === undefined) { 82 | return undefined 83 | } 84 | 85 | let path 86 | let line 87 | 88 | const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file)) 89 | if (src) { 90 | path = src.path 91 | line = src.line 92 | } 93 | 94 | return { 95 | path, 96 | line, 97 | message, 98 | details 99 | } 100 | } 101 | 102 | private getRelativePath(path: string): string { 103 | path = normalizeFilePath(path) 104 | const workDir = this.getWorkDir(path) 105 | if (workDir !== undefined && path.startsWith(workDir)) { 106 | path = path.substr(workDir.length) 107 | } 108 | return path 109 | } 110 | 111 | private getWorkDir(path: string): string | undefined { 112 | return ( 113 | this.options.workDir ?? 114 | this.assumedWorkDir ?? 115 | (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/parsers/mocha-json/mocha-json-types.ts: -------------------------------------------------------------------------------- 1 | export interface MochaJson { 2 | stats: MochaJsonStats 3 | passes: MochaJsonTest[] 4 | pending: MochaJsonTest[] 5 | failures: MochaJsonTest[] 6 | } 7 | 8 | export interface MochaJsonStats { 9 | duration: number 10 | } 11 | 12 | export interface MochaJsonTest { 13 | title: string 14 | fullTitle: string 15 | file: string 16 | duration?: number 17 | err: MochaJsonTestError 18 | } 19 | 20 | export interface MochaJsonTestError { 21 | stack?: string 22 | message?: string 23 | } 24 | -------------------------------------------------------------------------------- /src/parsers/mochawesome-json/mochawesome-json-parser.ts: -------------------------------------------------------------------------------- 1 | import {ParseOptions, TestParser} from '../../test-parser' 2 | import { 3 | TestCaseError, 4 | TestCaseResult, 5 | TestExecutionResult, 6 | TestGroupResult, 7 | TestRunResult, 8 | TestSuiteResult 9 | } from '../../test-results' 10 | import {getExceptionSource} from '../../utils/node-utils' 11 | import {getBasePath, normalizeFilePath} from '../../utils/path-utils' 12 | import {MochawesomeJson, MochawesomeJsonSuite, MochawesomeJsonTest} from './mochawesome-json-types' 13 | 14 | export class MochawesomeJsonParser implements TestParser { 15 | assumedWorkDir: string | undefined 16 | 17 | constructor(readonly options: ParseOptions) {} 18 | 19 | async parse(path: string, content: string): Promise { 20 | const mochawesome = this.getMochawesomeJson(path, content) 21 | const result = this.getTestRunResult(path, mochawesome) 22 | result.sort(true) 23 | return Promise.resolve(result) 24 | } 25 | 26 | private getMochawesomeJson(path: string, content: string): MochawesomeJson { 27 | try { 28 | return JSON.parse(content) 29 | } catch (e) { 30 | throw new Error(`Invalid JSON at ${path}\n\n${e}`) 31 | } 32 | } 33 | 34 | private getTestRunResult(resultsPath: string, mochawesome: MochawesomeJson): TestRunResult { 35 | const suitesMap: {[path: string]: TestSuiteResult} = {} 36 | 37 | const results = mochawesome.results 38 | 39 | const getSuite = (fullFile: string): TestSuiteResult => { 40 | const path = this.getRelativePath(fullFile) 41 | return suitesMap[path] ?? (suitesMap[path] = new TestSuiteResult(path, [])) 42 | } 43 | 44 | const processPassingTests = (tests: MochawesomeJsonTest[], suiteName: string): void => { 45 | const passingTests = tests?.filter(test => test.pass) 46 | 47 | if (passingTests) { 48 | for (const passingTest of passingTests) { 49 | const suite = getSuite(suiteName) 50 | this.processTest(suite, passingTest, 'success') 51 | } 52 | } 53 | } 54 | 55 | const processFailingTests = (tests: MochawesomeJsonTest[], suiteName: string): void => { 56 | const failingTests = tests?.filter(test => test.fail) 57 | 58 | if (failingTests) { 59 | for (const failingTest of failingTests) { 60 | const suite = getSuite(suiteName) 61 | this.processTest(suite, failingTest, 'failed') 62 | } 63 | } 64 | } 65 | 66 | const processPendingTests = (tests: MochawesomeJsonTest[], suiteName: string): void => { 67 | const pendingTests = tests?.filter(test => test.skipped) 68 | 69 | if (pendingTests) { 70 | for (const pendingTest of pendingTests) { 71 | const suite = getSuite(suiteName) 72 | this.processTest(suite, pendingTest, 'skipped') 73 | } 74 | } 75 | } 76 | 77 | const processAllTests = (tests: MochawesomeJsonTest[], suiteName: string): void => { 78 | processPassingTests(tests, suiteName) 79 | processFailingTests(tests, suiteName) 80 | processPendingTests(tests, suiteName) 81 | } 82 | 83 | // Handle nested suites 84 | const processNestedSuites = (suite: MochawesomeJsonSuite, nestedSuiteIndex: number, suiteName: string): void => { 85 | // Process suite tests 86 | processAllTests(suite.tests, suiteName) 87 | 88 | for (const innerSuite of suite.suites) { 89 | // Process inner suite tests 90 | processAllTests(innerSuite.tests, suiteName) 91 | 92 | if (innerSuite?.suites[nestedSuiteIndex]?.suites.length > 0) { 93 | processNestedSuites(innerSuite, 0, suiteName) 94 | } else { 95 | processAllTests(innerSuite?.suites[nestedSuiteIndex]?.tests, suiteName) 96 | nestedSuiteIndex++ 97 | 98 | // TODO - Figure out how to get 1.1.1.1.2 - suites with more than one object in them 99 | } 100 | } 101 | } 102 | 103 | // Process Mochawesome Data 104 | for (const result of results) { 105 | const suites = result?.suites 106 | const filePath = result?.fullFile 107 | const suitelessTests = result?.tests 108 | 109 | // Process tests that aren't in a suite 110 | if (suitelessTests?.length > 0) { 111 | processAllTests(suitelessTests, filePath) 112 | } 113 | 114 | // Process tests that are in a suite 115 | if (suites?.length > 0) { 116 | for (const suite of suites) { 117 | processNestedSuites(suite, 0, filePath ? filePath : suite.title) 118 | } 119 | } 120 | } 121 | 122 | const mappedSuites = Object.values(suitesMap) 123 | 124 | return new TestRunResult(resultsPath, mappedSuites, mochawesome.stats.duration) 125 | } 126 | 127 | private processTest(suite: TestSuiteResult, test: MochawesomeJsonTest, result: TestExecutionResult): void { 128 | const groupName = 129 | test.fullTitle !== test.title 130 | ? test.fullTitle.substr(0, test.fullTitle.length - test.title.length).trimEnd() 131 | : null 132 | 133 | let group = suite.groups.find(grp => grp.name === groupName) 134 | if (group === undefined) { 135 | group = new TestGroupResult(groupName, []) 136 | suite.groups.push(group) 137 | } 138 | 139 | const error = this.getTestCaseError(test) 140 | const testCase = new TestCaseResult(test.title, result, test.duration ?? 0, error) 141 | group.tests.push(testCase) 142 | } 143 | 144 | private getTestCaseError(test: MochawesomeJsonTest): TestCaseError | undefined { 145 | const details = test.err.estack 146 | const message = test.err.message 147 | if (details === undefined) { 148 | return undefined 149 | } 150 | 151 | let path 152 | let line 153 | 154 | const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file)) 155 | if (src) { 156 | path = src.path 157 | line = src.line 158 | } 159 | 160 | return { 161 | path, 162 | line, 163 | message, 164 | details 165 | } 166 | } 167 | 168 | private getRelativePath(path: string): string { 169 | path = normalizeFilePath(path) 170 | const workDir = this.getWorkDir(path) 171 | if (workDir !== undefined && path.startsWith(workDir)) { 172 | path = path.substr(workDir.length) 173 | } 174 | return path 175 | } 176 | 177 | private getWorkDir(path: string): string | undefined { 178 | return ( 179 | this.options.workDir ?? 180 | this.assumedWorkDir ?? 181 | (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles)) 182 | ) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/parsers/mochawesome-json/mochawesome-json-types.ts: -------------------------------------------------------------------------------- 1 | export interface MochawesomeJson { 2 | stats: MochawesomeJsonStat 3 | results: MochawesomeJsonSuite[] 4 | } 5 | 6 | export interface MochawesomeJsonStat { 7 | duration: number 8 | } 9 | 10 | export interface MochawesomeJsonSuite { 11 | tests: MochawesomeJsonTest[] 12 | suites: MochawesomeJsonSuite[] 13 | fullFile: string 14 | title: string 15 | } 16 | 17 | export interface MochawesomeJsonTest { 18 | title: string 19 | fullTitle: string 20 | duration: number 21 | pass: boolean 22 | fail: boolean 23 | skipped: boolean 24 | err: MochawesomeJsonTestError 25 | } 26 | 27 | export interface MochawesomeJsonTestError { 28 | message: string 29 | estack: string 30 | diff: string | null // TODO - Not sure if string 31 | } 32 | -------------------------------------------------------------------------------- /src/report/get-annotations.ts: -------------------------------------------------------------------------------- 1 | import {ellipsis, fixEol} from '../utils/markdown-utils' 2 | import {TestRunResult} from '../test-results' 3 | import {getFirstNonEmptyLine} from '../utils/parse-utils' 4 | 5 | type Annotation = { 6 | path: string 7 | start_line: number 8 | end_line: number 9 | start_column?: number 10 | end_column?: number 11 | annotation_level: 'notice' | 'warning' | 'failure' 12 | message: string 13 | title?: string 14 | raw_details?: string 15 | } 16 | 17 | interface TestError { 18 | testRunPaths: string[] 19 | suiteName: string 20 | testName: string 21 | path: string 22 | line: number 23 | message: string 24 | details: string 25 | } 26 | 27 | export function getAnnotations(results: TestRunResult[], maxCount: number): Annotation[] { 28 | if (maxCount === 0) { 29 | return [] 30 | } 31 | 32 | // Collect errors from TestRunResults 33 | // Merge duplicates if there are more test results files processed 34 | const errors: TestError[] = [] 35 | const mergeDup = results.length > 1 36 | for (const tr of results) { 37 | for (const ts of tr.suites) { 38 | for (const tg of ts.groups) { 39 | for (const tc of tg.tests) { 40 | const err = tc.error 41 | if (err === undefined) { 42 | continue 43 | } 44 | const path = err.path ?? tr.path 45 | const line = err.line ?? 0 46 | if (mergeDup) { 47 | const dup = errors.find(e => path === e.path && line === e.line && err.details === e.details) 48 | if (dup !== undefined) { 49 | dup.testRunPaths.push(tr.path) 50 | continue 51 | } 52 | } 53 | 54 | errors.push({ 55 | testRunPaths: [tr.path], 56 | suiteName: ts.name, 57 | testName: tg.name ? `${tg.name} ► ${tc.name}` : tc.name, 58 | details: err.details, 59 | message: err.message ?? getFirstNonEmptyLine(err.details) ?? 'Test failed', 60 | path, 61 | line 62 | }) 63 | } 64 | } 65 | } 66 | } 67 | 68 | // Limit number of created annotations 69 | errors.splice(maxCount + 1) 70 | 71 | const annotations = errors.map(e => { 72 | const message = [ 73 | 'Failed test found in:', 74 | e.testRunPaths.map(p => ` ${p}`).join('\n'), 75 | 'Error:', 76 | ident(fixEol(e.message), ' ') 77 | ].join('\n') 78 | 79 | return enforceCheckRunLimits({ 80 | path: e.path, 81 | start_line: e.line, 82 | end_line: e.line, 83 | annotation_level: 'failure', 84 | title: `${e.suiteName} ► ${e.testName}`, 85 | raw_details: fixEol(e.details), 86 | message 87 | }) 88 | }) 89 | 90 | return annotations 91 | } 92 | 93 | function enforceCheckRunLimits(err: Annotation): Annotation { 94 | err.title = ellipsis(err.title || '', 255) 95 | err.message = ellipsis(err.message, 65535) 96 | if (err.raw_details) { 97 | err.raw_details = ellipsis(err.raw_details, 65535) 98 | } 99 | return err 100 | } 101 | 102 | function ident(text: string, prefix: string): string { 103 | return text 104 | .split(/\n/g) 105 | .map(line => prefix + line) 106 | .join('\n') 107 | } 108 | -------------------------------------------------------------------------------- /src/test-parser.ts: -------------------------------------------------------------------------------- 1 | import {TestRunResult} from './test-results' 2 | 3 | export interface ParseOptions { 4 | parseErrors: boolean 5 | workDir?: string 6 | trackedFiles: string[] 7 | } 8 | 9 | export interface TestParser { 10 | parse(path: string, content: string): Promise 11 | } 12 | -------------------------------------------------------------------------------- /src/test-results.ts: -------------------------------------------------------------------------------- 1 | export class TestRunResult { 2 | constructor( 3 | readonly path: string, 4 | readonly suites: TestSuiteResult[], 5 | private totalTime?: number 6 | ) {} 7 | 8 | get tests(): number { 9 | return this.suites.reduce((sum, g) => sum + g.tests, 0) 10 | } 11 | 12 | get passed(): number { 13 | return this.suites.reduce((sum, g) => sum + g.passed, 0) 14 | } 15 | get failed(): number { 16 | return this.suites.reduce((sum, g) => sum + g.failed, 0) 17 | } 18 | get skipped(): number { 19 | return this.suites.reduce((sum, g) => sum + g.skipped, 0) 20 | } 21 | 22 | get time(): number { 23 | return this.totalTime ?? this.suites.reduce((sum, g) => sum + g.time, 0) 24 | } 25 | 26 | get result(): TestExecutionResult { 27 | return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success' 28 | } 29 | 30 | get failedSuites(): TestSuiteResult[] { 31 | return this.suites.filter(s => s.result === 'failed') 32 | } 33 | 34 | sort(deep: boolean): void { 35 | this.suites.sort((a, b) => a.name.localeCompare(b.name)) 36 | if (deep) { 37 | for (const suite of this.suites) { 38 | suite.sort(deep) 39 | } 40 | } 41 | } 42 | } 43 | 44 | export class TestSuiteResult { 45 | constructor( 46 | readonly name: string, 47 | readonly groups: TestGroupResult[], 48 | private totalTime?: number 49 | ) {} 50 | 51 | get tests(): number { 52 | return this.groups.reduce((sum, g) => sum + g.tests.length, 0) 53 | } 54 | 55 | get passed(): number { 56 | return this.groups.reduce((sum, g) => sum + g.passed, 0) 57 | } 58 | get failed(): number { 59 | return this.groups.reduce((sum, g) => sum + g.failed, 0) 60 | } 61 | get skipped(): number { 62 | return this.groups.reduce((sum, g) => sum + g.skipped, 0) 63 | } 64 | get time(): number { 65 | return this.totalTime ?? this.groups.reduce((sum, g) => sum + g.time, 0) 66 | } 67 | 68 | get result(): TestExecutionResult { 69 | return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success' 70 | } 71 | 72 | get failedGroups(): TestGroupResult[] { 73 | return this.groups.filter(grp => grp.result === 'failed') 74 | } 75 | 76 | sort(deep: boolean): void { 77 | this.groups.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) 78 | if (deep) { 79 | for (const grp of this.groups) { 80 | grp.sort() 81 | } 82 | } 83 | } 84 | } 85 | 86 | export class TestGroupResult { 87 | constructor( 88 | readonly name: string | undefined | null, 89 | readonly tests: TestCaseResult[] 90 | ) {} 91 | 92 | get passed(): number { 93 | return this.tests.reduce((sum, t) => (t.result === 'success' ? sum + 1 : sum), 0) 94 | } 95 | get failed(): number { 96 | return this.tests.reduce((sum, t) => (t.result === 'failed' ? sum + 1 : sum), 0) 97 | } 98 | get skipped(): number { 99 | return this.tests.reduce((sum, t) => (t.result === 'skipped' ? sum + 1 : sum), 0) 100 | } 101 | get time(): number { 102 | return this.tests.reduce((sum, t) => sum + t.time, 0) 103 | } 104 | 105 | get result(): TestExecutionResult { 106 | return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success' 107 | } 108 | 109 | get failedTests(): TestCaseResult[] { 110 | return this.tests.filter(tc => tc.result === 'failed') 111 | } 112 | 113 | sort(): void { 114 | this.tests.sort((a, b) => a.name.localeCompare(b.name)) 115 | } 116 | } 117 | 118 | export class TestCaseResult { 119 | constructor( 120 | readonly name: string, 121 | readonly result: TestExecutionResult, 122 | readonly time: number, 123 | readonly error?: TestCaseError 124 | ) {} 125 | } 126 | 127 | export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined 128 | 129 | export interface TestCaseError { 130 | path?: string 131 | line?: number 132 | message?: string 133 | details: string 134 | } 135 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Outputs { 2 | runHtmlUrl = 'runHtmlUrl' 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import {exec as execImpl, ExecOptions} from '@actions/exec' 2 | 3 | // Wraps original exec() function 4 | // Returns exit code and whole stdout/stderr 5 | export default async function exec(commandLine: string, args?: string[], options?: ExecOptions): Promise { 6 | options = options || {} 7 | let stdout = '' 8 | let stderr = '' 9 | options.listeners = { 10 | stdout: (data: Buffer) => (stdout += data.toString()), 11 | stderr: (data: Buffer) => (stderr += data.toString()) 12 | } 13 | const code = await execImpl(commandLine, args, options) 14 | return {code, stdout, stderr} 15 | } 16 | 17 | export interface ExecResult { 18 | code: number 19 | stdout: string 20 | stderr: string 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/git.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import exec from './exec' 3 | 4 | export async function listFiles(): Promise { 5 | core.startGroup('Listing all files tracked by git') 6 | let output = '' 7 | try { 8 | output = (await exec('git', ['ls-files', '-z'])).stdout 9 | } finally { 10 | fixStdOutNullTermination() 11 | core.endGroup() 12 | } 13 | 14 | return output.split('\u0000').filter(s => s.length > 0) 15 | } 16 | 17 | function fixStdOutNullTermination(): void { 18 | // Previous command uses NULL as delimiters and output is printed to stdout. 19 | // We have to make sure next thing written to stdout will start on new line. 20 | // Otherwise things like ::set-output wouldn't work. 21 | core.info('') 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/github-utils.ts: -------------------------------------------------------------------------------- 1 | import {createWriteStream} from 'fs' 2 | import * as core from '@actions/core' 3 | import * as github from '@actions/github' 4 | import {GitHub} from '@actions/github/lib/utils' 5 | import type {PullRequest} from '@octokit/webhooks-types' 6 | import * as stream from 'stream' 7 | import {promisify} from 'util' 8 | import got from 'got' 9 | const asyncStream = promisify(stream.pipeline) 10 | 11 | export function getCheckRunContext(): {sha: string; runId: number} { 12 | if (github.context.eventName === 'workflow_run') { 13 | core.info('Action was triggered by workflow_run: using SHA and RUN_ID from triggering workflow') 14 | const event = github.context.payload 15 | if (!event.workflow_run) { 16 | throw new Error("Event of type 'workflow_run' is missing 'workflow_run' field") 17 | } 18 | return { 19 | sha: event.workflow_run.head_commit.id, 20 | runId: event.workflow_run.id 21 | } 22 | } 23 | 24 | const runId = github.context.runId 25 | if (github.context.payload.pull_request) { 26 | core.info(`Action was triggered by ${github.context.eventName}: using SHA from head of source branch`) 27 | const pr = github.context.payload.pull_request as PullRequest 28 | return {sha: pr.head.sha, runId} 29 | } 30 | 31 | return {sha: github.context.sha, runId} 32 | } 33 | 34 | export async function downloadArtifact( 35 | octokit: InstanceType, 36 | artifactId: number, 37 | fileName: string, 38 | token: string 39 | ): Promise { 40 | core.startGroup(`Downloading artifact ${fileName}`) 41 | try { 42 | core.info(`Artifact ID: ${artifactId}`) 43 | 44 | const req = octokit.rest.actions.downloadArtifact.endpoint({ 45 | ...github.context.repo, 46 | artifact_id: artifactId, 47 | archive_format: 'zip' 48 | }) 49 | 50 | const headers = { 51 | Authorization: `Bearer ${token}` 52 | } 53 | 54 | core.info(`Request URL: ${req.url}`) 55 | const resp = await got(req.url, { 56 | headers, 57 | followRedirect: false 58 | }) 59 | 60 | core.info(`Fetch artifact URL: ${resp.statusCode} ${resp.statusMessage}`) 61 | if (resp.statusCode !== 302) { 62 | throw new Error('Fetch artifact URL failed: received unexpected status code') 63 | } 64 | 65 | const url = resp.headers.location 66 | if (url === undefined) { 67 | const receivedHeaders = Object.keys(resp.headers) 68 | core.info(`Received headers: ${receivedHeaders.join(', ')}`) 69 | throw new Error('Location header was not found in API response') 70 | } 71 | if (typeof url !== 'string') { 72 | throw new Error(`Location header has unexpected value: ${url}`) 73 | } 74 | 75 | const downloadStream = got.stream(url) 76 | const fileWriterStream = createWriteStream(fileName) 77 | 78 | core.info(`Downloading ${url}`) 79 | downloadStream.on('downloadProgress', ({transferred}) => { 80 | core.info(`Progress: ${transferred} B`) 81 | }) 82 | await asyncStream(downloadStream, fileWriterStream) 83 | } finally { 84 | core.endGroup() 85 | } 86 | } 87 | 88 | export async function listFiles(octokit: InstanceType, sha: string): Promise { 89 | core.startGroup('Fetching list of tracked files from GitHub') 90 | try { 91 | const commit = await octokit.rest.git.getCommit({ 92 | commit_sha: sha, 93 | ...github.context.repo 94 | }) 95 | const files = await listGitTree(octokit, commit.data.tree.sha, '') 96 | return files 97 | } finally { 98 | core.endGroup() 99 | } 100 | } 101 | 102 | async function listGitTree(octokit: InstanceType, sha: string, path: string): Promise { 103 | const pathLog = path ? ` at ${path}` : '' 104 | core.info(`Fetching tree ${sha}${pathLog}`) 105 | let truncated = false 106 | let tree = await octokit.rest.git.getTree({ 107 | recursive: 'true', 108 | tree_sha: sha, 109 | ...github.context.repo 110 | }) 111 | 112 | if (tree.data.truncated) { 113 | truncated = true 114 | tree = await octokit.rest.git.getTree({ 115 | tree_sha: sha, 116 | ...github.context.repo 117 | }) 118 | } 119 | 120 | const result: string[] = [] 121 | for (const tr of tree.data.tree) { 122 | const file = `${path}${tr.path}` 123 | if (tr.type === 'blob') { 124 | result.push(file) 125 | } else if (tr.type === 'tree' && truncated) { 126 | const files = await listGitTree(octokit, tr.sha as string, `${file}/`) 127 | result.push(...files) 128 | } 129 | } 130 | 131 | return result 132 | } 133 | -------------------------------------------------------------------------------- /src/utils/markdown-utils.ts: -------------------------------------------------------------------------------- 1 | export enum Align { 2 | Left = ':---', 3 | Center = ':---:', 4 | Right = '---:', 5 | None = '---' 6 | } 7 | 8 | export const Icon = { 9 | skip: '✖️', // ':heavy_multiplication_x:' 10 | success: '✔️', // ':heavy_check_mark:' 11 | fail: '❌' // ':x:' 12 | } 13 | 14 | export function link(title: string, address: string): string { 15 | return `[${title}](${address})` 16 | } 17 | 18 | type ToString = string | number | boolean | Date 19 | export function table(headers: ToString[], align: ToString[], ...rows: ToString[][]): string { 20 | const headerRow = `|${headers.map(tableEscape).join('|')}|` 21 | const alignRow = `|${align.join('|')}|` 22 | const contentRows = rows.map(row => `|${row.map(tableEscape).join('|')}|`).join('\n') 23 | return [headerRow, alignRow, contentRows].join('\n') 24 | } 25 | 26 | export function tableEscape(content: ToString): string { 27 | return content.toString().replace('|', '\\|') 28 | } 29 | 30 | export function fixEol(text?: string): string { 31 | return text?.replace(/\r/g, '') ?? '' 32 | } 33 | 34 | export function ellipsis(text: string, maxLength: number): string { 35 | if (text.length <= maxLength) { 36 | return text 37 | } 38 | 39 | return text.substr(0, maxLength - 3) + '...' 40 | } 41 | 42 | export function formatTime(ms: number): string { 43 | if (ms > 1000) { 44 | return `${Math.round(ms / 1000)}s` 45 | } 46 | 47 | return `${Math.round(ms)}ms` 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/node-utils.ts: -------------------------------------------------------------------------------- 1 | import {normalizeFilePath} from './path-utils' 2 | 3 | export function getExceptionSource( 4 | stackTrace: string, 5 | trackedFiles: string[], 6 | getRelativePath: (str: string) => string 7 | ): {path: string; line: number} | undefined { 8 | const lines = stackTrace.split(/\r?\n/) 9 | const re = /(?:\(| › )(.*):(\d+):\d+(?:\)$| › )/ 10 | 11 | for (const str of lines) { 12 | const match = str.match(re) 13 | if (match !== null) { 14 | const [_, fileStr, lineStr] = match 15 | const filePath = normalizeFilePath(fileStr) 16 | if (filePath.startsWith('internal/') || filePath.includes('/node_modules/')) { 17 | continue 18 | } 19 | const path = getRelativePath(filePath) 20 | if (!path) { 21 | continue 22 | } 23 | if (trackedFiles.includes(path)) { 24 | const line = parseInt(lineStr) 25 | 26 | return {path, line} 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/parse-utils.ts: -------------------------------------------------------------------------------- 1 | export function parseNetDuration(str: string): number { 2 | const durationRe = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)$/ 3 | const durationMatch = str.match(durationRe) 4 | if (durationMatch === null) { 5 | throw new Error(`Invalid format: "${str}" is not NET duration`) 6 | } 7 | 8 | const [_, hourStr, minStr, secStr] = durationMatch 9 | return (parseInt(hourStr) * 3600 + parseInt(minStr) * 60 + parseFloat(secStr)) * 1000 10 | } 11 | 12 | export function parseIsoDate(str: string): Date { 13 | const isoDateRe = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/ 14 | if (str === undefined || !isoDateRe.test(str)) { 15 | throw new Error(`Invalid format: "${str}" is not ISO date`) 16 | } 17 | 18 | return new Date(str) 19 | } 20 | 21 | export function getFirstNonEmptyLine(stackTrace: string): string | undefined { 22 | const lines = stackTrace.split(/\r?\n/g) 23 | return lines.find(str => !/^\s*$/.test(str)) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/path-utils.ts: -------------------------------------------------------------------------------- 1 | export function normalizeDirPath(path: string, addTrailingSlash: boolean): string { 2 | if (!path) { 3 | return path 4 | } 5 | 6 | path = normalizeFilePath(path) 7 | if (addTrailingSlash && !path.endsWith('/')) { 8 | path += '/' 9 | } 10 | return path 11 | } 12 | 13 | export function normalizeFilePath(path: string): string { 14 | if (!path) { 15 | return path 16 | } 17 | 18 | return path.trim().replace(/\\/g, '/') 19 | } 20 | 21 | export function getBasePath(path: string, trackedFiles: string[]): string | undefined { 22 | if (trackedFiles.includes(path)) { 23 | return '' 24 | } 25 | 26 | let max = '' 27 | for (const file of trackedFiles) { 28 | if (path.endsWith(file) && file.length > max.length) { 29 | max = file 30 | } 31 | } 32 | 33 | if (max === '') { 34 | return undefined 35 | } 36 | 37 | const base = path.substr(0, path.length - max.length) 38 | return base 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/slugger.ts: -------------------------------------------------------------------------------- 1 | // Returns HTML element id and href link usable as manual anchor links 2 | // This is needed because Github in check run summary doesn't automatically 3 | // create links out of headings as it normally does for other markdown content 4 | export function slug(name: string): {id: string; link: string} { 5 | const slugId = name 6 | .trim() 7 | .replace(/_/g, '') 8 | .replace(/[./\\]/g, '-') 9 | .replace(/[^\w-]/g, '') 10 | 11 | const id = `user-content-${slugId}` 12 | const link = `#${slugId}` 13 | return {id, link} 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "lib": ["ES2019"], 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "outDir": "./lib", /* Redirect output structure to the directory. */ 7 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 8 | "strict": true, /* Enable all strict type-checking options. */ 9 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | --------------------------------------------------------------------------------