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.
 
 
 
 

192 lines
5.0 KiB

import { ScriptLine, Shell } from "./shells/types.js"
import { execa, type ExecaChildProcess } from "execa"
import testTmpDir from "./test-tmp-dir.js"
import { Writable } from "node:stream"
import { dedent } from "ts-dedent"
import testCwd from "./test-cwd.js"
import path, { join } from "node:path"
import { writeFile } from "node:fs/promises"
import chalk from "chalk"
import testBinDir from "./test-bin-dir.js"
import { rmSync } from "node:fs"
class Script {
constructor(
private readonly config: {
fnmDir: string
},
private readonly lines: ScriptLine[],
private readonly extraEnvVars: Record<string, string> = {}
) {}
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
}
addExtraEnvVar(name: string, value: string): this {
return new Script(
this.config,
this.lines,
Object.assign({}, this.extraEnvVars, { [name]: value })
) as this
}
async execute(
shell: Pick<
Shell,
"binaryName" | "launchArgs" | "currentlySupported" | "forceFile"
>
): Promise<void> {
if (!shell.currentlySupported()) {
return
}
const args = [...shell.launchArgs()]
if (shell.forceFile) {
let filename = join(testTmpDir(), "script")
if (typeof shell.forceFile === "string") {
filename = filename + shell.forceFile
}
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: (() => {
const newProcessEnv: Record<string, string> = {
...this.extraEnvVars,
...removeAllFnmEnvVars(process.env),
PATH: [testBinDir(), fnmTargetDir(), process.env.PATH]
.filter(Boolean)
.join(path.delimiter),
FNM_DIR: this.config.fnmDir,
FNM_NODE_DIST_MIRROR: "http://localhost:8080",
}
delete newProcessEnv.NODE_OPTIONS
return newProcessEnv
})(),
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: ExecaChildProcess) {
const stdout: string[] = []
const stderr: string[] = []
const testName = expect.getState().currentTestName ?? "unknown"
const testPath = expect.getState().testPath ?? "unknown"
const stdoutPrefix = chalk.cyan.dim(`[stdout] ${testPath}/${testName}: `)
const stderrPrefix = chalk.magenta.dim(`[stderr] ${testPath}/${testName}: `)
if (child.stdout) {
child.stdout.on("data", (data) => {
const lines = data.toString().trim().split(/\r?\n/)
for (const line of lines) {
process.stdout.write(`${stdoutPrefix}${line}\n`)
}
stdout.push(data.toString())
})
}
if (child.stderr) {
child.stderr.on("data", (data) => {
const lines = data.toString().trim().split(/\r?\n/)
for (const line of lines) {
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 = path.join(testTmpDir(), "fnm")
rmSync(join(fnmDir, "aliases"), { recursive: true, force: true })
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.join(
process.cwd(),
"target",
process.env.FNM_TARGET_NAME ?? "debug"
)
}