From: Trevor Woerner <twoerner@gmail.com>
To: yocto-patches@lists.yoctoproject.org
Subject: [wic][PATCH 5/5] tests/unit/test_ksparser_types: cover ksparser's argparse types
Date: Wed, 15 Jul 2026 18:32:26 -0400 [thread overview]
Message-ID: <20260715223226.3371498-6-twoerner@gmail.com> (raw)
In-Reply-To: <20260715223226.3371498-1-twoerner@gmail.com>
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 <twoerner@gmail.com>
---
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
prev parent reply other threads:[~2026-07-15 22:32 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-15 22:32 [wic][PATCH 0/5] ksparser: four argparse-type fixes plus unit coverage Trevor Woerner
2026-07-15 22:32 ` [wic][PATCH 1/5] ksparser: reject negative sizes in sizetype() Trevor Woerner
2026-07-15 22:32 ` [wic][PATCH 2/5] ksparser: fix crash on out-of-range overhead factor Trevor Woerner
2026-07-15 22:32 ` [wic][PATCH 3/5] ksparser: reject non-finite overhead factors Trevor Woerner
2026-07-15 22:32 ` [wic][PATCH 4/5] ksparser: require an explicit 0x prefix for --system-id Trevor Woerner
2026-07-15 22:32 ` 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=20260715223226.3371498-6-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.