deleted file mode 100644
new file mode 100644
@@ -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()
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 <twoerner@gmail.com> --- 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