Skip to content

Commit

Permalink
feat: add output matcher (#6)
Browse files Browse the repository at this point in the history
Adds a function to match the output of a process.
  • Loading branch information
achingbrain authored Oct 25, 2023
1 parent a91145b commit 388f0c7
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 15 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,23 +154,24 @@
"lint": "aegir lint",
"dep-check": "aegir dep-check",
"build": "aegir build --bundle false",
"test": "npm run test:node",
"test:node": "aegir test -t node -f ./dist/test/*.spec.js",
"test:node": "aegir test -t node -f ./dist/test/node.spec.js",
"test:chrome": "aegir test -t node -f ./dist/test/browser.spec.js",
"release": "aegir release",
"docs": "aegir docs"
},
"dependencies": {
"@playwright/test": "^1.33.0",
"@types/polka": "^0.5.4",
"@types/stoppable": "^1.1.1",
"execa": "^7.1.1",
"execa": "^8.0.1",
"p-defer": "^4.0.0",
"polka": "^0.5.2",
"sirv": "^2.0.2",
"stoppable": "^1.1.0",
"uint8arrays": "^4.0.3"
},
"devDependencies": {
"aegir": "^39.0.3"
"aegir": "^41.0.8"
},
"bin": {
"test-browser-example": "./bin/test-browser-example.js",
Expand Down
3 changes: 1 addition & 2 deletions src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { test, expect, type TestType, type PlaywrightTestArgs, type PlaywrightWorkerArgs, type PlaywrightWorkerOptions } from '@playwright/test'
import { servers } from './servers.js'

Expand All @@ -16,7 +15,7 @@ export interface TestOptions {
export interface ServerFixture {
server: any
url: string
stop: () => Promise<void>
stop(): Promise<void>
}

export interface TestArgs extends PlaywrightTestArgs {
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@

export * as browser from './browser/index.js'
export * as node from './node/index.js'
7 changes: 4 additions & 3 deletions src/node/execa.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { execa, type ExecaChildProcess, type Options, type DefaultEncodingOption } from 'execa'

import { execa, type ExecaChildProcess, type Options } from 'execa'

function execaUtil (command: string, args: string[] = [], opts: Options<string> = {}, callback?: (proc: ExecaChildProcess<string>) => void): ExecaChildProcess<string> {
function execaUtil (command: string, args: string[], opts: Options<'buffer'>, callback?: (proc: ExecaChildProcess<Buffer>) => void): ExecaChildProcess<Buffer>
function execaUtil (command: string, args?: string[], opts?: Options<DefaultEncodingOption>, callback?: (proc: ExecaChildProcess<string>) => void): ExecaChildProcess<string>
function execaUtil (command: string, args: string[] = [], opts: any = {}, callback?: (proc: any) => void): any {
if (command.endsWith('.js')) {
args.unshift(command)
command = 'node'
Expand Down
2 changes: 1 addition & 1 deletion src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

export { matchOutput } from './match-output.js'
export { waitForOutput } from './wait-for-output.js'
export { default as execa } from './execa.js'
97 changes: 97 additions & 0 deletions src/node/match-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import pDefer from 'p-defer'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import execaUtil from './execa.js'
import type { DefaultEncodingOption, EncodingOption, ExecaChildProcess, Options } from 'execa'

export interface WaitForOutputOptions<EncodingType extends EncodingOption = DefaultEncodingOption> extends Options<EncodingType> {
timeout?: number
}

export interface MatchOutputResult {
matches: string[]
process: ExecaChildProcess<string>
}

/**
* Starts a process and matches the output against the passed regex. The match
* result and process are returned, the user must manually kill the process
* when they are finished with it.
*/
export async function matchOutput (matcher: RegExp, command: string, args: string[] = [], opts: WaitForOutputOptions = {}): Promise<MatchOutputResult> {
const foundMatch = pDefer<string[]>()

const proc = execaUtil(command, args, { ...opts, all: true }, (exec) => {
exec.all?.on('data', (data) => {
process.stdout.write(data)
output += uint8ArrayToString(data)

const matches = matcher.exec(output)

if (matches == null) {
return
}

foundExpectedOutput = true
foundMatch.resolve(matches)
cancelTimeout()
})
})

let output = ''
const time = opts.timeout ?? 120000

let foundExpectedOutput = false
let cancelTimeout = (): void => {}
const timeoutPromise = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(
`Did not match "${matcher}" in output from "${[command]
.concat(args)
.join(' ')}" after ${time / 1000}s`
)
)

setTimeout(() => {
proc.kill()
}, 100)
}, time)

cancelTimeout = () => {
clearTimeout(timeout)
resolve()
}
})

let result: string[] | undefined

try {
const res = await Promise.race([foundMatch.promise, timeoutPromise])

if (res != null) {
result = res
}
} catch (err: any) {
if (err.killed == null) {
throw err
}
}

if (!foundExpectedOutput) {
cancelTimeout()
throw new Error(
`Did not match "${matcher}" in output from "${[command]
.concat(args)
.join(' ')}"`
)
}

if (result == null) {
throw new Error('Process output was undefined')
}

return {
process: proc,
matches: result
}
}
8 changes: 6 additions & 2 deletions src/node/wait-for-output.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import execaUtil from './execa.js'
import type { Options } from 'execa'
import type { DefaultEncodingOption, EncodingOption, Options } from 'execa'

export interface WaitForOutputOptions extends Options<string> {
export interface WaitForOutputOptions<EncodingType extends EncodingOption = DefaultEncodingOption> extends Options<EncodingType> {
timeout?: number
}

/**
* Starts a process and waits for the passed string to appear in the output.
* When this happens the process is killed.
*/
export async function waitForOutput (expectedOutput: string, command: string, args: string[] = [], opts: WaitForOutputOptions = {}): Promise<void> {
const proc = execaUtil(command, args, { ...opts, all: true }, (exec) => {
exec.all?.on('data', (data) => {
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/node/match-output.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { matchOutput } from '../../../dist/src/node/index.js'

const result = await matchOutput(/(Here is some input)/mg, './test/fixtures/node/index.js')

result.process.kill()
File renamed without changes.
12 changes: 10 additions & 2 deletions test/node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ describe('node', function () {
this.timeout(540000)
this.slow(60000)

it('should have node success', async () => {
it('should match output', async () => {
await execa('./bin/test-node-example.js', [
'./test/fixtures/node/test.spec.js'
'./test/fixtures/node/match-output.spec.js'
], {
stdio: 'inherit'
})
})

it('should wait for output', async () => {
await execa('./bin/test-node-example.js', [
'./test/fixtures/node/wait-for-output.spec.js'
], {
stdio: 'inherit'
})
Expand Down

0 comments on commit 388f0c7

Please sign in to comment.