borp 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ open-pull-requests-limit: 10
8
+ ignore:
9
+ - dependency-name: standard
10
+ versions:
11
+ - 16.0.3
@@ -0,0 +1,35 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ paths-ignore:
6
+ - 'docs/**'
7
+ - '*.md'
8
+ pull_request:
9
+ paths-ignore:
10
+ - 'docs/**'
11
+ - '*.md'
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ${{matrix.os}}
16
+
17
+ strategy:
18
+ matrix:
19
+ node-version: [18.x, 20.x, 21.x]
20
+ os: [ubuntu-latest, windows-latest]
21
+ steps:
22
+ - uses: actions/checkout@v3
23
+
24
+ - name: Use Node.js
25
+ uses: actions/setup-node@v2
26
+ with:
27
+ node-version: ${{ matrix.node-version }}
28
+
29
+ - name: Install
30
+ run: |
31
+ npm install
32
+
33
+ - name: Run tests
34
+ run: |
35
+ npm run test
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # borp
2
+
3
+ Borp is a typescript-aware runner for tests written using `node:test`.
4
+ It also support code coverage via [c8](http://npm.im/c8).
5
+
6
+ Borp is self-hosted, i.e. Borp runs its own tests.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm i borp --save-dev
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ borp --coverage
18
+ ```
19
+
20
+ Borp will autumatically run all tests files matching `*.test.{js|ts}`.
21
+
22
+ ### Example project setup
23
+
24
+ ```
25
+ .
26
+ ├── src
27
+ │   ├── lib
28
+ │   │   └── add.ts
29
+ │   └── test
30
+ │   └── add.test.ts
31
+ └── tsconfig.json
32
+
33
+ ```
34
+
35
+ As an example, consider having a `src/lib/add.ts` file
36
+
37
+ ```typescript
38
+ export function add (x: number, y: number): number {
39
+ return x + y
40
+ }
41
+ ```
42
+
43
+ and a `src/test/add.test.ts` file:
44
+
45
+ ```typescript
46
+ import { test } from 'node:test'
47
+ import { add } from '../lib/add.js'
48
+ import { strictEqual } from 'node:assert'
49
+
50
+ test('add', () => {
51
+ strictEqual(add(1, 2), 3)
52
+ })
53
+ ```
54
+
55
+ and the following `tsconfig.json`:
56
+
57
+ ```json
58
+ {
59
+ "$schema": "https://json.schemastore.org/tsconfig",
60
+ "compilerOptions": {
61
+ "outDir": "dist",
62
+ "sourceMap": true,
63
+ "target": "ES2022",
64
+ "module": "NodeNext",
65
+ "moduleResolution": "NodeNext",
66
+ "esModuleInterop": true,
67
+ "strict": true,
68
+ "resolveJsonModule": true,
69
+ "removeComments": true,
70
+ "newLine": "lf",
71
+ "noUnusedLocals": true,
72
+ "noFallthroughCasesInSwitch": true,
73
+ "isolatedModules": true,
74
+ "forceConsistentCasingInFileNames": true,
75
+ "skipLibCheck": true,
76
+ "lib": [
77
+ "ESNext"
78
+ ],
79
+ "incremental": true
80
+ }
81
+ }
82
+ ```
83
+
84
+ Note the use of `incremental: true`, which speed up compilation massively.
85
+
86
+ ## Options
87
+
88
+ * `--coverage` or `-C`, enables code coverage
89
+ * `--only` or `-o`, only run `node:test` with the `only` option set
90
+ * `--watch` or `-w`, re-run tests on changes
91
+ * `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms
92
+ * `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default.
93
+ * `--ignore` or `-i`, ignore a glob pattern, and not look for tests there
94
+ * `--pattern` or `-p`, run tests matching the given glob pattern
95
+
96
+ ## License
97
+
98
+ MIT
package/borp.js CHANGED
@@ -2,27 +2,19 @@
2
2
 
3
3
  import { parseArgs } from 'node:util'
4
4
  import { tap, spec } from 'node:test/reporters'
5
- import { run } from 'node:test'
6
- import { glob } from 'glob'
7
- import { findUp } from 'find-up'
8
- import { createRequire } from 'node:module'
9
- import { resolve, join, dirname } from 'node:path'
10
- import { access, readFile } from 'node:fs/promises'
11
- import { execa } from 'execa'
12
-
13
- async function isFileAccessible (filename, directory) {
14
- try {
15
- const filePath = directory ? resolve(directory, filename) : filename
16
- await access(filePath)
17
- return true
18
- } catch (err) {
19
- return false
20
- }
21
- }
5
+ import { mkdtemp, rm, readFile } from 'node:fs/promises'
6
+ import { finished } from 'node:stream/promises'
7
+ import { join, relative } from 'node:path'
8
+ import posix from 'node:path/posix'
9
+ import runWithTypeScript from './lib/run.js'
10
+ import { Report } from 'c8'
11
+ import os from 'node:os'
22
12
 
23
13
  let reporter
14
+ /* c8 ignore next 4 */
24
15
  if (process.stdout.isTTY) {
25
- reporter = spec()
16
+ /* eslint new-cap: "off" */
17
+ reporter = new spec()
26
18
  } else {
27
19
  reporter = tap
28
20
  }
@@ -33,65 +25,77 @@ const args = parseArgs({
33
25
  only: { type: 'boolean', short: 'o' },
34
26
  watch: { type: 'boolean', short: 'w' },
35
27
  pattern: { type: 'string', short: 'p' },
36
- concurrency: { type: 'string', short: 'c' }
28
+ concurrency: { type: 'string', short: 'c', default: os.availableParallelism() - 1 + '' },
29
+ coverage: { type: 'boolean', short: 'C' },
30
+ timeout: { type: 'string', short: 't', default: '30000' },
31
+ 'coverage-exclude': { type: 'string', short: 'X', multiple: true },
32
+ ignore: { type: 'string', short: 'i', multiple: true },
33
+ help: { type: 'boolean', short: 'h' }
37
34
  },
38
35
  allowPositionals: true
39
36
  })
40
37
 
38
+ /* c8 ignore next 4 */
39
+ if (args.values.help) {
40
+ console.log(await readFile(new URL('./README.md', import.meta.url), 'utf8'))
41
+ process.exit(0)
42
+ }
43
+
41
44
  if (args.values.concurrency) {
42
45
  args.values.concurrency = parseInt(args.values.concurrency)
43
46
  }
44
47
 
45
- const tsconfigPath = await findUp('tsconfig.json')
46
-
47
- let prefix = ''
48
-
49
- if (tsconfigPath) {
50
- try {
51
- const _require = createRequire(process.cwd())
52
- const typescriptPathCWD = _require.resolve('typescript')
53
- const tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
54
- if (tscPath) {
55
- const isAccessible = await isFileAccessible(tscPath)
56
- if (isAccessible) {
57
- await execa(tscPath, { cwd: dirname(tsconfigPath), stdio: 'inherit' })
58
- }
59
- }
60
- const tsconfig = JSON.parse(await readFile(tsconfigPath))
61
- const outDir = tsconfig.compilerOptions.outDir
62
- if (outDir) {
63
- prefix = join(dirname(tsconfigPath), outDir)
64
- }
65
- } catch (err) {
66
- console.log(err)
67
- }
48
+ if (args.values.timeout) {
49
+ args.values.timeout = parseInt(args.values.timeout)
68
50
  }
69
51
 
70
- let files
71
- if (args.positionals.length > 0) {
72
- if (prefix) {
73
- files = args.positionals.map((file) => join(prefix, file.replace(/ts$/, 'js')))
74
- } else {
75
- files = args.positionals
76
- }
77
- } else if (args.values.pattern) {
78
- if (prefix) {
79
- args.values.pattern = join(prefix, args.values.pattern)
80
- }
81
- files = await glob(args.values.pattern, { ignore: 'node_modules/**' })
82
- } else {
83
- if (prefix) {
84
- files = await glob(join(prefix, 'test/**/*.test.{cjs,mjs,js}'), { ignore: 'node_modules/**' })
85
- } else {
86
- files = await glob('test/**/*.test.{cjs,mjs,js}', { ignore: 'node_modules/**' })
87
- }
52
+ let covDir
53
+ if (args.values.coverage) {
54
+ covDir = await mkdtemp(join(os.tmpdir(), 'coverage-'))
55
+ process.env.NODE_V8_COVERAGE = covDir
88
56
  }
89
57
 
90
58
  const config = {
91
59
  ...args.values,
92
- files
60
+ files: args.positionals,
61
+ pattern: args.values.pattern,
62
+ cwd: process.cwd()
93
63
  }
94
64
 
95
- run(config)
96
- .compose(reporter)
97
- .pipe(process.stdout)
65
+ try {
66
+ const stream = await runWithTypeScript(config)
67
+
68
+ stream.on('test:fail', () => {
69
+ process.exitCode = 1
70
+ })
71
+
72
+ stream.compose(reporter).pipe(process.stdout)
73
+
74
+ await finished(stream)
75
+
76
+ if (covDir) {
77
+ let exclude = args.values['coverage-exclude']
78
+
79
+ if (exclude && config.prefix) {
80
+ const localPrefix = relative(process.cwd(), config.prefix)
81
+ exclude = exclude.map((file) => posix.join(localPrefix, file))
82
+ }
83
+ const report = Report({
84
+ reporter: ['text'],
85
+ tempDirectory: covDir,
86
+ exclude
87
+ })
88
+
89
+ await report.run()
90
+ }
91
+ /* c8 ignore next 3 */
92
+ } catch (err) {
93
+ console.error(err)
94
+ } finally {
95
+ if (covDir) {
96
+ try {
97
+ await rm(covDir, { recursive: true, maxRetries: 10, retryDelay: 100 })
98
+ /* c8 ignore next 2 */
99
+ } catch {}
100
+ }
101
+ }
@@ -0,0 +1,6 @@
1
+ import { strictEqual } from 'node:assert'
2
+ import { test } from 'node:test'
3
+
4
+ test('this will fail', () => {
5
+ strictEqual(1, 2)
6
+ })
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from './add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add', () => {
6
+ strictEqual(add(1, 2), 3)
7
+ })
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from './add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add2', () => {
6
+ strictEqual(add(3, 2), 5)
7
+ })
@@ -18,6 +18,7 @@
18
18
  "skipLibCheck": true,
19
19
  "lib": [
20
20
  "ESNext"
21
- ]
21
+ ],
22
+ "incremental": true
22
23
  }
23
24
  }
@@ -0,0 +1,4 @@
1
+
2
+ export function add (x: number, y: number): number {
3
+ return x + y
4
+ }
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from '../lib/add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add', () => {
6
+ strictEqual(add(1, 2), 3)
7
+ })
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from '../lib/add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add2', () => {
6
+ strictEqual(add(3, 2), 5)
7
+ })
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "sourceMap": true,
6
+ "target": "ES2022",
7
+ "module": "NodeNext",
8
+ "moduleResolution": "NodeNext",
9
+ "esModuleInterop": true,
10
+ "strict": true,
11
+ "resolveJsonModule": true,
12
+ "removeComments": true,
13
+ "newLine": "lf",
14
+ "noUnusedLocals": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "isolatedModules": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "lib": [
20
+ "ESNext"
21
+ ],
22
+ "incremental": true
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,4 @@
1
+
2
+ export function add (x: number, y: number): number {
3
+ return x + y
4
+ }
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from '../src/add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add2', () => {
6
+ strictEqual(add(3, 2), 5)
7
+ })
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "sourceMap": true,
6
+ "target": "ES2022",
7
+ "module": "NodeNext",
8
+ "moduleResolution": "NodeNext",
9
+ "esModuleInterop": true,
10
+ "strict": true,
11
+ "resolveJsonModule": true,
12
+ "removeComments": true,
13
+ "newLine": "lf",
14
+ "noUnusedLocals": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "isolatedModules": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "lib": [
20
+ "ESNext"
21
+ ],
22
+ "incremental": true
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+
2
+ export function add (x: number, y: number): number {
3
+ return x + y
4
+ }
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { add } from '../src/add.js'
3
+ import { strictEqual } from 'node:assert'
4
+
5
+ test('add2', () => {
6
+ strictEqual(add(3, 2), 5)
7
+ })
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "sourceMap": true,
6
+ "target": "ES2022",
7
+ "module": "NodeNext",
8
+ "moduleResolution": "NodeNext",
9
+ "esModuleInterop": true,
10
+ "strict": true,
11
+ "resolveJsonModule": true,
12
+ "removeComments": true,
13
+ "newLine": "lf",
14
+ "noUnusedLocals": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "isolatedModules": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "lib": [
20
+ "ESNext"
21
+ ],
22
+ "incremental": true
23
+ }
24
+ }
package/lib/run.js ADDED
@@ -0,0 +1,135 @@
1
+ import { run } from 'node:test'
2
+ import { glob } from 'glob'
3
+ import { findUp } from 'find-up'
4
+ import { createRequire } from 'node:module'
5
+ import { join, dirname } from 'node:path'
6
+ import { access, readFile } from 'node:fs/promises'
7
+ import { execa } from 'execa'
8
+
9
+ function deferred () {
10
+ let resolve
11
+ let reject
12
+ const promise = new Promise((_resolve, _reject) => {
13
+ resolve = _resolve
14
+ reject = _reject
15
+ })
16
+ return { resolve, reject, promise }
17
+ }
18
+
19
+ export default async function runWithTypeScript (config) {
20
+ const { cwd } = config
21
+ let pushable = []
22
+ const tsconfigPath = await findUp('tsconfig.json', { cwd })
23
+
24
+ let prefix = ''
25
+ let tscPath
26
+
27
+ if (tsconfigPath) {
28
+ const _require = createRequire(cwd)
29
+ const typescriptPathCWD = _require.resolve('typescript')
30
+ tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
31
+ if (tscPath) {
32
+ // This will throw if we cannot find the `tsc` binary
33
+ await access(tscPath)
34
+
35
+ // Watch is handled aftterwards
36
+ if (!config.watch) {
37
+ const start = Date.now()
38
+ await execa('node', [tscPath], { cwd: dirname(tsconfigPath) })
39
+ pushable.push({
40
+ type: 'test:diagnostic',
41
+ data: {
42
+ nesting: 0,
43
+ message: `TypeScript compilation complete (${Date.now() - start}ms)`
44
+ }
45
+ })
46
+ }
47
+ }
48
+ const tsconfig = JSON.parse(await readFile(tsconfigPath))
49
+ const outDir = tsconfig.compilerOptions.outDir
50
+ if (outDir) {
51
+ prefix = join(dirname(tsconfigPath), outDir)
52
+ }
53
+ }
54
+ config.prefix = prefix
55
+ config.setup = (test) => {
56
+ for (const chunk of pushable) {
57
+ test.reporter.push(chunk)
58
+ }
59
+ pushable = test.reporter
60
+ }
61
+
62
+ let tscChild
63
+ /* eslint prefer-const: "off" */
64
+ let stream
65
+ let p
66
+
67
+ if (config.watch) {
68
+ p = deferred()
69
+ const start = Date.now()
70
+ tscChild = execa('node', [tscPath, '--watch'], { cwd })
71
+ tscChild.stdout.setEncoding('utf8')
72
+ tscChild.stdout.on('data', (data) => {
73
+ if (data.includes('Watching for file changes')) {
74
+ pushable.push({
75
+ type: 'test:diagnostic',
76
+ data: {
77
+ nesting: 0,
78
+ message: `TypeScript compilation complete (${Date.now() - start}ms)`
79
+ }
80
+ })
81
+
82
+ p.resolve()
83
+ }
84
+ if (data.includes('error TS')) {
85
+ pushable.push({
86
+ type: 'test:fail',
87
+ data: {
88
+ nesting: 0,
89
+ name: data.trim()
90
+ }
91
+ })
92
+ }
93
+ })
94
+ if (config.signal) {
95
+ config.signal.addEventListener('abort', () => {
96
+ tscChild.kill()
97
+ })
98
+ }
99
+ }
100
+
101
+ if (p) {
102
+ await p.promise
103
+ }
104
+
105
+ let files = config.files || []
106
+ const ignore = config.ignore || []
107
+ ignore.unshift('node_modules/**/*')
108
+ if (files.length > 0) {
109
+ if (prefix) {
110
+ files = files.map((file) => join(prefix, file.replace(/ts$/, 'js')))
111
+ }
112
+ } else if (config.pattern) {
113
+ let pattern = config.pattern
114
+ if (prefix) {
115
+ pattern = join(prefix, pattern)
116
+ pattern = pattern.replace(/ts$/, 'js')
117
+ }
118
+ files = await glob(pattern, { ignore, cwd, windowsPathsNoEscape: true })
119
+ } else if (prefix) {
120
+ files = await glob(join(prefix, join('**', '*.test.{cjs,mjs,js}')), { ignore, cwd, windowsPathsNoEscape: true })
121
+ } else {
122
+ files = await glob(join('**', '*.test.{cjs,mjs,js}'), { ignore, cwd, windowsPathsNoEscape: true })
123
+ }
124
+
125
+ config.files = files
126
+
127
+ stream = run(config)
128
+
129
+ stream.on('close', () => {
130
+ if (tscChild) {
131
+ tscChild.kill()
132
+ }
133
+ })
134
+ return stream
135
+ }
package/package.json CHANGED
@@ -1,22 +1,28 @@
1
1
  {
2
2
  "name": "borp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "node:test wrapper with TypeScript support",
6
6
  "main": "borp.js",
7
7
  "scripts": {
8
- "test": "standard | snazzy"
8
+ "clean": "rm -rf fixtures/*/dist .test-*",
9
+ "lint": "standard | snazzy",
10
+ "unit": "node borp.js --ignore \"fixtures/**/*\" --coverage --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test/**/*\"",
11
+ "test": "npm run clean ; npm run lint && npm run unit"
9
12
  },
10
13
  "keywords": [],
11
14
  "author": "Matteo Collina <hello@matteocollina.com>",
12
15
  "license": "MIT",
13
16
  "devDependencies": {
17
+ "@matteo.collina/tspl": "^0.1.0",
14
18
  "@types/node": "^20.10.0",
19
+ "desm": "^1.3.0",
15
20
  "snazzy": "^9.0.0",
16
21
  "standard": "^17.1.0",
17
22
  "typescript": "^5.3.2"
18
23
  },
19
24
  "dependencies": {
25
+ "c8": "^8.0.1",
20
26
  "execa": "^8.0.1",
21
27
  "find-up": "^7.0.0",
22
28
  "glob": "^10.3.10"
@@ -0,0 +1,167 @@
1
+ import { test } from 'node:test'
2
+ import { tspl } from '@matteo.collina/tspl'
3
+ import runWithTypeScript from '../lib/run.js'
4
+ import { join } from 'desm'
5
+
6
+ test('ts-esm', async (t) => {
7
+ const { strictEqual, completed, match } = tspl(t, { plan: 4 })
8
+ const config = {
9
+ files: [],
10
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
11
+ }
12
+
13
+ const stream = await runWithTypeScript(config)
14
+
15
+ const names = new Set(['add', 'add2'])
16
+
17
+ stream.once('data', (test) => {
18
+ strictEqual(test.type, 'test:diagnostic')
19
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
20
+ })
21
+
22
+ stream.on('test:pass', (test) => {
23
+ strictEqual(names.has(test.name), true)
24
+ names.delete(test.name)
25
+ })
26
+
27
+ await completed
28
+ })
29
+
30
+ test('ts-cjs', async (t) => {
31
+ const { strictEqual, completed } = tspl(t, { plan: 2 })
32
+ const config = {
33
+ files: [],
34
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-cjs')
35
+ }
36
+
37
+ const stream = await runWithTypeScript(config)
38
+
39
+ const names = new Set(['add', 'add2'])
40
+
41
+ stream.on('test:pass', (test) => {
42
+ strictEqual(names.has(test.name), true)
43
+ names.delete(test.name)
44
+ })
45
+
46
+ await completed
47
+ })
48
+
49
+ test('ts-esm with named failes', async (t) => {
50
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
51
+ const config = {
52
+ files: ['test/add.test.ts'],
53
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
54
+ }
55
+
56
+ const stream = await runWithTypeScript(config)
57
+
58
+ const names = new Set(['add'])
59
+
60
+ stream.once('data', (test) => {
61
+ strictEqual(test.type, 'test:diagnostic')
62
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
63
+ })
64
+
65
+ stream.on('test:pass', (test) => {
66
+ strictEqual(names.has(test.name), true)
67
+ names.delete(test.name)
68
+ })
69
+
70
+ await completed
71
+ })
72
+
73
+ test('pattern', async (t) => {
74
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
75
+ const config = {
76
+ files: [],
77
+ pattern: 'test/*2.test.ts',
78
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
79
+ }
80
+
81
+ const stream = await runWithTypeScript(config)
82
+
83
+ const names = new Set(['add2'])
84
+
85
+ stream.once('data', (test) => {
86
+ strictEqual(test.type, 'test:diagnostic')
87
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
88
+ })
89
+
90
+ stream.on('test:pass', (test) => {
91
+ strictEqual(names.has(test.name), true)
92
+ names.delete(test.name)
93
+ })
94
+
95
+ await completed
96
+ })
97
+
98
+ test('no files', async (t) => {
99
+ const { strictEqual, completed, match } = tspl(t, { plan: 4 })
100
+ const config = {
101
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
102
+ }
103
+
104
+ const stream = await runWithTypeScript(config)
105
+
106
+ const names = new Set(['add', 'add2'])
107
+
108
+ stream.once('data', (test) => {
109
+ strictEqual(test.type, 'test:diagnostic')
110
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
111
+ })
112
+
113
+ stream.on('test:pass', (test) => {
114
+ strictEqual(names.has(test.name), true)
115
+ names.delete(test.name)
116
+ })
117
+
118
+ await completed
119
+ })
120
+
121
+ test('src-to-dist', async (t) => {
122
+ const { strictEqual, completed, match } = tspl(t, { plan: 4 })
123
+ const config = {
124
+ files: [],
125
+ cwd: join(import.meta.url, '..', 'fixtures', 'src-to-dist')
126
+ }
127
+
128
+ const stream = await runWithTypeScript(config)
129
+
130
+ const names = new Set(['add', 'add2'])
131
+
132
+ stream.once('data', (test) => {
133
+ strictEqual(test.type, 'test:diagnostic')
134
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
135
+ })
136
+
137
+ stream.on('test:pass', (test) => {
138
+ strictEqual(names.has(test.name), true)
139
+ names.delete(test.name)
140
+ })
141
+
142
+ await completed
143
+ })
144
+
145
+ test('only-src', async (t) => {
146
+ const { strictEqual, completed, match } = tspl(t, { plan: 4 })
147
+ const config = {
148
+ files: [],
149
+ cwd: join(import.meta.url, '..', 'fixtures', 'only-src')
150
+ }
151
+
152
+ const stream = await runWithTypeScript(config)
153
+
154
+ const names = new Set(['add', 'add2'])
155
+
156
+ stream.once('data', (test) => {
157
+ strictEqual(test.type, 'test:diagnostic')
158
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
159
+ })
160
+
161
+ stream.on('test:pass', (test) => {
162
+ strictEqual(names.has(test.name), true)
163
+ names.delete(test.name)
164
+ })
165
+
166
+ await completed
167
+ })
@@ -0,0 +1,25 @@
1
+ import { test } from 'node:test'
2
+ import { execa } from 'execa'
3
+ import { join } from 'desm'
4
+ import { rejects } from 'node:assert'
5
+
6
+ const borp = join(import.meta.url, '..', 'borp.js')
7
+
8
+ test('limit concurrency', async () => {
9
+ await execa('node', [
10
+ borp,
11
+ '--concurrency',
12
+ '1'
13
+ ], {
14
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
15
+ })
16
+ })
17
+
18
+ test('failing test set correct status code', async () => {
19
+ // execa rejects if status code is not 0
20
+ await rejects(execa('node', [
21
+ borp
22
+ ], {
23
+ cwd: join(import.meta.url, '..', 'fixtures', 'fails')
24
+ }))
25
+ })
@@ -0,0 +1,36 @@
1
+ import { test } from 'node:test'
2
+ import { match, doesNotMatch } from 'node:assert'
3
+ import { execa } from 'execa'
4
+ import { join } from 'desm'
5
+
6
+ const borp = join(import.meta.url, '..', 'borp.js')
7
+
8
+ test('coverage', async () => {
9
+ const res = await execa('node', [
10
+ borp,
11
+ '--coverage'
12
+ ], {
13
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
14
+ })
15
+
16
+ match(res.stdout, /% Stmts/)
17
+ match(res.stdout, /All files/)
18
+ match(res.stdout, /add\.ts/)
19
+ })
20
+
21
+ test('coverage excludes', async () => {
22
+ const res = await execa('node', [
23
+ borp,
24
+ '--coverage',
25
+ '--coverage-exclude=src'
26
+ ], {
27
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
28
+ })
29
+
30
+ match(res.stdout, /% Stmts/)
31
+ match(res.stdout, /All files/)
32
+ doesNotMatch(res.stdout, /add\.ts/)
33
+ // The test files are shown
34
+ match(res.stdout, /add\.test\.ts/)
35
+ match(res.stdout, /add2\.test\.ts/)
36
+ })
@@ -0,0 +1,111 @@
1
+ import { test } from 'node:test'
2
+ import { tspl } from '@matteo.collina/tspl'
3
+ import runWithTypeScript from '../lib/run.js'
4
+ import { join } from 'desm'
5
+ import { mkdtemp, cp, writeFile, rm } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import { once } from 'node:events'
8
+
9
+ test('watch', async (t) => {
10
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
11
+
12
+ const dir = path.resolve(await mkdtemp('.test-watch'))
13
+ await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
14
+ recursive: true
15
+ })
16
+
17
+ const controller = new AbortController()
18
+ t.after(async () => {
19
+ controller.abort()
20
+ try {
21
+ await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
22
+ } catch {}
23
+ })
24
+
25
+ const config = {
26
+ files: [],
27
+ cwd: dir,
28
+ signal: controller.signal,
29
+ watch: true
30
+ }
31
+
32
+ const stream = await runWithTypeScript(config)
33
+
34
+ const fn = (test) => {
35
+ if (test.type === 'test:fail') {
36
+ strictEqual(test.data.name, 'add')
37
+ stream.removeListener('data', fn)
38
+ }
39
+ }
40
+ stream.on('data', fn)
41
+
42
+ const [test] = await once(stream, 'data')
43
+ strictEqual(test.type, 'test:diagnostic')
44
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
45
+
46
+ const toWrite = `
47
+ import { test } from 'node:test'
48
+ import { add } from '../src/add.js'
49
+ import { strictEqual } from 'node:assert'
50
+
51
+ test('add', () => {
52
+ strictEqual(add(1, 2), 4)
53
+ })
54
+ `
55
+ const file = path.join(dir, 'test', 'add.test.ts')
56
+ await writeFile(file, toWrite)
57
+
58
+ await completed
59
+ })
60
+
61
+ test('watch file syntax error', async (t) => {
62
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
63
+
64
+ const dir = path.resolve(await mkdtemp('.test-watch'))
65
+ await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
66
+ recursive: true
67
+ })
68
+
69
+ const controller = new AbortController()
70
+ t.after(async () => {
71
+ controller.abort()
72
+ try {
73
+ await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
74
+ } catch {}
75
+ })
76
+
77
+ const config = {
78
+ files: [],
79
+ cwd: dir,
80
+ signal: controller.signal,
81
+ watch: true
82
+ }
83
+
84
+ const stream = await runWithTypeScript(config)
85
+
86
+ const fn = (test) => {
87
+ if (test.type === 'test:fail') {
88
+ match(test.data.name, /add\.test\.ts/)
89
+ stream.removeListener('data', fn)
90
+ }
91
+ }
92
+ stream.on('data', fn)
93
+
94
+ const [test] = await once(stream, 'data')
95
+ strictEqual(test.type, 'test:diagnostic')
96
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
97
+
98
+ const toWrite = `
99
+ import { test } from 'node:test'
100
+ import { add } from '../src/add.js'
101
+ import { strictEqual } from 'node:assert'
102
+
103
+ test('add', () => {
104
+ strictEqual(add(1, 2), 3
105
+ })
106
+ `
107
+ const file = path.join(dir, 'test', 'add.test.ts')
108
+ await writeFile(file, toWrite)
109
+
110
+ await completed
111
+ })
File without changes
File without changes