Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reporters #10

Merged
merged 35 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,27 @@ jobs:
node-version: [18.x, 20.x, 21.x]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install
run: |
npm install

- name: Lint
run: |
npm run lint

- name: Run tests
run: |
npm run test
npm run unit -- --reporter spec --reporter md:report.md --reporter gh

- name: Upload report
shell: bash
if: success() || failure()
run: |
cat report.md >> "$GITHUB_STEP_SUMMARY"
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,70 @@ Note the use of `incremental: true`, which speed up compilation massively.
* `--ignore` or `-i`, ignore a glob pattern, and not look for tests there
* `--expose-gc`, exposes the gc() function to tests
* `--pattern` or `-p`, run tests matching the given glob pattern
* `--reporter` or `-r`, set up a reporter, use a colon to set a file destination. Default: `spec`.

## Reporters

Here are the available reporters:

* `md`: creates a markdown table, useful for setting up a Summary in your GitHub Action
* `gh`: emits `::error` workflow commands for GitHub Actions to show inlined error. Enabled by default when running on GHA.
* `tap`: outputs the test results in the TAP format.
* `spec`: outputs the test results in a human-readable format.
* `dot`: outputs the test results in a compact format, where each passing test is represented by a ., and each failing test is represented by a X.
* `junit`: outputs test results in a jUnit XML format

## GitHub Action Summary

The following will automatically show the summary of the test run in the summary page of GitHub Actions.

```yaml
name: ci

on:
push:
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'

jobs:
test:
runs-on: ${{matrix.os}}

strategy:
matrix:
node-version: [18.x, 20.x, 21.x]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install
run: |
npm install

- name: Lint
run: |
npm run lint

- name: Run tests
run: |
npm run unit -- --reporter spec --reporter md:report.md

- name: Upload report
shell: bash
if: success() || failure()
run: |
cat report.md >> "$GITHUB_STEP_SUMMARY"
```

## License

Expand Down
56 changes: 46 additions & 10 deletions borp.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
#! /usr/bin/env node

import { parseArgs } from 'node:util'
import { tap, spec } from 'node:test/reporters'
import Reporters from 'node:test/reporters'
import { mkdtemp, rm, readFile } from 'node:fs/promises'
import { createWriteStream } from 'node:fs'
import { finished } from 'node:stream/promises'
import { join, relative } from 'node:path'
import posix from 'node:path/posix'
import runWithTypeScript from './lib/run.js'
import { MarkdownReporter, GithubWorkflowFailuresReporter } from './lib/reporters.js'
import { Report } from 'c8'
import os from 'node:os'
import { execa } from 'execa'

let reporter
/* c8 ignore next 4 */
if (process.stdout.isTTY) {
/* eslint new-cap: "off" */
reporter = new spec()
} else {
reporter = tap
}
process.on('unhandledRejection', (err) => {
console.error(err)
process.exit(1)
})

const args = parseArgs({
args: process.argv.slice(2),
Expand All @@ -32,7 +31,13 @@ const args = parseArgs({
'coverage-exclude': { type: 'string', short: 'X', multiple: true },
ignore: { type: 'string', short: 'i', multiple: true },
'expose-gc': { type: 'boolean' },
help: { type: 'boolean', short: 'h' }
help: { type: 'boolean', short: 'h' },
reporter: {
type: 'string',
short: 'r',
default: ['spec'],
multiple: true
}
},
allowPositionals: true
})
Expand Down Expand Up @@ -79,13 +84,44 @@ const config = {
}

try {
const pipes = []

const reporters = {
...Reporters,
md: new MarkdownReporter(config),
gh: new GithubWorkflowFailuresReporter(config),
/* eslint new-cap: "off" */
spec: new Reporters.spec()
}

// If we're running in a GitHub action, adds the gh reporter
// by default so that we can report failures to GitHub
if (process.env.GITHUB_ACTION) {
args.values.reporter.push('gh')
}

for (const input of args.values.reporter) {
const [name, dest] = input.split(':')
const reporter = reporters[name]
if (!reporter) {
throw new Error(`Unknown reporter: ${name}`)
}
let output = process.stdout
if (dest) {
output = createWriteStream(dest)
}
pipes.push([reporter, output])
}

const stream = await runWithTypeScript(config)

stream.on('test:fail', () => {
process.exitCode = 1
})

stream.compose(reporter).pipe(process.stdout)
for (const [reporter, output] of pipes) {
stream.compose(reporter).pipe(output)
}

await finished(stream)

Expand Down
114 changes: 114 additions & 0 deletions lib/reporters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Transform } from 'node:stream'
import { fileURLToPath } from 'node:url'

function normalizeFile (file, cwd) {
let res = file
if (file.startsWith('file://')) {
try {
res = fileURLToPath(new URL(file))
} catch (err) {
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
res = fileURLToPath(new URL(file.replace('file:///', 'file://')))
}
}
Comment on lines +7 to +13
Copy link
Contributor

@aduh95 aduh95 Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think your assesment is correct here, my understanding is that the URL that failed to be converted to path is file:///foo/test/add.test.ts, which indeed does not make sense on Windows. But that's a value constructed in your tests, not one generated by the test runner IIUC.

Suggested change
try {
res = fileURLToPath(new URL(file))
} catch (err) {
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
res = fileURLToPath(new URL(file.replace('file:///', 'file://')))
}
}
res = fileURLToPath(file)

I'll try to send a PR.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was failing with a Windows path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#13

}
res = res.replace(cwd, '')
if (res.startsWith('/') || res.startsWith('\\')) {
res = res.slice(1)
}
return res
}

function eventToLine (event) {
return `* __${event.data.name}__, duration ${event.data.details.duration_ms}ms, line ${event.data.line}\n`
}

export class MarkdownReporter extends Transform {
constructor (opts) {
super({
...opts,
objectMode: true
})

this._files = {}
this._cwd = opts?.cwd
}

getFile (path) {
const file = this._files[path] || {
pass: [],
fail: []
}
this._files[path] = file
return file
}

_transform (event, encoding, callback) {
if (!event.data.file) {
callback()
return
}

const path = normalizeFile(event.data.file, this._cwd)
const file = this.getFile(path)
switch (event.type) {
case 'test:pass':
file.pass.push(event)
break
case 'test:fail':
file.fail.push(event)
break
}

callback()
}

_flush (callback) {
this.push('# Summary\n')
for (const [path, file] of Object.entries(this._files)) {
this.push(`## ${path}\n`)
if (file.pass.length > 0) {
this.push('### :white_check_mark: Pass\n')
for (const event of file.pass) {
this.push(eventToLine(event))
}
}
if (file.fail.length > 0) {
this.push('### :x: Fail\n')
for (const event of file.fail) {
this.push(eventToLine(event))
}
}
}
this.push(null)
callback()
}
}

export class GithubWorkflowFailuresReporter extends Transform {
constructor (opts) {
super({
...opts,
objectMode: true
})

this._files = {}
this._cwd = opts?.cwd
}

_transform (event, encoding, callback) {
if (!event.data.file) {
callback()
return
}

const path = normalizeFile(event.data.file, this._cwd)
switch (event.type) {
case 'test:fail':
this.push(`::error file=${path},line=${event.data.line}::${event.data.name}\n`)
break
}

callback()
}
}
1 change: 1 addition & 0 deletions lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default async function runWithTypeScript (config) {
}
config.prefix = prefix
config.setup = (test) => {
/* c8 ignore next 12 */
if (test.reporter) {
for (const chunk of pushable) {
test.reporter.push(chunk)
Expand Down
2 changes: 2 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { rejects } from 'node:assert'

const borp = join(import.meta.url, '..', 'borp.js')

delete process.env.GITHUB_ACTION

test('limit concurrency', async () => {
await execa('node', [
borp,
Expand Down
1 change: 1 addition & 0 deletions test/coverage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { match, doesNotMatch } from 'node:assert'
import { execa } from 'execa'
import { join } from 'desm'

delete process.env.GITHUB_ACTION
const borp = join(import.meta.url, '..', 'borp.js')

test('coverage', async () => {
Expand Down
Loading