You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

170 lines
4.2 KiB

import { ScriptLine, Shell } from "./shells/types"
import execa from "execa"
import testTmpDir from "./test-tmp-dir"
import { Writable } from "node:stream"
import dedent from "ts-dedent"
import testCwd from "./test-cwd"
import path, { join } from "node:path"
import { writeFile } from "node:fs/promises"
import chalk from "chalk"
import testBinDir from "./test-bin-dir"
class Script {
constructor(
private readonly config: {
fnmDir: string
},
private readonly lines: ScriptLine[]
) {}
then(line: ScriptLine): Script {
return new Script(this.config, [...this.lines, line])
}
takeSnapshot(shell: Pick<Shell, "name">): this {
const script = this.lines.join("\n")
expect(script).toMatchSnapshot(shell.name())
return this
}
async execute(
shell: Pick<
Shell,
"binaryName" | "launchArgs" | "currentlySupported" | "forceFile"
>
): Promise<void> {
if (!shell.currentlySupported()) {
return
}
const args = [...shell.launchArgs()]
if (shell.forceFile) {
const filename = join(testTmpDir(), "script")
await writeFile(filename, [...this.lines, "exit 0"].join("\n"))
args.push(filename)
}
const child = execa(shell.binaryName(), args, {
stdio: [shell.forceFile ? "ignore" : "pipe", "pipe", "pipe"],
cwd: testCwd(),
env: {
...removeAllFnmEnvVars(process.env),
PATH: [testBinDir(), fnmTargetDir(), process.env.PATH]
.filter(Boolean)
.join(path.delimiter),
FNM_DIR: this.config.fnmDir,
},
extendEnv: false,
reject: false,
})
if (child.stdin) {
const childStdin = child.stdin
for (const line of this.lines) {
await write(childStdin, `${line}\n`)
}
await write(childStdin, "exit 0\n")
}
const { stdout, stderr } = streamOutputsAndBuffer(child)
const finished = await child
if (finished.failed) {
console.error(
dedent`
Script failed.
code ${finished.exitCode}
signal ${finished.signal}
stdout:
${padAllLines(stdout.join(""), 2)}
stderr:
${padAllLines(stderr.join(""), 2)}
`
)
throw new Error(
`Script failed on ${testCwd()} with code ${finished.exitCode}`
)
}
}
asLine(): ScriptLine {
return this.lines.join("\n")
}
}
function streamOutputsAndBuffer(child: execa.ExecaChildProcess) {
const stdout: string[] = []
const stderr: string[] = []
const testName = expect.getState().currentTestName ?? "unknown"
const testPath = expect.getState().testPath ?? "unknown"
const stdoutPrefix = chalk.yellow.dim(`[stdout] ${testPath}/${testName}: `)
const stderrPrefix = chalk.red.dim(`[stderr] ${testPath}/${testName}: `)
if (child.stdout) {
child.stdout.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stdoutPrefix}${line}\n`)
}
stdout.push(data.toString())
})
}
if (child.stderr) {
child.stderr.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stderrPrefix}${line}\n`)
}
stderr.push(data.toString())
})
}
return { stdout, stderr }
}
function padAllLines(text: string, padding: number): string {
return text
.split("\n")
.map((line) => " ".repeat(padding) + line)
.join("\n")
}
function write(writable: Writable, text: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
writable.write(text, (err) => {
if (err) return reject(err)
return resolve()
})
})
}
export function script(shell: Pick<Shell, "dieOnErrors">): Script {
const fnmDir = `${testTmpDir()}/fnm`
return new Script({ fnmDir }, shell.dieOnErrors ? [shell.dieOnErrors()] : [])
}
function removeAllFnmEnvVars(obj: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result: NodeJS.ProcessEnv = {}
for (const [key, value] of Object.entries(obj)) {
if (!key.startsWith("FNM_")) {
result[key] = value
}
}
return result
}
function fnmTargetDir(): string {
return path.resolve(
__dirname,
`../../target/${process.env.FNM_TARGET_NAME ?? "debug"}`
)
}