diff --git a/src/services/testRunner/formatOutput.ts b/src/services/testRunner/formatOutput.ts new file mode 100644 index 00000000..279a5612 --- /dev/null +++ b/src/services/testRunner/formatOutput.ts @@ -0,0 +1,15 @@ +// @ts-ignore no declaration files +import * as clc from 'cli-color' +import { ParserOutput } from './parser' + +// TODO: implement better success ouput +// export const formatSuccessOutput = (tap: ParserOutput): string => {} + +export const formatFailOutput = (tap: ParserOutput): string => { + let output = `'TESTS FAILED\n` + tap.failed.forEach((fail) => { + const details = fail.details ? `\n${fail.details}\n\n` : '' + output += ` ✘ ${fail.message}\n${details}` + }) + return output +} diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts index 4dba1966..b6fec26e 100644 --- a/src/services/testRunner/index.ts +++ b/src/services/testRunner/index.ts @@ -4,6 +4,7 @@ import parser from './parser' import { debounce, throttle } from './throttle' import onError from '../sentry/onError' import { clearOutput, displayOutput } from './output' +import { formatFailOutput } from './formatOutput' export interface Payload { stepId: string @@ -52,11 +53,12 @@ const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => { const tap = parser(stdout || '') if (stderr) { - // failures also trigger stderr + // FAIL also trigger stderr if (stdout && stdout.length && !tap.ok) { - const message = tap.message ? tap.message : '' - callbacks.onFail(payload, message) - displayOutput(stdout) + const firstFailMessage = tap.failed[0].message + callbacks.onFail(payload, firstFailMessage) + const output = formatFailOutput(tap) + displayOutput(output) return } else { callbacks.onError(payload) @@ -66,7 +68,7 @@ const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => { } } - // success! + // PASS if (tap.ok) { clearOutput() callbacks.onSuccess(payload) diff --git a/src/services/testRunner/output.ts b/src/services/testRunner/output.ts index ce988d5c..871a1de4 100644 --- a/src/services/testRunner/output.ts +++ b/src/services/testRunner/output.ts @@ -11,22 +11,11 @@ const getOutputChannel = (name: string): vscode.OutputChannel => { const outputChannelName = 'CodeRoad Output' -const parseOutput = (text: string): string => { - let result = '' - for (const line of text.split(/\r?\n/)) { - if (line.match(/^#/) || line.match(/^not ok/)) { - result += line + '\n' - } - } - return result -} - export const displayOutput = (text: string) => { const channel = getOutputChannel(outputChannelName) channel.clear() channel.show(true) - const output = parseOutput(text) - channel.append(output) + channel.append(text) } export const clearOutput = () => { diff --git a/src/services/testRunner/parser.test.ts b/src/services/testRunner/parser.test.ts index 4a85ca6d..d381dc75 100644 --- a/src/services/testRunner/parser.test.ts +++ b/src/services/testRunner/parser.test.ts @@ -1,22 +1,25 @@ import parser from './parser' describe('parser', () => { - test('should detect success', () => { + test('should pass single success', () => { const example = ` -1..2 +1..1 ok 1 - Should pass -ok 2 - Should also pass ` - expect(parser(example)).toEqual({ ok: true }) + expect(parser(example)).toEqual({ ok: true, passed: [{ message: 'Should pass' }], failed: [] }) }) - test('should detect failure', () => { + test('should detect multiple successes', () => { const example = ` -1..3 +1..2 ok 1 - Should pass -not ok 2 - This one fails -ok 3 - Also passes +ok 2 - Should also pass ` - expect(parser(example).ok).toBe(false) + const result = parser(example) + expect(result).toEqual({ + ok: true, + passed: [{ message: 'Should pass' }, { message: 'Should also pass' }], + failed: [], + }) }) test('should detect failure if no tests passed', () => { const example = ` @@ -26,6 +29,15 @@ ok 3 - Also passes # FAIL __tests__/sum.test.js not ok 1 ● sum › should add two numbers together +` + expect(parser(example).ok).toBe(false) + }) + test('should detect single failure among successes', () => { + const example = ` +1..3 +ok 1 - Should pass +not ok 2 - This one fails +ok 3 - Also passes ` expect(parser(example).ok).toBe(false) }) @@ -37,7 +49,7 @@ not ok 2 - First to fail ok 3 - Also passes not ok 4 - Second to fail ` - expect(parser(example).message).toBe('First to fail') + expect(parser(example).failed).toEqual([{ message: 'First to fail' }, { message: 'Second to fail' }]) }) test('should parse mocha tap example', () => { @@ -65,6 +77,68 @@ ok 3 sumItems should total numbers accurately # fail 1 # skip 0 ` - expect(parser(example).message).toBe("sumItems shouldn't return NaN") + expect(parser(example).failed).toEqual([{ message: "sumItems shouldn't return NaN" }]) + }) + test('should capture single error details', () => { + const example = ` +not ok 1 package.json should have a valid "author" key +# AssertionError [ERR_ASSERTION]: no "author" key provided +# at Context. (test/packagejson.test.js:11:12) +# at processImmediate (internal/timers.js:439:21) +# tests 1 +# pass 0 +# fail 1 +# skip 0 +` + const result = parser(example) + expect(result.failed[0].message).toBe('package.json should have a valid "author" key') + expect(result.failed[0].details).toBe(`AssertionError [ERR_ASSERTION]: no "author" key provided +at Context. (test/packagejson.test.js:11:12) +at processImmediate (internal/timers.js:439:21)`) + }) + test('should capture multiple error details', () => { + const example = ` +not ok 1 package.json should have a valid "author" key +# AssertionError [ERR_ASSERTION]: no "author" key provided +# at Context. (test/packagejson.test.js:11:12) +# at processImmediate (internal/timers.js:439:21) +not ok 2 package.json should have a valid "description" key +# AssertionError [ERR_ASSERTION]: no "description" key provided +# tests 1 +# pass 0 +# fail 1 +# skip 0 +` + const result = parser(example) + expect(result.failed[0].message).toBe('package.json should have a valid "author" key') + expect(result.failed[0].details).toBe(`AssertionError [ERR_ASSERTION]: no "author" key provided +at Context. (test/packagejson.test.js:11:12) +at processImmediate (internal/timers.js:439:21)`) + expect(result.failed[1].message).toBe('package.json should have a valid "description" key') + expect(result.failed[1].details).toBe(`AssertionError [ERR_ASSERTION]: no "description" key provided`) + }) + test('should capture multiple error details between successes', () => { + const example = ` +ok 1 first passing test +not ok 2 package.json should have a valid "author" key +# AssertionError [ERR_ASSERTION]: no "author" key provided +# at Context. (test/packagejson.test.js:11:12) +# at processImmediate (internal/timers.js:439:21) +ok 3 some passing test +not ok 4 package.json should have a valid "description" key +# AssertionError [ERR_ASSERTION]: no "description" key provided +ok 5 some passing test +# tests 1 +# pass 0 +# fail 1 +# skip 0 +` + const result = parser(example) + expect(result.failed[0].message).toBe('package.json should have a valid "author" key') + expect(result.failed[0].details).toBe(`AssertionError [ERR_ASSERTION]: no "author" key provided +at Context. (test/packagejson.test.js:11:12) +at processImmediate (internal/timers.js:439:21)`) + expect(result.failed[1].message).toBe('package.json should have a valid "description" key') + expect(result.failed[1].details).toBe(`AssertionError [ERR_ASSERTION]: no "description" key provided`) }) }) diff --git a/src/services/testRunner/parser.ts b/src/services/testRunner/parser.ts index 1f45acd1..fc952f19 100644 --- a/src/services/testRunner/parser.ts +++ b/src/services/testRunner/parser.ts @@ -1,29 +1,72 @@ -interface ParserOutput { +export interface ParserOutput { ok: boolean - message?: string + passed: Array<{ message: string }> + failed: Array<{ message: string; details?: string }> } -const fail = /^not ok \d+\s(\-\s)?(.+)+$/ -const ok = /^ok/ +const r = { + fail: /^not ok \d+\s(\-\s)?(.+)+$/, + pass: /^ok \d+\s(\-\s)?(.+)+$/, + details: /^#\s{2}(.+)$/, +} + +const detect = (type: 'fail' | 'pass' | 'details', text: string) => r[type].exec(text) const parser = (text: string): ParserOutput => { const lines = text.split('\n') - let hasPass = false + + const result: ParserOutput = { + ok: true, + passed: [], + failed: [], + } + + // temporary holder of error detail strings + let currentDetails: string | null = null + + const addCurrentDetails = () => { + const failLength: number = result.failed.length + if (currentDetails && !!failLength) { + result.failed[failLength - 1].details = currentDetails + currentDetails = null + } + } + for (const line of lines) { - if (line.length) { - // parse failed test - const failRegex = fail.exec(line) - if (!!failRegex) { - return { ok: false, message: failRegex[2] } - } - if (!hasPass) { - if (!!ok.exec(line)) { - hasPass = true - } + if (!line.length) { + continue + } + // be optimistic! check for success + const isPass = detect('pass', line) + if (!!isPass) { + result.passed.push({ message: isPass[2].trim() }) + addCurrentDetails() + continue + } + + // check for failure + const isFail = detect('fail', line) + if (!!isFail) { + result.ok = false + addCurrentDetails() + result.failed.push({ message: isFail[2].trim() }) + continue + } + + // check for error details + const isDetails = detect('details', line) + if (!!isDetails) { + const lineDetails: string = isDetails[1].trim() + if (!currentDetails) { + currentDetails = lineDetails + } else { + // @ts-ignore ignore as it must be a string + currentDetails += `\n${lineDetails}` } } } - return { ok: hasPass } + addCurrentDetails() + return result } export default parser