git.iko.soy/typeable/nixpkgs
on master help sign in
nixos/test-runner: Fix execute() flakiness

Instead of using the magic string, we now just base64-encode everything and check for a newline.

commit1640359f333d8af40bf1f3e7961943ea04c6d1a1 authorJanne Heß <janne@hess.ooo> dateSun 24 Oct 2021, 15:46 (5 years ago) parent5f917bc
5 files changed+43−16
nixos/doc/manual/development/writing-nixos-tests.section.md+5 nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml+7−1 nixos/lib/test-driver/test-driver.py+28−12 nixos/tests/hibernate.nix+2−2 nixos/tests/kexec.nix+1−1
nixos/doc/manual/development/writing-nixos-tests.section.md+5
@@ -159,6 +159,11 @@ The following methods are available on machine objects:
159159`execute`
160160
161161: Execute a shell command, returning a list `(status, stdout)`.
162+ Takes an optional parameter `check_return` that defaults to `True`.
163+ Setting this parameter to `False` will not check for the return code
164+ and return -1 instead. This can be used for commands that shut down
165+ the VM and would therefore break the pipe that would be used for
166+ retrieving the return code.
162167
163168`succeed`
164169
nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml+7−1
@@ -266,7 +266,13 @@ start_all()
266266 <listitem>
267267 <para>
268268 Execute a shell command, returning a list
269− <literal>(status, stdout)</literal>.
269+ <literal>(status, stdout)</literal>. Takes an optional
270+ parameter <literal>check_return</literal> that defaults to
271+ <literal>True</literal>. Setting this parameter to
272+ <literal>False</literal> will not check for the return code
273+ and return -1 instead. This can be used for commands that shut
274+ down the VM and would therefore break the pipe that would be
275+ used for retrieving the return code.
270276 </para>
271277 </listitem>
272278 </varlistentry>
nixos/lib/test-driver/test-driver.py+28−12
@@ -581,24 +581,40 @@ class Machine:
581581 + "'{}' but it is in state ‘{}’".format(require_state, state)
582582 )
583583
584− def execute(self, command: str) -> Tuple[int, str]:
584+ def _next_newline_closed_block_from_shell(self) -> str:
585+ assert self.shell
586+ output_buffer = []
587+ while True:
588+ # This receives up to 4096 bytes from the socket
589+ chunk = self.shell.recv(4096)
590+ if not chunk:
591+ # Probably a broken pipe, return the output we have
592+ break
593+
594+ decoded = chunk.decode()
595+ output_buffer += [decoded]
596+ if decoded[-1] == "\n":
597+ break
598+ return "".join(output_buffer)
599+
600+ def execute(self, command: str, check_return: bool = True) -> Tuple[int, str]:
585601 self.connect()
586602
587− out_command = "( set -euo pipefail; {} ); echo '|!=EOF' $?\n".format(command)
603+ out_command = f"( set -euo pipefail; {command} ) | (base64 --wrap 0; echo)\n"
588604 assert self.shell
589605 self.shell.send(out_command.encode())
590606
591− output = ""
592− status_code_pattern = re.compile(r"(.*)\|\!=EOF\s+(\d+)")
607+ # Get the output
608+ output = base64.b64decode(self._next_newline_closed_block_from_shell())
593609
594− while True:
595− chunk = self.shell.recv(4096).decode(errors="ignore")
596− match = status_code_pattern.match(chunk)
597− if match:
598− output += match[1]
599− status_code = int(match[2])
600− return (status_code, output)
601− output += chunk
610+ if not check_return:
611+ return (-1, output.decode())
612+
613+ # Get the return code
614+ self.shell.send("echo ${PIPESTATUS[0]}\n".encode())
615+ rc = int(self._next_newline_closed_block_from_shell().strip())
616+
617+ return (rc, output.decode())
602618
603619 def shell_interact(self) -> None:
604620 """Allows you to interact with the guest shell
nixos/tests/hibernate.nix+2−2
@@ -95,7 +95,7 @@ in makeTest {
9595 "mkswap /dev/vda1 -L swap",
9696 # Install onto /mnt
9797 "nix-store --load-db < ${pkgs.closureInfo {rootPaths = [installedSystem];}}/registration",
98− "nixos-install --root /mnt --system ${installedSystem} --no-root-passwd",
98+ "nixos-install --root /mnt --system ${installedSystem} --no-root-passwd --no-channel-copy >&2",
9999 )
100100 machine.shutdown()
101101
@@ -110,7 +110,7 @@ in makeTest {
110110 )
111111
112112 # Hibernate machine
113− hibernate.succeed("systemctl hibernate &")
113+ hibernate.execute("systemctl hibernate &", check_return=False)
114114 hibernate.wait_for_shutdown()
115115
116116 # Restore machine from hibernation, validate our ramfs file is there.
nixos/tests/kexec.nix+1−1
@@ -18,7 +18,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
1818 testScript =
1919 ''
2020 machine.wait_for_unit("multi-user.target")
21− machine.execute("systemctl kexec &")
21+ machine.execute("systemctl kexec &", check_return=False)
2222 machine.connected = False
2323 machine.wait_for_unit("multi-user.target")
2424 '';