diff mbox series

[wic,v4,6/6] tests/unit/test_bb_utils: cover mkdirhier()

Message ID 20260706222904.664863-7-twoerner@gmail.com
State New
Headers show
Series tests: standalone test-suite framework plus the first unit test | expand

Commit Message

Trevor Woerner July 6, 2026, 10:29 p.m. UTC
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
diff mbox series

Patch

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()