Fix setup env precedence and validate directives

This commit is contained in:
Chris Wanstrath 2026-03-10 10:03:17 -07:00
parent 86eba1a624
commit e97be11a0c
2 changed files with 42 additions and 24 deletions

View File

@ -90,37 +90,46 @@ $ true
const results: TestResult[] = []
const cwd = process.cwd()
const portFrom = opts.portFrom ? parseInt(opts.portFrom, 10) : undefined
if (portFrom !== undefined && Number.isNaN(portFrom)) {
console.error("--port-from must be an integer")
process.exit(1)
}
let nextPort = portFrom ?? 0
const runOne = async (filePath: string, port?: number) => {
const runOne = async (filePath: string) => {
const content = await readFile(filePath, "utf-8")
const parsed = parse(relative(cwd, filePath), content)
// Collect env vars: --port-from, then @env directives
// Resolve directives in a single pass. Setup @env is collected separately
// so that the user file's @env always takes precedence.
const envVars: Record<string, string> = {}
if (port !== undefined) envVars["PORT"] = String(port)
for (const d of parsed.directives) {
if (d.type === "env") envVars[d.key] = d.value
}
// Resolve @setup directives: prepend setup commands, collect their @env
if (portFrom !== undefined) envVars["PORT"] = String(nextPort++)
const setupEnvVars: Record<string, string> = {}
const userEnvVars: Record<string, string> = {}
const setupCommands: Command[] = []
for (const d of parsed.directives) {
if (d.type !== "setup") continue
const setupPath = resolve(dirname(filePath), d.path)
const setupContent = await readFile(setupPath, "utf-8")
const setupParsed = parse(relative(cwd, setupPath), setupContent)
for (const sd of setupParsed.directives) {
if (sd.type === "env") envVars[sd.key] = sd.value
if (d.type === "setup") {
const setupPath = resolve(dirname(filePath), d.path)
const setupContent = await readFile(setupPath, "utf-8")
const setupParsed = parse(relative(cwd, setupPath), setupContent)
for (const sd of setupParsed.directives) {
if (sd.type === "setup") {
throw new Error(`${relative(cwd, setupPath)}: @setup not allowed in setup files`)
}
if (sd.type === "env") setupEnvVars[sd.key] = sd.value
}
setupCommands.push(...setupParsed.commands)
} else if (d.type === "env") {
userEnvVars[d.key] = d.value
}
setupCommands.push(...setupParsed.commands)
}
Object.assign(envVars, setupEnvVars, userEnvVars)
const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands] }
const fileResult = await runFile(merged, {
cleanEnv: opts.cleanEnv ?? false,
pathDirs: opts.path,
envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
envVars,
timeout: timeoutMs,
verbose: opts.verbose ?? false,
onCommand: opts.verbose
@ -128,6 +137,18 @@ $ true
: undefined,
})
// Check setup commands for failures
for (let i = 0; i < setupCommands.length; i++) {
const r = fileResult.results[i]
if (r && r.exitCode !== 0) {
return evaluateFile(
parsed.path,
[],
`setup command failed (exit ${r.exitCode}): $ ${setupCommands[i]!.command}`,
)
}
}
const fileOwnResults = fileResult.results.slice(setupCommands.length)
const testResult = evaluateFile(
@ -167,10 +188,7 @@ $ true
}
if (opts.parallel) {
const tasks = files.map(f => {
const port = portFrom !== undefined ? nextPort++ : undefined
return runOne(f, port)
})
const tasks = files.map(f => runOne(f))
const all = await Promise.all(tasks)
for (const r of all) {
printDots(r)
@ -179,8 +197,7 @@ $ true
process.stdout.write("\n")
} else {
for (const filePath of files) {
const port = portFrom !== undefined ? nextPort++ : undefined
const r = await runOne(filePath, port)
const r = await runOne(filePath)
printDots(r)
results.push(r)
}

View File

@ -76,9 +76,10 @@ export function parse(path: string, content: string): ShoutFile {
} else if (line.startsWith("@env ")) {
const rest = line.slice(5).trim()
const eq = rest.indexOf("=")
if (eq > 0) {
directives.push({ type: "env", key: rest.slice(0, eq), value: rest.slice(eq + 1), line: i + 1 })
if (eq <= 0) {
throw new Error(`${path}:${i + 1}: malformed @env directive (expected KEY=VALUE): ${line}`)
}
directives.push({ type: "env", key: rest.slice(0, eq), value: rest.slice(eq + 1), line: i + 1 })
} else {
throw new Error(`${path}:${i + 1}: unknown directive: ${line}`)
}