[wic][PATCH 5/5] tests/unit/test_ksparser_types: cover ksparser's argparse types
Trevor Woerner <[email protected]> Wed, 15 Jul 2026 18:32:26 -0400
| Newsgroups | org.yoctoproject.lists.yocto-patches |
|---|---|
| Message-ID | <[email protected]> |
Add direct unit coverage for sizetype(), overheadtype(), and systemidtype(): the accepted KiB/byte-mode units and default-suffix handling, the >= 1.0 overhead contract, and the 0x-prefixed system-id byte range, together with the boundary and malformed inputs each rejects. The cases pin behaviour that is easy to regress: sizetype() rejects a negative count, overheadtype() raises a clean ArgumentTypeError (not a TypeError) for a sub-1.0 factor and refuses nan/inf, and systemidtype() accepts only an explicit 0x-prefixed hex byte. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner <[email protected]> --- tests/unit/test_ksparser_types.py | 226 ++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/unit/test_ksparser_types.py diff --git a/tests/unit/test_ksparser_types.py b/tests/unit/test_ksparser_types.py new file mode 100644 index 000000000000..49ac38ba5397 --- /dev/null +++ b/tests/unit/test_ksparser_types.py @@ -0,0 +1,226 @@ +""" +Unit tests for wic/ksparser.py's custom argparse types: sizetype, +overheadtype, and systemidtype. These are wic's own input validators for +kickstart/command-line values, so the tests pin the accepted forms, the +rejected forms, and the boundaries between them. +""" +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)) + +from argparse import ArgumentTypeError + +from wic.ksparser import sizetype, overheadtype, systemidtype + + +class TestSizetype: + """sizetype(default, size_in_bytes=False) returns f(arg) that turns a + "<num>[S|s|K|k|M|G]" string into an integer. With size_in_bytes=False + the multiplier is 1 (K->1, M->1024, G->1024*1024, i.e. KiB counts); + with size_in_bytes=True the multiplier is 1024 and S/s means sectors of + 512 bytes. A bare number takes the default suffix.""" + + @pytest.mark.parametrize("arg,expected", [ + ("0", 0), + ("0M", 0), + ("1K", 1), + ("1k", 1), + ("3K", 3), + ("7K", 7), + ("512k", 512), + ("999k", 999), + ("1024K", 1024), + ("1000K", 1000), + ("1M", 1 * 1024), + ("10M", 10 * 1024), + ("13M", 13 * 1024), + ("100M", 100 * 1024), + ("512M", 512 * 1024), + ("1G", 1 * 1024 * 1024), + ("2G", 2 * 1024 * 1024), + ("3G", 3 * 1024 * 1024), + # a large magnitude past 2**31, to catch any 32-bit truncation + ("5000000K", 5000000), + ]) + def test_kib_units_with_M_default(self, arg, expected): + assert sizetype("M")(arg) == expected + + def test_bare_number_uses_M_default(self): + assert sizetype("M")("100") == 100 * 1024 + + def test_bare_number_uses_K_default(self): + assert sizetype("K")("512") == 512 + + @pytest.mark.parametrize("arg,expected", [ + ("1S", 512), + ("1s", 512), + ("2S", 2 * 512), + ("7S", 7 * 512), + ("1K", 1024), + ("3K", 3 * 1024), + ("17M", 17 * 1024 * 1024), + ("1M", 1024 * 1024), + ("1G", 1024 ** 3), + # past 2**63 to confirm Python's arbitrary-precision ints hold + ("10000000G", 10000000 * 1024 ** 3), + ]) + def test_size_in_bytes_units(self, arg, expected): + assert sizetype("K", size_in_bytes=True)(arg) == expected + + def test_sectors_only_when_size_in_bytes(self): + # 'S' is a valid suffix only in byte mode; without it the suffix is + # unknown and rejected. + with pytest.raises(ArgumentTypeError): + sizetype("M")("1S") + + # --- suffix case sensitivity ------------------------------------------- + + @pytest.mark.parametrize("arg", ["1m", "1g"]) + def test_lowercase_m_and_g_rejected(self, arg): + # Only k/K are case-insensitive; m and g are not accepted. + with pytest.raises(ArgumentTypeError): + sizetype("M")(arg) + + # --- negative sizes ----------------------------------------------------- + + @pytest.mark.parametrize("arg", ["-1M", "-5", "-1K", "-1024K", "-1G"]) + def test_negative_size_rejected(self, arg): + with pytest.raises(ArgumentTypeError): + sizetype("M")(arg) + + # --- invalid formats ---------------------------------------------------- + + @pytest.mark.parametrize("bad", [ + "abc", # no digits + "M", # suffix only + "1X", # unsupported suffix + "1.5M", # float not accepted + "1M ", # trailing space becomes the suffix + "1e3M", # scientific notation + "", # empty + " ", # whitespace only + "1;M", # shell metacharacters + "1|M", + "$(1)M", + "1`M", + ]) + def test_invalid_format_rejected(self, bad): + with pytest.raises(ArgumentTypeError): + sizetype("M")(bad) + + def test_none_rejected(self): + with pytest.raises((ArgumentTypeError, TypeError)): + sizetype("M")(None) + + +class TestOverheadtype: + """overheadtype(arg) -> float, and the factor must be >= 1.0. Anything + below 1.0, non-numeric, or non-finite is an ArgumentTypeError.""" + + @pytest.mark.parametrize("arg,expected", [ + ("1.0", 1.0), + ("1.001", 1.001), + ("1.1", 1.1), + ("1.3", 1.3), + ("1.37", 1.37), + ("3.14159", 3.14159), + ("7", 7.0), + ("2", 2.0), + ("2.0", 2.0), + ("100.0", 100.0), + ("1000000.0", 1000000.0), + ]) + def test_valid_factor(self, arg, expected): + assert overheadtype(arg) == pytest.approx(expected) + + def test_leading_whitespace_accepted(self): + # float() strips surrounding whitespace. + assert overheadtype(" 1.1") == pytest.approx(1.1) + + def test_trailing_whitespace_accepted(self): + assert overheadtype("1.1 ") == pytest.approx(1.1) + + # --- below 1.0: a clean ArgumentTypeError, not a TypeError crash -------- + + @pytest.mark.parametrize("arg", ["0.0", "0.5", "0.999", "-1.0", "-0.5"]) + def test_below_one_rejected(self, arg): + with pytest.raises(ArgumentTypeError): + overheadtype(arg) + + # --- non-finite --------------------------------------------------------- + + @pytest.mark.parametrize("arg", ["nan", "NaN", "inf", "Infinity", "-inf"]) + def test_non_finite_rejected(self, arg): + with pytest.raises(ArgumentTypeError): + overheadtype(arg) + + # --- non-numeric -------------------------------------------------------- + + @pytest.mark.parametrize("bad", [ + "abc", "1.1.1", "M", "1,3", "1 1", "--1.1", "", + ]) + def test_non_numeric_rejected(self, bad): + with pytest.raises(ArgumentTypeError): + overheadtype(bad) + + def test_none_rejected(self): + with pytest.raises((ArgumentTypeError, TypeError)): + overheadtype(None) + + +class TestSystemidtype: + """systemidtype(arg) validates an MBR partition type id as an explicit + 0x-prefixed hex byte in 0x1..0xFF and returns the original string + unchanged.""" + + @pytest.mark.parametrize("arg", [ + "0x1", "0x01", "0x0b", "0x0B", "0x82", "0x83", "0xFE", "0xFF", + "0xff", "0xFf", "0X82", + # odd, non-round bytes across the range + "0x7", "0x2a", "0x5b", "0x93", "0xa7", "0xd3", + ]) + def test_valid_returned_unchanged(self, arg): + assert systemidtype(arg) == arg + + # --- out of range ------------------------------------------------------- + + @pytest.mark.parametrize("arg", ["0x0", "0x00", "0x100", "0xFFFF"]) + def test_out_of_range_rejected(self, arg): + with pytest.raises(ArgumentTypeError): + systemidtype(arg) + + # --- must be an explicit 0x-prefixed hex string ------------------------- + + @pytest.mark.parametrize("arg", [ + "82", # no 0x prefix (would parse as hex 130) + "130", # decimal that names a valid value + "+0x82", # leading sign + "-0x82", + "0b1010", # binary prefix + ]) + def test_non_prefixed_forms_rejected(self, arg): + with pytest.raises(ArgumentTypeError): + systemidtype(arg) + + @pytest.mark.parametrize("bad", [ + "0x", # prefix with no digit + "0xGG", # invalid hex digit + "abc", # not hex + "0x 82", # embedded whitespace + " 0x82", # leading whitespace + "", # empty + " ", # whitespace only + "0x" + "F" * 10000, # very long + ]) + def test_invalid_format_rejected(self, bad): + with pytest.raises(ArgumentTypeError): + systemidtype(bad) + + def test_none_rejected(self): + with pytest.raises((ArgumentTypeError, TypeError)): + systemidtype(None) -- 2.50.0.173.g8b6f19ccfc3a