Merge pull request #19 from dorny/jest-annotations

Improve JEST tests reports with check-run annotations
This commit is contained in:
Michal Dorner 2020-11-29 21:58:54 +01:00 committed by GitHub
commit 27b542dbb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1652 additions and 136 deletions

View file

@ -12,6 +12,7 @@
"eslint-comments/no-use": "off", "eslint-comments/no-use": "off",
"import/no-namespace": "off", "import/no-namespace": "off",
"no-shadow": "off", "no-shadow": "off",
"no-unused-vars": "off",
"prefer-template": "off", "prefer-template": "off",
"semi": [ "error", "never"], "semi": [ "error", "never"],
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
@ -34,7 +35,7 @@
"@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error", "@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn", "@typescript-eslint/prefer-for-of": "warn",

View file

@ -1,40 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jest-junit tests matches report snapshot 1`] = ` exports[`jest-junit tests matches report snapshot 1`] = `
"# jest tests ❌ Object {
**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed. "annotations": Array [
Object {
"annotation_level": "failure",
"end_line": 10,
"message": "Error: expect(received).toBeTruthy()
Received: false
at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:10:21)
at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12
at new Promise (<anonymous>)
at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41
at processTicksAndRejections (internal/process/task_queues.js:97:5)",
"path": "__tests__/main.test.js",
"start_line": 10,
"title": "[__tests__\\\\main.test.js] Failing test",
},
Object {
"annotation_level": "failure",
"end_line": 2,
"message": "Error: Some error
at Object.throwError (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\lib\\\\main.js:2:9)
at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:14:11)
at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12
at new Promise (<anonymous>)
at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41
at processTicksAndRejections (internal/process/task_queues.js:97:5)",
"path": "lib/main.js",
"start_line": 2,
"title": "[__tests__\\\\main.test.js] Exception in target unit",
},
Object {
"annotation_level": "failure",
"end_line": 21,
"message": "Error: Some error
at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:21:11)
at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12
at new Promise (<anonymous>)
at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)
at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41
at processTicksAndRejections (internal/process/task_queues.js:97:5)",
"path": "__tests__/main.test.js",
"start_line": 21,
"title": "[__tests__\\\\main.test.js] Exception in test",
},
Object {
"annotation_level": "failure",
"end_line": 1,
"message": ": 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:
at new Spec (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Spec.js:116:22)
at new Spec (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\setup_jest_globals.js:78:9)
at specFactory (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Env.js:523:24)
at Env.it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Env.js:592:24)
at Env.it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:134:23)
at it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\jasmineLight.js:100:21)
at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\second.test.js:1:34)
at Runtime._execModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:1245:24)
at Runtime._loadModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:844:12)
at Runtime.requireModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:694:10)
at jasmine2 (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\index.js:230:13)
at runTestInternal (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runner\\\\build\\\\runTest.js:380:22)
at runTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runner\\\\build\\\\runTest.js:472:34)",
"path": "__tests__/second.test.js",
"start_line": 1,
"title": "[__tests__\\\\second.test.js] Timeout test",
},
],
"summary": "**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed.
| Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ | | Result | Suite | Tests | Time | Passed ✔️ | Failed ❌ | Skipped ✖️ |
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | | :---: | :--- | ---: | ---: | ---: | ---: | ---: |
| ❌ | [__tests__\\\\main.test.js](#ts-0-tests-main-test-js) | 4 | 0.486s | 1 | 3 | 0 | | ❌ | [__tests__\\\\main.test.js](#ts-0-tests-main-test-js) | 4 | 0.486s | 1 | 3 | 0 |
| ❌ | [__tests__\\\\second.test.js](#ts-1-tests-second-test-js) | 2 | 0.082s | 0 | 1 | 1 | | ❌ | [__tests__\\\\second.test.js](#ts-1-tests-second-test-js) | 2 | 0.082s | 0 | 1 | 1 |
## Test Suites # Test Suites
### <a id=\\"user-content-ts-0-tests-main-test-js\\" href=\\"#ts-0-tests-main-test-js\\">__tests__\\\\main.test.js</a> ❌ ## <a id=\\"user-content-ts-0-tests-main-test-js\\" href=\\"#ts-0-tests-main-test-js\\">__tests__\\\\main.test.js</a> ❌
#### Test 1 ### Test 1
| Result | Test | Time | Details | | Result | Test | Time |
| :---: | :--- | ---: | --- | | :---: | :--- | ---: |
| ✔️ | Passing test | 1ms | | | ✔️ | Passing test | 1ms |
#### Test 1 Test 1.1 ### Test 1 Test 1.1
| Result | Test | Time | Details | | Result | Test | Time |
| :---: | :--- | ---: | --- | | :---: | :--- | ---: |
| ❌ | Failing test | 2ms | <details><summary>Error: expect(received).toBeTruthy()</summary><pre>Received: false<br> at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:10:21)<br> at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12<br> at new Promise (<anonymous>)<br> at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41<br> at processTicksAndRejections (internal/process/task_queues.js:97:5)</pre></details> | | ❌ | Failing test | 2ms |
| ❌ | Exception in target unit | 0ms | <details><summary>Error: Some error</summary><pre> at Object.throwError (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\lib\\\\main.js:2:9)<br> at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:14:11)<br> at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12<br> at new Promise (<anonymous>)<br> at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41<br> at processTicksAndRejections (internal/process/task_queues.js:97:5)</pre></details> | | ❌ | Exception in target unit | 0ms |
#### Test 2 ### Test 2
| Result | Test | Time | Details | | Result | Test | Time |
| :---: | :--- | ---: | --- | | :---: | :--- | ---: |
| ❌ | Exception in test | 0ms | <details><summary>Error: Some error</summary><pre> at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\main.test.js:21:11)<br> at Object.asyncJestTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:106:37)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:45:12<br> at new Promise (<anonymous>)<br> at mapper (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:28:19)<br> at C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\queueRunner.js:75:41<br> at processTicksAndRejections (internal/process/task_queues.js:97:5)</pre></details> | | ❌ | Exception in test | 0ms |
### <a id=\\"user-content-ts-1-tests-second-test-js\\" href=\\"#ts-1-tests-second-test-js\\">__tests__\\\\second.test.js</a> ❌ ## <a id=\\"user-content-ts-1-tests-second-test-js\\" href=\\"#ts-1-tests-second-test-js\\">__tests__\\\\second.test.js</a> ❌
| Result | Test | Time | Details | | Result | Test | Time |
| :---: | :--- | ---: | --- | | :---: | :--- | ---: |
| ❌ | Timeout test | 4ms | <details><summary>: 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:</summary><pre> at new Spec (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Spec.js:116:22)<br> at new Spec (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\setup_jest_globals.js:78:9)<br> at specFactory (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Env.js:523:24)<br> at Env.it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\Env.js:592:24)<br> at Env.it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmineAsyncInstall.js:134:23)<br> at it (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\jasmine\\\\jasmineLight.js:100:21)<br> at Object.<anonymous> (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\__tests__\\\\second.test.js:1:34)<br> at Runtime._execModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:1245:24)<br> at Runtime._loadModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:844:12)<br> at Runtime.requireModule (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runtime\\\\build\\\\index.js:694:10)<br> at jasmine2 (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-jasmine2\\\\build\\\\index.js:230:13)<br> at runTestInternal (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runner\\\\build\\\\runTest.js:380:22)<br> at runTest (C:\\\\Users\\\\Michal\\\\Workspace\\\\dorny\\\\test-check\\\\reports\\\\jest\\\\node_modules\\\\jest-runner\\\\build\\\\runTest.js:472:34)</pre></details> | | ❌ | Timeout test | 4ms |
| ✖️ | Skipped test | 0ms | Skipped | | ✖️ | Skipped test | 0ms |
" ",
"title": "jest tests ❌",
}
`; `;

View file

@ -2,17 +2,24 @@ import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {parseJestJunit} from '../src/parsers/jest-junit/jest-junit-parser' import {parseJestJunit} from '../src/parsers/jest-junit/jest-junit-parser'
import {ParseOptions} from '../src/parsers/test-parser'
const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'jest-junit.xml'), {encoding: 'utf8'}) const xmlFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'jest-junit.xml'), {encoding: 'utf8'})
const outputPath = __dirname + '/__outputs__/jest-junit.md' const outputPath = __dirname + '/__outputs__/jest-junit.md'
describe('jest-junit tests', () => { describe('jest-junit tests', () => {
it('matches report snapshot', async () => { it('matches report snapshot', async () => {
const result = await parseJestJunit(xmlFixture) const opts: ParseOptions = {
annotations: true,
trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/'
}
const result = await parseJestJunit(xmlFixture, opts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true}) fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, result?.output?.summary ?? '') fs.writeFileSync(outputPath, result?.output?.summary ?? '')
expect(result.success).toBeFalsy() expect(result.success).toBeFalsy()
expect(result?.output?.summary).toMatchSnapshot() expect(result?.output).toMatchSnapshot()
}) })
}) })

View file

@ -8,6 +8,10 @@ description: |
author: 'Michal Dorner <dorner.michal@gmail.com>' author: 'Michal Dorner <dorner.michal@gmail.com>'
inputs: inputs:
annotations:
description: 'Annotate code where exceptions in tests were thrown'
required: true
default: 'true'
fail-on-error: fail-on-error:
description: 'Set this action as failed if test report contains any failed test' description: 'Set this action as failed if test report contains any failed test'
required: true required: true
@ -30,6 +34,9 @@ inputs:
description: 'GitHub Access Token' description: 'GitHub Access Token'
required: false required: false
default: ${{ github.token }} default: ${{ github.token }}
working-directory:
description: 'Relative path under $GITHUB_WORKSPACE where the repository was checked out.'
required: false
outputs: outputs:
conclusion: conclusion:
description: | description: |

1391
dist/index.js generated vendored

File diff suppressed because it is too large Load diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

6
dist/licenses.txt generated vendored
View file

@ -10,6 +10,9 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/exec
MIT
@actions/github @actions/github
MIT MIT
@ -38,6 +41,9 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/io
MIT
@octokit/auth-token @octokit/auth-token
MIT MIT
The MIT License The MIT License

13
package-lock.json generated
View file

@ -9,6 +9,14 @@
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
}, },
"@actions/exec": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
"integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
"requires": {
"@actions/io": "^1.0.1"
}
},
"@actions/github": { "@actions/github": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-4.0.0.tgz",
@ -28,6 +36,11 @@
"tunnel": "0.0.6" "tunnel": "0.0.6"
} }
}, },
"@actions/io": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.5.5", "version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",

View file

@ -30,6 +30,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.4",
"@actions/github": "^4.0.0", "@actions/github": "^4.0.0",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },

View file

@ -1,8 +1,9 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser' import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser'
import {ParseTestResult} from './parsers/test-parser' import {ParseOptions, ParseTestResult} from './parsers/test-parser'
import {getFileContent} from './utils/file-utils' import {getFileContent, normalizeDirPath} from './utils/file-utils'
import {listFiles} from './utils/git'
import {getCheckRunSha} from './utils/github-utils' import {getCheckRunSha} from './utils/github-utils'
async function run(): Promise<void> { async function run(): Promise<void> {
@ -14,18 +15,34 @@ async function run(): Promise<void> {
} }
async function main(): Promise<void> { async function main(): Promise<void> {
const annotations = core.getInput('annotations', {required: true}) === 'true'
const failOnError = core.getInput('fail-on-error', {required: true}) === 'true' const failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
const name = core.getInput('name', {required: true}) const name = core.getInput('name', {required: true})
const path = core.getInput('path', {required: true}) const path = core.getInput('path', {required: true})
const reporter = core.getInput('reporter', {required: true}) const reporter = core.getInput('reporter', {required: true})
const token = core.getInput('token', {required: true}) const token = core.getInput('token', {required: true})
const workDirInput = core.getInput('working-directory', {required: false})
if (workDirInput) {
process.chdir(workDirInput)
}
const workDir = normalizeDirPath(workDirInput || process.cwd(), true)
const octokit = github.getOctokit(token) const octokit = github.getOctokit(token)
const sha = getCheckRunSha() const sha = getCheckRunSha()
// We won't need tracked files if we are not going to create annotations
const trackedFiles = annotations ? await listFiles() : []
const opts: ParseOptions = {
annotations,
trackedFiles,
workDir
}
const parser = getParser(reporter) const parser = getParser(reporter)
const content = getFileContent(path) const content = getFileContent(path)
const result = await parser(content) const result = await parser(content, opts)
const conclusion = result.success ? 'success' : 'failure' const conclusion = result.success ? 'success' : 'failure'
await octokit.checks.create({ await octokit.checks.create({

View file

@ -1,23 +1,26 @@
import {TestResult} from '../test-parser' import {Annotation, ParseOptions, TestResult} from '../test-parser'
import {parseStringPromise} from 'xml2js' import {parseStringPromise} from 'xml2js'
import {JunitReport, TestCase, TestSuite, TestSuites} from './jest-junit-types' import {JunitReport, TestCase, TestSuite, TestSuites} from './jest-junit-types'
import {Align, Icon, link, table, exceptionCell} from '../../utils/markdown-utils' import {Align, Icon, link, table} from '../../utils/markdown-utils'
import {normalizeFilePath} from '../../utils/file-utils'
import {slug} from '../../utils/slugger' import {slug} from '../../utils/slugger'
import {parseAttribute} from '../../utils/xml-utils' import {parseAttribute} from '../../utils/xml-utils'
export async function parseJestJunit(content: string): Promise<TestResult> { export async function parseJestJunit(content: string, options: ParseOptions): Promise<TestResult> {
const junit = (await parseStringPromise(content, { const junit = (await parseStringPromise(content, {
attrValueProcessors: [parseAttribute] attrValueProcessors: [parseAttribute]
})) as JunitReport })) as JunitReport
const testsuites = junit.testsuites const testsuites = junit.testsuites
const success = !(testsuites.$?.failures > 0 || testsuites.$?.errors > 0) const success = !(testsuites.$?.failures > 0 || testsuites.$?.errors > 0)
const icon = success ? Icon.success : Icon.fail
return { return {
success, success,
output: { output: {
title: junit.testsuites.$.name, title: `${junit.testsuites.$.name.trim()} ${icon}`,
summary: getSummary(success, junit) summary: getSummary(success, junit),
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
} }
} }
} }
@ -25,14 +28,12 @@ export async function parseJestJunit(content: string): Promise<TestResult> {
function getSummary(success: boolean, junit: JunitReport): string { function getSummary(success: boolean, junit: JunitReport): string {
const stats = junit.testsuites.$ const stats = junit.testsuites.$
const icon = success ? Icon.success : Icon.fail
const time = `${stats.time.toFixed(3)}s` const time = `${stats.time.toFixed(3)}s`
const skipped = getSkippedCount(junit.testsuites) const skipped = getSkippedCount(junit.testsuites)
const failed = stats.errors + stats.failures const failed = stats.errors + stats.failures
const passed = stats.tests - failed - skipped const passed = stats.tests - failed - skipped
const heading = `# ${stats.name} ${icon}`
const headingLine = `**${stats.tests}** tests were completed in **${time}** with **${passed}** passed, **${skipped}** skipped and **${failed}** failed.` const headingLine = `**${stats.tests}** tests were completed in **${time}** with **${passed}** passed, **${skipped}** skipped and **${failed}** failed.`
const suitesSummary = junit.testsuites.testsuite.map((ts, i) => { const suitesSummary = junit.testsuites.testsuite.map((ts, i) => {
@ -41,7 +42,7 @@ function getSummary(success: boolean, junit: JunitReport): string {
const pass = ts.$.tests - fail - skip const pass = ts.$.tests - fail - skip
const tm = `${ts.$.time.toFixed(3)}s` const tm = `${ts.$.time.toFixed(3)}s`
const result = success ? Icon.success : Icon.fail const result = success ? Icon.success : Icon.fail
const tsName = ts.$.name const tsName = ts.$.name.trim()
const tsAddr = makeSuiteSlug(i, tsName).link const tsAddr = makeSuiteSlug(i, tsName).link
const tsNameLink = link(tsName, tsAddr) const tsNameLink = link(tsName, tsAddr)
return [result, tsNameLink, ts.$.tests, tm, pass, fail, skip] return [result, tsNameLink, ts.$.tests, tm, pass, fail, skip]
@ -54,9 +55,9 @@ function getSummary(success: boolean, junit: JunitReport): string {
) )
const suites = junit.testsuites?.testsuite?.map((ts, i) => getSuiteSummary(ts, i)).join('\n') const suites = junit.testsuites?.testsuite?.map((ts, i) => getSuiteSummary(ts, i)).join('\n')
const suitesSection = `## Test Suites\n\n${suites}` const suitesSection = `# Test Suites\n\n${suites}`
return `${heading}\n${headingLine}\n${summary}\n${suitesSection}` return `${headingLine}\n${summary}\n${suitesSection}`
} }
function getSkippedCount(suites: TestSuites): number { function getSkippedCount(suites: TestSuites): number {
@ -79,16 +80,15 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
const content = groups const content = groups
.map(grp => { .map(grp => {
const header = grp.describe !== '' ? `#### ${grp.describe}\n\n` : '' const header = grp.describe !== '' ? `### ${grp.describe.trim()}\n\n` : ''
const tests = table( const tests = table(
['Result', 'Test', 'Time', 'Details'], ['Result', 'Test', 'Time'],
[Align.Center, Align.Left, Align.Right, Align.None], [Align.Center, Align.Left, Align.Right],
...grp.tests.map(tc => { ...grp.tests.map(tc => {
const name = tc.$.name const name = tc.$.name.trim()
const time = `${Math.round(tc.$.time * 1000)}ms` const time = `${Math.round(tc.$.time * 1000)}ms`
const result = getTestCaseIcon(tc) const result = getTestCaseIcon(tc)
const ex = getTestCaseDetails(tc) return [result, name, time]
return [result, name, time, ex]
}) })
) )
@ -96,10 +96,10 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
}) })
.join('\n') .join('\n')
const tsName = suite.$.name const tsName = suite.$.name.trim()
const tsSlug = makeSuiteSlug(index, tsName) const tsSlug = makeSuiteSlug(index, tsName)
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>` const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
return `### ${tsNameLink} ${icon}\n\n${content}` return `## ${tsNameLink} ${icon}\n\n${content}`
} }
function getTestCaseIcon(test: TestCase): string { function getTestCaseIcon(test: TestCase): string {
@ -108,20 +108,58 @@ function getTestCaseIcon(test: TestCase): string {
return Icon.success return Icon.success
} }
function getTestCaseDetails(test: TestCase): string {
if (test.skipped !== undefined) {
return 'Skipped'
}
if (test.failure !== undefined) {
const failure = test.failure.join('\n')
return exceptionCell(failure)
}
return ''
}
function makeSuiteSlug(index: number, name: string): {id: string; link: string} { function makeSuiteSlug(index: number, name: string): {id: string; link: string} {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths // use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
return slug(`ts-${index}-${name}`) return slug(`ts-${index}-${name}`)
} }
function getAnnotations(junit: JunitReport, workDir: string, trackedFiles: string[]): Annotation[] {
const annotations: Annotation[] = []
for (const suite of junit.testsuites.testsuite) {
for (const tc of suite.testcase) {
if (!tc.failure) {
continue
}
for (const ex of tc.failure) {
const src = exceptionThrowSource(ex, workDir, trackedFiles)
if (src === null) {
continue
}
annotations.push({
annotation_level: 'failure',
start_line: src.line,
end_line: src.line,
path: src.file,
message: ex,
title: `[${suite.$.name}] ${tc.$.name.trim()}`
})
}
}
}
return annotations
}
export function exceptionThrowSource(
ex: string,
workDir: string,
trackedFiles: string[]
): {file: string; line: number; column: number} | null {
const lines = ex.split(/\r?\n/)
const re = /\((.*):(\d+):(\d+)\)$/
for (const str of lines) {
const match = str.match(re)
if (match !== null) {
const [_, fileStr, lineStr, colStr] = match
const filePath = normalizeFilePath(fileStr)
const file = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath
if (trackedFiles.includes(file)) {
const line = parseInt(lineStr)
const column = parseInt(colStr)
return {file, line, column}
}
}
}
return null
}

View file

@ -1,8 +1,25 @@
import {Endpoints} from '@octokit/types' import {Endpoints} from '@octokit/types'
type OutputParameters = Endpoints['POST /repos/:owner/:repo/check-runs']['parameters']['output'] export type OutputParameters = Endpoints['POST /repos/:owner/:repo/check-runs']['parameters']['output']
export type Annotation = {
path: string
start_line: number
end_line: number
start_column?: number
end_column?: number
annotation_level: 'notice' | 'warning' | 'failure'
message: string
title?: string
raw_details?: string
}
export type ParseTestResult = (content: string) => Promise<TestResult> export type ParseTestResult = (content: string, options: ParseOptions) => Promise<TestResult>
export interface ParseOptions {
annotations: boolean
workDir: string
trackedFiles: string[]
}
export interface TestResult { export interface TestResult {
success: boolean success: boolean

21
src/utils/exec.ts Normal file
View file

@ -0,0 +1,21 @@
import {exec as execImpl, ExecOptions} from '@actions/exec'
// Wraps original exec() function
// Returns exit code and whole stdout/stderr
export default async function exec(commandLine: string, args?: string[], options?: ExecOptions): Promise<ExecResult> {
options = options || {}
let stdout = ''
let stderr = ''
options.listeners = {
stdout: (data: Buffer) => (stdout += data.toString()),
stderr: (data: Buffer) => (stderr += data.toString())
}
const code = await execImpl(commandLine, args, options)
return {code, stdout, stderr}
}
export interface ExecResult {
code: number
stdout: string
stderr: string
}

View file

@ -11,3 +11,23 @@ export function getFileContent(path: string): string {
return fs.readFileSync(path, {encoding: 'utf8'}) return fs.readFileSync(path, {encoding: 'utf8'})
} }
export function normalizeDirPath(path: string, trailingSeparator: boolean): string {
if (!path) {
return path
}
path = normalizeFilePath(path)
if (trailingSeparator && !path.endsWith('/')) {
path += '/'
}
return path
}
export function normalizeFilePath(path: string): string {
if (!path) {
return path
}
return path.trim().replace(/\\/g, '/')
}

22
src/utils/git.ts Normal file
View file

@ -0,0 +1,22 @@
import * as core from '@actions/core'
import exec from './exec'
export async function listFiles(): Promise<string[]> {
core.startGroup('Listing all files tracked by git')
let output = ''
try {
output = (await exec('git', ['ls-files', '-z'])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}
return output.split('\u0000').filter(s => s.length > 0)
}
function fixStdOutNullTermination(): void {
// Previous command uses NULL as delimiters and output is printed to stdout.
// We have to make sure next thing written to stdout will start on new line.
// Otherwise things like ::set-output wouldn't work.
core.info('')
}

View file

@ -21,32 +21,12 @@ export function link(title: string, address: string): string {
type ToString = string | number | boolean | Date type ToString = string | number | boolean | Date
export function table(headers: ToString[], align: ToString[], ...rows: ToString[][]): string { export function table(headers: ToString[], align: ToString[], ...rows: ToString[][]): string {
const headerRow = `| ${headers.join(' | ')} |` const headerRow = `| ${headers.map(tableEscape).join(' | ')} |`
const alignRow = `| ${align.join(' | ')} |` const alignRow = `| ${align.join(' | ')} |`
const contentRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n') const contentRows = rows.map(row => `| ${row.map(tableEscape).join(' | ')} |`).join('\n')
return [headerRow, alignRow, contentRows].join('\n') return [headerRow, alignRow, contentRows].join('\n')
} }
export function exceptionCell(ex: string): string { export function tableEscape(content: ToString): string {
const lines = ex.split(/\r?\n/) return content.toString().replace('|', '\\|')
if (lines.length === 0) {
return ''
}
const summary = tableEscape(lines.shift()?.trim() || '')
const emptyLine = /^\s*$/
const firstNonEmptyLine = lines.findIndex(l => !emptyLine.test(l))
if (firstNonEmptyLine === -1) {
return summary
}
const contentLines = firstNonEmptyLine > 0 ? lines.slice(firstNonEmptyLine) : lines
const content = '<pre>' + tableEscape(contentLines.join('<br>')) + '</pre>'
return details(summary, content)
}
export function tableEscape(content: string): string {
return content.replace('|', '\\|')
} }