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",
"import/no-namespace": "off",
"no-shadow": "off",
"no-unused-vars": "off",
"prefer-template": "off",
"semi": [ "error", "never"],
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
@ -34,7 +35,7 @@
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "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-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",

View file

@ -1,40 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jest-junit tests matches report snapshot 1`] = `
"# jest tests ❌
**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed.
Object {
"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 ✖️ |
| :---: | :--- | ---: | ---: | ---: | ---: | ---: |
| ❌ | [__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 |
## 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 |
| :---: | :--- | ---: | --- |
| ✔️ | Passing test | 1ms | |
| Result | Test | Time |
| :---: | :--- | ---: |
| ✔️ | Passing test | 1ms |
#### Test 1 Test 1.1
### Test 1 Test 1.1
| Result | Test | Time | Details |
| :---: | :--- | ---: | --- |
| ❌ | 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> |
| ❌ | 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> |
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Failing test | 2ms |
| ❌ | Exception in target unit | 0ms |
#### Test 2
### Test 2
| Result | Test | Time | Details |
| :---: | :--- | ---: | --- |
| ❌ | 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> |
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | 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 |
| :---: | :--- | ---: | --- |
| ❌ | 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> |
| ✖️ | Skipped test | 0ms | Skipped |
"
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Timeout test | 4ms |
| ✖️ | Skipped test | 0ms |
",
"title": "jest tests ❌",
}
`;

View file

@ -2,17 +2,24 @@ import * as fs from 'fs'
import * as path from 'path'
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 outputPath = __dirname + '/__outputs__/jest-junit.md'
describe('jest-junit tests', () => {
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.writeFileSync(outputPath, result?.output?.summary ?? '')
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>'
inputs:
annotations:
description: 'Annotate code where exceptions in tests were thrown'
required: true
default: 'true'
fail-on-error:
description: 'Set this action as failed if test report contains any failed test'
required: true
@ -30,6 +34,9 @@ inputs:
description: 'GitHub Access Token'
required: false
default: ${{ github.token }}
working-directory:
description: 'Relative path under $GITHUB_WORKSPACE where the repository was checked out.'
required: false
outputs:
conclusion:
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.
@actions/exec
MIT
@actions/github
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.
@actions/io
MIT
@octokit/auth-token
MIT
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",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-4.0.0.tgz",
@ -28,6 +36,11 @@
"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": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",

View file

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

View file

@ -1,8 +1,9 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser'
import {ParseTestResult} from './parsers/test-parser'
import {getFileContent} from './utils/file-utils'
import {ParseOptions, ParseTestResult} from './parsers/test-parser'
import {getFileContent, normalizeDirPath} from './utils/file-utils'
import {listFiles} from './utils/git'
import {getCheckRunSha} from './utils/github-utils'
async function run(): Promise<void> {
@ -14,18 +15,34 @@ async function run(): 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 name = core.getInput('name', {required: true})
const path = core.getInput('path', {required: true})
const reporter = core.getInput('reporter', {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 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 content = getFileContent(path)
const result = await parser(content)
const result = await parser(content, opts)
const conclusion = result.success ? 'success' : 'failure'
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 {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 {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, {
attrValueProcessors: [parseAttribute]
})) as JunitReport
const testsuites = junit.testsuites
const success = !(testsuites.$?.failures > 0 || testsuites.$?.errors > 0)
const icon = success ? Icon.success : Icon.fail
return {
success,
output: {
title: junit.testsuites.$.name,
summary: getSummary(success, junit)
title: `${junit.testsuites.$.name.trim()} ${icon}`,
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 {
const stats = junit.testsuites.$
const icon = success ? Icon.success : Icon.fail
const time = `${stats.time.toFixed(3)}s`
const skipped = getSkippedCount(junit.testsuites)
const failed = stats.errors + stats.failures
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 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 tm = `${ts.$.time.toFixed(3)}s`
const result = success ? Icon.success : Icon.fail
const tsName = ts.$.name
const tsName = ts.$.name.trim()
const tsAddr = makeSuiteSlug(i, tsName).link
const tsNameLink = link(tsName, tsAddr)
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 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 {
@ -79,16 +80,15 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
const content = groups
.map(grp => {
const header = grp.describe !== '' ? `#### ${grp.describe}\n\n` : ''
const header = grp.describe !== '' ? `### ${grp.describe.trim()}\n\n` : ''
const tests = table(
['Result', 'Test', 'Time', 'Details'],
[Align.Center, Align.Left, Align.Right, Align.None],
['Result', 'Test', 'Time'],
[Align.Center, Align.Left, Align.Right],
...grp.tests.map(tc => {
const name = tc.$.name
const name = tc.$.name.trim()
const time = `${Math.round(tc.$.time * 1000)}ms`
const result = getTestCaseIcon(tc)
const ex = getTestCaseDetails(tc)
return [result, name, time, ex]
return [result, name, time]
})
)
@ -96,10 +96,10 @@ function getSuiteSummary(suite: TestSuite, index: number): string {
})
.join('\n')
const tsName = suite.$.name
const tsName = suite.$.name.trim()
const tsSlug = makeSuiteSlug(index, tsName)
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 {
@ -108,20 +108,58 @@ function getTestCaseIcon(test: TestCase): string {
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} {
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths
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'
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 {
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'})
}
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
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 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')
}
export function exceptionCell(ex: string): string {
const lines = ex.split(/\r?\n/)
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('|', '\\|')
export function tableEscape(content: ToString): string {
return content.toString().replace('|', '\\|')
}