Skip to content

Commit 293962a

Browse files
committed
Ensure exec/spawn failures include stderr
Without this it would be difficult for users to debug.
1 parent 6369ba1 commit 293962a

File tree

5 files changed

+43
-16
lines changed

5 files changed

+43
-16
lines changed

fixtures/coder

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,46 @@
22
# Coder CLI mock
33

44
need_login() {
5-
echo "fatal: failed to read session credentials"
6-
echo " | hint: did you run \"coder login [https://coder.domain.com]\"?"
5+
>&2 echo "fatal: failed to read session credentials"
6+
>&2 echo " | hint: did you run \"coder login [https://coder.domain.com]\"?"
77
exit 1
88
}
99

10-
fail() {
11-
echo fail
10+
invalid_token() {
11+
>&2 echo "fatal: Get \"https://coder.domain.com/api\": net/http: invalid header field value \"some value\" for key Session-Token"
1212
exit 1
1313
}
1414

15-
success () {
16-
echo success
17-
exit 0
15+
fail() {
16+
>&2 echo stderr message from fail state
17+
exit 1
1818
}
1919

2020
workspaces() {
2121
cat ./workspaces.json
2222
exit 0
2323
}
2424

25+
help() {
26+
echo help
27+
exit 0
28+
}
29+
2530
main() {
2631
cd "$(dirname "${0}")"
2732

2833
# Use CODER_MOCK_STATE to force certain states.
2934
case "${CODER_MOCK_STATE-}" in
3035
"need_login") need_login ;;
36+
"invalid_token") invalid_token ;;
37+
"fail_stderr") fail_stderr ;;
38+
"fail") fail ;;
3139
esac
3240

3341
# Mock the output based on the passed arguments.
3442
case "$*" in
3543
"envs ls --output json") workspaces ;;
36-
"test fail") fail ;;
37-
"test success") success ;;
44+
"--help") help ;;
3845
*) echo "$* is not mocked" ; exit 1 ;;
3946
esac
4047
}

src/download.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ import * as download from "./download"
55
suite("Download", () => {
66
vscode.window.showInformationMessage("Start download tests.")
77

8+
teardown(() => {
9+
delete process.env.CODER_MOCK_STATE
10+
})
11+
812
test("binaryExists", async () => {
913
assert.strictEqual(await download.binaryExists("sh"), true)
1014
assert.strictEqual(await download.binaryExists("surely-no-binary-named-like-this-exists"), false)
1115
})
1216

1317
test("execCoder", async () => {
14-
assert.strictEqual(await download.execCoder("test success"), "success\n")
15-
await assert.rejects(download.execCoder("test fail"), {
18+
assert.strictEqual(await download.execCoder("--help"), "help\n")
19+
20+
// This will attempt to authenticate first, which will fail.
21+
process.env.CODER_MOCK_STATE = "fail"
22+
await assert.rejects(download.execCoder("--help"), {
1623
name: "Error",
17-
message: /Command failed: .+ test fail/,
24+
message: /Command failed: .+ --help\nstderr message from fail state\n/,
1825
})
1926
})
2027

@@ -23,6 +30,7 @@ suite("Download", () => {
2330
name: "Error",
2431
message: `Command "false" failed with code 1`,
2532
})
33+
// TODO: Test successful download.
2634
})
2735

2836
// TODO: Implement.

src/download.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export const binaryExists = async (bin: string): Promise<boolean> => {
1717
}
1818

1919
/**
20-
* Run a command with the Coder CLI after making sure it is installed. Stderr is
21-
* ignored; only stdout is returned.
20+
* Run a command with the Coder CLI after making sure it is installed. On
21+
* success stdout is returned. On failure the error will include stderr in the
22+
* message.
2223
*/
2324
export const execCoder = async (command: string): Promise<string> => {
2425
debug(`Run command: ${command}`)

src/utils.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ suite("Utils", () => {
5353
name: "Error",
5454
message: `Command "false" failed with code 1`,
5555
})
56+
await assert.rejects(utils.wrapExit(cp.spawn("bash", ["-c", ">&2 printf stderr && exit 42"])), {
57+
name: "Error",
58+
message: `Command "bash" failed with code 42: stderr`,
59+
})
5660
await assert.rejects(utils.wrapExit(cp.spawn("surely-no-executable-named-like-this-exists")), {
5761
name: "Error",
5862
message: `spawn surely-no-executable-named-like-this-exists ENOENT`,

src/utils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const split = (str: string, delimiter: string): [string, string] => {
5050
*
5151
* The callback will always fire at least once (even with just a blank string)
5252
* even if the process has no output.
53+
*
54+
* This will set the encoding on the stream to utf8.
5355
*/
5456
export const onLine = (stream: stream.Readable, callback: (line: string) => void): void => {
5557
let buffer = ""
@@ -73,18 +75,23 @@ export const onLine = (stream: stream.Readable, callback: (line: string) => void
7375
}
7476

7577
/**
76-
* Wrap a promise around a spawned process's exit.
78+
* Wrap a promise around a spawned process's exit. Rejects if the code is
79+
* non-zero. The error will include the code and the stderr if any in the
80+
* message.
7781
*
7882
* Use in conjunction with `child_process.spawn()`.
7983
*/
8084
export function wrapExit(proc: cp.ChildProcess): Promise<void> {
8185
return new Promise<void>((resolve, reject) => {
86+
const stderr: string[] = []
87+
proc.stderr?.on("data", (d) => stderr.push(d.toString()))
8288
proc.on("error", reject) // Catches ENOENT for example.
8389
proc.on("exit", (code) => {
8490
if (code === 0) {
8591
resolve()
8692
} else {
87-
reject(new Error(`Command "${proc.spawnfile}" failed with code ${code}`))
93+
const details = stderr.length > 0 ? `: ${stderr.join()}` : ""
94+
reject(new Error(`Command "${proc.spawnfile}" failed with code ${code}${details}`))
8895
}
8996
})
9097
})

0 commit comments

Comments
 (0)