From 72bae42096e7e8807e306a1a4cfbd263191bc1af Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:16:13 -0300 Subject: [PATCH] test(dhcptest): write "no boot file" as absent, which is what a reply carries Four scenarios asserted `bootfile matches ^$` for the cases where the point is that the client is handed nothing to fetch: OPAL-v3 and s390x, which get a conf-file instead, a petitboot node, and the architecture whose loader was taken off disk. That assertion cannot pass. A server naming no boot file sends an empty BOOTP file header and no option 67, and an empty value is absent -- so the run reported "bootfile is not present in the reply" against both backends, in every case, whatever the server had done. Four failures that said nothing about either implementation. `bootfile absent` is the assertion those scenarios wanted. A unit test pins it, so the unsatisfiable form cannot come back unnoticed. --- xCAT-test/dhcptest/README.md | 4 ++++ xCAT-test/dhcptest/conf/loader-absent.conf | 2 +- xCAT-test/dhcptest/conf/netboot-methods.conf | 2 +- xCAT-test/dhcptest/conf/pxe-arch-matrix.conf | 4 ++-- xCAT-test/dhcptest/tests/test_assertions.py | 11 +++++++++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/xCAT-test/dhcptest/README.md b/xCAT-test/dhcptest/README.md index f09ef3dba..a775d8926 100644 --- a/xCAT-test/dhcptest/README.md +++ b/xCAT-test/dhcptest/README.md @@ -140,6 +140,10 @@ One per line under a single multi-line `assert` key, as `target op value`. | `matches` `contains` `starts-with` `ends-with` | text | | `<` `<=` `>` `>=` | numeric | +"Nothing to fetch" is `bootfile absent`, not a match against an empty string: +a server that names no boot file sends an empty `file` header and no option 67, +which is an absent value, and `matches ^$` can never hold against it. + Assert on `bootfile`, not on `file`, unless the header itself is the point. Servers genuinely differ — ISC dhcpd fills the BOOTP header, dnsmasq answers in option 67 once the client has asked for it — and firmware reads whichever diff --git a/xCAT-test/dhcptest/conf/loader-absent.conf b/xCAT-test/dhcptest/conf/loader-absent.conf index d60ddebdf..1aa5bb57e 100644 --- a/xCAT-test/dhcptest/conf/loader-absent.conf +++ b/xCAT-test/dhcptest/conf/loader-absent.conf @@ -33,7 +33,7 @@ client_arch = 0x0000 assert = msgtype == OFFER yiaddr in %(pool)s - bootfile matches ^$ + bootfile absent [scenario present-loader-is-still-named] description = S-12: An architecture whose loader is still there is unaffected diff --git a/xCAT-test/dhcptest/conf/netboot-methods.conf b/xCAT-test/dhcptest/conf/netboot-methods.conf index 71005282c..bc88bae97 100644 --- a/xCAT-test/dhcptest/conf/netboot-methods.conf +++ b/xCAT-test/dhcptest/conf/netboot-methods.conf @@ -160,7 +160,7 @@ client_arch = 0x000e assert = msgtype == OFFER option:209 == %(petitboot_conf)s - bootfile matches ^$ + bootfile absent [scenario noip-interface-is-denied] description = S-07: An interface marked *NOIP* is not answered at all diff --git a/xCAT-test/dhcptest/conf/pxe-arch-matrix.conf b/xCAT-test/dhcptest/conf/pxe-arch-matrix.conf index 8d1cdc67c..a2048ac9e 100644 --- a/xCAT-test/dhcptest/conf/pxe-arch-matrix.conf +++ b/xCAT-test/dhcptest/conf/pxe-arch-matrix.conf @@ -177,7 +177,7 @@ client_arch = 0x000e assert = msgtype == OFFER option:209 == %(opal_conf)s - bootfile matches ^$ + bootfile absent [scenario s390x-qemu] description = S-16: A QEMU s390x client is given a conf-file and no loader @@ -188,7 +188,7 @@ client_arch = 0x001f assert = msgtype == OFFER option:209 == %(s390x_conf)s - bootfile matches ^$ + bootfile absent [scenario vendor-class-etherboot] description = S-18: A client that identifies itself by vendor class alone still boots diff --git a/xCAT-test/dhcptest/tests/test_assertions.py b/xCAT-test/dhcptest/tests/test_assertions.py index d9fc6f28f..f11b342c5 100644 --- a/xCAT-test/dhcptest/tests/test_assertions.py +++ b/xCAT-test/dhcptest/tests/test_assertions.py @@ -72,6 +72,17 @@ class Evaluation(unittest.TestCase): self.assertTrue(self.check("siaddr == 10.0.0.1").ok) self.assertFalse(self.check("siaddr == 10.0.0.2").ok) + def test_a_reply_naming_no_boot_file_is_absent_not_empty(self): + # A server that hands out no boot file sends an empty `file` header and + # no option 67. "Nothing to fetch" is therefore written `bootfile + # absent`: a regex against an empty string can never hold, so a + # scenario written that way would fail whatever the server did. + nothing = offer(file="", options={54: "10.0.0.1"}) + self.assertTrue(self.check("bootfile absent", nothing).ok) + self.assertFalse(self.check("bootfile present", nothing).ok) + self.assertFalse(self.check("bootfile matches ^$", nothing).ok) + self.assertTrue(self.check("bootfile present").ok) + def test_options_by_number_and_name(self): self.assertTrue(self.check("option:54 == 10.0.0.1").ok) self.assertTrue(self.check("option:server_id == 10.0.0.1").ok)