From: Trevor Woerner <twoerner@gmail.com>
To: yocto-patches@lists.yoctoproject.org
Subject: [wic][PATCH 2/2] tests/unit/test_ksparser_parse: cover the .wks KickStart parser
Date: Fri, 17 Jul 2026 14:35:07 -0400 [thread overview]
Message-ID: <20260717183507.3539287-3-twoerner@gmail.com> (raw)
In-Reply-To: <20260717183507.3539287-1-twoerner@gmail.com>
Add end-to-end coverage for KickStart(): happy-path part/bootloader
lines, degenerate files (empty, comment-only, CRLF, unknown directive),
mutually-exclusive and invalid option combinations, multiple-bootloader
rejection, diskid parsing for both ptables, and include handling. The
bitbake variable lookup is neutralised so the parser runs with no
BitBake and no host tools.
The msdos --diskid cases assert that a non-integer value raises a
KickStartError naming the offending value.
AI-Generated: codex/claude-opus 4.8 (xhigh)
Signed-off-by: Trevor Woerner <twoerner@gmail.com>
---
tests/unit/test_ksparser_parse.py | 217 ++++++++++++++++++++++++++++++
1 file changed, 217 insertions(+)
create mode 100644 tests/unit/test_ksparser_parse.py
diff --git a/tests/unit/test_ksparser_parse.py b/tests/unit/test_ksparser_parse.py
new file mode 100644
index 000000000000..d5edc1250efc
--- /dev/null
+++ b/tests/unit/test_ksparser_parse.py
@@ -0,0 +1,217 @@
+r"""
+End-to-end tests for the KickStart .wks parser (wic/ksparser.py).
+
+KickStart(confpath) reads a .wks file line by line, runs each line
+through an argparse-based grammar, validates filesystem/option
+combinations, and builds Partition objects plus a bootloader config.
+It is pure logic apart from get_bitbake_var() (used for ${VAR}
+expansion and the bootloader APPEND), which these tests neutralise so
+no bitbake or host tools are needed.
+
+Each test writes a small .wks file and asserts on the parsed result or
+on the specific error raised.
+"""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+_SRC = Path(__file__).resolve().parent.parent.parent / "src"
+if str(_SRC) not in sys.path:
+ sys.path.insert(0, str(_SRC))
+
+import wic.misc as misc
+import wic.ksparser as ksparser
+from wic.ksparser import KickStart, KickStartError
+
+
+@pytest.fixture(autouse=True)
+def no_bitbake(monkeypatch):
+ """Neutralise bitbake variable lookups in both modules.
+
+ get_bitbake_var is imported into ksparser's namespace and also used
+ via misc; patch both so ${VAR} expansion and APPEND resolve to None
+ instead of trying to run `bitbake -e`.
+ """
+ monkeypatch.setattr(misc, "get_bitbake_var", lambda *a, **k: None)
+ monkeypatch.setattr(ksparser, "get_bitbake_var", lambda *a, **k: None)
+
+
+def _wks(tmp_path, text):
+ path = tmp_path / "test.wks"
+ path.write_text(text)
+ return str(path)
+
+
+def _parse(tmp_path, text):
+ return KickStart(_wks(tmp_path, text))
+
+
+# ---------------------------------------------------------------------------
+# Happy path
+# ---------------------------------------------------------------------------
+
+class TestParseHappyPath:
+
+ def test_single_part(self, tmp_path):
+ ks = _parse(tmp_path, "part / --source rootfs --fstype ext4 --size 100M\n")
+ assert len(ks.partitions) == 1
+ p = ks.partitions[0]
+ assert p.fstype == "ext4"
+ assert p.size == 100 * 1024
+ assert p.mountpoint == "/"
+
+ def test_defaults_applied_without_fixed_size(self, tmp_path):
+ ks = _parse(tmp_path, "part / --source rootfs --fstype ext4 --size 1M\n")
+ p = ks.partitions[0]
+ assert p.overhead_factor == KickStart.DEFAULT_OVERHEAD_FACTOR
+ assert p.extra_filesystem_space == KickStart.DEFAULT_EXTRA_FILESYSTEM_SPACE
+
+ def test_partnum_increments(self, tmp_path):
+ ks = _parse(
+ tmp_path,
+ "part /boot --source bootimg --fstype vfat --size 50M\n"
+ "part / --source rootfs --fstype ext4 --size 100M\n",
+ )
+ assert [p.lineno for p in ks.partitions] == [1, 2]
+ assert len(ks.partitions) == 2
+
+ def test_bootloader_parsed(self, tmp_path):
+ ks = _parse(tmp_path, "bootloader --ptable gpt\n")
+ assert ks.bootloader.ptable == "gpt"
+
+ def test_comment_and_blank_lines_ignored(self, tmp_path):
+ ks = _parse(
+ tmp_path,
+ "# a comment\n"
+ "\n"
+ " \n"
+ "part / --source rootfs --fstype ext4 --size 1M\n",
+ )
+ assert len(ks.partitions) == 1
+
+
+# ---------------------------------------------------------------------------
+# Degenerate files
+# ---------------------------------------------------------------------------
+
+class TestParseDegenerateFiles:
+
+ def test_empty_file(self, tmp_path):
+ ks = _parse(tmp_path, "")
+ assert ks.partitions == []
+ assert ks.bootloader is not None # defaults filled in
+
+ def test_comment_only_file(self, tmp_path):
+ ks = _parse(tmp_path, "# only comments\n# nothing else\n")
+ assert ks.partitions == []
+
+ def test_crlf_line_endings(self, tmp_path):
+ ks = _parse(tmp_path, "part / --source rootfs --fstype ext4 --size 1M\r\n")
+ assert len(ks.partitions) == 1
+
+ def test_unknown_directive_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "frobnicate everything\n")
+
+
+# ---------------------------------------------------------------------------
+# Mutually-exclusive and invalid option combinations
+# ---------------------------------------------------------------------------
+
+class TestParseInvalidCombinations:
+
+ def test_size_and_fixed_size_conflict(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype ext4 "
+ "--size 100M --fixed-size 200M\n")
+
+ def test_fixed_size_with_overhead_factor_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype ext4 "
+ "--fixed-size 100M --overhead-factor 1.5\n")
+
+ def test_squashfs_with_label_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype squashfs --label boot\n")
+
+ def test_squashfs_with_fsuuid_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype squashfs --fsuuid 0x1234\n")
+
+ def test_erofs_with_label_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype erofs --label x --size 1M\n")
+
+ def test_use_label_without_label_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype ext4 --size 1M --use-label\n")
+
+ def test_msdos_fsuuid_too_long_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "part / --source rootfs --fstype msdos "
+ "--size 1M --fsuuid 0xDEADBEEF12\n")
+
+
+# ---------------------------------------------------------------------------
+# Multiple bootloaders
+# ---------------------------------------------------------------------------
+
+class TestParseBootloader:
+
+ def test_two_bootloaders_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "bootloader --ptable gpt\nbootloader --ptable msdos\n")
+
+ def test_msdos_diskid_integer(self, tmp_path):
+ ks = _parse(tmp_path, "bootloader --ptable msdos --diskid 0x12345678\n")
+ assert ks.bootloader.diskid == 0x12345678
+
+ def test_gpt_bad_diskid_raises_kickstart_error(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "bootloader --ptable gpt --diskid not-a-uuid\n")
+
+ def test_msdos_bad_diskid_raises_kickstart_error(self, tmp_path):
+ # A non-integer --diskid on an msdos ptable must raise a described
+ # KickStartError, not an opaque AttributeError from the error path
+ # interpolating a missing attribute.
+ with pytest.raises(KickStartError) as excinfo:
+ _parse(tmp_path, "bootloader --ptable msdos --diskid not-an-int\n")
+ # The message names the offending value so the user can see what
+ # was rejected.
+ assert "not-an-int" in str(excinfo.value)
+
+ def test_msdos_bad_diskid_hex_string_rejected(self, tmp_path):
+ # int(x, 0) parses "0x..." but a stray non-hex digit still fails;
+ # the same described KickStartError path handles it.
+ with pytest.raises(KickStartError) as excinfo:
+ _parse(tmp_path, "bootloader --ptable msdos --diskid 0xZZ\n")
+ assert "0xZZ" in str(excinfo.value)
+
+ def test_gpt_diskid_uuid_parsed(self, tmp_path):
+ ks = _parse(
+ tmp_path,
+ "bootloader --ptable gpt "
+ "--diskid 12345678-1234-5678-1234-567812345678\n",
+ )
+ assert str(ks.bootloader.diskid) == \
+ "12345678-1234-5678-1234-567812345678"
+
+
+# ---------------------------------------------------------------------------
+# Include handling
+# ---------------------------------------------------------------------------
+
+class TestParseInclude:
+
+ def test_include_missing_file_rejected(self, tmp_path):
+ with pytest.raises(KickStartError):
+ _parse(tmp_path, "include does-not-exist.wks\n")
+
+ def test_include_pulls_in_partitions(self, tmp_path):
+ inc = tmp_path / "inc.wks"
+ inc.write_text("part / --source rootfs --fstype ext4 --size 5M\n")
+ ks = _parse(tmp_path, "include inc.wks\n")
+ assert len(ks.partitions) == 1
+ assert ks.partitions[0].size == 5 * 1024
--
2.50.0.173.g8b6f19ccfc3a
prev parent reply other threads:[~2026-07-17 18:35 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-17 18:35 [wic][PATCH 0/2] tests/unit/test_ksparser_parse: parse the .wks file, fix a diskid crash Trevor Woerner
2026-07-17 18:35 ` [wic][PATCH 1/2] ksparser: fix crash on an invalid msdos --diskid Trevor Woerner
2026-07-17 18:35 ` Trevor Woerner [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260717183507.3539287-3-twoerner@gmail.com \
--to=twoerner@gmail.com \
--cc=yocto-patches@lists.yoctoproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.