Skip to content
This repository has been archived by the owner on Dec 28, 2024. It is now read-only.

Commit

Permalink
feat: add TAP reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
andreidmt committed Feb 6, 2023
1 parent 444ce0e commit 8015a5e
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 48 deletions.
3 changes: 0 additions & 3 deletions lib/formatter.tap.js

This file was deleted.

16 changes: 0 additions & 16 deletions lib/lib.node.js

This file was deleted.

58 changes: 42 additions & 16 deletions lib/formatter.fancy.js → lib/reporters/fancy.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
/** @typedef {import("./fn.runTest").Diagnosis} Diagnosis */
/** @typedef {import("./types").IntroFormatter} IntroFormatter */
/** @typedef {import("./types").TestFormatter} TestFormatter */
/** @typedef {import("./types").SuiteFormatter} SuiteFormatter */
/** @typedef {import("./types").Reporter} Reporter */

import path from "node:path"
import { red, green, black, gray, white, bold } from "./lib.colors.js"
import { formatHrTime } from "./lib.node.js"

import { red, green, black, gray, white, bold } from "../utils/colors.js"
import { formatHrTime } from "../utils/node.js"

const PASS_TAG = green.bgSecondary(black.fg(" PASS "))
const FAIL_TAG = red.bgSecondary(black.fg(" FAIL "))

/**
* Format the intro message similar to Jest's default reporter
*
* @type {IntroFormatter}
*
* @example
* formatIntro({ count: 1 })
* // => "Running 1 file through tsd-lite\n"
*
* @example
* formatIntro({ count: 4 })
* // => "Running 4 files through tsd-lite\n"
*/
const formatIntro = ({ count }) => {
const files = count === 1 ? "file" : "files"

return bold(`Running ${count} ${files} through tsd-lite\n`)
}

/**
* Emphasize the file name by contrasting it with the rest of the path
*
Expand All @@ -26,20 +49,19 @@ const highlightFileName = filePath => {
}

/**
* Format each test file result similar to Jest's default reporter
* Format test file result similar to Jest's default reporter
*
* @param {Diagnosis} input
* @returns {string}
* @type {TestFormatter}
*
* @example
* formatTestResult({
* formatTest({
* name: "src/__tests__/index.test.js",
* errors: [],
* })
* // => " PASS src/__tests__/index.test.js 0.8s"
*
* @example
* formatTestResult({
* formatTest({
* name: "src/__tests__/index.test.js",
* errors: [
* { row: 1, column: 1, message: "Unexpected token" },
Expand All @@ -49,7 +71,7 @@ const highlightFileName = filePath => {
* // => " (1:1) Unexpected token
* // => ""
*/
export const formatTestResult = ({ name, duration, errors = [] }) => {
const formatTest = ({ result: { name, duration, errors = [] } }) => {
const isPass = errors.length === 0
const friendlyDuration = formatHrTime(duration)
const title = `${isPass ? PASS_TAG : FAIL_TAG} ${highlightFileName(
Expand All @@ -70,23 +92,20 @@ export const formatTestResult = ({ name, duration, errors = [] }) => {
}

/**
* Format the summary of multiple test files results
* Format the summary of multiple tests similar to Jest's default reporter
*
* @param {Object} props
* @param {number} props.passCount
* @param {number} props.failCount
* @param {[number, number]} props.duration
* @type {SuiteFormatter}
*
* @example
* formatSuiteResult({
* formatSuite({
* passCount: 1,
* failCount: 1,
* duration: [5, 420000000]
* })
* // => "Summary: 1 failed, 1 passed, 2 total"
* // => "Duration: 5.42s"
*/
export const formatSuiteResult = ({ passCount, failCount, duration }) => {
const formatSuite = ({ passCount, failCount, duration }) => {
const filesFail = red.fgSecondary(`${failCount} failed`)
const filesPass = green.fgSecondary(`${passCount} passed`)
const filesTotal = `${failCount + passCount} total`
Expand All @@ -100,3 +119,10 @@ export const formatSuiteResult = ({ passCount, failCount, duration }) => {
`${durationLabel} ${formatHrTime(duration)}`,
].join("\n")
}

/** @type {Reporter} */
export default {
formatIntro,
formatTest,
formatSuite,
}
108 changes: 108 additions & 0 deletions lib/reporters/tap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/** @typedef {import("./types").IntroFormatter} IntroFormatter */
/** @typedef {import("./types").TestFormatter} TestFormatter */
/** @typedef {import("./types").SuiteFormatter} SuiteFormatter */
/** @typedef {import("./types").Reporter} Reporter */

import { formatHrTime } from "../utils/node.js"

/**
* Format intro to TAP format
*
* @type {IntroFormatter}
*
* @example
* formatIntro({ count: 2 })
* // => "TAP version 13"
* // => "1..2"
*/
const formatIntro = ({ count, description }) =>
["TAP version 14", `1..${count}`, description ? `# ${description}` : ""].join(
"\n"
)

/**
* Format test file result to TAP format
*
* @type {TestFormatter}
*
* @example
* formatTestResult({
* name: "src/__tests__/index.test.js",
* errors: [],
* })
* // => "ok 1 - src/__tests__/index.test.js 0.8s"
*
* @example
* formatTestResult({
* name: "src/__tests__/index.test.js",
* errors: [
* { row: 1, column: 1, message: "Unexpected token" },
* ],
* })
* // => "not ok 1 - src/__tests__/index.test.js 1.2s"
* // => " ---"
* // => " message: 'Unexpected token'
* // => " severity: fail"
* // => " data:"
* // => " row: 1"
* // => " column: 1"
* // => " ..."
*/
const formatTest = ({ index, result: { name, duration, errors } }) => {
const humanReadableDuration = formatHrTime(duration)

if (errors.length === 0) {
return `ok ${index} - ${name} ${humanReadableDuration}`
}

const output = [`not ok - ${index} ${name} ${humanReadableDuration}`]

for (const error of errors) {
output.push(
` ---`,
` message: '${error.message}'`,
` severity: fail`,
` data:`,
` row: ${error.row}`,
` column: ${error.column}`,
` ...`
)
}

return output.join("\n")
}

/**
* Format the summary of multiple tests to TAP format
*
* @type {SuiteFormatter}
*
* @example
* formatSuiteResult({
* passCount: 1,
* failCount: 1,
* duration: [5, 420000000]
* })
* // => "1..2"
* // => "# tests 2"
* // => "# pass 1"
* // => "# fail 1"
* // => "Duration: 5.42s"
*/
const formatSuite = ({ passCount, failCount, duration }) => {
const totalCount = passCount + failCount

return [
`# tests ${totalCount}`,
`# pass ${passCount}`,
`# fail ${failCount}`,
`# time ${formatHrTime(duration)}`,
].join("\n")
}

/** @type {Reporter} */
export default {
formatIntro,
formatTest,
formatSuite,
}
23 changes: 23 additions & 0 deletions lib/reporters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { TestResult } from "../runTest.js"

export type IntroFormatter = (props: {
count: number
description?: string
}) => string

export type TestFormatter = (props: {
index?: number
result: TestResult
}) => string

export type SuiteFormatter = (props: {
passCount: number
failCount: number
duration: [number, number]
}) => string

export type Reporter = {
formatIntro: IntroFormatter
formatTest: TestFormatter
formatSuite: SuiteFormatter
}
42 changes: 42 additions & 0 deletions lib/runSuite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { runTest } from "./runTest.js"

/**
* @callback OnTestFinish
* @param {ReturnType<runTest>} result
* @param {number} index
* @returns {void}
*/

/**
* @callback OnSuiteFinish
* @param {ReturnType<runTest>[]} results
* @returns {void}
*/

/**
* Run a suite of test files and return a more friendly result format
*
* @param {string[]} absolutePaths
* @param {Object} props
* @param {OnTestFinish} props.onTestFinish
* @param {OnSuiteFinish} props.onSuiteFinish
* @returns {void}
*
* @example
* runSuite(["/home/lorem/src/fn.test-d.ts"], {
* onTestFinish: (result, index) => {},
* onSuiteDone: (results) => {},
* })
*/
export const runSuite = (absolutePaths, { onTestFinish, onSuiteFinish }) => {
const results = absolutePaths.map((item, index) => {
// TODO: async via worker threads maybe?
const result = runTest(item)

onTestFinish(result, index)

return result
})

onSuiteFinish(results)
}
27 changes: 20 additions & 7 deletions lib/fn.runTest.js → lib/runTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,47 @@ import tsdLite from "tsd-lite"
const tsd = /** @type {typeof tsdLite} */ (tsdLite.default)

/**
* @typedef DiagnosisError
*
* @typedef TestError
* @property {string} message
* @property {number} [row]
* @property {number} [column]
*/

/**
* @typedef Diagnosis
*
* @typedef TestResult
* @property {string} name
* @property {number} assertionCount
* @property {[number, number]} duration
* @property {DiagnosisError[]} errors
* @property {TestError[]} errors
*/

/**
* Run a test file and return a more friendly result format
*
* @param {string} absolutePath
* @returns {Diagnosis}
* @returns {TestResult}
*
* @example
* runTest("/home/lorem/src/fn.test-d.ts")
* // {
* // name: "src/fn.test-d.ts",
* // pass: true,
* // assertionCount: 1,
* // duration: [1, 123456789],
* // errors: []
* // }
*
* @example
* runTest("/home/lorem/src/fn-with-error.test-d.ts")
* // {
* // name: "src/fn.test-d.ts",
* // assertionCount: 1,
* // duration: [1, 123456789],
* // errors: [{
* // message: "Argument of type 'string' is not assignable ...",
* // row: 1,
* // column: 1,
* // }]
* // }
*/
export const runTest = absolutePath => {
const startAt = process.hrtime()
Expand Down
5 changes: 5 additions & 0 deletions lib/lib.colors.js → lib/utils/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,10 @@ export const black = {
fg: input => `\u001B[30m${input}\u001B[0m`,
}

export const cyan = {
/** @param {string} input */
fg: input => `\u001B[36m${input}\u001B[0m`,
}

/** @param {string} input */
export const bold = input => `\u001B[1m${input}\u001B[0m`
Loading

0 comments on commit 8015a5e

Please sign in to comment.