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.
171 lines
4.2 KiB
171 lines
4.2 KiB
![]()
2 years ago
|
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"}`
|
||
|
)
|
||
|
}
|