import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert"; import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context"; import { join } from "node:path"; import { tempDir, until } from "@berry/util"; import { localEnvironment } from "./environment.ts"; Deno.test({ name: "pseudo-terminals preserve argv, Unicode, cwd and environment, and support input and resizing", ignore: !["linux", "windows"].includes(Deno.build.os), async fn() { await using root = await tempDir(); const cwd = join(root.path, "terminal 🐈"); await Deno.mkdir(cwd); const env = localEnvironment(cwd); const args = ["", 'a "quote"', "C:\\trailing\\", "héllo 🐈"]; const script = ` console.log('args=' + JSON.stringify(Deno.args)); console.log('cwd=' + Deno.cwd()); console.log('env=' + Deno.env.get('BERRY_PTY_TEST')); console.log('tty=' + Deno.stdin.isTerminal() + ',' + Deno.stdout.isTerminal()); if (Deno.build.os === 'windows') { const kernel = Deno.dlopen('kernel32.dll', { SetConsoleCP: { parameters: ['u32'], result: 'i32' }, }); // ReadFile uses the application's console code page, not ConPTY's UTF-8 transport if (!kernel.symbols.SetConsoleCP(65001)) throw new Error('SetConsoleCP failed'); kernel.close(); } Deno.stdin.setRaw(true); console.log('ready'); const input = new Uint8Array(1024); const decoder = new TextDecoder(); let typed = ''; while (!typed.includes('\\r')) { const count = await Deno.stdin.read(input); if (count === null) break; typed += decoder.decode(input.subarray(0, count), { stream: true }); } console.log('input=' + typed.trimEnd()); console.log('size=' + JSON.stringify(Deno.consoleSize())); Deno.exit(7); `; const pty = await env.pty([Deno.execPath(), "eval", script, ...args], { cols: 160, rows: 30, env: { BERRY_PTY_TEST: "value 🐈" }, }, BACKGROUND_CONTEXT); let output = ""; const reading = (async () => { const decoder = new TextDecoder(); for await (const chunk of pty.readable) output += decoder.decode(chunk, { stream: true }); output += decoder.decode(); })(); const input = pty.writable.getWriter(); try { await until(() => output.includes("ready")); assertStringIncludes(output, "args=" + JSON.stringify(args)); assertStringIncludes(output, "cwd=" + cwd); assertStringIncludes(output, "env=value 🐈"); assertStringIncludes(output, "tty=true,true"); pty.resize(140, 40); await input.write(new TextEncoder().encode("typed héllo\r")); assertEquals(await pty.exited, 7); await reading; assertStringIncludes(output, "input=typed héllo"); assertStringIncludes(output, 'size={"columns":140,"rows":40}'); } finally { pty.kill(); await reading; await pty.exited; input.releaseLock(); } }, }); Deno.test({ name: "ConPTY closes on abort and output cancellation, and can start again after a failed spawn", ignore: Deno.build.os !== "windows", async fn() { const env = localEnvironment(Deno.cwd()); await assertRejects(() => env.pty(["berry-nonexistent-program.exe"], { cols: 80, rows: 24, }, BACKGROUND_CONTEXT) ); const controller = new AbortController(); const pty = await env.pty([Deno.execPath(), "eval", "setInterval(() => {}, 1000)"], { cols: 80, rows: 24, }, withAbortSignal(controller.signal, BACKGROUND_CONTEXT)); const drained = pty.readable.pipeTo(new WritableStream()); controller.abort(); assertEquals(await pty.exited, null); await drained; const noisy = await env.pty( [ Deno.execPath(), "eval", ` const block = new TextEncoder().encode('x'.repeat(65536)); while (true) await Deno.stdout.write(block); `, ], { cols: 80, rows: 24 }, BACKGROUND_CONTEXT, ); const reader = noisy.readable.getReader(); assert(!(await reader.read()).done); await reader.cancel(); await noisy.exited; noisy.kill(); }, });