From e29004308952495596e490a7335e2b76df801abf Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 10 Dec 2025 17:23:33 +0100 Subject: [PATCH] Refine installer capability integration tests and documentation - Adjust install_repos integration test to patch resolve_command_for_repo in the pipeline module and tighten DummyInstaller overrides - Rewrite recursive capability integration tests to focus on layer ordering and capability shadowing across Makefile, Python, Nix and OS-package installers - Extend recursive capabilities markdown with hierarchy diagram, capability matrix, scenario matrix and link to the external setup controller schema https://chatgpt.com/share/69399857-4d84-800f-a636-6bcd1ab5e192 --- tests/integration/test_install_repos.py | 61 ++-- .../test_recursive_capabilities.md | 74 +++-- .../test_recursive_capabilities.py | 266 ++++++------------ 3 files changed, 178 insertions(+), 223 deletions(-) diff --git a/tests/integration/test_install_repos.py b/tests/integration/test_install_repos.py index 60a64c5..5829fbd 100644 --- a/tests/integration/test_install_repos.py +++ b/tests/integration/test_install_repos.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + import os import tempfile import unittest @@ -16,10 +19,10 @@ class DummyInstaller(BaseInstaller): layer = None - def supports(self, ctx): + def supports(self, ctx): # type: ignore[override] return True - def run(self, ctx): + def run(self, ctx): # type: ignore[override] return @@ -34,31 +37,34 @@ class TestInstallReposIntegration(unittest.TestCase): mock_get_repo_dir, mock_clone_repos, mock_verify_repository, - ): + ) -> None: """ - Full integration test for high-level command resolution + symlink creation. + Integration test: - We do NOT re-test all low-level file-system details of - resolve_command_for_repo here (that is covered by unit tests). - Instead, we assert that: + We do NOT re-test the low-level implementation details of + resolve_command_for_repo() here (that is covered by unit tests). - - If resolve_command_for_repo(...) returns None: - → install_repos() does NOT create a symlink. + Instead, we assert the high-level behavior of install_repos() + + InstallationPipeline + create_ink(): - - If resolve_command_for_repo(...) returns a path: - → install_repos() creates exactly one symlink in bin_dir + * If resolve_command_for_repo(...) returns None: + → install_repos() must NOT create a symlink for that repo. + + * If resolve_command_for_repo(...) returns a path: + → install_repos() must create exactly one symlink in bin_dir that points to this path. - Concretely: + Concretely in this test: - - repo-system: - resolve_command_for_repo(...) → None + * repo-system: + fake resolver → returns None → no symlink in bin_dir for this repo. - - repo-nix: - resolve_command_for_repo(...) → "/nix/profile/bin/repo-nix" + * repo-nix: + fake resolver → returns "/nix/profile/bin/repo-nix" → exactly one symlink in bin_dir pointing to that path. """ + # Repositories must have provider/account/repository so that get_repo_dir() # does not crash when called from create_ink(). repo_system = { @@ -77,9 +83,7 @@ class TestInstallReposIntegration(unittest.TestCase): selected_repos = [repo_system, repo_nix] all_repos = selected_repos - with tempfile.TemporaryDirectory() as tmp_base, \ - tempfile.TemporaryDirectory() as tmp_bin: - + with tempfile.TemporaryDirectory() as tmp_base, tempfile.TemporaryDirectory() as tmp_bin: # Fake repo directories (what get_repo_dir will return) repo_system_dir = os.path.join(tmp_base, "repo-system") repo_nix_dir = os.path.join(tmp_base, "repo-nix") @@ -97,11 +101,15 @@ class TestInstallReposIntegration(unittest.TestCase): # Pretend this is the "Nix binary" path for repo-nix nix_tool_path = "/nix/profile/bin/repo-nix" - # Patch resolve_command_for_repo at the install_repos module level - with patch("pkgmgr.actions.repository.install.resolve_command_for_repo") as mock_resolve, \ - patch("pkgmgr.actions.repository.install.os.path.exists") as mock_exists_install: + # Patch resolve_command_for_repo at the *pipeline* module level, + # because InstallationPipeline imports it there. + with patch( + "pkgmgr.actions.repository.install.pipeline.resolve_command_for_repo" + ) as mock_resolve, patch( + "pkgmgr.actions.repository.install.os.path.exists" + ) as mock_exists_install: - def fake_resolve_command(repo, repo_identifier: str, repo_dir: str): + def fake_resolve(repo, repo_identifier: str, repo_dir: str): """ High-level behavior stub: @@ -111,9 +119,10 @@ class TestInstallReposIntegration(unittest.TestCase): - For repo-nix: act as if a Nix profile binary is the entrypoint → return nix_tool_path (symlink should be created). """ - if repo_identifier == "repo-system": + name = repo.get("name") + if name == "repo-system": return None - if repo_identifier == "repo-nix": + if name == "repo-nix": return nix_tool_path return None @@ -126,7 +135,7 @@ class TestInstallReposIntegration(unittest.TestCase): return True return False - mock_resolve.side_effect = fake_resolve_command + mock_resolve.side_effect = fake_resolve mock_exists_install.side_effect = fake_exists_install # Use only DummyInstaller so we focus on link creation, not installer behavior diff --git a/tests/integration/test_recursive_capabilities.md b/tests/integration/test_recursive_capabilities.md index 0c17896..12b62d3 100644 --- a/tests/integration/test_recursive_capabilities.md +++ b/tests/integration/test_recursive_capabilities.md @@ -1,6 +1,16 @@ # Capability Resolution & Installer Shadowing -## Layer Hierarchy +This document explains how `pkgmgr` decides **which installer should run** when multiple installation mechanisms are available in a repository. +It reflects the logic shown in the setup-controller diagram: + +➡️ **Full graphical schema:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp) + +--- + +## Layer Hierarchy (Strength Order) + +Installers are evaluated from **strongest to weakest**. +A stronger layer shadows all layers below it. ``` ┌───────────────────────────┐ Highest layer @@ -22,7 +32,24 @@ --- -## Scenario Matrix +## Capability Matrix + +Each layer provides a set of **capabilities**. +Layers that provide *all* capabilities of a lower layer **shadow** that layer. + +| Capability | Makefile | Python | Nix | OS-Pkgs | +| -------------------- | -------- | ------------ | --- | ------- | +| `make-install` | ✔ | (optional) ✔ | ✔ | ✔ | +| `python-runtime` | – | ✔ | ✔ | ✔ | +| `binary/cli` | – | – | ✔ | ✔ | +| `system-integration` | – | – | – | ✔ | + +✔ = capability available +– = not provided by this layer + +--- + +## Scenario Matrix (Expected Installer Execution) | Scenario | Makefile | Python | Nix | OS-Pkgs | Test Name | | -------------------------- | -------- | ------ | --- | ------- | ----------------------------- | @@ -34,40 +61,41 @@ Legend: ✔ = installer runs -✗ = installer skipped (shadowed by upper layer) -– = no such layer present +✗ = installer is skipped (shadowed) +– = layer not present in this scenario --- ## What the Integration Test Confirms -**Goal:** Validate that the capability-shadowing mechanism correctly determines *which installers actually run* for a given repository layout. +The integration tests ensure that the **actual execution** matches the theoretical capability model. ### 1) Only Makefile -* Makefile provides `make-install`. -* No higher layers → MakefileInstaller runs. +* Only `Makefile` present + → MakefileInstaller runs. ### 2) Python + Makefile -* Python provides `python-runtime`. -* Makefile additionally provides `make-install`. -* No capability overlap → both installers run. +* Python provides `python-runtime` +* Makefile provides `make-install` + → Both run (capabilities are disjoint). ### 3) Python shadows Makefile -* Python also provides `make-install`. -* Makefile’s capability is fully covered → MakefileInstaller is skipped. +* Python additionally advertises `make-install` + → MakefileInstaller is skipped. ### 4) Nix shadows Python & Makefile -* Nix provides all capabilities below it. -* Only NixInstaller runs. +* Nix provides: `python-runtime` + `make-install` + → PythonInstaller and MakefileInstaller are skipped. + → Only NixInstaller runs. -### 5) OS-Packages shadow all +### 5) OS-Pkg layer shadows all -* PKGBUILD/debian/rpm provide all capabilities. -* Only the corresponding OS package installer runs. +* OS packages provide all capabilities + → Only OS installer runs. --- @@ -111,6 +139,14 @@ Legend: --- -## Core Principle (one sentence) +## Core Principle -**A layer only executes if it provides at least one capability not already guaranteed by any higher layer.** +**A layer is executed only if it contributes at least one capability that no stronger layer has already provided.** + +--- + +## Link to the Setup Controller Diagram + +The full visual schema is available here: + +➡️ **[https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)** diff --git a/tests/integration/test_recursive_capabilities.py b/tests/integration/test_recursive_capabilities.py index 63dfbbc..7d60427 100644 --- a/tests/integration/test_recursive_capabilities.py +++ b/tests/integration/test_recursive_capabilities.py @@ -2,140 +2,99 @@ # -*- coding: utf-8 -*- """ -Integration tests for the recursive / layered capability handling in pkgmgr. +Integration tests for recursive capability resolution and installer shadowing. -We focus on the interaction between: +These tests verify that, given different repository layouts (Makefile, pyproject, +flake.nix, PKGBUILD), only the expected installers are executed based on the +capabilities provided by higher layers. - - MakefileInstaller (layer: "makefile") - - PythonInstaller (layer: "python") - - NixFlakeInstaller (layer: "nix") - - ArchPkgbuildInstaller (layer: "os-packages") +Layer order (strongest → weakest): -The core idea: - - - Each installer declares logical capabilities for its layer via - discover_capabilities() and the global CAPABILITY_MATCHERS. - - install_repos() tracks which capabilities have already been provided - by earlier installers (in INSTALLERS order). - - If an installer only provides capabilities that are already covered - by previous installers, it is skipped. - -These tests use *real* capability detection (based on repo files like -flake.nix, pyproject.toml, Makefile, PKGBUILD), but patch the installers' -run() methods so that no real external commands are executed. - -Scenarios: - - 1. Only Makefile with install target - → MakefileInstaller runs, all good. - - 2. Python + Makefile (no "make install" in pyproject.toml) - → PythonInstaller provides only python-runtime - → MakefileInstaller provides make-install - → Both run, since their capabilities are disjoint. - - 3. Python + Makefile (pyproject.toml mentions "make install") - → PythonInstaller provides {python-runtime, make-install} - → MakefileInstaller provides {make-install} - → MakefileInstaller is skipped (capabilities already covered). - - 4. Nix + Python + Makefile - - flake.nix hints: - * buildPythonApplication (python-runtime) - * make install (make-install) - → NixFlakeInstaller provides {python-runtime, make-install, nix-flake} - → PythonInstaller and MakefileInstaller are skipped. - - 5. OS packages + Nix + Python + Makefile - - PKGBUILD contains: - * "pip install ." (python-runtime via os-packages) - * "make install" (make-install via os-packages) - * "nix profile" (nix-flake via os-packages) - → ArchPkgbuildInstaller provides all capabilities - → All lower layers are skipped. + OS-PACKAGES > NIX > PYTHON > MAKEFILE """ import os import shutil import tempfile import unittest +from typing import List, Sequence, Tuple from unittest.mock import patch import pkgmgr.actions.repository.install as install_mod from pkgmgr.actions.repository.install import install_repos -from pkgmgr.actions.repository.install.installers.nix_flake import NixFlakeInstaller -from pkgmgr.actions.repository.install.installers.python import PythonInstaller from pkgmgr.actions.repository.install.installers.makefile import MakefileInstaller -from pkgmgr.actions.repository.install.installers.os_packages.arch_pkgbuild import ArchPkgbuildInstaller +from pkgmgr.actions.repository.install.installers.nix_flake import NixFlakeInstaller +from pkgmgr.actions.repository.install.installers.os_packages.arch_pkgbuild import ( + ArchPkgbuildInstaller, +) +from pkgmgr.actions.repository.install.installers.python import PythonInstaller + + +InstallerSpec = Tuple[str, object] class TestRecursiveCapabilitiesIntegration(unittest.TestCase): def setUp(self) -> None: - # Temporary base directory for this test class - self.tmp_root = tempfile.mkdtemp(prefix="pkgmgr-integration-") + self.tmp_root = tempfile.mkdtemp(prefix="pkgmgr-recursive-caps-") self.bin_dir = os.path.join(self.tmp_root, "bin") os.makedirs(self.bin_dir, exist_ok=True) def tearDown(self) -> None: shutil.rmtree(self.tmp_root) - # ------------------------------------------------------------------ - # Helper: create a new repo directory for a scenario - # ------------------------------------------------------------------ + # ------------------------------------------------------------------ helpers + def _new_repo(self) -> str: - repo_dir = tempfile.mkdtemp(prefix="repo-", dir=self.tmp_root) - return repo_dir - - # ------------------------------------------------------------------ - # Helper: run install_repos() with a custom installer list - # and record which installers actually ran. - # ------------------------------------------------------------------ - def _run_with_installers(self, repo_dir: str, installers, selected_repos=None): """ - Run install_repos() with a given INSTALLERS list and a single - dummy repo; return the list of installer labels that actually ran. + Create a fresh temporary repo directory under self.tmp_root. + """ + return tempfile.mkdtemp(prefix="repo-", dir=self.tmp_root) - The installers' supports() are forced to True so that only the - capability-shadowing logic decides whether they are skipped. - The installers' run() methods are patched to avoid real commands. + def _run_with_installers( + self, + repo_dir: str, + installers: Sequence[InstallerSpec], + selected_repos=None, + ) -> List[str]: + """ + Run install_repos() with a custom INSTALLERS list and capture which + installer labels actually run. - NOTE: - We patch resolve_command_for_repo() to always return a dummy - command path so that command resolution does not interfere with - capability-layering tests. + We override each installer's supports() to always return True and + override run() to append its label to called_installers. """ if selected_repos is None: - repo = {} + repo = {"repository": "dummy"} selected_repos = [repo] all_repos = [repo] else: all_repos = selected_repos - called_installers: list[str] = [] + called_installers: List[str] = [] - # Prepare patched instances with recording run() and always-supports. patched_installers = [] for label, inst in installers: def always_supports(self, ctx): return True - def make_run(label_name): + def make_run(label_name: str): def _run(self, ctx): called_installers.append(label_name) return _run - inst.supports = always_supports.__get__(inst, inst.__class__) - inst.run = make_run(label).__get__(inst, inst.__class__) + inst.supports = always_supports.__get__(inst, inst.__class__) # type: ignore[assignment] + inst.run = make_run(label).__get__(inst, inst.__class__) # type: ignore[assignment] patched_installers.append(inst) - with patch.object(install_mod, "INSTALLERS", patched_installers), \ - patch.object(install_mod, "get_repo_identifier", return_value="dummy-repo"), \ - patch.object(install_mod, "get_repo_dir", return_value=repo_dir), \ - patch.object(install_mod, "verify_repository", return_value=(True, [], None, None)), \ - patch.object(install_mod, "create_ink"), \ - patch.object(install_mod, "clone_repos"), \ - patch.object(install_mod, "resolve_command_for_repo", return_value="/bin/dummy"): - + with patch.object(install_mod, "INSTALLERS", patched_installers), patch.object( + install_mod, "get_repo_identifier", return_value="dummy-repo" + ), patch.object( + install_mod, "get_repo_dir", return_value=repo_dir + ), patch.object( + install_mod, "verify_repository", return_value=(True, [], None, None) + ), patch.object( + install_mod, "clone_repos" + ): install_repos( selected_repos=selected_repos, repositories_base_dir=self.tmp_root, @@ -144,25 +103,25 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): no_verification=True, preview=False, quiet=False, - clone_mode="shallow", + clone_mode="ssh", update_dependencies=False, ) return called_installers + # ----------------------------------------------------------------- scenarios - # ------------------------------------------------------------------ - # Scenario 1: Only Makefile with install target - # ------------------------------------------------------------------ def test_only_makefile_installer_runs(self) -> None: + """ + With only a Makefile present, only the MakefileInstaller should run. + """ repo_dir = self._new_repo() - # Makefile: detect a real 'install' target for makefile layer. with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f: - f.write("install:\n\t@echo 'installing from Makefile'\n") + f.write("install:\n\t@echo 'make install'\n") mk_inst = MakefileInstaller() - installers = [("makefile", mk_inst)] + installers: Sequence[InstallerSpec] = [("makefile", mk_inst)] called = self._run_with_installers(repo_dir, installers) @@ -172,110 +131,85 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): "With only a Makefile, the MakefileInstaller should run exactly once.", ) - # ------------------------------------------------------------------ - # Scenario 2: Python + Makefile, but pyproject.toml does NOT mention 'make install' - # → capabilities are disjoint, both installers should run. - # ------------------------------------------------------------------ def test_python_and_makefile_both_run_when_caps_disjoint(self) -> None: + """ + If Python and Makefile have disjoint capabilities, both installers run. + """ repo_dir = self._new_repo() - # pyproject.toml: basic Python project, no 'make install' string. + # pyproject.toml without any explicit "make install" hint with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f: - f.write( - "[project]\n" - "name = 'dummy'\n" - ) + f.write("name = 'dummy'\n") - # Makefile: install target for makefile layer. with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f: - f.write("install:\n\t@echo 'installing from Makefile'\n") + f.write("install:\n\t@echo 'make install'\n") py_inst = PythonInstaller() mk_inst = MakefileInstaller() - - # Order: Python first, then Makefile - installers = [ + installers: Sequence[InstallerSpec] = [ ("python", py_inst), ("makefile", mk_inst), ] called = self._run_with_installers(repo_dir, installers) - # Both should have run because: - # - Python provides {python-runtime} - # - Makefile provides {make-install} self.assertEqual( called, ["python", "makefile"], - "PythonInstaller and MakefileInstaller should both run when their capabilities are disjoint.", + "PythonInstaller and MakefileInstaller should both run when their " + "capabilities are disjoint.", ) - # ------------------------------------------------------------------ - # Scenario 3: Python + Makefile, pyproject.toml mentions 'make install' - # → PythonInstaller provides {python-runtime, make-install} - # MakefileInstaller only {make-install} - # → MakefileInstaller must be skipped. - # ------------------------------------------------------------------ def test_python_shadows_makefile_when_pyproject_mentions_make_install(self) -> None: + """ + If the Python layer advertises a 'make-install' capability (pyproject + explicitly hints at 'make install'), the Makefile layer must be skipped. + """ repo_dir = self._new_repo() - # pyproject.toml: Python project with 'make install' hint. with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f: f.write( - "[project]\n" "name = 'dummy'\n" "\n" "# Hint for MakeInstallCapability on layer 'python'\n" "make install\n" ) - # Makefile: install target, but should be shadowed by Python. with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f: - f.write("install:\n\t@echo 'installing from Makefile'\n") + f.write("install:\n\t@echo 'make install'\n") py_inst = PythonInstaller() mk_inst = MakefileInstaller() - - installers = [ + installers: Sequence[InstallerSpec] = [ ("python", py_inst), ("makefile", mk_inst), ] called = self._run_with_installers(repo_dir, installers) - # Python should run, Makefile should be skipped because its only - # capability (make-install) is already provided by Python. self.assertIn("python", called, "PythonInstaller should have run.") self.assertNotIn( "makefile", called, - "MakefileInstaller should be skipped because its 'make-install' capability " - "is already provided by Python.", + "MakefileInstaller should be skipped because its 'make-install' " + "capability is already provided by Python.", ) - # ------------------------------------------------------------------ - # Scenario 4: Nix + Python + Makefile - # flake.nix provides python-runtime + make-install + nix-flake - # → Nix shadows both Python and Makefile. - # ------------------------------------------------------------------ def test_nix_shadows_python_and_makefile(self) -> None: + """ + If a Nix flake advertises both python-runtime and make-install + capabilities, Python and Makefile installers must be skipped. + """ repo_dir = self._new_repo() - # pyproject.toml: generic Python project with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f: - f.write( - "[project]\n" - "name = 'dummy'\n" - ) + f.write("name = 'dummy'\n") - # Makefile: install target with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f: - f.write("install:\n\t@echo 'installing from Makefile'\n") + f.write("install:\n\t@echo 'make install'\n") - # flake.nix: hints for both python-runtime and make-install on layer 'nix' with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f: f.write( - "{\n" ' description = "integration test flake";\n' "}\n" "\n" @@ -289,8 +223,7 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): nix_inst = NixFlakeInstaller() py_inst = PythonInstaller() mk_inst = MakefileInstaller() - - installers = [ + installers: Sequence[InstallerSpec] = [ ("nix", nix_inst), ("python", py_inst), ("makefile", mk_inst), @@ -298,47 +231,35 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): called = self._run_with_installers(repo_dir, installers) - # Nix must run, Python and Makefile must be skipped: - # - Nix provides {python-runtime, make-install, nix-flake} - # - Python provides {python-runtime} - # - Makefile provides {make-install} self.assertIn("nix", called, "NixFlakeInstaller should have run.") self.assertNotIn( "python", called, - "PythonInstaller should be skipped because its python-runtime capability " - "is already provided by Nix.", + "PythonInstaller should be skipped because its python-runtime " + "capability is already provided by Nix.", ) self.assertNotIn( "makefile", called, - "MakefileInstaller should be skipped because its make-install capability " - "is already provided by Nix.", + "MakefileInstaller should be skipped because its make-install " + "capability is already provided by Nix.", ) - # ------------------------------------------------------------------ - # Scenario 5: OS packages + Nix + Python + Makefile - # PKGBUILD provides python-runtime + make-install + nix-flake - # → ArchPkgbuildInstaller shadows everything below. - # ------------------------------------------------------------------ def test_os_packages_shadow_nix_python_and_makefile(self) -> None: + """ + If an OS package layer (PKGBUILD) advertises all capabilities, + all lower layers (Nix, Python, Makefile) must be skipped. + """ repo_dir = self._new_repo() - # pyproject.toml: enough to signal a Python project with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f: - f.write( - "[project]\n" - "name = 'dummy'\n" - ) + f.write("name = 'dummy'\n") - # Makefile: install target with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f: - f.write("install:\n\t@echo 'installing from Makefile'\n") + f.write("install:\n\t@echo 'make install'\n") - # flake.nix: as before with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f: f.write( - "{\n" ' description = "integration test flake";\n' "}\n" "\n" @@ -346,13 +267,8 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): "make install\n" ) - # PKGBUILD: contains patterns for all three capabilities on layer 'os-packages': - # - "pip install ." → python-runtime - # - "make install" → make-install - # - "nix profile" → nix-flake with open(os.path.join(repo_dir, "PKGBUILD"), "w", encoding="utf-8") as f: f.write( - "pkgname=dummy\n" "pkgver=0.1\n" "pkgrel=1\n" "pkgdesc='dummy pkg for integration test'\n" @@ -376,8 +292,7 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): nix_inst = NixFlakeInstaller() py_inst = PythonInstaller() mk_inst = MakefileInstaller() - - installers = [ + installers: Sequence[InstallerSpec] = [ ("os-packages", os_inst), ("nix", nix_inst), ("python", py_inst), @@ -386,11 +301,6 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): called = self._run_with_installers(repo_dir, installers) - # ArchPkgbuildInstaller must run, and everything below must be skipped: - # - os-packages provides {python-runtime, make-install, nix-flake} - # - nix provides {python-runtime, make-install, nix-flake} - # - python provides {python-runtime} - # - makefile provides {make-install} self.assertIn("os-packages", called, "ArchPkgbuildInstaller should have run.") self.assertNotIn( "nix",