From: Trevor Woerner <twoerner@gmail.com>
To: yocto-patches@lists.yoctoproject.org
Subject: [wic][PATCH v3 09/10] tests/unit/test_bb_utils: test mkdirhier() and fix its missing errno import
Date: Wed, 1 Jul 2026 03:40:29 -0400 [thread overview]
Message-ID: <20260701074030.1090807-10-twoerner@gmail.com> (raw)
In-Reply-To: <20260701074030.1090807-1-twoerner@gmail.com>
mkdirhier() wraps os.makedirs() and, on OSError, re-checks the error to
decide whether to swallow a benign "already exists" race or re-raise:
except OSError as e:
if e.errno != errno.EEXIST or not os.path.isdir(directory):
raise e
but the module never imports errno. The reference to errno.EEXIST is
only evaluated when os.makedirs() actually raises, so the defect is
invisible on the happy path and on the unexpanded-${} guard. The moment
a real OSError occurs -- a parent component that is a file, a permission
error, anything -- the handler itself raises NameError: name 'errno' is
not defined, masking the original error with a misleading one.
The fix is a one-line import. With errno available the handler behaves
as intended: an EEXIST on an existing directory is swallowed, while any
other OSError (and an EEXIST whose path is not a directory) propagates
unchanged.
This commit adds tests/unit/test_bb_utils.py, covering mkdirhier() end
to end:
- the happy path (nested, single-level, idempotent, existing dir);
- the unexpanded-${} guard (rejected, nothing created, and that a
bare brace without a dollar is allowed);
- the OSError handler: a real mkdir under a file component surfaces an
OSError, an arbitrary OSError propagates with its errno intact, an
EEXIST on an existing directory is swallowed, and an EEXIST on a
regular file propagates.
The error-path tests fail with NameError without the import and pass
with it, so the test and the fix belong together in this one change.
AI-Generated: codex/claude-opus 4.8 (xhigh)
Signed-off-by: Trevor Woerner <twoerner@gmail.com>
---
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.
---
src/wic/bb/utils.py | 1 +
tests/unit/test_bb_utils.py | 116 ++++++++++++++++++++++++++++++++++++
2 files changed, 117 insertions(+)
create mode 100644 tests/unit/test_bb_utils.py
diff --git a/src/wic/bb/utils.py b/src/wic/bb/utils.py
index 3750056ba563..0b40dd0c1d59 100644
--- a/src/wic/bb/utils.py
+++ b/src/wic/bb/utils.py
@@ -1,6 +1,7 @@
"""
Minimal subset of BitBake's bb.utils used by standalone wic.
"""
+import errno
import os
# from bitbake/lib/bb/utils.py
diff --git a/tests/unit/test_bb_utils.py b/tests/unit/test_bb_utils.py
new file mode 100644
index 000000000000..aeca054109ff
--- /dev/null
+++ b/tests/unit/test_bb_utils.py
@@ -0,0 +1,116 @@
+r"""
+Unit tests for wic.bb.utils -- a small standalone subset of BitBake's
+bb.utils. mkdirhier() is a mkdir -p wrapper with two guards: an
+unexpanded-${} check and an OSError handler that re-checks EEXIST.
+"""
+import errno
+import os
+import sys
+import unittest.mock as mock
+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
+
+
+# ---------------------------------------------------------------------------
+# Happy path
+# ---------------------------------------------------------------------------
+
+class TestMkdirhierHappyPath:
+ def test_creates_nested_directory(self, tmp_path):
+ target = os.path.join(str(tmp_path), "a", "b", "c")
+ mkdirhier(target)
+ assert os.path.isdir(target)
+
+ def test_idempotent_on_existing_directory(self, tmp_path):
+ target = os.path.join(str(tmp_path), "x")
+ mkdirhier(target)
+ # Second call must not raise (exist_ok=True).
+ mkdirhier(target)
+ assert os.path.isdir(target)
+
+ def test_single_level(self, tmp_path):
+ target = os.path.join(str(tmp_path), "single")
+ mkdirhier(target)
+ assert os.path.isdir(target)
+
+ def test_existing_root_directory(self, tmp_path):
+ # An already-existing directory (the tmpdir itself) is a no-op.
+ mkdirhier(str(tmp_path))
+ assert os.path.isdir(str(tmp_path))
+
+
+# ---------------------------------------------------------------------------
+# Unexpanded bitbake-variable guard
+# ---------------------------------------------------------------------------
+
+class TestMkdirhierUnexpandedVar:
+ def test_unexpanded_var_rejected(self):
+ with pytest.raises(Exception) as ei:
+ mkdirhier("/tmp/${WORKDIR}/x")
+ assert "unexpanded bitbake variable" in str(ei.value)
+
+ def test_unexpanded_var_not_created(self, tmp_path):
+ bad = os.path.join(str(tmp_path), "${VAR}", "sub")
+ with pytest.raises(Exception):
+ mkdirhier(bad)
+ # Nothing should have been created.
+ assert not os.path.exists(os.path.join(str(tmp_path), "${VAR}"))
+
+ def test_brace_without_dollar_is_allowed(self, tmp_path):
+ # Only the literal '${' marker triggers the guard; a bare '{' is a
+ # legal (if unusual) directory name.
+ target = os.path.join(str(tmp_path), "plain{brace")
+ mkdirhier(target)
+ assert os.path.isdir(target)
+
+
+# ---------------------------------------------------------------------------
+# OSError handler
+# ---------------------------------------------------------------------------
+
+class TestMkdirhierErrorPath:
+ def test_oserror_under_a_file_component(self, tmp_path):
+ # Create a regular file, then try to mkdir a subdir *under* it. On
+ # Linux os.makedirs raises NotADirectoryError (an OSError subclass);
+ # the handler must let it surface rather than swallow it.
+ f = os.path.join(str(tmp_path), "afile")
+ Path(f).write_text("x")
+ target = os.path.join(f, "sub")
+ with pytest.raises(OSError):
+ mkdirhier(target)
+
+ def test_generic_oserror_propagates(self):
+ # An arbitrary OSError (EACCES != EEXIST) must propagate unchanged.
+ err = OSError()
+ err.errno = errno.EACCES
+ with mock.patch("wic.bb.utils.os.makedirs", side_effect=err):
+ with pytest.raises(OSError) as ei:
+ mkdirhier("/whatever/path")
+ assert ei.value.errno == errno.EACCES
+
+ def test_eexist_on_existing_dir_is_swallowed(self, tmp_path):
+ # An EEXIST OSError on a path that is already a directory is the
+ # benign race the handler exists to absorb; it must not propagate.
+ err = OSError()
+ err.errno = errno.EEXIST
+ with mock.patch("wic.bb.utils.os.makedirs", side_effect=err):
+ mkdirhier(str(tmp_path)) # exists and is a dir -> swallowed, no raise
+
+ def test_eexist_on_existing_file_propagates(self, tmp_path):
+ # EEXIST but the path is a regular file, not a directory: the
+ # handler's isdir() re-check fails, so the error must propagate.
+ f = os.path.join(str(tmp_path), "afile")
+ Path(f).write_text("x")
+ err = OSError()
+ err.errno = errno.EEXIST
+ with mock.patch("wic.bb.utils.os.makedirs", side_effect=err):
+ with pytest.raises(OSError) as ei:
+ mkdirhier(f)
+ assert ei.value.errno == errno.EEXIST
--
2.50.0.173.g8b6f19ccfc3a
next prev parent reply other threads:[~2026-07-01 7:40 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-01 7:40 [wic][PATCH v3 00/10] tests: standalone test-suite framework plus the first unit test Trevor Woerner
2026-07-01 7:40 ` [wic][PATCH v3 01/10] tests: add the standalone test-suite skeleton Trevor Woerner
2026-07-06 8:18 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 02/10] tests: add a session banner via conftest.py Trevor Woerner
2026-07-06 8:17 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 03/10] tests: add the run-tests.sh wrapper Trevor Woerner
2026-07-06 8:21 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 04/10] tests: add optional coverage reporting to run-tests.sh Trevor Woerner
2026-07-06 8:33 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 05/10] tests: add ruff linting " Trevor Woerner
2026-07-06 8:40 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 06/10] tests/docs: add the suite overview README Trevor Woerner
2026-07-06 8:47 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 07/10] tests/docs: add the test-authoring guide Trevor Woerner
2026-07-06 8:52 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 08/10] tests: ignore E402 in the test tree for the sys.path bootstrap Trevor Woerner
2026-07-06 8:55 ` [yocto-patches] " Paul Barker
2026-07-01 7:40 ` Trevor Woerner [this message]
2026-07-06 9:06 ` [yocto-patches] [wic][PATCH v3 09/10] tests/unit/test_bb_utils: test mkdirhier() and fix its missing errno import Paul Barker
2026-07-01 7:40 ` [wic][PATCH v3 10/10] tests/docs: add the review rubric Trevor Woerner
2026-07-06 9:08 ` [yocto-patches] " Paul Barker
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=20260701074030.1090807-10-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.