Skip to content

Commit b7749c7

Browse files
committed
nixos/test-driver: Run commands with error handling
Bash's standard behavior of not propagating non-zero exit codes through a pipeline is unexpected and almost universally unwanted. Default to setting `pipefail` for the command being run; it can still be turned off by prefixing the pipeline with `set +o pipefail` if needed. Also, set `errexit` and `nonunset` options to make the first command of consecutive commands separated by `;` fail, and disallow dereferencing unset variables respectively.
1 parent f36a65f commit b7749c7

4 files changed

Lines changed: 30 additions & 9 deletions

File tree

nixos/doc/manual/development/writing-nixos-tests.xml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,29 @@ start_all()
274274
</term>
275275
<listitem>
276276
<para>
277-
Execute a shell command, raising an exception if the exit status is not
278-
zero, otherwise returning the standard output.
277+
Execute a shell command, raising an exception if the exit status
278+
is not zero, otherwise returning the standard output. Commands
279+
are run with <literal>set -euo pipefail</literal> set:
280+
<itemizedlist>
281+
<listitem>
282+
<para>
283+
If several commands are separated by <literal>;</literal>
284+
and one fails, the command as a whole will fail.
285+
</para>
286+
</listitem>
287+
<listitem>
288+
<para>
289+
For pipelines, the last non-zero exit status will be
290+
returned (if there is one, zero will be returned
291+
otherwise).
292+
</para>
293+
</listitem>
294+
<listitem>
295+
<para>
296+
Dereferencing unset variables fail the command.
297+
</para>
298+
</listitem>
299+
</itemizedlist>
279300
</para>
280301
</listitem>
281302
</varlistentry>

nixos/lib/test-driver/test-driver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def require_unit_state(self, unit: str, require_state: str = "active") -> None:
441441
def execute(self, command: str) -> Tuple[int, str]:
442442
self.connect()
443443

444-
out_command = "( {} ); echo '|!=EOF' $?\n".format(command)
444+
out_command = "( set -euo pipefail; {} ); echo '|!=EOF' $?\n".format(command)
445445
self.shell.send(out_command.encode())
446446

447447
output = ""

nixos/tests/docker-tools.nix

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ import ./make-test-python.nix ({ pkgs, ... }: {
2323
with subtest("includeStorePath"):
2424
with subtest("assumption"):
2525
docker.succeed("${examples.helloOnRoot} | docker load")
26-
docker.succeed("set -euo pipefail; docker run --rm hello | grep -i hello")
26+
docker.succeed("docker run --rm hello | grep -i hello")
2727
docker.succeed("docker image rm hello:latest")
2828
with subtest("includeStorePath = false; breaks example"):
2929
docker.succeed("${examples.helloOnRootNoStore} | docker load")
30-
docker.fail("set -euo pipefail; docker run --rm hello | grep -i hello")
30+
docker.fail("docker run --rm hello | grep -i hello")
3131
docker.succeed("docker image rm hello:latest")
3232
with subtest("includeStorePath = false; works with mounted store"):
3333
docker.succeed("${examples.helloOnRootNoStore} | docker load")
34-
docker.succeed("set -euo pipefail; docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello")
34+
docker.succeed("docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello")
3535
docker.succeed("docker image rm hello:latest")
3636
3737
with subtest("Ensure Docker images use a stable date by default"):

nixos/tests/wiki-js.nix

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
119119
120120
with subtest("Setup"):
121121
result = machine.succeed(
122-
"set -o pipefail; curl -sSf localhost:3000/finalize -X POST -d "
122+
"curl -sSf localhost:3000/finalize -X POST -d "
123123
+ "@${payloads.finalize} -H 'Content-Type: application/json' "
124124
+ "| jq .ok | xargs echo"
125125
)
@@ -132,15 +132,15 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
132132
133133
with subtest("Base functionality"):
134134
auth = machine.succeed(
135-
"set -o pipefail; curl -sSf localhost:3000/graphql -X POST "
135+
"curl -sSf localhost:3000/graphql -X POST "
136136
+ "-d @${payloads.login} -H 'Content-Type: application/json' "
137137
+ "| jq '.[0].data.authentication.login.jwt' | xargs echo"
138138
).strip()
139139
140140
assert auth
141141
142142
create = machine.succeed(
143-
"set -o pipefail; curl -sSf localhost:3000/graphql -X POST "
143+
"curl -sSf localhost:3000/graphql -X POST "
144144
+ "-d @${payloads.content} -H 'Content-Type: application/json' "
145145
+ f"-H 'Authorization: Bearer {auth}' "
146146
+ "| jq '.[0].data.pages.create.responseResult.succeeded'|xargs echo"

0 commit comments

Comments
 (0)