Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</p>


# tea/cli 0.30.0
# tea/cli 0.31.0

`tea` puts the whole open source ecosystem at your fingertips:

Expand Down
20 changes: 10 additions & 10 deletions src/hooks/useRun.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Path from "path"
import { isArray } from "is_what"

export interface RunOptions extends Omit<Deno.RunOptions, 'cmd'|'cwd'|'stdout'|'stderr'|'stdin'> {
export interface RunOptions extends Omit<Deno.CommandOptions, 'args'|'cwd'|'stdout'|'stderr'|'stdin'> {
cmd: (string | Path)[] | Path
cwd?: (string | Path)
clearEnv?: boolean //NOTE might not be cross platform!
Expand All @@ -21,33 +21,33 @@ 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<Deno.RunOptions, 'stdout'|'stderr'|'stdin'>
const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick<Deno.CommandOptions, 'stdout'|'stderr'|'stdin'>
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) {
if (spin && proc) {
//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 = {
Expand Down
8 changes: 4 additions & 4 deletions src/vendor/Path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
35 changes: 18 additions & 17 deletions tests/functional/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Deno.ProcessStatus> => Promise.resolve({success: false, code: 123}),
procStatus: (): Promise<Deno.CommandStatus> => Promise.resolve({success: false, code: 123, signal: null}),
expectedErr: "exiting with code: 123",
},
},
{
name: "normal error",
procStatus: (): Promise<Deno.ProcessStatus> => Promise.reject(new Error("test error")),
procStatus: (): Promise<Deno.CommandStatus> => Promise.reject(new Error("test error")),
expectedErr: "exiting with code: 1",
},
},
{
name: "tea error",
procStatus: (): Promise<Deno.ProcessStatus> => Promise.reject(new TeaError("confused: interpreter", {})),
procStatus: (): Promise<Deno.CommandStatus> => Promise.reject(new TeaError("confused: interpreter", {})),
expectedErr: "exiting with code: 1",
},
},
{
name: "not found",
procStatus: (): Promise<Deno.ProcessStatus> => Promise.reject(new Deno.errors.NotFound()),
procStatus: (): Promise<Deno.CommandStatus> => Promise.reject(new Deno.errors.NotFound()),
expectedErr: "exiting with code: 127",
},
},
{
name: "permission denied",
procStatus: (): Promise<Deno.ProcessStatus> => Promise.reject(new Deno.errors.PermissionDenied()),
procStatus: (): Promise<Deno.CommandStatus> => Promise.reject(new Deno.errors.PermissionDenied()),
expectedErr: "exiting with code: 127",
},
},
]

for (const { name, procStatus, expectedErr } of tests) {
Expand All @@ -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()
}
Expand All @@ -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")
Expand Down
23 changes: 11 additions & 12 deletions tests/functional/repl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\\] %~ "},
Expand Down Expand Up @@ -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)
Expand All @@ -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()
}
Expand All @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 23 additions & 11 deletions tests/functional/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config> = {}) => {
Expand All @@ -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)
Expand Down Expand Up @@ -75,15 +75,27 @@ function updateConfig(updated: Partial<Config>) {
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<Type> = {
-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.ProcessStatus>): Deno.Process {
const status = statusFunction ?? (() => Promise.resolve({success: true, code: 0}))
export function newMockProcess(status?: () => Promise<Deno.CommandStatus>): Deno.Command {
return {
status,
output: () => Promise.resolve(""),
stderrOutput: () => Promise.resolve(""),
close: () => {
},
} as unknown as Deno.Process
output: function(): Promise<Deno.CommandOutput> { throw new Error("UNIMPLEMENTED") },
outputSync(): Deno.CommandOutput { throw new Error("UNIMPLEMENTED") },
spawn: () => ({
pid: 10,
stdin: new WritableStream<Uint8Array>(),
stdout: new ReadableStream<Uint8Array>(),
stderr: new ReadableStream<Uint8Array>(),
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: () => {}
})
}
}