Compare commits
No commits in common. "ebeedac78ac38c3c6dbccb8b18f8d7317988bfbb" and "24314c9c95ffe820955b99f42b1254a8ac8d0545" have entirely different histories.
ebeedac78a
...
24314c9c95
|
|
@ -6,10 +6,9 @@ import { program } from "commander"
|
||||||
import ansis from "ansis"
|
import ansis from "ansis"
|
||||||
|
|
||||||
import { parse, parseSetup, type Command, type ShoutFile } from "../parse.ts"
|
import { parse, parseSetup, type Command, type ShoutFile } from "../parse.ts"
|
||||||
import { runFile, cleanupTmpDir, type CommandResult } from "../run.ts"
|
import { runFile, cleanupTmpDir } from "../run.ts"
|
||||||
import { evaluateFile, formatFailure, formatSummary } from "../format.ts"
|
import { evaluateFile, formatFailure, formatSummary } from "../format.ts"
|
||||||
import type { TestResult } from "../format.ts"
|
import type { TestResult } from "../format.ts"
|
||||||
import { matchOutput } from "../match.ts"
|
|
||||||
import { parseDuration } from "../duration.ts"
|
import { parseDuration } from "../duration.ts"
|
||||||
import { rewriteFile } from "../update.ts"
|
import { rewriteFile } from "../update.ts"
|
||||||
|
|
||||||
|
|
@ -139,26 +138,6 @@ program
|
||||||
}
|
}
|
||||||
const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands, ...teardownCommands] }
|
const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands, ...teardownCommands] }
|
||||||
|
|
||||||
const setupLen = setupCommands.length
|
|
||||||
const userLen = parsed.commands.length
|
|
||||||
const printDot = (result: CommandResult) => {
|
|
||||||
const { command, actual, exitCode } = result
|
|
||||||
const outputMatches = matchOutput(command.expected, actual)
|
|
||||||
let exitCodeMismatch = false
|
|
||||||
if (command.exitCode === null) {
|
|
||||||
exitCodeMismatch = exitCode !== 0
|
|
||||||
} else if (command.exitCode === "*") {
|
|
||||||
exitCodeMismatch = exitCode === 0
|
|
||||||
} else {
|
|
||||||
exitCodeMismatch = exitCode !== command.exitCode
|
|
||||||
}
|
|
||||||
if (outputMatches && !exitCodeMismatch) {
|
|
||||||
process.stdout.write(ansis.green("."))
|
|
||||||
} else {
|
|
||||||
process.stdout.write(ansis.red("F"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResult = await runFile(merged, {
|
const fileResult = await runFile(merged, {
|
||||||
cleanEnv: opts.cleanEnv ?? false,
|
cleanEnv: opts.cleanEnv ?? false,
|
||||||
pathDirs: opts.path,
|
pathDirs: opts.path,
|
||||||
|
|
@ -169,11 +148,6 @@ program
|
||||||
onCommand: opts.verbose
|
onCommand: opts.verbose
|
||||||
? (cmd) => process.stderr.write(ansis.dim(` $ ${cmd.command}\n`))
|
? (cmd) => process.stderr.write(ansis.dim(` $ ${cmd.command}\n`))
|
||||||
: undefined,
|
: undefined,
|
||||||
onCommandResult: (index, result) => {
|
|
||||||
if (index >= setupLen && index < setupLen + userLen) {
|
|
||||||
printDot(result)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check setup commands for failures
|
// Check setup commands for failures
|
||||||
|
|
@ -237,16 +211,24 @@ program
|
||||||
return testResult
|
return testResult
|
||||||
}
|
}
|
||||||
|
|
||||||
const printErrorDot = (r: TestResult) => {
|
const printDots = (r: TestResult) => {
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
process.stdout.write(ansis.red("F"))
|
process.stdout.write(ansis.red("F"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const passed = r.commandCount - r.failures.length
|
||||||
|
for (let i = 0; i < passed; i++) {
|
||||||
|
process.stdout.write(ansis.green("."))
|
||||||
|
}
|
||||||
|
for (let i = 0; i < r.failures.length; i++) {
|
||||||
|
process.stdout.write(ansis.red("F"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.parallel) {
|
if (opts.parallel) {
|
||||||
const promises = files.map(async f => {
|
const promises = files.map(async f => {
|
||||||
const r = await runOne(f, nextPort++)
|
const r = await runOne(f, nextPort++)
|
||||||
printErrorDot(r)
|
printDots(r)
|
||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
results.push(...await Promise.all(promises))
|
results.push(...await Promise.all(promises))
|
||||||
|
|
@ -254,7 +236,7 @@ program
|
||||||
} else {
|
} else {
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
const r = await runOne(filePath, nextPort++)
|
const r = await runOne(filePath, nextPort++)
|
||||||
printErrorDot(r)
|
printDots(r)
|
||||||
results.push(r)
|
results.push(r)
|
||||||
}
|
}
|
||||||
process.stdout.write("\n")
|
process.stdout.write("\n")
|
||||||
|
|
@ -271,8 +253,7 @@ program
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = performance.now() - start
|
const elapsed = performance.now() - start
|
||||||
const singleFile = files.length === 1 ? relative(cwd, files[0]) : undefined
|
console.log(formatSummary(results, elapsed))
|
||||||
console.log(formatSummary(results, elapsed, singleFile))
|
|
||||||
|
|
||||||
process.exit(failures.length > 0 ? 1 : 0)
|
process.exit(failures.length > 0 ? 1 : 0)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ export function formatFailure(test: TestResult): string {
|
||||||
export function formatSummary(
|
export function formatSummary(
|
||||||
results: TestResult[],
|
results: TestResult[],
|
||||||
elapsed: number,
|
elapsed: number,
|
||||||
singleFile?: string,
|
|
||||||
): string {
|
): string {
|
||||||
const totalCommands = results.reduce((n, r) => n + r.commandCount, 0)
|
const totalCommands = results.reduce((n, r) => n + r.commandCount, 0)
|
||||||
const failedCommands = results.reduce((n, r) => n + r.failures.length, 0)
|
const failedCommands = results.reduce((n, r) => n + r.failures.length, 0)
|
||||||
|
|
@ -119,6 +118,5 @@ export function formatSummary(
|
||||||
? `${Math.round(elapsed)}ms`
|
? `${Math.round(elapsed)}ms`
|
||||||
: `${(elapsed / 1000).toFixed(1)}s`
|
: `${(elapsed / 1000).toFixed(1)}s`
|
||||||
|
|
||||||
const label = singleFile ? ` in ${singleFile}` : ""
|
return `${parts.join(", ")} in ${time}`
|
||||||
return `${parts.join(", ")}${label} ${ansis.dim(`[${time}]`)}`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
src/run.ts
42
src/run.ts
|
|
@ -25,9 +25,7 @@ type RunOptions = {
|
||||||
sourceDir?: string
|
sourceDir?: string
|
||||||
projectDir?: string
|
projectDir?: string
|
||||||
timeout: number
|
timeout: number
|
||||||
verbose?: boolean
|
|
||||||
onCommand?: (cmd: Command) => void
|
onCommand?: (cmd: Command) => void
|
||||||
onCommandResult?: (index: number, result: CommandResult) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function killTree(pid: number): void {
|
function killTree(pid: number): void {
|
||||||
|
|
@ -194,7 +192,7 @@ export async function runFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentinel = SENTINEL_PREFIX
|
const sentinel = SENTINEL_PREFIX
|
||||||
const verbose = !!(options.verbose && options.onCommand)
|
const verbose = options.verbose && !!options.onCommand
|
||||||
const script = buildScript(file.commands, sentinel, verbose)
|
const script = buildScript(file.commands, sentinel, verbose)
|
||||||
|
|
||||||
const env: Record<string, string> = options.cleanEnv
|
const env: Record<string, string> = options.cleanEnv
|
||||||
|
|
@ -238,21 +236,7 @@ export async function runFile(
|
||||||
|
|
||||||
const totalTimeout = options.timeout * file.commands.length
|
const totalTimeout = options.timeout * file.commands.length
|
||||||
const lastSentinelSuffix = `_${file.commands.length - 1}__`
|
const lastSentinelSuffix = `_${file.commands.length - 1}__`
|
||||||
const onSentinel = options.onCommandResult
|
const stdout = await readUntilSentinel(proc.stdout, sentinel, lastSentinelSuffix, totalTimeout)
|
||||||
? (index: number, exitCode: number, rawOutput: string) => {
|
|
||||||
let lines = rawOutput.split("\n")
|
|
||||||
if (lines.length > 0 && lines[0] === "") lines.shift()
|
|
||||||
lines = trimTrailingEmpty(lines)
|
|
||||||
if (lines.length === 1 && lines[0] === "") lines = []
|
|
||||||
const result: CommandResult = {
|
|
||||||
command: file.commands[index]!,
|
|
||||||
actual: lines.map(stripAnsi),
|
|
||||||
exitCode,
|
|
||||||
}
|
|
||||||
options.onCommandResult!(index, result)
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
const stdout = await readUntilSentinel(proc.stdout, sentinel, lastSentinelSuffix, totalTimeout, onSentinel)
|
|
||||||
if (!verbose) {
|
if (!verbose) {
|
||||||
await readWithTimeout(proc.stderr, 1000).catch(() => "")
|
await readWithTimeout(proc.stderr, 1000).catch(() => "")
|
||||||
}
|
}
|
||||||
|
|
@ -288,13 +272,10 @@ async function readUntilSentinel(
|
||||||
sentinelPrefix: string,
|
sentinelPrefix: string,
|
||||||
sentinelSuffix: string,
|
sentinelSuffix: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
onSentinel?: (index: number, exitCode: number, outputBefore: string) => void,
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const reader = stream.getReader()
|
const reader = stream.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let accumulated = ""
|
let accumulated = ""
|
||||||
let sentinelsReported = 0
|
|
||||||
let lastSentinelEnd = 0
|
|
||||||
|
|
||||||
let timerId: ReturnType<typeof setTimeout>
|
let timerId: ReturnType<typeof setTimeout>
|
||||||
const timeout = new Promise<never>((_, reject) =>
|
const timeout = new Promise<never>((_, reject) =>
|
||||||
|
|
@ -307,25 +288,6 @@ async function readUntilSentinel(
|
||||||
if (done) break
|
if (done) break
|
||||||
if (value) {
|
if (value) {
|
||||||
accumulated += decoder.decode(value, { stream: true })
|
accumulated += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
// Detect new sentinels incrementally for streaming results
|
|
||||||
if (onSentinel) {
|
|
||||||
const regex = new RegExp(`${sentinelPrefix}(\\d+)_(\\d+)__`, "g")
|
|
||||||
regex.lastIndex = lastSentinelEnd
|
|
||||||
let match
|
|
||||||
while ((match = regex.exec(accumulated)) !== null) {
|
|
||||||
const idx = parseInt(match[2]!, 10)
|
|
||||||
if (idx >= sentinelsReported) {
|
|
||||||
const exitCode = parseInt(match[1]!, 10)
|
|
||||||
const output = accumulated.slice(lastSentinelEnd, match.index)
|
|
||||||
onSentinel(idx, exitCode, output)
|
|
||||||
sentinelsReported = idx + 1
|
|
||||||
lastSentinelEnd = match.index + match[0].length
|
|
||||||
if (accumulated[lastSentinelEnd] === "\n") lastSentinelEnd++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the last sentinel has appeared (prefix + exitcode + suffix)
|
// Check if the last sentinel has appeared (prefix + exitcode + suffix)
|
||||||
const prefixIdx = accumulated.lastIndexOf(sentinelPrefix)
|
const prefixIdx = accumulated.lastIndexOf(sentinelPrefix)
|
||||||
if (prefixIdx !== -1 && accumulated.indexOf(sentinelSuffix, prefixIdx) !== -1) break
|
if (prefixIdx !== -1 && accumulated.indexOf(sentinelSuffix, prefixIdx) !== -1) break
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user