Skip to content

Commit

Permalink
refactor CLI.. fix exitCode
Browse files Browse the repository at this point in the history
  • Loading branch information
75lb committed Aug 31, 2024
1 parent 47a3127 commit e2493b4
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 92 deletions.
2 changes: 1 addition & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import TestRunner from 'test-runner'

const runner = new TestRunner()
runner.start(process.argv.slice(2)).catch(err => console.error(err))
runner.cli(process.argv.slice(2))
86 changes: 23 additions & 63 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
import TestRunnerCore from './lib/core.js'
import Test from './lib/test.js'
import ansi from 'ansi-escape-sequences'
import { pathToFileURL } from 'node:url'
import os from 'node:os'
import util from 'node:util'
import { promises as fs } from 'node:fs'

/* TODO: Factor out node-specific code to enable isomorphism */

function indent (input, indentWith) {
const lines = input.split(os.EOL).map(line => {
return indentWith + line
})
return lines.join(os.EOL)
}

function createTests (arr, map, file) {
for (const [name, testFn] of map) {
const test = new Test(name, testFn)
test.metadata.file = file
arr.push(test)
}
}

process.on('uncaughtException', (err, origin) => {
console.error(`\nAn ${origin} was thrown, possibly in a separate tick.\n`)
console.error(err)
Expand All @@ -35,37 +19,22 @@ process.on('unhandledRejection', (reason, promise) => {
process.exitCode = 1
})

class TestRunner {
tests

constructor (tests) {
this.tests = tests
}

async * run () {
for (const test of this.tests) {
console.log(`${ansi.format(test.metadata.file || '', ['magenta'])} ${test.name}`)
try {
await test.run()
yield test
} catch (err) {
console.log(`${ansi.format(test.metadata.file || '', ['magenta'])} ${test.name} - ${ansi.format('Failed', ['red'])}`)
/* Crash the process */
process.exitCode = 1
throw err
}
}
}
function indent (input, indentWith) {
const lines = input.split(os.EOL).map(line => {
return indentWith + line
})
return lines.join(os.EOL)
}

/* not used by start() */
async runAll () {
const result = []
for await (const test of this.run()) {
result.push(test)
}
return result
function createTests (arr, map, file) {
for (const [name, testFn] of map) {
const test = new Test(name, testFn)
test.metadata.file = file
arr.push(test)
}
}

class TestRunner extends TestRunnerCore {
async start (files) {
const tests = []
const only = []
Expand Down Expand Up @@ -101,24 +70,15 @@ class TestRunner {
}
}
}

async cli (argv) {
try {
await this.start(argv)
} catch (err) {
process.exitCode = 1
console.error(err)
}
}
}

export default TestRunner

/*
- return an EventEmitter from test files.. enables test suite to emit progress, performance, state etc information to the runner UI.
- test other people's projects
- interchangeable iterators, what else can be interchanged?
- Runner has no concept of "skip", if you want to skip a test don't pass it in - same could apply to only
- TestSuite/TestRunner
- states: pending, passed, failed
- properties: test, skip and only maps, testIterator, logger,
- behaviours: run()
# Interchange object to get data from the user into the Runner. Neither the Tests nor Runner need to have any concept of where the tests originated from (files, API etc)
- TestMap
- collection: <test metadata, test Fn> pairs
- properties: metadata (file source etc)
*/
43 changes: 43 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ansi from 'ansi-escape-sequences'

/* TODO: Factor out node-specific code to enable isomorphism */

class TestRunner {
tests

constructor (tests) {
this.tests = tests
}

async * run () {
for (const test of this.tests) {
console.log(`${ansi.format(test.metadata.file || '', ['magenta'])} ${test.name}`)
try {
await test.run()
yield test
} catch (err) {
console.log(`${ansi.format(test.metadata.file || '', ['magenta'])} ${test.name} - ${ansi.format('Failed', ['red'])}`)
/* Crash the process */
throw err
}
}
}

/* not used by start() */
async runAll () {
const result = []
for await (const test of this.run()) {
result.push(test)
}
return result
}
}

export default TestRunner

/*
- test other people's projects
- Interchangeable logger
- interchangeable iterators, what else can be interchanged?
- TestRunner has no concept of "skip", if you want to skip a test don't pass it in - same could apply to only. TestRunnerCli handles them.
*/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"index.js"
],
"scripts": {
"test": "node test/test.js"
"test": "node test/test.js && node test/cli.js"
},
"standard": {
"ignore": [
Expand Down
23 changes: 23 additions & 0 deletions test/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import TestRunner from 'test-runner'
import { strict as a } from 'assert'

/* Node.js version 12 compatible - no module-level await. */

/* Cli loads and runs a test file, test passes */
async function cliPass () {
const runner = new TestRunner()
runner.cli(['./test/fixture/one.js'])
}

/* Cli loads and runs a test file, test fails.. `.cli()` handles and sets exitCode. */
async function cliFail () {
const runner = new TestRunner()
await runner.cli(['./test/fixture/two.js'])
a.equal(process.exitCode, 1)
process.exitCode = 0
}

Promise.all([
cliPass(),
cliFail()
])
37 changes: 10 additions & 27 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import TestRunner from 'test-runner'
import TestRunnerCore from '../lib/core.js'
import Test from '../lib/test.js'
import { strict as a } from 'assert'

Expand All @@ -11,12 +11,11 @@ async function one () {
actuals.push('one')
return 'one'
})
const runner = new TestRunner([test1])
const runner = new TestRunnerCore([test1])
await runner.runAll()
a.equal(test1.result, 'one')
a.deepEqual(actuals, ['one'])
}
one()

/* Async test passes, storing the result */
async function onea () {
Expand All @@ -25,48 +24,32 @@ async function onea () {
actuals.push('one')
return 'one'
})
const runner = new TestRunner([test1])
const runner = new TestRunnerCore([test1])
await runner.runAll()
a.equal(test1.result, 'one')
a.deepEqual(actuals, ['one'])
}
onea()

/* Sync test fails, crashing the process */
/* Sync test fails, crashing the process - no exception handling nor exitCodes */
async function syncFail () {
const actuals = []
const test1 = new Test('syncFail', function syncFail () {
actuals.push('syncFail')
throw new Error('broken')
})
const runner = new TestRunner([test1])
const runner = new TestRunnerCore([test1])
try {
await runner.runAll()
throw new Error('Should not reach here')
} catch (err) {
a.equal(err.message, 'broken')
a.equal(test1.result, undefined)
a.deepEqual(actuals, ['syncFail'])
a.equal(process.exitCode, 1)
process.exitCode = 0
}
}
syncFail()

{ /* Cli loads and runs a test file, test passes */
const runner = new TestRunner()
runner.start(['./test/fixture/one.js'])
}

{ /* Cli loads and runs a test file, test fails */
const runner = new TestRunner()
runner.start(['./test/fixture/two.js'])
.then(() => {
throw new Error('Should not reach here')
})
.catch(err => {
a.equal(err.message, 'broken')
a.equal(process.exitCode, 1)
process.exitCode = 0
})
}
Promise.all([
one(),
onea(),
syncFail()
])

0 comments on commit e2493b4

Please sign in to comment.