[wic][PATCH v4 6/6] tests/unit/test_bb_utils: cover mkdirhier()
Trevor Woerner <[email protected]> Mon, 6 Jul 2026 18:29:04 -0400
| Newsgroups | org.yoctoproject.lists.yocto-patches |
|---|---|
| Message-ID | <[email protected]> |
Add unit tests for mkdirhier(), the one function in wic.bb.utils. They
pin the behaviour wic relies on so it cannot regress: it creates missing
directories, accepts a directory that already exists, rejects a path
containing an unexpanded bitbake variable (${...}), and allows a name
with a lone brace or a lone dollar, neither of which is the ${ marker.
Three tests cover the OSError handler that the errno import repairs: a
path whose parent component is a regular file, a target that already
exists as a file rather than a directory, and a directory that appears
concurrently mid-call (a create race), which must be treated as success.
Run against a wic.bb.utils that does not import errno, all three fail
with NameError instead, so they catch that bug directly.
The tests replace the tests/unit/.gitkeep placeholder.
AI-Generated: codex/claude-opus 4.8 (xhigh)
Signed-off-by: Trevor Woerner <[email protected]>
---
changes in v4:
- reframe the tests to target mkdirhier's own behaviour: drop the
cases that only re-exercised os.makedirs and the assertions that
pinned implementation details.
- the errno fix moved to its own preceding commit; this is tests
only.
changes in v3:
- switch the tests from tempfile.mkdtemp() to pytest's tmp_path
fixture so each test's scratch directory is cleaned up instead of
leaking under /tmp; no change to what is tested.
changes in v2:
- v1 recorded this bug with an xfail marker in one large commit;
v2 drops the xfail, asserts the correct behaviour directly, and
lands the one-line errno-import fix in this same commit so the
test passes green.
---
tests/unit/.gitkeep | 0
tests/unit/test_bb_utils.py | 81 +++++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+)
delete mode 100644 tests/unit/.gitkeep
create mode 100644 tests/unit/test_bb_utils.py
diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/tests/unit/test_bb_utils.py b/tests/unit/test_bb_utils.py
new file mode 100644
index 000000000000..0300b8d4a1a0
--- /dev/null
+++ b/tests/unit/test_bb_utils.py
@@ -0,0 +1,81 @@
+"""
+Unit tests for wic.bb.utils.mkdirhier: a mkdir -p wrapper that rejects
+unexpanded bitbake variables and, on error, tolerates an already-existing
+directory while re-raising every real failure.
+"""
+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 wic.bb.utils import mkdirhier
+
+
+class TestMkdirhier:
+ def test_creates_missing_directories(self, tmp_path):
+ target = tmp_path / "a" / "b" / "c"
+ mkdirhier(str(target))
+ assert target.is_dir()
+
+ def test_existing_directory_is_accepted(self, tmp_path):
+ # Calling it on a directory that already exists is not an error.
+ mkdirhier(str(tmp_path))
+ mkdirhier(str(tmp_path))
+ assert tmp_path.is_dir()
+
+ def test_unexpanded_bitbake_variable_is_rejected(self, tmp_path):
+ target = tmp_path / "${WORKDIR}" / "sub"
+ with pytest.raises(Exception, match="unexpanded bitbake variable"):
+ mkdirhier(str(target))
+ assert not (tmp_path / "${WORKDIR}").exists()
+
+ def test_plain_brace_is_not_treated_as_a_variable(self, tmp_path):
+ # Only the '${' marker trips the guard; a bare brace is a legal
+ # (if unusual) directory name.
+ target = tmp_path / "plain{brace"
+ mkdirhier(str(target))
+ assert target.is_dir()
+
+ def test_dollar_without_brace_is_allowed(self, tmp_path):
+ # The guard keys on the literal '${' marker; a '$' on its own is
+ # not an unexpanded variable and is a legal directory name.
+ target = tmp_path / "price$5"
+ mkdirhier(str(target))
+ assert target.is_dir()
+
+ def test_path_under_a_file_raises(self, tmp_path):
+ # A parent component that is a regular file makes the underlying
+ # mkdir fail; the error must surface rather than be swallowed.
+ afile = tmp_path / "afile"
+ afile.write_text("x")
+ with pytest.raises(OSError):
+ mkdirhier(str(afile / "sub"))
+
+ def test_existing_file_at_target_raises(self, tmp_path):
+ # The target already exists but is a file, not a directory: the
+ # error must propagate rather than be tolerated.
+ afile = tmp_path / "afile"
+ afile.write_text("x")
+ with pytest.raises(OSError):
+ mkdirhier(str(afile))
+
+ def test_concurrent_creation_is_treated_as_success(self, tmp_path, monkeypatch):
+ # If the directory appears while mkdirhier runs (a create race
+ # with another process), that is success, not an error.
+ import errno
+
+ import wic.bb.utils as bb_utils
+
+ target = tmp_path / "made-concurrently"
+
+ def racing_makedirs(path, exist_ok=False):
+ target.mkdir() # someone else wins the race
+ raise OSError(errno.EEXIST, "File exists", str(path))
+
+ monkeypatch.setattr(bb_utils.os, "makedirs", racing_makedirs)
+ mkdirhier(str(target)) # must not raise
+ assert target.is_dir()
--
2.50.0.173.g8b6f19ccfc3a