From 132bf7a845a3b2e49459dc160c5d96c4ab2cac47 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 4 Oct 2026 22:08:51 +0200 Subject: [PATCH 1/3] test: etc_default: retry reads that lose the ssh connection runsh() hides ssh's own errors, so when ssh could not connect right after the previous test rebooted the DUT, the first read came back empty and the test reported /etc/default/chronyd as missing. The removal check had the opposite problem: a failed connection passed as a removed file. Signed-off-by: Joachim Wiberg --- test/case/system/etc_default/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/case/system/etc_default/test.py b/test/case/system/etc_default/test.py index 6eb4c2818..610f5aaf1 100755 --- a/test/case/system/etc_default/test.py +++ b/test/case/system/etc_default/test.py @@ -7,6 +7,7 @@ disabled, and that a file is removed when the entry is deleted. """ import base64 +import subprocess import infamy from infamy.util import parallel @@ -20,7 +21,8 @@ def content(text): def cat(tgtssh, name): - return tgtssh.runsh(f"cat /etc/default/{name}").stdout + return tgtssh.run_retry(f"cat /etc/default/{name}", text=True, + stdout=subprocess.PIPE).stdout with infamy.Test() as test: @@ -80,6 +82,6 @@ def cat(tgtssh, name): target.delete_xpath(f"/ietf-system:system/infix-system:advanced/defaults/default[name='{NEW}']") with test.step("Verify /etc/default/infamy-test is removed"): - assert tgtssh.runsh(f"test -e /etc/default/{NEW}").returncode != 0 + assert tgtssh.run_retry(f"test -e /etc/default/{NEW}").returncode == 1 test.succeed() From 699a106275a8f7720a600d1b8e88b0282b1a374d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 4 Oct 2026 22:09:00 +0200 Subject: [PATCH 2/3] test: rc_d: retry reads that lose the ssh connection runsh() hides ssh's own errors, so a failed connection after the reboot read as an empty script list, and the check that the disabled script did not run passed on a failed connection. Signed-off-by: Joachim Wiberg --- test/case/system/rc_d/test.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/case/system/rc_d/test.py b/test/case/system/rc_d/test.py index e9f67a320..504e86ba0 100755 --- a/test/case/system/rc_d/test.py +++ b/test/case/system/rc_d/test.py @@ -10,6 +10,7 @@ and rebooting, the file must list the two names in configured order. """ import base64 +import subprocess import infamy from infamy.util import parallel, wait_boot @@ -63,7 +64,8 @@ def cleanup(env): with test.step("Verify scripts are extracted to /etc/rc.d in order"): tgtssh = env.attach("target", "mgmt", "ssh") - files = tgtssh.runsh("ls /etc/rc.d").stdout.split() + files = tgtssh.run_retry("ls /etc/rc.d", text=True, + stdout=subprocess.PIPE).stdout.split() assert files == ["01-first", "02-second"], f"unexpected /etc/rc.d contents: {files}" with test.step("Save to startup-config and reboot"): @@ -77,10 +79,11 @@ def cleanup(env): lambda: env.attach("target", "mgmt", "ssh")) with test.step("Verify enabled scripts ran once, in order"): - out = tgtssh.runsh(f"cat {ORDER}").stdout.split() + out = tgtssh.run_retry(f"cat {ORDER}", text=True, + stdout=subprocess.PIPE).stdout.split() assert out == ["first", "second"], f"unexpected run order: {out}" with test.step("Verify disabled script did not run"): - assert tgtssh.runsh(f"test -e {NEVER}").returncode != 0, "disabled script was run" + assert tgtssh.run_retry(f"test -e {NEVER}").returncode == 1, "disabled script was run" test.succeed() From ba74ca1548efa7db1e3c4c96ed3a00e4b7a3f8a4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 5 Oct 2026 06:16:08 +0200 Subject: [PATCH 3/3] test: dhcp: server_host: configure the server before its clients With all three DUTs configured at once, a client could send its three discovers before the server answered, then back off for 30 seconds, longer than the test waits for the lease. Signed-off-by: Joachim Wiberg --- test/case/dhcp/server_host/test.py | 150 ++++++++++++++--------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/test/case/dhcp/server_host/test.py b/test/case/dhcp/server_host/test.py index 73b6c8124..857042026 100755 --- a/test/case/dhcp/server_host/test.py +++ b/test/case/dhcp/server_host/test.py @@ -33,84 +33,84 @@ lambda: env.attach("client2", "mgmt")) with test.step("Configure DHCP client and server DUTs"): - parallel( - lambda: server.put_config_dicts({ - "ietf-interfaces": { - "interfaces": { - "interface": [ - { - "name": server["link1"], - "ipv4": { - "address": [{ - "ip": "192.168.1.1", - "prefix-length": 24 - }] - } - }, { - "name": server["link2"], - "ipv4": { - "address": [{ - "ip": "192.168.2.1", - "prefix-length": 24 - }] - } - }, - ] - } - }, - "infix-dhcp-server": { - "dhcp-server": { - "option": [{ - "id": "router", "address": "auto" - }], - "subnet": [ - { - "subnet": "192.168.1.0/24", - "pool": { - "start-address": POOL1, - "end-address": POOL1 - }, - "host": [{ - "address": ADDRESS1, - "match": { - "client-id": {"hex": HOSTCID1} - }, - "option": [ - { - "id": "hostname", - "name": HOSTNM11 - }, { - "id": "classless-static-route", - "static-route": [{ - "destination": "0.0.0.0/0", - "next-hop": GW1 - }] - } - ] + server.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": server["link1"], + "ipv4": { + "address": [{ + "ip": "192.168.1.1", + "prefix-length": 24 }] - }, { - "subnet": "192.168.2.0/24", - "pool": { - "start-address": POOL2, - "end-address": POOL2 - }, - "host": [{ - "address": ADDRESS2, - "match": { - "client-id": {"str": HOSTCID2} - }, - "option": [ - { - "id": "hostname", - "name": HOSTNM22 - } - ], - "lease-time": "infinite" + } + }, { + "name": server["link2"], + "ipv4": { + "address": [{ + "ip": "192.168.2.1", + "prefix-length": 24 }] + } + }, + ] + } + }, + "infix-dhcp-server": { + "dhcp-server": { + "option": [{ + "id": "router", "address": "auto" + }], + "subnet": [ + { + "subnet": "192.168.1.0/24", + "pool": { + "start-address": POOL1, + "end-address": POOL1 }, - ] - } - }}), + "host": [{ + "address": ADDRESS1, + "match": { + "client-id": {"hex": HOSTCID1} + }, + "option": [ + { + "id": "hostname", + "name": HOSTNM11 + }, { + "id": "classless-static-route", + "static-route": [{ + "destination": "0.0.0.0/0", + "next-hop": GW1 + }] + } + ] + }] + }, { + "subnet": "192.168.2.0/24", + "pool": { + "start-address": POOL2, + "end-address": POOL2 + }, + "host": [{ + "address": ADDRESS2, + "match": { + "client-id": {"str": HOSTCID2} + }, + "option": [ + { + "id": "hostname", + "name": HOSTNM22 + } + ], + "lease-time": "infinite" + }] + }, + ] + } + }}) + parallel( # We request hostname option just to ensure we don't get it. lambda: client1.put_config_dicts({ "ietf-system": {