Skip to content

Commit

Permalink
feat: new oclif test utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed May 15, 2024
1 parent d1dcc8a commit 006c90c
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 511 deletions.
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"author": "Salesforce",
"bugs": "https://github.com/oclif/test/issues",
"dependencies": {
"@oclif/core": "^3.26.6",
"chai": "^4.4.1",
"fancy-test": "^3.0.15"
"ansis": "^3.2.0",
"debug": "^4.3.4"
},
"peerDependencies": {
"@oclif/core": "^4.0.0-beta.6"
},
"devDependencies": {
"@commitlint/config-conventional": "^18.6.3",
"@oclif/core": "^4.0.0-beta.6",
"@oclif/prettier-config": "^0.2.1",
"@types/cli-progress": "^3.11.5",
"@types/debug": "^4.1.12",
"@types/mocha": "^10",
"@types/node": "^18",
"commitlint": "^18.6.1",
Expand All @@ -23,7 +26,6 @@
"husky": "^9.0.3",
"lint-staged": "^15.2.2",
"mocha": "^10",
"nock": "^13.5.4",
"prettier": "^3.2.5",
"shx": "^0.3.3",
"ts-node": "^10.9.2",
Expand Down
30 changes: 0 additions & 30 deletions src/command.ts

This file was deleted.

22 changes: 0 additions & 22 deletions src/exit.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/hook.ts

This file was deleted.

173 changes: 149 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,78 @@
import * as fancyTest from 'fancy-test'
import {Config, Errors, Interfaces, run} from '@oclif/core'
import ansis from 'ansis'
import makeDebug from 'debug'
import {dirname} from 'node:path'

import {command} from './command'
import exit from './exit'
import hook from './hook'
import {loadConfig} from './load-config'
const debug = makeDebug('test')

type CaptureOptions = {
print?: boolean
stripAnsi?: boolean
}

const RECORD_OPTIONS: CaptureOptions = {
print: false,
stripAnsi: true,
}

const originals = {
stderr: process.stderr.write,
stdout: process.stdout.write,
}

const output: Record<'stderr' | 'stdout', Array<Uint8Array | string>> = {
stderr: [],
stdout: [],
}

function mockedStdout(str: Uint8Array | string, cb?: (err?: Error) => void): boolean
function mockedStdout(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean
function mockedStdout(
str: Uint8Array | string,
encoding?: ((err?: Error) => void) | BufferEncoding,
cb?: (err?: Error) => void,
): boolean {
output.stdout.push(str)
if (!RECORD_OPTIONS.print) return true

if (typeof encoding === 'string') {
return originals.stdout.bind(process.stdout)(str, encoding, cb)
}

return originals.stdout.bind(process.stdout)(str, cb)
}

function mockedStderr(str: Uint8Array | string, cb?: (err?: Error) => void): boolean
function mockedStderr(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean
function mockedStderr(
str: Uint8Array | string,
encoding?: ((err?: Error) => void) | BufferEncoding,
cb?: (err?: Error) => void,
): boolean {
output.stderr.push(str)
if (!RECORD_OPTIONS.print) return true
if (typeof encoding === 'string') {
return originals.stdout.bind(process.stderr)(str, encoding, cb)
}

return originals.stdout.bind(process.stderr)(str, cb)
}

const restore = (): void => {
process.stderr.write = originals.stderr
process.stdout.write = originals.stdout
}

const reset = (): void => {
output.stderr = []
output.stdout = []
}

const toString = (str: Uint8Array | string): string =>
RECORD_OPTIONS.stripAnsi ? ansis.strip(str.toString()) : str.toString()

const getStderr = (): string => output.stderr.map((b) => toString(b)).join('')
const getStdout = (): string => output.stdout.map((b) => toString(b)).join('')

function traverseFilePathUntil(filename: string, predicate: (filename: string) => boolean): string {
let current = filename
Expand All @@ -15,27 +83,84 @@ function traverseFilePathUntil(filename: string, predicate: (filename: string) =
return current
}

/* eslint-disable unicorn/prefer-module */
loadConfig.root =
process.env.OCLIF_TEST_ROOT ??
Object.values(require.cache).find((m) => m?.children.includes(module))?.filename ??
traverseFilePathUntil(
require.main?.path ?? module.path,
(p) => !(p.includes('node_modules') || p.includes('.pnpm') || p.includes('.yarn')),
function makeLoadOptions(loadOpts?: Interfaces.LoadOptions): Interfaces.LoadOptions {
return (
loadOpts ?? {
root: traverseFilePathUntil(
// eslint-disable-next-line unicorn/prefer-module
require.main?.path ?? module.path,
(p) => !(p.includes('node_modules') || p.includes('.pnpm') || p.includes('.yarn')),
),
}
)
/* eslint-enable unicorn/prefer-module */
}

// Using a named export to import fancy causes this issue: https://github.com/oclif/test/issues/516
export const test = fancyTest.fancy
.register('loadConfig', loadConfig)
.register('command', command)
.register('exit', exit)
.register('hook', hook)
.env({NODE_ENV: 'test'})
export async function captureOutput<T>(
fn: () => Promise<unknown>,
opts?: CaptureOptions,
): Promise<{
error?: Error & Partial<Errors.CLIError>
result?: T
stderr: string
stdout: string
}> {
RECORD_OPTIONS.print = opts?.print ?? false
RECORD_OPTIONS.stripAnsi = opts?.stripAnsi ?? true
process.stderr.write = mockedStderr
process.stdout.write = mockedStdout

export default test
try {
const result = await fn()
return {
result: result as T,
stderr: getStderr(),
stdout: getStdout(),
}
} catch (error) {
return {
...(error instanceof Errors.CLIError && {error}),
...(error instanceof Error && {error}),
stderr: getStderr(),
stdout: getStdout(),
}
} finally {
restore()
reset()
}
}

export {command} from './command'
export async function runCommand<T>(
args: string[],
loadOpts?: Interfaces.LoadOptions,
captureOpts?: CaptureOptions,
): Promise<{
error?: Error & Partial<Errors.CLIError>
result?: T
stderr: string
stdout: string
}> {
const loadOptions = makeLoadOptions(loadOpts)
debug('loadOpts: %O', loadOpts)
return captureOutput<T>(async () => run(args, loadOptions), captureOpts)
}

export {Config} from '@oclif/core'
export {FancyTypes, expect} from 'fancy-test'
export async function runHook<T>(
hook: string,
options: Record<string, unknown>,
loadOpts?: Interfaces.LoadOptions,
recordOpts?: CaptureOptions,
): Promise<{
error?: Error & Partial<Errors.CLIError>
result?: T
stderr: string
stdout: string
}> {
const loadOptions = makeLoadOptions(loadOpts)

debug('loadOpts: %O', loadOpts)

return captureOutput<T>(async () => {
const config = await Config.load(loadOptions)
return config.runHook(hook, options)
}, recordOpts)
}
26 changes: 0 additions & 26 deletions src/load-config.ts

This file was deleted.

Loading

0 comments on commit 006c90c

Please sign in to comment.