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.

talyz b7749c76 f36a65f6

Changed files
+30 -9
nixos
doc
manual
lib
test-driver
tests
+23 -2
nixos/doc/manual/development/writing-nixos-tests.xml
···
</term>
<listitem>
<para>
-
Execute a shell command, raising an exception if the exit status is not
-
zero, otherwise returning the standard output.
+
Execute a shell command, raising an exception if the exit status
+
is not zero, otherwise returning the standard output. Commands
+
are run with <literal>set -euo pipefail</literal> set:
+
<itemizedlist>
+
<listitem>
+
<para>
+
If several commands are separated by <literal>;</literal>
+
and one fails, the command as a whole will fail.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
For pipelines, the last non-zero exit status will be
+
returned (if there is one, zero will be returned
+
otherwise).
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
Dereferencing unset variables fail the command.
+
</para>
+
</listitem>
+
</itemizedlist>
</para>
</listitem>
</varlistentry>
+1 -1
nixos/lib/test-driver/test-driver.py
···
def execute(self, command: str) -> Tuple[int, str]:
self.connect()
-
out_command = "( {} ); echo '|!=EOF' $?\n".format(command)
+
out_command = "( set -euo pipefail; {} ); echo '|!=EOF' $?\n".format(command)
self.shell.send(out_command.encode())
output = ""
+3 -3
nixos/tests/docker-tools.nix
···
with subtest("includeStorePath"):
with subtest("assumption"):
docker.succeed("${examples.helloOnRoot} | docker load")
-
docker.succeed("set -euo pipefail; docker run --rm hello | grep -i hello")
+
docker.succeed("docker run --rm hello | grep -i hello")
docker.succeed("docker image rm hello:latest")
with subtest("includeStorePath = false; breaks example"):
docker.succeed("${examples.helloOnRootNoStore} | docker load")
-
docker.fail("set -euo pipefail; docker run --rm hello | grep -i hello")
+
docker.fail("docker run --rm hello | grep -i hello")
docker.succeed("docker image rm hello:latest")
with subtest("includeStorePath = false; works with mounted store"):
docker.succeed("${examples.helloOnRootNoStore} | docker load")
-
docker.succeed("set -euo pipefail; docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello")
+
docker.succeed("docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello")
docker.succeed("docker image rm hello:latest")
with subtest("Ensure Docker images use a stable date by default"):
+3 -3
nixos/tests/wiki-js.nix
···
with subtest("Setup"):
result = machine.succeed(
-
"set -o pipefail; curl -sSf localhost:3000/finalize -X POST -d "
+
"curl -sSf localhost:3000/finalize -X POST -d "
+ "@${payloads.finalize} -H 'Content-Type: application/json' "
+ "| jq .ok | xargs echo"
)
···
with subtest("Base functionality"):
auth = machine.succeed(
-
"set -o pipefail; curl -sSf localhost:3000/graphql -X POST "
+
"curl -sSf localhost:3000/graphql -X POST "
+ "-d @${payloads.login} -H 'Content-Type: application/json' "
+ "| jq '.[0].data.authentication.login.jwt' | xargs echo"
).strip()
···
assert auth
create = machine.succeed(
-
"set -o pipefail; curl -sSf localhost:3000/graphql -X POST "
+
"curl -sSf localhost:3000/graphql -X POST "
+ "-d @${payloads.content} -H 'Content-Type: application/json' "
+ f"-H 'Authorization: Bearer {auth}' "
+ "| jq '.[0].data.pages.create.responseResult.succeeded'|xargs echo"