diff --git a/README.md b/README.md index ddf3f0938..4d76b70ae 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

-# tea/cli 0.30.0 +# tea/cli 0.31.0 `tea` puts the whole open source ecosystem at your fingertips: diff --git a/src/hooks/useRun.ts b/src/hooks/useRun.ts index fa11bbf41..890ed1883 100644 --- a/src/hooks/useRun.ts +++ b/src/hooks/useRun.ts @@ -1,7 +1,7 @@ import Path from "path" import { isArray } from "is_what" -export interface RunOptions extends Omit { +export interface RunOptions extends Omit { cmd: (string | Path)[] | Path cwd?: (string | Path) clearEnv?: boolean //NOTE might not be cross platform! @@ -21,15 +21,15 @@ export default async function useRun({ spin, ...opts }: RunOptions) { const cwd = opts.cwd?.toString() console.verbose({ cwd, ...opts, cmd }) - const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick + const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick if (spin) { stdio.stderr = stdio.stdout = 'piped' } - let proc: Deno.Process | undefined + let proc: Deno.ChildProcess | undefined try { - proc = _internals.nativeRun({ ...opts, cmd, cwd, ...stdio }) - const exit = await proc.status() + proc = _internals.nativeRun(cmd.shift()!, { ...opts, args: cmd, cwd, ...stdio }).spawn() + const exit = await proc.status console.verbose({ exit }) if (!exit.success) throw new RunError(exit.code, cmd) } catch (err) { @@ -37,17 +37,17 @@ export default async function useRun({ spin, ...opts }: RunOptions) { //FIXME this doesn’t result in the output being correctly interlaced // ie. stderr and stdout may (probably) have been output interleaved rather than sequentially const decode = (() => { const e = new TextDecoder(); return e.decode.bind(e) })() - console.error(decode(await proc.output())) - console.error(decode(await proc.stderrOutput())) + console.error(decode((await proc.output()).stdout)) + console.error(decode((await proc.output()).stderr)) } + err.cmd = cmd // help us out since deno-devs don’t want to throw err - } finally { - proc?.close() } } -const nativeRun = (runOptions: Deno.RunOptions) => Deno.run(runOptions) + +const nativeRun = (cmd: string, opts: Deno.CommandOptions) => new Deno.Command(cmd, opts) // _internals are used for testing export const _internals = { diff --git a/src/vendor/Path.ts b/src/vendor/Path.ts index 79a30642a..6d7dcde23 100644 --- a/src/vendor/Path.ts +++ b/src/vendor/Path.ts @@ -317,12 +317,12 @@ export default class Path { let opts = "-s" if (force) opts += "fn" - const status = await Deno.run({ - cmd: ["/bin/ln", opts, src, dst], + const status = await new Deno.Command("/bin/ln", { + args: [opts, src, dst], cwd: this.string - }).status() + }).spawn().status - if (status.code != 0) throw new Error(`failed: cd ${this} && ln -sf ${src} ${dst}`) + if (!status.success) throw new Error(`failed: cd ${this} && ln -sf ${src} ${dst}`) return to } diff --git a/tests/functional/exec.test.ts b/tests/functional/exec.test.ts index 896772eaf..920e06258 100644 --- a/tests/functional/exec.test.ts +++ b/tests/functional/exec.test.ts @@ -8,54 +8,55 @@ Deno.test("exec", { sanitizeResources: false, sanitizeOps: false }, async () => const useRunSpy = spy(useRunInternals, "nativeRun") try { - await run(["node", "--version"]) + await run(["node", "--version"]) } finally { useRunSpy.restore() } - assertEquals(useRunSpy.calls[0].args[0].cmd, ["node", "--version"], "should have run node --version") + const foo = [useRunSpy.calls[0].args[0], ...useRunSpy.calls[0].args[1].args!] + assertEquals(foo, ["node", "--version"], "should have run node --version") }) -Deno.test("forward env to exec", { sanitizeResources: false, sanitizeOps: false }, async () => { +Deno.test("forward env to exec", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX, useRunInternals } = await createTestHarness() const useRunSpy = spy(useRunInternals, "nativeRun") try { - await run(["sh", "-c", "echo $TEA_PREFIX"]) + await run(["sh", "-c", "echo $TEA_PREFIX"]) } finally { useRunSpy.restore() } - assertEquals(useRunSpy.calls[0].args[0].env?.["TEA_PREFIX"], TEA_PREFIX.string) + assertEquals(useRunSpy.calls[0].args[1].env?.["TEA_PREFIX"], TEA_PREFIX.string) }) Deno.test("exec run errors", { sanitizeResources: false, sanitizeOps: false }, async test => { const tests = [ { name: "exit error", - procStatus: (): Promise => Promise.resolve({success: false, code: 123}), + procStatus: (): Promise => Promise.resolve({success: false, code: 123, signal: null}), expectedErr: "exiting with code: 123", - }, + }, { name: "normal error", - procStatus: (): Promise => Promise.reject(new Error("test error")), + procStatus: (): Promise => Promise.reject(new Error("test error")), expectedErr: "exiting with code: 1", - }, + }, { name: "tea error", - procStatus: (): Promise => Promise.reject(new TeaError("confused: interpreter", {})), + procStatus: (): Promise => Promise.reject(new TeaError("confused: interpreter", {})), expectedErr: "exiting with code: 1", - }, + }, { name: "not found", - procStatus: (): Promise => Promise.reject(new Deno.errors.NotFound()), + procStatus: (): Promise => Promise.reject(new Deno.errors.NotFound()), expectedErr: "exiting with code: 127", - }, + }, { name: "permission denied", - procStatus: (): Promise => Promise.reject(new Deno.errors.PermissionDenied()), + procStatus: (): Promise => Promise.reject(new Deno.errors.PermissionDenied()), expectedErr: "exiting with code: 127", - }, + }, ] for (const { name, procStatus, expectedErr } of tests) { @@ -66,7 +67,7 @@ Deno.test("exec run errors", { sanitizeResources: false, sanitizeOps: false }, a const useRunStub = stub(useRunInternals, "nativeRun", returnsNext([mockProc])) await assertRejects(async () => { try { - await run(["node", "--version"]) + await run(["node", "--version"]) } finally { useRunStub.restore() } @@ -76,7 +77,7 @@ Deno.test("exec run errors", { sanitizeResources: false, sanitizeOps: false }, a }) Deno.test("exec forkbomb protector", { sanitizeResources: false, sanitizeOps: false }, async () => { - const {run } = await createTestHarness() + const { run } = await createTestHarness() await assertRejects( () => run(["sh", "-c", "echo $TEA_PREFIX"], { env: {TEA_FORK_BOMB_PROTECTOR: "21" }}), "FORK BOMB KILL SWITCH ACTIVATED") diff --git a/tests/functional/repl.test.ts b/tests/functional/repl.test.ts index 2648769b7..5d31a022d 100644 --- a/tests/functional/repl.test.ts +++ b/tests/functional/repl.test.ts @@ -3,9 +3,9 @@ import { stub, returnsNext } from "deno/testing/mock.ts" import { ExitError } from "types" import { createTestHarness, newMockProcess } from "./testUtils.ts" -Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: false }, async test => { +Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: false }, async test => { const tests = [ - { + { shell: "/bin/sh", expectedCmd: ["/bin/sh", "-i"], expectedEnv: {"PS1": "\\[\\033[38;5;86m\\]tea\\[\\033[0m\\] %~ "}, @@ -41,14 +41,15 @@ Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: fal const useRunStub = stub(useRunInternals, "nativeRun", returnsNext([newMockProcess()])) try { - await run(["sh"], { env: { SHELL: shell } }) + await run(["sh"], { env: { SHELL: shell } }) } finally { useRunStub.restore() } - assertEquals(useRunStub.calls[0].args[0].cmd, expectedCmd) + const foo = [useRunStub.calls[0].args[0], ...useRunStub.calls[0].args[1].args!] + assertEquals(foo, expectedCmd) - const { env } = useRunStub.calls[0].args[0] + const { env } = useRunStub.calls[0].args[1] assertEquals(env?.["TEA_PREFIX"], TEA_PREFIX.string) Object.entries(expectedEnv).forEach(([key, value]) => { assertEquals(env?.[key], value) @@ -58,18 +59,17 @@ Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: fal }) -Deno.test("repl errors", { sanitizeResources: false, sanitizeOps: false }, async test => { +Deno.test("repl errors", { sanitizeResources: false, sanitizeOps: false }, async test => { await test.step("run error", async () => { const {run, useRunInternals } = await createTestHarness() - const mockProc = newMockProcess() - mockProc.status = () => Promise.resolve({success: false, code: 123}) + const mockProc = newMockProcess(() => Promise.resolve({success: false, code: 123, signal: null})) const useRunStub = stub(useRunInternals, "nativeRun", returnsNext([mockProc])) await assertRejects(async () => { try { - await run(["sh"]) + await run(["sh"]) } finally { useRunStub.restore() } @@ -79,14 +79,13 @@ Deno.test("repl errors", { sanitizeResources: false, sanitizeOps: false }, async await test.step("other error", async () => { const {run, useRunInternals } = await createTestHarness() - const mockProc = newMockProcess() - mockProc.status = () => Promise.reject(new Error("test error")) + const mockProc = newMockProcess(() => Promise.reject(new Error("test error"))) const useRunStub = stub(useRunInternals, "nativeRun", returnsNext([mockProc])) await assertRejects(async () => { try { - await run(["sh"]) + await run(["sh"]) } finally { useRunStub.restore() } diff --git a/tests/functional/script.test.ts b/tests/functional/script.test.ts index 5f3c92da9..e9b6ca34c 100644 --- a/tests/functional/script.test.ts +++ b/tests/functional/script.test.ts @@ -11,12 +11,12 @@ Deno.test("run a python script", { sanitizeResources: false, sanitizeOps: false const useRunSpy = spy(useRunInternals, "nativeRun") try { - await run([scriptFile]) + await run([scriptFile]) } finally { useRunSpy.restore() } - const [python, script] = useRunSpy.calls[0].args[0].cmd + const [python, script] = [useRunSpy.calls[0].args[0], ...useRunSpy.calls[0].args[1].args!] assert(python.toString().startsWith("python3.")) assertEquals(script, scriptFile) diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index b36220d88..3c56a90e9 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -28,7 +28,7 @@ export const createTestHarness = async (config?: TestConfig) => { const [syncArgs, flags] = parseArgs(["--sync", "--silent"], teaDir.string) init(flags) updateConfig({ teaPrefix: new Path(TEA_PREFIX.string), env: { NO_COLOR: "1" } }) - await run(syncArgs) + await run(syncArgs) } const runTea = async (args: string[], configOverrides: Partial = {}) => { @@ -46,7 +46,7 @@ export const createTestHarness = async (config?: TestConfig) => { init(flags) updateConfig({ execPath: teaDir, teaPrefix: new Path(TEA_PREFIX.string), ...configOverrides }) - await run(appArgs) + await run(appArgs) } finally { usePrintSpy.restore() Deno.chdir(cwd) @@ -75,15 +75,27 @@ function updateConfig(updated: Partial) { useConfigInternals.setConfig({...config, ...updated, env: {...config.env, ...updated.env}}) } -// the Deno.Process object cannot be created externally with `new` so we'll just return a +// we need Deno.ChildProcress.status to be mutable +type Mutable = { + -readonly [Key in keyof Type]: Type[Key]; +} + +// the Deno.Process object cannot be created externally with `new` so we'll just return a // ProcessLike object -export function newMockProcess(statusFunction?: () => Promise): Deno.Process { - const status = statusFunction ?? (() => Promise.resolve({success: true, code: 0})) +export function newMockProcess(status?: () => Promise): Deno.Command { return { - status, - output: () => Promise.resolve(""), - stderrOutput: () => Promise.resolve(""), - close: () => { - }, - } as unknown as Deno.Process + output: function(): Promise { throw new Error("UNIMPLEMENTED") }, + outputSync(): Deno.CommandOutput { throw new Error("UNIMPLEMENTED") }, + spawn: () => ({ + pid: 10, + stdin: new WritableStream(), + stdout: new ReadableStream(), + stderr: new ReadableStream(), + status: status ? status() : Promise.resolve({ success: true, code: 0, signal: null }), + output: () => Promise.resolve({ stdout: new Uint8Array(), stderr: new Uint8Array(), success: false, code: 1, signal: null }), + kill: _ => {}, + ref: () => {}, + unref: () => {} + }) + } }