From patchwork Tue Jun 30 16:06:04 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91417 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 853B2C43602 for ; Tue, 30 Jun 2026 16:06:44 +0000 (UTC) Received: from mail-qk1-f170.google.com (mail-qk1-f170.google.com [209.85.222.170]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.24713.1782835602157344881 for ; Tue, 30 Jun 2026 09:06:42 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=fd+v5LDC; spf=pass (domain: gmail.com, ip: 209.85.222.170, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f170.google.com with SMTP id af79cd13be357-92e52c9fddfso136689185a.3 for ; Tue, 30 Jun 2026 09:06:42 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835601; x=1783440401; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=XEnH1o5WZikDExsG+MafdSzmrumcvJBCuOI3ycqhk54=; b=fd+v5LDCoXnp3sMUiLqhUgVOwYZQimZHccnviurH2xDhjmkwdz4QtHwm6CXwGQQ542 va8hXfb74cYoHDPy913bHeuOFU23uBwnEMSke2QKavP4guHI+EIiPV7cnMYkSU8L67mO xdPu/KDmuz6ycLXmjE22N+X0GJUTBy3ZxR2iCCWLuXdWKF96qz8J43MQn06rlxOzPD9F WJjSwRrB+KgLuCrr4X2mjvAiWXtXajYzFatUkMtgtyvLIfpWCcRD69tw8YUBmKasA2kx 91iS/de2IgmyUoO4//ygvQ0bOD1HchU0xfhGudjPXGM9ldAUTXmk/2DvkzPTXZRVa7xr i6NA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835601; x=1783440401; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=XEnH1o5WZikDExsG+MafdSzmrumcvJBCuOI3ycqhk54=; b=I0A8WeyyejFOhipsTyCEXNxbTuIZgMhg/0XRogp88yR+2nPnO+6TS+y06FwJXf8VXs +RtyWJvw9ZnJVxrxxh/B1jZoFMbRV/B4fXFC0t6+LlwbrGmURVjAR9tu6Y0ujAWbYGmk ij1dPvX7wG9M6l2bPVXWHpyRVrd4UjV7GO0dNJktEFUuf9eozCndAY1KLnFL0DzDhvZS lQ6FUbH7k+M5ajLRhqK2EJSWyPh1LOz9wh1IOytlbG40I6Wapz902eo27cnZ+x4DAUSj ej8YCdT4l3+39viBoodLd6GxoEPsG4Z/QHQENo7PkfRGE2zo5NwxDDXjO23GjtutfwVK gHjw== X-Gm-Message-State: AOJu0YwchrqZL8HDc8FMKd1c4z0/ghx1AoXjShJK/nN4nz0SaQWWJUXn YDhjxzHtA7m81eyBsoLi77j7ukWDO1kjBQ6fuetKFKpaE8HcMBAT9ciy0YYVTQ== X-Gm-Gg: AfdE7cn4G9bZ7bv8AQwvt+usVZkF1VEaUFGB4tbdPHBCdG1eri16SU1nUK5emAhx4fy uwavrPLE9ObcbTwdj01Mxfdr5qmsB5gJJNqCzV/JuxRo+FI8cAXxoqTFfY8/VuWgQhPvw4Hr3uW MWNaZk6z2sUx9t3VKSOkuos77ZANqwOStgaeunC2SIZ/rh0FRJC7dXLY6UGVj2ida6P0tUNSR+d yEM5XjDwwRgHIw69yQ4rd+lIQYpqu4SGG3vpMHvUjvRVdvxG0ORq7q9SM8ynVRudcEAL2cXF4eM lBAe5HjmhBAyWRAqvjdSrTpDI5Ah4Zl+Gwn8jvjlTBRGl0Xxl4+om21PIZ1CgfIhMuA1SPpz5S3 PgaPWpMPkOU0kcENxpkuKqyXpzZ+YNhhdH8GDvftoxCu7Xwkrf3hxu2ZTbWFqW7oERHP+S5DT4t BoNkkc003PMit7nwc2qxFIEaj6brgD+/6ZbK9aakReYw6oiUylRX48oB8= X-Received: by 2002:a05:622a:1b93:b0:519:8998:661b with SMTP id d75a77b69052e-51c107593c4mr59896081cf.20.1782835600754; Tue, 30 Jun 2026 09:06:40 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.39 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:39 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 1/9] tests: add the standalone test-suite skeleton Date: Tue, 30 Jun 2026 12:06:04 -0400 Message-ID: <20260630160612.1005451-2-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:44 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4322 wic currently has no test mechanism of its own; it relies on the oe-selftest from oe-core for all its testing, which means a full bitbake build is needed to exercise even pure-Python logic. This commit lays the groundwork for a small standalone suite that runs from a plain checkout with nothing but pytest, so that logic can be pinned down and kept stable as the code evolves. This commit adds the skeleton only; it does not add any tests. What this adds: - pyproject.toml: a "tests" optional-dependency group (pytest only) so that "pip install -e .[tests]" pulls in what the suite needs, plus a [tool.pytest.ini_options] section. The pytest options keep a test's scratch directory only when that test fails, so repeated runs do not accumulate leftover files. - tests/unit/ and tests/docs/: the directories that hold the in-process unit suites and the suite documentation. Git cannot track an empty directory, so a placeholder .gitkeep is committed in each to create it. The .gitkeep files have no content and are removed once real files land in those directories. - .gitignore: ignore the .pytest_cache/ directory that pytest writes at the top of the checkout. - README.md: a Testing section pointing at the suite and at tests/docs/, and a tests/* entry in the project layout list. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- .gitignore | 3 +++ README.md | 14 ++++++++++++++ pyproject.toml | 12 ++++++++++++ tests/docs/.gitkeep | 0 tests/unit/.gitkeep | 0 5 files changed, 29 insertions(+) create mode 100644 tests/docs/.gitkeep create mode 100644 tests/unit/.gitkeep diff --git a/.gitignore b/.gitignore index eeb8a6ec4087..07992096c0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ **/__pycache__ + +# pytest cache +/.pytest_cache/ diff --git a/README.md b/README.md index 75229421763c..497ebb1de0f0 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,20 @@ environment file or folder (generated via `bitbake -c rootfs_wicenv - `src/wic/*`: core engine, plugins, and helpers. - `src/bb/*`: various bitbake helpers that were brought along and used in other parts of wic. - `src/oe/*`: various oe-core helpers that were brought along and used in other parts of wic. +- `tests/*`: the standalone test suite and its documentation (see Testing below). + +## Testing + +wic ships a standalone test suite under `tests/` that runs from a plain +checkout, with no bitbake and no OpenEmbedded build required. The test +extras pull in everything the suite needs: + +``` +pip install -e ".[tests]" +``` + +See [tests/docs/](tests/docs/) for how to run the suite and the +conventions it follows. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index fdc1ce0f5ece..d660e39007c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,11 @@ classifiers = [ Homepage = "https://git.yoctoproject.org/wic" Repository = "https://git.yoctoproject.org/wic" +[project.optional-dependencies] +tests = [ + "pytest >= 7.0", +] + [project.scripts] wic = "wic.cli:main" @@ -36,3 +41,10 @@ build-backend = "hatchling.build" [tool.hatch.version] path = "src/wic/cli.py" + +[tool.pytest.ini_options] +# Keep a test's scratch directory only when it fails; a passing test's +# tmp_path is removed automatically so repeated runs do not accumulate +# leftover files under the pytest base temp directory. +tmp_path_retention_policy = "failed" +tmp_path_retention_count = 1 diff --git a/tests/docs/.gitkeep b/tests/docs/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 From patchwork Tue Jun 30 16:06:05 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91419 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 51F37C43458 for ; Tue, 30 Jun 2026 16:06:54 +0000 (UTC) Received: from mail-qk1-f176.google.com (mail-qk1-f176.google.com [209.85.222.176]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.25078.1782835606600549001 for ; Tue, 30 Jun 2026 09:06:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=WgVlzqNF; spf=pass (domain: gmail.com, ip: 209.85.222.176, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f176.google.com with SMTP id af79cd13be357-92e501244f5so132055485a.1 for ; Tue, 30 Jun 2026 09:06:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835605; x=1783440405; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=Au9wk365wXNJ0WVz+5fX5E5Lq6YpU6FI82ckvlMjCfM=; b=WgVlzqNFwKjWDmaT+fQJp5lb7jlu8Pl6gkyGN1txpizXLNi3gW0AVNzrWl38ZVdAVF g1Rjc+ikvWxBIbudE2uQGUUmZ3Mw8pmfv/e/wDKaVstGFdbAIyb4WZET+kOtzkjsUznC p3d6b5jTMaxWxBsw761oMFO0GbMkFT9WWqfpciFpgjE5UTKa72WSvIwuVzgSEd8hQLzZ q7qbBo7vOsLyzR2+6CfJsXjdZdhEXTvPS2XkfQHtIGFEamOx0nTPc6nsrs0TrU+B+K4l WsW/E6V3+KRMOoIzZOuuJOz2c78X+tiebZsTF6ShC/u0C03+IzcZJUTnCvWKiNc9CG4r KWAA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835605; x=1783440405; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=Au9wk365wXNJ0WVz+5fX5E5Lq6YpU6FI82ckvlMjCfM=; b=FH0mcYReG1r009Jq+d7ioseQs+gYFQ0fvLl9GNu5LUfCIKcvxxBt1Fgbq/ctFNYHe3 s39CCLuUNFQ8kmEE/RaLFU8Haa1eQruxaPX7FXgPmo3k9LJh1QJpSMqA8+Buiq87qFUS Q1uorDJLIiW9DbVx9gxe68A9a0iQ6Ab4iwGXLUoTJddar+GXWhW+WdmWcXlTUj0PZuMG K9IaiS6dO/xxBCGy3+5eCZ6UbifAC7a2p0lhDJTuq8NPmyUwDtb48BgSQK/AM/O9KRxN fjRXLhXCXzz+4+4kjeRl2p8aunIZzvZMy1wUPPdfGcnlFBBfyYkpu2JnKxNJJa08zwTB hgYA== X-Gm-Message-State: AOJu0YzyajVJcAjtzwOQHoLJ6sS9w0Jepu19NxY49zRHMGND0KakJANV mvSlc3AbiYF2b3lp9KUO5gOXvwTuioXlMpWGiwIwoJAgUrxTR4FROq1R+aorlQ== X-Gm-Gg: AfdE7ckJYExxlq3ypYflKzGoZzk260waYDaQGHamLqMNesq4pqqLBRqlZSKUvBn5CzD REremTWVBFE9G19ngoI6zBNM36NpSyUZOg3Iy88CGJV6dZKf3ObU6o7EVoSSk/wO03ON5ocguez 4S2fQeukALRxIE0n7ZKDtjSemuDf3YXEwbr0+EtbtGqZFwWM9N7yY1dwAtTHdx933ZotUnyHlSS LM6lg17Lr2yPNHiu78u86kCUtRY1ckB5XBSfoeI87XpRe7qpIWVg5UfDB5U7JwEosys0ze0/H4Y kTctpJezsR3gYVBSGTkSsh2aD6LnAaRP1fkc9wZxfB/LkYYvUeW/VqvemW1uQELpjS0R4x1QCkt 0oHjIDBF/rKTDLSCd4Iq5dB877lTojySh8lVBCabmWky5AWHSKCEhodjFgEb+pCAbMtpFs2zsr/ 57s+7kdlnfUwM5IqN0pwc8TA4kX4xszpyFCyaqKLKjfn6KLOp0f7USn7M= X-Received: by 2002:a05:620a:2304:20b0:92e:6430:3c73 with SMTP id af79cd13be357-92e64304068mr419713885a.21.1782835602669; Tue, 30 Jun 2026 09:06:42 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.40 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:41 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 2/9] tests: add a session banner via conftest.py Date: Tue, 30 Jun 2026 12:06:05 -0400 Message-ID: <20260630160612.1005451-3-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:54 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4323 When a test run starts it is useful to see, at a glance, exactly what is being tested: which host the run is on, which Python and pytest are in use, and -- most importantly -- which wic the suite imported and what version it reports. Without that, a passing or failing run is hard to attribute, especially when more than one wic checkout or virtualenv is in play. pytest looks for a file named conftest.py and, among other things, calls its pytest_report_header() hook to print extra lines in the session header before collection begins. This commit adds that file with a single hook that prints a short banner: - the host node name, operating system, and machine type; - the running Python version and the pytest version; - the version wic reports and the filesystem path the wic module was imported from. The wic lookup is defensive: if wic cannot be imported (for example the test extras were not installed, or the suite is being run outside the checkout) the banner says so rather than aborting the run, so the failure is visible in the header instead of as an opaque collection error. The file name conftest.py is mandatory; pytest discovers it by name, so it cannot be called anything else. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- tests/conftest.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000000..1c368ebf8595 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,38 @@ +# Session banner for the wic test suite. +# +# Before collection, print the environment the tests exercise: +# - the host +# - the Python and pytest versions +# - the wic under test (its version and the module path of its import) + +import platform + +import pytest + + +def _wic_under_test(): + """Return (version, module_path) for the wic being tested.""" + try: + import wic.cli as wic_cli + except Exception as exc: # pragma: no cover - reported in the banner + return ("(import failed: %s)" % exc, "(unimported)") + version = getattr(wic_cli, "__version__", "(unknown)") + module_path = getattr(wic_cli, "__file__", "(unknown)") + return (version, module_path) + + +def _format_banner(): + wic_version, wic_path = _wic_under_test() + lines = [ + "wic test suite", + " host: %s %s %s" % ( + platform.node(), platform.system(), platform.machine()), + " python: %s pytest: %s" % ( + platform.python_version(), pytest.__version__), + " wic: %s (%s)" % (wic_version, wic_path), + ] + return "\n".join(lines) + + +def pytest_report_header(config): + return _format_banner() From patchwork Tue Jun 30 16:06:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91420 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9E6F4C44500 for ; Tue, 30 Jun 2026 16:06:54 +0000 (UTC) Received: from mail-qk1-f177.google.com (mail-qk1-f177.google.com [209.85.222.177]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.25079.1782835608841556482 for ; Tue, 30 Jun 2026 09:06:49 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=fIYXgbeB; spf=pass (domain: gmail.com, ip: 209.85.222.177, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f177.google.com with SMTP id af79cd13be357-92c7a0a7059so305134885a.0 for ; Tue, 30 Jun 2026 09:06:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835608; x=1783440408; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=UYdajNMvxe9R8ztWnBBJNV55DFrMi6iRzDu+6HIWlpg=; b=fIYXgbeBQPIeXQIXPMHt00hJBdkbgla0WQNInnvsqdzTqHCF0DC99qAFB/K7jm/ICa HaB+FSE/gvGcH1sTIyMcwDDIdvB3CGQw6eqgn6uB7S0RrJQYlYapNdMmYOAWlkruNek0 n1UxZUYVjjykpinkw695Ny/zB2ILC9lgNXIWDD79NxzO9vO+Xe+p5bXdncJspqackmLJ CKfAogvEDx/vIqTzBWFEIDBdN731xSFZ/KQlrKY6laxQjf9VpPc1OyWi8KwCfx2q1gj2 FIIAyGuOTY413dapalIeX7cWHCJZaPKhyrqJU4e9IojSCPlNvxQkQSxCiXMn3Gbw6PjS /OaQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835608; x=1783440408; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=UYdajNMvxe9R8ztWnBBJNV55DFrMi6iRzDu+6HIWlpg=; b=S7WvtY0LOJRUHEig+aj2KIoDl6iOmo4xTEk13xasI3Wb9XM5h+4n5vkJLeX5almP5M xOIKwgo0+ew7fs7uKmsWO2IYyGQmTQjBsVRrheM2wV9bQj49NA+AWz5lTKzOG4CNpOBH 1EVTuOVLnWVYWCAK8gliujxkF9+MNQjrX3wTFKAQ25aFzqmHfrw3VHgEwizVE0haqJtn So6NvVDRk7vRPGs1mjRuxH7GkNKR6YdNQX5UbsW9426KGKa3gdnSlxtX5ad9Wo1DoBmi xl5CH1EpawlwRfQSD/3PxmuRmBp324h5jSB+tJMQXBHAaAnWLcxnvim0q5GO6OMj6smJ uNMg== X-Gm-Message-State: AOJu0YwAi21ksgjufnRbsRmQynUHt3NWMC0joe2UBse9R4KDrtoVrgje CavyJ4gX5oYVJ7JF/zUc9w36T9akmd1QZLzJnjycwSkbTTiDgCyBNqaUBLlokQ== X-Gm-Gg: AfdE7cnFDeRRTaAYFs+di/lfP/zWe68mr38Gnzwx17RdxK2UguD1Iu0oNFObkZkPitq iKqsnqSxK5rT2N8DEXE/P6FBJN5eWctetd7NGigC/Kmt2ABsR3JgBqxh1gVS3rO5lLmR+Slkd/M v1kTpG46y/4cIkK+PAICbQJv8c4ECigHHNM+IZQCIldQc4HMwQ9Qf0R4I7coBLNroEp/v2BrJIy 8DVyt/gVGzipxL+/h+vCTzGHKHnvrucu4mcRqE5AJuRp5UFoxymtvTUTS1m7pJNFF/NO9nO4ZUm Kq/L5xdgdM0EPuAlIbhtaUbqOcTFstviBAiL4cEQ3MgtFeiZYj4x5RswX+s2i1cQcGh235Tccso ssTBGsIuidrnn5qR4y4YwlPxLAO870ovaRvcMgPayNVqT5cnj5Gj4CWTGKEF3KeBhb2ZN6FoCeW c2HtYWsN9rljFqrb0kKZNi/1c83qCDzFWD4bDGqoJI5hagyzjp3SKTgWE= X-Received: by 2002:a05:620a:8807:b0:92b:67e6:8abf with SMTP id af79cd13be357-92e62852736mr623878885a.73.1782835607201; Tue, 30 Jun 2026 09:06:47 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.45 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:45 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 3/9] tests: add the run-tests.sh wrapper Date: Tue, 30 Jun 2026 12:06:06 -0400 Message-ID: <20260630160612.1005451-4-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:54 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4324 The suite is run with pytest, but invoking pytest directly has two sharp edges: it must be run from the right directory for the pyproject configuration to apply, and it happily runs against whatever Python happens to be first on PATH, even one that cannot import wic. A run against the wrong interpreter produces a misleading banner and, worse, can silently exercise a different wic than the one in the checkout. This commit adds tests/run-tests.sh, a thin wrapper that removes both hazards: - it locates the repository root from its own path and changes there before running, so it works regardless of the caller's directory; - it checks that the interpreter can import wic before handing off to pytest, and fails loudly with the install command if it cannot; - with no arguments it runs the whole suite under tests/; any argument it does not recognise (a path, -k EXPR, -v, ...) is passed straight through to pytest, and an explicit -- forwards everything after it. The interpreter can be overridden with the PYTHON environment variable (default python3). -h/--help prints usage and exits. The README Testing section gains the run-tests.sh invocation alongside the install step. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- README.md | 1 + tests/run-tests.sh | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100755 tests/run-tests.sh diff --git a/README.md b/README.md index 497ebb1de0f0..e557eb435316 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ extras pull in everything the suite needs: ``` pip install -e ".[tests]" +tests/run-tests.sh ``` See [tests/docs/](tests/docs/) for how to run the suite and the diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 000000000000..9db6d50338d6 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Run the wic test suite. +# +# Thin wrapper around pytest that works from anywhere in the checkout: +# it locates the repository root, makes sure the interpreter can import +# wic, and then hands off to pytest. +# +# Needs the test extras: pip install -e ".[tests]" + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + tests/run-tests.sh [pytest args] + +Options: + -h, --help show this help and exit + +Anything else is passed straight through to pytest (for example a +path, -k EXPR, or -v). With no such argument the whole suite under +tests/ is run. + +Examples: + tests/run-tests.sh # whole suite + tests/run-tests.sh -k filemap -v # pass args through to pytest + tests/run-tests.sh tests/unit # a single tier or file + +Needs the test extras: pip install -e ".[tests]" +USAGE +} + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PY="${PYTHON:-python3}" + +pytest_args=() +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --) + shift + pytest_args+=("$@") + break + ;; + *) + pytest_args+=("$1") + ;; + esac + shift +done + +cd "$REPO_ROOT" + +# Make sure the interpreter that will run pytest can actually import wic. +# The suites pass even without an install (each adds src/ to sys.path), +# but the session banner and any install-dependent behaviour would be +# wrong, so fail loudly instead of running against the wrong interpreter. +if ! "$PY" -c "import wic" >/dev/null 2>&1; then + echo "error: '$PY' cannot import wic." >&2 + echo " install wic with its test extras:" >&2 + echo " $PY -m pip install -e \".[tests]\"" >&2 + exit 1 +fi + +# Default target: the whole suite. Overridden if the caller passed a +# path or -k/-m selector to pytest. +if [ ${#pytest_args[@]} -eq 0 ]; then + pytest_args=("tests") +fi + +exec "$PY" -m pytest "${pytest_args[@]}" From patchwork Tue Jun 30 16:06:07 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91422 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 603F7C43327 for ; Tue, 30 Jun 2026 16:06:54 +0000 (UTC) Received: from mail-qk1-f182.google.com (mail-qk1-f182.google.com [209.85.222.182]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.24720.1782835610833409333 for ; Tue, 30 Jun 2026 09:06:50 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=Q7jd/fQ6; spf=pass (domain: gmail.com, ip: 209.85.222.182, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f182.google.com with SMTP id af79cd13be357-92e602d2c0fso77265885a.0 for ; Tue, 30 Jun 2026 09:06:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835610; x=1783440410; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=+R6If7c6OZw39q32lIaEALMs2iTGuDqe9/Yq38qAI7A=; b=Q7jd/fQ6y/5uderQmIN/asB2zs/Jpf4NIEbYg/GNjj/AgDQY2Pm9GghaXOo+1VL5Uu xihWif1yCdSRK/dQvcdEQsv5gJi4chZwRLiP5DNlclw5H98D2LaEC6w7F1T0WAD1VngF yeoUWJiSZaLEFMbmSyQsT5nRHuKYV/DwcfsB2HO2hfYnnKpTcHiTSOf6yZmsbVRpoQUV 65x+JfIwxp3hHenXbxLgQRVYBjk/ODyMp1epCfEWPGFiLaOaFXCtdnfRoPFgmNJWJM1A CLNYNzRbHkWU/MLyVUa33B4VJba2WYD1/Xbj82eTnBmsBmYKJzpoOC0MpZXvCPZPrSK9 5pFg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835610; x=1783440410; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=+R6If7c6OZw39q32lIaEALMs2iTGuDqe9/Yq38qAI7A=; b=FZn9Lqf+itWXaK+33vqzhx/PIL8upBKcUIIJ4vaFhXYdgODCJLqeqBtpogwXf7bqLS 1U/SXaGc3hzE9gs02nl7feRbi3CMdCDytZob0u11KP1qGRpPx41DCxie2LC2pw748Tr5 lyVUXELPwKwKALEpMcNixNiOGF5FgB88PrtNowmqLxJlgHguWqsDWUQwBaPEZcZT1oks 6Mqsh1YVMC32jqRGBK8gwyrQ54gs5AA93PJ00Accd7e6VZItQqex+UgS0/oNAFHhq/VF lzedpMR7oxkXhBinFkBE8CULhOydlI/qD7AIpkaiDQgg4nAxzuF+6uh+G1tI5atBZPN7 MytA== X-Gm-Message-State: AOJu0YzUi1IeIzNft/yGGEtZuq9xY0fGcq/qjwWiob6nwoMbS+tNDchP /f4Sl3fBdVDWT0gBNho+Tk6Zmt3gfxdFclFZu5BMz3MRo/Blxgsd4XF8s8UMtQ== X-Gm-Gg: AfdE7cmLxKBDvhsZmF6yQG99APOsbFRzsWA23Eozx6DIc02BQtCUSfyR9hCLuE1Wq/E V3XtxKmb5vFMSAqV1A1H7X/I4h6a/KGTnHDRztmuKe5F6OgfcK3br1n2LdKplx7tNqcBLendyXk Wb2a9GoD2A3TUHJNBj2V62dH6OTmefcYoC3UAZTeNUAxzqlZityJHLL5xzJ0L9qAtBzBx4T5goL 6o5BpsS1iyYck9Ee2CZif76nSbBm4eT5E57qqaeOKnAN1Zok2cVQrcP3ZrzEZ0TY8jNVhr5YYnv DW9Wn0TaMkw239hmDQ2QxsI2ObYrxrRduLjz8+fiV4lBbhOCugzh9B/mbkJE0Mnv5sGyDsat8rq OPoKaQGrEq3ytuqYSph3sFl/UdlFEdxGh8ldwOXdTWSkCFjKxM2O7d/yGP9QFfqq+EXqfCIkIpt HcvWbAuc/XG3b0rKJEtiv6YHrK2kvIxr1+C2SaRCgBdsIAWOjeI0zl03o= X-Received: by 2002:a05:620a:2a01:b0:910:c308:159f with SMTP id af79cd13be357-92e697a3307mr322256985a.28.1782835609342; Tue, 30 Jun 2026 09:06:49 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.47 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:47 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 4/9] tests: add optional coverage reporting to run-tests.sh Date: Tue, 30 Jun 2026 12:06:07 -0400 Message-ID: <20260630160612.1005451-5-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:54 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4325 Knowing which lines a test run actually exercised is the difference between "the suite is green" and "the suite is green and we know what it touched". This commit wires branch coverage of the wic source into the runner, kept entirely opt-in so a plain run stays fast and quiet. pyproject.toml gains coverage and pytest-cov in the tests extra, so "pip install -e .[tests]" pulls in what the new flags need. run-tests.sh gains two options: - --coverage measures branch coverage of src/wic during the run and prints a terminal report listing the lines that were missed; - --html [DIR] additionally writes a browsable HTML report (default htmlcov/, or DIR if given) and implies --coverage. If --coverage is requested but pytest-cov is not installed the runner fails loudly with the install command rather than running without the measurement it was asked for. The coverage data file and the HTML report directory are build artifacts, so .gitignore learns to ignore .coverage and htmlcov/. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- .gitignore | 4 ++++ pyproject.toml | 2 ++ tests/run-tests.sh | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 07992096c0fb..534c49538091 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ # pytest cache /.pytest_cache/ + +# coverage data and reports +/.coverage +/htmlcov/ diff --git a/pyproject.toml b/pyproject.toml index d660e39007c4..ece2757bb686 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ Repository = "https://git.yoctoproject.org/wic" [project.optional-dependencies] tests = [ "pytest >= 7.0", + "coverage >= 7.0", + "pytest-cov >= 4.0", ] [project.scripts] diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 9db6d50338d6..a483da6a63a6 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -13,9 +13,13 @@ set -euo pipefail usage() { cat <<'USAGE' Usage: - tests/run-tests.sh [pytest args] + tests/run-tests.sh [--coverage] [--html [DIR]] [pytest args] Options: + --coverage also measure branch coverage of src/wic and print a + terminal report listing the lines that were missed + --html [DIR] also write an HTML coverage report (default dir: + htmlcov/); implies --coverage -h, --help show this help and exit Anything else is passed straight through to pytest (for example a @@ -24,6 +28,9 @@ tests/ is run. Examples: tests/run-tests.sh # whole suite + tests/run-tests.sh --coverage # + terminal coverage report + tests/run-tests.sh --html # + HTML report in htmlcov/ + tests/run-tests.sh --html /tmp/cov # + HTML report in /tmp/cov tests/run-tests.sh -k filemap -v # pass args through to pytest tests/run-tests.sh tests/unit # a single tier or file @@ -34,9 +41,25 @@ USAGE REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PY="${PYTHON:-python3}" +coverage=0 +html=0 +html_dir="htmlcov" pytest_args=() while [ $# -gt 0 ]; do case "$1" in + --coverage) + coverage=1 + ;; + --html) + coverage=1 + html=1 + # Optional directory argument: consume the next token only if + # it is not another option or the pytest pass-through marker. + case "${2:-}" in + ""|-*|--) ;; + *) html_dir="$2"; shift ;; + esac + ;; -h|--help) usage exit 0 @@ -72,4 +95,18 @@ if [ ${#pytest_args[@]} -eq 0 ]; then pytest_args=("tests") fi +if [ "$coverage" -eq 1 ]; then + if ! "$PY" -c "import pytest_cov" >/dev/null 2>&1; then + echo "error: coverage requested but pytest-cov is not installed." >&2 + echo " run: $PY -m pip install -e \".[tests]\"" >&2 + exit 1 + fi + cov_args=(--cov=wic --cov-branch --cov-report=term-missing) + if [ "$html" -eq 1 ]; then + cov_args+=(--cov-report="html:${html_dir}") + echo "HTML coverage report: ${html_dir}/index.html" >&2 + fi + exec "$PY" -m pytest "${pytest_args[@]}" "${cov_args[@]}" +fi + exec "$PY" -m pytest "${pytest_args[@]}" From patchwork Tue Jun 30 16:06:08 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91421 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 6F0D5C44501 for ; Tue, 30 Jun 2026 16:06:54 +0000 (UTC) Received: from mail-qk1-f169.google.com (mail-qk1-f169.google.com [209.85.222.169]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.24722.1782835612627355961 for ; Tue, 30 Jun 2026 09:06:52 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=Rzs5e0XO; spf=pass (domain: gmail.com, ip: 209.85.222.169, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f169.google.com with SMTP id af79cd13be357-92c7a0a701aso231948985a.3 for ; Tue, 30 Jun 2026 09:06:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835611; x=1783440411; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=wn2DjkNRyw10/1RTSalbIpM7FIRHeNivQ5guJ8/Cmdg=; b=Rzs5e0XOEEJHiAepFW3t0rJ79szsfZE3I/2k2y25gzUDZrXNrnvNljONa9oKdzkNit EQGESbqWm3G7Jw8kbUK16DzNnJHO4yvnb0E4y1UBRnCttLHd+rGqZuNpWCiyMDLKCiXW 5blum+DiGt9NetugiZ8TWQyUkooyxsCzpWvNz8ky2RvpdYW6yuh5bcbMKas3rgl3tjyG sSJlXhfQBp8LqbXTNt0hvUpJUIBDfw0H3GkIrVRG1TZhPmdNcK7wImHIu4JjPTAuW5Zo UnTwIBHM1OtTuox+JcVeq9TbGTp7fqmUtTbCoVdCJrNY6yE6XvqwElmhKHg3wX2AbG/9 Jsyw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835611; x=1783440411; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=wn2DjkNRyw10/1RTSalbIpM7FIRHeNivQ5guJ8/Cmdg=; b=Z8HeUw0sdAB3eL9PEYlYrGBz2b3hBxCEmIdmEixwurlkhxuOeBirTQE9s95kb6MTy/ vPgbejsaQcSInwBj8vrRaFnWwCCG+YVzuvaR0Xm41MMKxevQH8ZQ55cHkRv64ilzJT06 ZJEd2OO7DigODOw//alUYAERcjMQx/Z5OA2CcCKLph6q+onIcZEtIToR7N18+ujke6c8 8xnv0G+eCUDgx376ZD7pL2+7u0nVgtKowtf1D95zqCjJlLDJMpwYAfGDrV7g9RW6GN6S VIIgcROv/fMqQeY8w3mjEd5FRjsNjJ+rhkMrj04bG2EEDi2M28zridjAwoU2+IOB2rDK zEoQ== X-Gm-Message-State: AOJu0YxcfEGCWMZW47rsJTNBezaSZkqbM9ko5Ds1tJvqvR9oyepwmMCX 32Op10dr52sBtaOd2iCCG47NPiHulkNu6XNar8MjNvdmwo1wWOFl28d5qV4KJw== X-Gm-Gg: AfdE7clQraYyl/2YPmCEHEBbpXT4AD14LWJVypUI/LGmunjubHkd353JSgafNuraJDt Goz5v94o/AbsHFAX9CS0dtHXbyHvdVyA0hjCSR4VJnbqO+UN4G2vbQ4xUWtSoKSh05hLXw5Xa+x D6WUQG6F/Lo1REX33GcM9bl+YV7KzNza4Bu2tc+ADL6jJ7N8PyWxxLiWc0o41TeRUcqjR3NwtGO J+YgMcLKxZXpg1fLZDKqqfyxris41WlATkzjb8dR8Xe6fueJu2l1gq/0UCemHwueVMyea+qZmzJ Bk2hmeLE7E6wfmY4oE71c5IrwhGDTfyb2kGkxDmTfcjGXtxsf4iXaYBMSP6qRmFbb5mLDO8/D1p XO3hVPQZbPhY1xUDjUDmHrbjQDr9oKQP6UNWkJ1pEG1zD678ldE+6lL4u35RzTog2lAtREiHvMY leU0EFyePeSGp8KLz4ytFWeoXdjVVw+HV/5Ov/VaL0T8OUM3cMzhL0Oto= X-Received: by 2002:a05:620a:8391:b0:92e:4867:95af with SMTP id af79cd13be357-92e62af0f9amr643052085a.73.1782835611050; Tue, 30 Jun 2026 09:06:51 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.49 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:49 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 5/9] tests: add ruff linting to run-tests.sh Date: Tue, 30 Jun 2026 12:06:08 -0400 Message-ID: <20260630160612.1005451-6-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:54 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4326 A test suite is only trustworthy if its own code is clean, so this commit brings ruff into the runner and holds the test tree to a clean bar. It also makes it easy to preview what ruff thinks of the wic source, without yet enforcing it. pyproject.toml gains ruff in the tests extra and a [tool.ruff] section. The configuration is deliberately minimal for now: the test suite is the only tree under an enforced clean bar; the wic source under src/ is not yet ruff-clean and is reported, not gated. run-tests.sh gains two lint modes, each used on its own: - --lint-tests runs ruff over tests/ and exits. The test suite must report nothing; a finding here is a bug in our own test code and is expected to be fixed. - --lint-src runs ruff over src/ and exits. The source is not yet ruff-clean, so this is a preview: the runner prints ruff's findings and exits with its status, but nothing in the suite asserts on them. Keeping the two trees on separate flags means cleaning up the source later does not disturb the test-tree gate. A lint mode cannot be combined with coverage, with the other lint mode, or with pytest arguments; the runner rejects such combinations loudly rather than silently dropping the extras. If ruff is not installed it fails with the install command. tests/docs/linting.md documents the two modes and why src/ is held back for now. That file replaces the tests/docs/.gitkeep placeholder, which is no longer needed now that the directory has real content. .gitignore learns to ignore ruff's .ruff_cache/ directory. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- .gitignore | 3 +++ pyproject.toml | 8 ++++++++ tests/docs/.gitkeep | 0 tests/docs/linting.md | 39 +++++++++++++++++++++++++++++++++++ tests/run-tests.sh | 47 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) delete mode 100644 tests/docs/.gitkeep create mode 100644 tests/docs/linting.md diff --git a/.gitignore b/.gitignore index 534c49538091..3c3cfb328fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # coverage data and reports /.coverage /htmlcov/ + +# ruff cache +/.ruff_cache/ diff --git a/pyproject.toml b/pyproject.toml index ece2757bb686..656adcd4930a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ tests = [ "pytest >= 7.0", "coverage >= 7.0", "pytest-cov >= 4.0", + "ruff >= 0.5", ] [project.scripts] @@ -50,3 +51,10 @@ path = "src/wic/cli.py" # leftover files under the pytest base temp directory. tmp_path_retention_policy = "failed" tmp_path_retention_count = 1 + +[tool.ruff] +# For now only the test suite is actually linted (run-tests.sh +# --lint-tests passes the tests/ path); the wic source under src/ is +# not yet ruff-clean and is left out until its findings are fixed (see +# tests/docs/linting.md). --lint-src can still be run to preview the +# source findings, but it is reported, not enforced. diff --git a/tests/docs/.gitkeep b/tests/docs/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tests/docs/linting.md b/tests/docs/linting.md new file mode 100644 index 000000000000..71b4de21c100 --- /dev/null +++ b/tests/docs/linting.md @@ -0,0 +1,39 @@ +# Linting + +## Contents + +- [Running the linter](#running-the-linter) +- [tests/ must be clean](#tests-must-be-clean) +- [src/ is not linted yet](#src-is-not-linted-yet) + +The test suite is linted with [ruff](https://docs.astral.sh/ruff/). It +is configured in `pyproject.toml` (`[tool.ruff]`). + +## Running the linter + +The runner exposes ruff through two separate modes, each used on its +own: + +```bash +tests/run-tests.sh --lint-tests # ruff over tests/ +tests/run-tests.sh --lint-src # ruff over src/ (preview only) +``` + +A lint mode cannot be combined with coverage, with the other lint +mode, or with pytest arguments; the runner rejects such combinations. + +## tests/ must be clean + +Our own test code is held to a clean bar: `tests/run-tests.sh +--lint-tests` reports nothing. If you add a test that trips a rule, fix +the test before the change lands. + +## src/ is not linted yet + +`--lint-src` runs ruff over the wic source, but the source is **not** +yet ruff-clean, so its findings are a preview report rather than a +gate: the runner prints them and exits with ruff's status, but nothing +in the suite asserts on them. Treating `src/` findings as a hard +failure now would block every run on fixes that have not landed. Once +the source is cleaned up, `src/` can be promoted to the same clean bar +as `tests/`. diff --git a/tests/run-tests.sh b/tests/run-tests.sh index a483da6a63a6..085dcc93f91d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -14,23 +14,34 @@ usage() { cat <<'USAGE' Usage: tests/run-tests.sh [--coverage] [--html [DIR]] [pytest args] + tests/run-tests.sh --lint-tests + tests/run-tests.sh --lint-src Options: --coverage also measure branch coverage of src/wic and print a terminal report listing the lines that were missed --html [DIR] also write an HTML coverage report (default dir: htmlcov/); implies --coverage + --lint-tests run ruff over tests/ and exit; the test suite is held + to a clean bar, so this must report nothing + --lint-src run ruff over src/ and exit; src/ is not yet ruff-clean, + so this is a preview report and is not enforced -h, --help show this help and exit Anything else is passed straight through to pytest (for example a path, -k EXPR, or -v). With no such argument the whole suite under tests/ is run. +The two lint modes each run on their own; they cannot be combined with +coverage, with each other, or with pytest arguments. + Examples: tests/run-tests.sh # whole suite tests/run-tests.sh --coverage # + terminal coverage report tests/run-tests.sh --html # + HTML report in htmlcov/ tests/run-tests.sh --html /tmp/cov # + HTML report in /tmp/cov + tests/run-tests.sh --lint-tests # ruff over tests/ + tests/run-tests.sh --lint-src # ruff over src/ (preview) tests/run-tests.sh -k filemap -v # pass args through to pytest tests/run-tests.sh tests/unit # a single tier or file @@ -44,6 +55,8 @@ PY="${PYTHON:-python3}" coverage=0 html=0 html_dir="htmlcov" +lint_tests=0 +lint_src=0 pytest_args=() while [ $# -gt 0 ]; do case "$1" in @@ -60,6 +73,12 @@ while [ $# -gt 0 ]; do *) html_dir="$2"; shift ;; esac ;; + --lint-tests) + lint_tests=1 + ;; + --lint-src) + lint_src=1 + ;; -h|--help) usage exit 0 @@ -78,6 +97,34 @@ done cd "$REPO_ROOT" +# The lint modes run ruff and exit, so each must be used on its own. +# Reject combining them with coverage, with each other, or with pytest +# arguments loudly instead of silently ignoring the extras. +if [ $((lint_tests + lint_src)) -gt 0 ]; then + if [ "$lint_tests" -eq 1 ] && [ "$lint_src" -eq 1 ]; then + echo "error: --lint-tests and --lint-src cannot be combined." >&2 + echo " run one lint mode at a time." >&2 + exit 2 + fi + if [ "$coverage" -eq 1 ] || [ "$html" -eq 1 ] || [ ${#pytest_args[@]} -gt 0 ]; then + echo "error: a lint mode must be used on its own." >&2 + echo " lint, or drop the lint flag to run the suite." >&2 + exit 2 + fi + if ! "$PY" -c "import ruff" >/dev/null 2>&1 && ! command -v ruff >/dev/null 2>&1; then + echo "error: lint requested but ruff is not installed." >&2 + echo " run: $PY -m pip install -e \".[tests]\"" >&2 + exit 1 + fi + if [ "$lint_tests" -eq 1 ]; then + # The test suite is held to a clean bar; a finding here is a bug + # in our own test code and must be fixed. + exec "$PY" -m ruff check tests + fi + # src/ is not yet ruff-clean; this is a preview report, not a gate. + exec "$PY" -m ruff check src +fi + # Make sure the interpreter that will run pytest can actually import wic. # The suites pass even without an install (each adds src/ to sys.path), # but the session banner and any install-dependent behaviour would be From patchwork Tue Jun 30 16:06:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91418 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 52E74C43602 for ; Tue, 30 Jun 2026 16:06:54 +0000 (UTC) Received: from mail-qk1-f171.google.com (mail-qk1-f171.google.com [209.85.222.171]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.24723.1782835614020880711 for ; Tue, 30 Jun 2026 09:06:54 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=oPXHG7Gu; spf=pass (domain: gmail.com, ip: 209.85.222.171, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f171.google.com with SMTP id af79cd13be357-92e5cb052edso165794885a.2 for ; Tue, 30 Jun 2026 09:06:53 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835613; x=1783440413; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=GynnNe/oliScUbaXjPQLVBOJFXzLoT7k1lO6apXO8Us=; b=oPXHG7GuMeoWKEySLrwRFSieVps61BFlzggV+HUM2JR1E+BGyJZS62rplr++1hb+ti hPAY0q61gXMt10gncVIkvQKMb0fpl9cowtcfcPoOLjZWW5NKBCsVo7vv1fCdSSsSZNzs sZ7OeYspI6nJY/GlK7ndfCTiCW0j1FTGBkrvxtzmzfnUv6v7gH0tlvzpywOhtNim534H A1FfZbrEOC21abSyMtyONVkaTSgsvxUWEz/hlvhptbm01KlNYNcMpkMOn2x5qZNmkBcQ 7KqWRYx8oW+g+8BHANchXKPY2Lw9b2ibNzD1jPcS5Ip+AagNMnQLjpoAmrQAdaZxKlH5 cePw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835613; x=1783440413; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=GynnNe/oliScUbaXjPQLVBOJFXzLoT7k1lO6apXO8Us=; b=jtuhSv4mdRVk20atzeZJdmm7/sa/O+4O4/J0HDQyN/UwnXIRI+JwgccpeUG31l9KEo xMuQ+5oa1I6qtLoLr4aeTN89csWfwlfdHAOt8A9V4djUFA3QlCgO8yblFmaR+WkjLk9g vgUPftbg3C9fQxjCRGB4oSI1P0KBX6XuI9vff4Q4BXUPHtvLSbhKN4Mz1c+ydApXRpCH hZWU38lZTyrC0di8vWLL2GCrKKG1p+RlVuh63CflMxFvp7Iw4s47q71tWsmG0hZuu4Fh oq3Lmq0R44zC3eLtv5GwVk28cLGOb20F3zn3XL2CzTXdxdLQU9G6DkfjlC1V5NxMjAzX xHXA== X-Gm-Message-State: AOJu0YxnBowcDZw5W491Pg0vaw3wUXxC7wnboQeJrScYtrVwQhFSbRu6 RfJHER0PL3CJOg2bh+PEkyzryCd0G0/efi/pGX3DzdfDbQlCm3iygd33LqscaA== X-Gm-Gg: AfdE7cnF6eOlqgETkTdGkHwzhYXBSSqlSHTx55Vx7W922g5It7vve6uWKyP4Onjbahu sE3yfKUHkvB09SpZ0z0EC/sESKqS4mhFL3eYlEmQidwK0IUyu6eTnOr59l6pq9bLUHPIt7sC0UX aG17VR19oUEGVYA0cBJF5KXIUUvqygm4F4zuEEGcllTr/LoWLkyxU6l9B9VoWvMo1Krs+ecuryM yQTMlrTOy3cHdqPmGJJb0DIlYd0+gWY+DU+PyIkwBOIRe8b2lcE7z3cuVTUtj2EvYBxLqXZXNvr 6JrI2cpTd3GYRu/5Hx0BRYVbHXbM3lgy8LwYKWwuWZuls3UvcLKoOeqh7st7bK9uVeTvI8WmpAp V1mEHHdG9MnnaRnRt4mobRQ86rt6J/0bdT7JtU2q4DEIGc77+W77YUUULeJAGNMuTj7utN565rs tD6zoYP+nJp9aFuaetP0qrGoV57h2gZfvnXm90r/CILgyRaC2LmRG0gJQ= X-Received: by 2002:a05:620a:1a11:b0:92e:56f7:c5fb with SMTP id af79cd13be357-92e624a9cd8mr692951285a.12.1782835612603; Tue, 30 Jun 2026 09:06:52 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.51 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:51 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 6/9] tests/docs: add the suite overview README Date: Tue, 30 Jun 2026 12:06:09 -0400 Message-ID: <20260630160612.1005451-7-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:06:54 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4327 The test suite needs a front door: a short document that says what the suite is, how it is laid out, and how to run it. This commit adds tests/docs/README.md to be that overview. The README covers: - the layout of tests/, file by file; - that the suite is unit-only and needs no host tools or build environment, so it runs from a plain checkout; - how to install the test extras and invoke run-tests.sh, pointing at run-tests.sh --help as the authoritative, always-current list of options rather than duplicating them here where they would drift; - that anything run-tests.sh does not recognise is passed through to pytest; - a pointer to linting.md for how ruff is applied; - a small table of the other documents under tests/docs/. It deliberately does not enumerate every run-tests.sh option, since the runner is expected to grow more of them; the table and the --help output stay the source of truth. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- tests/docs/README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/docs/README.md diff --git a/tests/docs/README.md b/tests/docs/README.md new file mode 100644 index 000000000000..7f59c9efb84d --- /dev/null +++ b/tests/docs/README.md @@ -0,0 +1,61 @@ +# wic test suite + +A standalone test suite for the wic source tree. It runs from a plain +checkout with nothing but `pytest` -- no bitbake, no OpenEmbedded +build, and no target image -- so wic's logic can be exercised and kept +stable as the code changes. + +## Contents + +- [Layout](#layout) +- [Running](#running) +- [Linting](#linting) +- [Documentation](#documentation) + +## Layout + +``` +tests/ + conftest.py session banner describing the run + run-tests.sh wrapper for running the suite + unit/ unit tests that import wic modules directly + docs/ this documentation +``` + +The suite is unit-only: every test under `tests/unit/` imports a wic +module in-process and asserts on its behaviour, so none of it needs +host tools or a build environment. + +## Running + +Install wic with its test extras, then run the suite: + +```bash +pip install -e ".[tests]" +tests/run-tests.sh +``` + +`run-tests.sh` works from anywhere in the checkout. It can also report +branch coverage of the wic source; run `tests/run-tests.sh --help` for +the current list of options. + +Anything `run-tests.sh` does not recognise is handed straight to +pytest, so the whole pytest command line is available: + +```bash +tests/run-tests.sh -k filemap -v # one area, verbose +tests/run-tests.sh tests/unit # a single tier or file +``` + +## Linting + +The test suite is linted with ruff and is held to a clean bar. See +[linting.md](linting.md) for the lint modes and how the source tree is +treated. + +## Documentation + +| File | Content | +|------|---------| +| [authoring.md](authoring.md) | How to add a unit test to the suite | +| [linting.md](linting.md) | How ruff is used on the suite | From patchwork Tue Jun 30 16:06:10 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91424 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 870D1C43458 for ; Tue, 30 Jun 2026 16:07:04 +0000 (UTC) Received: from mail-qt1-f174.google.com (mail-qt1-f174.google.com [209.85.160.174]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.25084.1782835616016887189 for ; Tue, 30 Jun 2026 09:06:56 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=YLrmMExl; spf=pass (domain: gmail.com, ip: 209.85.160.174, mailfrom: twoerner@gmail.com) Received: by mail-qt1-f174.google.com with SMTP id d75a77b69052e-51bfbe05683so18094651cf.2 for ; Tue, 30 Jun 2026 09:06:55 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835615; x=1783440415; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=+a7CM3rIlK7cLIeHcfsiwYl6KjuhJqv+7Ztj9/XJWAc=; b=YLrmMExletVsmCzg/EmvAlSMFgVuGvA3IDlNI6U8DopywliGu9RkneNbxaZIxyBr/I ktHwWhkhUJo1gLeE5vhM9WRIF0bb/v3LVUQk/SXZtwFbTNVp5HI0RY4H9GfUI+XWxmjc PewYCvYGZbKcjZLTeWWgXR/RJn7zb0BwBnlhc3AUL1dSvef35+nShcgJt8CSDXX0jK0G SnumIigkM44R52LsrpXRiz8WREraixV9ymur9UEfiSh1QGdznvDjCDag2QI7VFxaVl3q uDql8mqCSeQzNqbC7yX25toJOw90OWzmbepsmIYUjbWJbygPM6t3U3oyH+vGxnhEixsM cT9A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835615; x=1783440415; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=+a7CM3rIlK7cLIeHcfsiwYl6KjuhJqv+7Ztj9/XJWAc=; b=lYC6ZdLkVRSUMBBnozr/Ochj9yK9KciFzBJLSBz2FJgeN5kIioIJFHBU5ao8NMZH+V 5TRk9OmxT+qade5F4I+weiuyHysUfM3HXuFLv1owRIhfGnKjFtvafntSyUQMqbBWxLaj ZnKxfR7aSFjNRJJrkPf4nTPRioZf5q5zK1uPW77OD6NIy/3UEoWRtYNCnDGL2fBoruSd /MeIYGe/fjUYDsGOJB/6/MBPpTZfF+lAVsJwwehX+LTRI2Q3GcNsE0tQpcA8Q5foqmGd GZvI1wjS9qYDbWw9HpjSVqUmy2aA7LgOnIyTZmSWbqaiyKmZORDgNVR2qlIQwVsKjUaK PUEQ== X-Gm-Message-State: AOJu0Yy5JZXIAammF2N5YRHJ2UazkDOhfH7F92cHXKbby5ZlI+32dlg9 1HEmOZaTwMyxnGlDO9m1xNDSriROjKbkvdEFWPBNTSZsgQaQrBrLp8qjF8dLCw== X-Gm-Gg: AfdE7clOw1LpXqMpTx3Y9jG0GGPB21rUZA3hnBTrA5o06FPTEBN+SBB7i5K7Rg9MhOW t39eMhzgAK4ko4Y/STVWuDquDpX1hp7jivI2THksj+MdwTEYROT3KJ9UlULoA1wwcLaBBUkQLGM wE2HDU0vZRQ9QKP5+zgI9ENQjxjedkSrzbN5E4X6Mu9dS0/38nHysnXeCdtc7kHfQOl3sq2f/sJ 4zP1Tdof7EKM4Oixf06FkT+i9AdM2OHs2JIxr58srv36RXXuw0MSFvBYJxluCnm5e87PvPy4icV /yJnoOMWjDALQsj3nltj43HwRKJWfXeqpbSxrmdNJCbKecuPoY0xl08SYIa6EHpoKlIECau6wxw 5NUNPcSc7Ko+4tay4dQgujNj7Mv2R0rs7Ak1R29AN+QkFbdqAkOpAc/Hb0+Vv+YfrN4oBSf9voV dLFJUTAVl0rc+sr8eJ0fZYo4A4hX8rHkPG6J7+OCs87N5HmHxRfnIrwdw= X-Received: by 2002:a05:622a:5cb:b0:517:71d2:37d2 with SMTP id d75a77b69052e-51c104cea19mr59740681cf.0.1782835614221; Tue, 30 Jun 2026 09:06:54 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.52 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:53 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 7/9] tests/docs: add the test-authoring guide Date: Tue, 30 Jun 2026 12:06:10 -0400 Message-ID: <20260630160612.1005451-8-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:07:04 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4328 A suite is only as good as the tests added to it over time, so it needs a guide that says where a new test goes, what one looks like, and how to choose its inputs and assertions. This commit adds tests/docs/authoring.md. The guide covers: - where a test goes: one test_.py per wic module under tests/unit/, extending an existing file or starting a new one; - the shape of a test: a small worked example, a preference for parametrize over in-test loops so each input/output pair is its own named case, and use of the tmp_path fixture for scratch files; - asserting the correct behaviour rather than whatever wic currently does, so a test never bakes in a bug; when a test exposes a defect, the source fix lands in the same change so the test passes; - probing the boundaries: a checklist of input classes worth covering for numbers, strings, paths, types/shape, and state, each asserting a specific value or exception rather than merely "it did not crash"; - keeping the assertion strong: never weaken an assertion to get green; - how to run just the test you wrote, with and without coverage. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- tests/docs/authoring.md | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/docs/authoring.md diff --git a/tests/docs/authoring.md b/tests/docs/authoring.md new file mode 100644 index 000000000000..c60cceb2d32d --- /dev/null +++ b/tests/docs/authoring.md @@ -0,0 +1,124 @@ +# Authoring a unit test + +Unit tests live under `tests/unit/`, one module per area of wic, named +`test_.py`. They import the wic module under test directly and +run in-process, so they need no host tools and no build environment. + +## Contents + +- [Where a test goes](#where-a-test-goes) +- [Shape of a test](#shape-of-a-test) +- [Assert the correct behaviour](#assert-the-correct-behaviour) +- [Probe the boundaries](#probe-the-boundaries) +- [Keep the assertion strong](#keep-the-assertion-strong) +- [Running what you wrote](#running-what-you-wrote) + +## Where a test goes + +Group tests by the wic module they exercise, one `test_.py` per +module. Start a new file when you begin covering a module that has no +file yet; otherwise extend the existing one. + +## Shape of a test + +```python +import pytest + +from wic.ksparser import sizetype + + +class TestSizetype: + # sizetype(default, size_in_bytes=False) returns a parser f(arg); + # with default "M", a bare number is read as mebibytes-in-KiB. + def test_plain_value_uses_default_unit(self): + parse = sizetype("M") + assert parse("100") == 100 * 1024 + + @pytest.mark.parametrize("arg,expected", [ + ("1K", 1), + ("1G", 1 * 1024 * 1024), + ("0", 0), + ]) + def test_suffixes(self, arg, expected): + assert sizetype("M")(arg) == expected +``` + +Prefer `@pytest.mark.parametrize` to express each input/output pair as +its own case rather than looping inside one test, so a single bad +value shows up as one named failure. + +Use `pytest`'s `tmp_path` fixture for any scratch files; a passing +test's scratch directory is removed automatically (see the retention +policy in `pyproject.toml`), and a failing one is kept for inspection. + +## Assert the correct behaviour + +Every assertion states the behaviour wic is *expected* to provide, +never the behaviour it currently happens to have. A test that locks in +a wrong result is worse than no test: it gives false confidence and it +turns red exactly when someone repairs the code. + +When a test exposes a defect, the fix to the wic source lands in the +same change as the test, so the test asserts the correct behaviour and +passes. Do not commit a test that asserts a known-wrong result. + +Wrong -- bakes in the bug: + +```python +# value lost its comma; asserting the broken output +assert result["file"] == "s3://bucket/a" +``` + +Right -- asserts the correct output (and the fix lands with it): + +```python +assert result["file"] == "s3://bucket/a,b.img" +``` + +## Probe the boundaries + +For a parameter, probe the boundary and just past it rather than only +the comfortable middle of its range. The classes worth covering: + +- numbers: empty, zero, negative, the maximum and one beyond it, + non-power-of-two and non-multiple-of-1024/1000 values, primes, + fractional values where an integer is assumed, `0` versus `0.0` + versus `"0"`, off-by-one at a block or sector boundary, and very + large magnitudes near `2**31` and `2**63` +- strings: the empty string, whitespace only, embedded spaces, tabs, + newlines and CRLF endings, non-ASCII characters, shell + metacharacters in a value that becomes part of a command line, + leading-dash values that look like options, over-delimited or + malformed forms (stray commas, doubled or missing separators), and + very long values +- paths: traversal (`../`), doubled slashes, a trailing slash or its + absence, `.` and `..` components, broken or looping symlinks, and a + non-existent path +- types and shape: each wrong type the parameter might receive (a + string where an integer is expected, a list where a string is + expected, `None`, and so on), the wrong argument count, and + duplicate keys or entries +- state: a second call that must not see stale cached data, a + `cache=False` path that must evict, calling an operation twice, and + finalising a half-constructed object + +Each case asserts a specific outcome -- an exact value, or a specific +exception -- rather than merely "it did not crash." A test that only +checks for the absence of an exception will pass against badly wrong +output. + +## Keep the assertion strong + +If a test is hard to make pass, the answer is in the code or in +understanding the correct behaviour, never in weakening the assertion +to something vague enough to pass. Loosening an assertion to get green +is how a suite quietly stops catching regressions. + +## Running what you wrote + +```bash +tests/run-tests.sh tests/unit/test_.py -v +tests/run-tests.sh --coverage tests/unit/test_.py +``` + +A new test should leave the suite green, with zero failures. From patchwork Tue Jun 30 16:06:11 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91425 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id ABEA7C44500 for ; Tue, 30 Jun 2026 16:07:04 +0000 (UTC) Received: from mail-qk1-f182.google.com (mail-qk1-f182.google.com [209.85.222.182]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.24724.1782835617821504144 for ; Tue, 30 Jun 2026 09:06:57 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=irsIrQXH; spf=pass (domain: gmail.com, ip: 209.85.222.182, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f182.google.com with SMTP id af79cd13be357-92b21f65b60so77864185a.1 for ; Tue, 30 Jun 2026 09:06:57 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835617; x=1783440417; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=9BDSA1QKxTReYbw9OKs+BRslFftrUHOqJgjFiom/7Kk=; b=irsIrQXHUjurd4Mc9RhJ2bnnbFAJR0wBfQO6RXxZGX3GhAVtXTm8LwlDwEkbzSg+G6 LAYkRanDv/0XkH5pe3gteNmiFtgeAM0kwsuTifpOnvZnPs1OtEZjtZKgBnwIOOhKe9YS m0015qQlRDIz6UZMUtNMt/CPxzUo3PZW9reayG7qgrLWkpQleSUSUZ+3WVgch7Ct2+mp DgNF8HpWQOSL3+JEKPUK0lbMa1DRaHRIVrK/wFU2Y5l6lElmGRnnI0YWQwTwvIyp0tdS 7a/ep3n736XFQsDDgyGrEU7hadbCY6wPkckF4cmWqwS0abp1O8/KWthz72sd+IHMvRU5 iZUQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835617; x=1783440417; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=9BDSA1QKxTReYbw9OKs+BRslFftrUHOqJgjFiom/7Kk=; b=N8cj3jUUYrvVr0ctjuqnx+YiRScP1m7nzyZNokHGZwdaLgqnP3Jv1Y5598rt4GU1y2 nUNr/Y+yAldTMzWHapiSdIliX5BFwmCKRTpoiZYAT+PLNTBZawCgG+5LoX8JAlCLq40l RrqxXbHL1ddlZPpW0kj66ncIebe1OkRrPgtbY2nThs7KXsVWEP302orGcrhfmkLWRUXR RS0CZyWKNE1vH/HmSa5k0yxC0zxE5nxnCoPbtnY9fAQL+nnHF2ULHl04CxLk6U77mko3 GD2CL+n6VKaL/Uv8k4kwr6m9EmUC/tYMnDGBNprXJItVHhVvFw7oRRd9mOdD8TTM1V6w 1ptQ== X-Gm-Message-State: AOJu0Yym1oxLPKxz6vJGduOFIe6trBqb9yJRDdDtwpZnxH5cO8TRpcKc S/5KdV0z4VJt27tSDD1eoLzK8vU8/ZCJ8My+sYZjervH9OBgqRjSGUbqsSotTg== X-Gm-Gg: AfdE7cmkCiGvZP8WJQXLuoQYiTWjAmYuXMNOvugolHatAgim+qSNz75vMaMWP7SUezY N9Nz5CXVVCDR7dwyMQEhglBJG57RHSsdSG0A+LEqoTPJKjRpb/kbq31yrmHnbZrV/yg86MRoY8/ 2hvhWfWGO1sonYx/brj45UjKLG27Rjw8KxrVofZCI16OTwignT8ew8PBkzw4/24D7MrZRLpnoKD qFVnGHvvko8AVxHABPJVYp7Gkp7duRm90nULdAzaKa9kRadM+NmqimRa2OlHQNEkCiEyFE+CQdV EGBojspr1icPGBrufzNsNj9e//49jGxExCjHEEIaiz+2hkDI8bhnCxjNWjUjYXmqSlTIjwLOcTk ezPfVnwpfFME4i33N/kxu56O1mPl72erQx8/KbEvCcGijlSqzoND4PuPRiMKB9Gjd3x+ZA0NPyY n476Osz0pp0QfAFkUipxZCeUxjjMP7vYRMCBizGY03dRpb9HW3qWspjS8= X-Received: by 2002:a05:620a:600c:b0:92e:56ea:ec69 with SMTP id af79cd13be357-92e697cbdb7mr308557085a.31.1782835616181; Tue, 30 Jun 2026 09:06:56 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.54 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:55 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 8/9] tests: ignore E402 in the test tree for the sys.path bootstrap Date: Tue, 30 Jun 2026 12:06:11 -0400 Message-ID: <20260630160612.1005451-9-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:07:04 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4329 The unit tests that follow run against a plain checkout that has not been installed. To do that, each test module prepends the in-tree src/ directory to sys.path and only then imports wic: _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 That bootstrap necessarily runs before the wic imports, which trips ruff's E402 (module-level import not at top of file). The ordering is required, not accidental: the import would fail without the sys.path adjustment ahead of it. Rather than scatter per-line noqa comments through every test module, this commit relaxes E402 for the test tree once, via a [tool.ruff.lint.per-file-ignores] entry scoped to tests/**. It is the only rule relaxed for tests/; the suite is otherwise held to a clean bar by --lint-tests. tests/docs/linting.md gains a section describing this single exception and why it exists. AI-Generated: codex/claude-opus 4.7 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - v1 submitted the entire test suite as a single commit; v2 breaks the work into a reviewable series, and this patch is one step of it. --- pyproject.toml | 8 ++++++++ tests/docs/linting.md | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 656adcd4930a..fb42fe6dd135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,3 +58,11 @@ tmp_path_retention_count = 1 # not yet ruff-clean and is left out until its findings are fixed (see # tests/docs/linting.md). --lint-src can still be run to preview the # source findings, but it is reported, not enforced. + +[tool.ruff.lint.per-file-ignores] +# Test modules prepend the in-tree src/ directory to sys.path before +# importing wic, so the suite runs against a checkout that has not been +# installed. That bootstrap necessarily runs before the wic imports, +# which trips E402 (module-level import not at top of file). The +# ordering is deliberate; ignore E402 for the test tree only. +"tests/**" = ["E402"] diff --git a/tests/docs/linting.md b/tests/docs/linting.md index 71b4de21c100..1c1bfcc06f82 100644 --- a/tests/docs/linting.md +++ b/tests/docs/linting.md @@ -4,6 +4,7 @@ - [Running the linter](#running-the-linter) - [tests/ must be clean](#tests-must-be-clean) +- [The one intentional exception in tests/](#the-one-intentional-exception-in-tests) - [src/ is not linted yet](#src-is-not-linted-yet) The test suite is linted with [ruff](https://docs.astral.sh/ruff/). It @@ -28,6 +29,25 @@ Our own test code is held to a clean bar: `tests/run-tests.sh --lint-tests` reports nothing. If you add a test that trips a rule, fix the test before the change lands. +## The one intentional exception in tests/ + +Test modules prepend the in-tree `src/` directory to `sys.path` before +importing `wic`, so the suite runs against a plain checkout that has not +been installed: + +```python +_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 +``` + +That bootstrap necessarily runs before the `wic` imports, which trips +`E402` (module-level import not at top of file). The ordering is +required, so `E402` is ignored for the test tree via `per-file-ignores` +in `pyproject.toml`. This is the only rule relaxed for `tests/`. + ## src/ is not linted yet `--lint-src` runs ruff over the wic source, but the source is **not** From patchwork Tue Jun 30 16:06:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 91423 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 931E1C43602 for ; Tue, 30 Jun 2026 16:07:04 +0000 (UTC) Received: from mail-qk1-f181.google.com (mail-qk1-f181.google.com [209.85.222.181]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.25091.1782835619928831644 for ; Tue, 30 Jun 2026 09:07:00 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=S58160zx; spf=pass (domain: gmail.com, ip: 209.85.222.181, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f181.google.com with SMTP id af79cd13be357-92e67555e24so75771785a.3 for ; Tue, 30 Jun 2026 09:06:59 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782835619; x=1783440419; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=A5ahhGnLahXnO8Z7/NI7nZYav1I5X6A7dXXYRegvcCg=; b=S58160zxkbqVdm+Blj+Y5LJ3D3jRmlCDXfT08PVj99zASyHnLJfFEp4Hnile7KDkK+ LglEVEYCkusK0+OEtZaanT5iEw5v/t6fpG9G+e4F2pqRWfKStGhoHPSbhXsUlOTHF0eO SxHNy739cvaQRLwh7bKqqYMDOniCc1E0Ban7g48a57S717vBLBx64tvtAkw1m8XQn187 BO9CerLyWWF6IXiqkYG0zgUjGl+Y04FKh+NecGkEE4CyWWE99hk2/UFcw/QXN8LJ//3+ MJ+GWX+MIfbrXHmI2EVecxIGL3uH9Bw9MKbLiwH+dgr9Xwx0rfGRsndJvI/Yt0sbmzf2 w3XQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782835619; x=1783440419; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=A5ahhGnLahXnO8Z7/NI7nZYav1I5X6A7dXXYRegvcCg=; b=DG9sDheDBbgRMLtZMclGvA11UJVtIa4s8okGkshj1LP3/7Sd5odGCH3MQjczGzE+Rm POUS2K4poUzRhu1SMB7Desi+UUH+zLWii+0oFkpytc4aA8oGrpve618mpc5GccLWBGUi jIzkOM3cjI9OYhZWCwYIk7b/XqgVYo2YsJnZKQMsC1V9gFHfKZ8YF+d//th5lD7O5mij m4OVoNA1AbKG7hFj5GoKrlXw8LYAhksC4SFlYkgouWCeRlLSnPKfvRx1vzihtiVd/aCC 7iGtSqSzWZWwAN5zq2l7fO+KLieRGe8kbXpkWEnOwCh7t/bwDl6Fez6RaIXBBjuU6OIR VuAQ== X-Gm-Message-State: AOJu0YxCOGKmqmmtOojRG5fBhZNT9g36smAg/ZoWd9r2VZvX1Yaqms42 zJeHQsSaRbYyV2k4Q2a9YyiX2N4YBg1VrtQwk8MKYRwMj9tjY+ft+l548SZVLw== X-Gm-Gg: AfdE7cniSzhG008J6MSB1fax2uA0Av5hnQqkMLkS1nkBNAR7rha/eOdH6XWe4w10C/n L915LQu0sLVbJ6fGkNAuM8tuQlmu24XwtdsculeV7YJt9AyuNNfIsnQ4/paJKOQA7b4ytDY1COl mERyrlikCfkitMK2whqJFRvjeIUI7GIz936jQqUhqbjiUtYQ9fqst9C1VWU1h9gh1d55bJC1vwh SbFXFZ/uzZLT2NoWjKZEYeMRhx9BhLKhynzTLmmu1kG/zgwgGkNTlBdMmmCFzQE01knNZAchomH N6N0r5X1DdmwNjc9Ab1xR4FEDKria1Q0I1P3vW7sy4FozgiltU8xvrb4LFNdjQ0WMHPYvIuREt1 6vHPNUNnwTmNbe/vE/ezcUoIMk6eBqlWfKZkBQoFE3Pu+0jiADgE0dtyLk4EIAtOhrsxp3/R1Zg /KLWBycRFtSA6BH7EZ3q7Eb5mMtPFdWrJkKgwApSCXfNMTh6Aijx5Oamc= X-Received: by 2002:a05:620a:390c:b0:92e:529b:b802 with SMTP id af79cd13be357-92e62815e60mr680923685a.70.1782835618662; Tue, 30 Jun 2026 09:06:58 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-92e621374dbsm272461785a.4.2026.06.30.09.06.56 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 30 Jun 2026 09:06:56 -0700 (PDT) From: Trevor Woerner To: yocto-patches@lists.yoctoproject.org Subject: [wic][PATCH v2 9/9] tests/unit/test_bb_utils: test mkdirhier() and fix its missing errno import Date: Tue, 30 Jun 2026 12:06:12 -0400 Message-ID: <20260630160612.1005451-10-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260630160612.1005451-1-twoerner@gmail.com> References: <20260630160612.1005451-1-twoerner@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 30 Jun 2026 16:07:04 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/4330 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.7 (xhigh) Signed-off-by: Trevor Woerner --- 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 | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 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..5cd6035ca453 --- /dev/null +++ b/tests/unit/test_bb_utils.py @@ -0,0 +1,126 @@ +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 tempfile +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): + d = tempfile.mkdtemp() + target = os.path.join(d, "a", "b", "c") + mkdirhier(target) + assert os.path.isdir(target) + + def test_idempotent_on_existing_directory(self): + d = tempfile.mkdtemp() + target = os.path.join(d, "x") + mkdirhier(target) + # Second call must not raise (exist_ok=True). + mkdirhier(target) + assert os.path.isdir(target) + + def test_single_level(self): + d = tempfile.mkdtemp() + target = os.path.join(d, "single") + mkdirhier(target) + assert os.path.isdir(target) + + def test_existing_root_directory(self): + # An already-existing directory (the tmpdir itself) is a no-op. + d = tempfile.mkdtemp() + mkdirhier(d) + assert os.path.isdir(d) + + +# --------------------------------------------------------------------------- +# 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): + d = tempfile.mkdtemp() + bad = os.path.join(d, "${VAR}", "sub") + with pytest.raises(Exception): + mkdirhier(bad) + # Nothing should have been created. + assert not os.path.exists(os.path.join(d, "${VAR}")) + + def test_brace_without_dollar_is_allowed(self): + # Only the literal '${' marker triggers the guard; a bare '{' is a + # legal (if unusual) directory name. + d = tempfile.mkdtemp() + target = os.path.join(d, "plain{brace") + mkdirhier(target) + assert os.path.isdir(target) + + +# --------------------------------------------------------------------------- +# OSError handler +# --------------------------------------------------------------------------- + +class TestMkdirhierErrorPath: + def test_oserror_under_a_file_component(self): + # 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. + d = tempfile.mkdtemp() + f = os.path.join(d, "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): + # An EEXIST OSError on a path that is already a directory is the + # benign race the handler exists to absorb; it must not propagate. + d = tempfile.mkdtemp() + err = OSError() + err.errno = errno.EEXIST + with mock.patch("wic.bb.utils.os.makedirs", side_effect=err): + mkdirhier(d) # d exists and is a dir -> swallowed, no raise + + def test_eexist_on_existing_file_propagates(self): + # EEXIST but the path is a regular file, not a directory: the + # handler's isdir() re-check fails, so the error must propagate. + d = tempfile.mkdtemp() + f = os.path.join(d, "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