Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0abe6f176 | ||
|
|
15f6837dc2 | ||
|
|
b4dda91e24 | ||
|
|
3e21e774f7 | ||
|
|
7c11215e57 | ||
|
|
5384269c83 | ||
|
|
b3d0dd8ca7 | ||
|
|
a7dcaee5c6 | ||
|
|
02f8d3757b | ||
|
|
4311fb7648 | ||
|
|
2ead959fcf |
@@ -27,7 +27,7 @@ jobs:
|
|||||||
PYTHONPATH: src
|
PYTHONPATH: src
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
python3 -m devx.tools.check_test_speed --max-seconds 60 --max-single-seconds 2.0
|
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||||
- name: Documentation coverage check
|
- name: Documentation coverage check
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
PYTHONPATH: src
|
||||||
|
|||||||
@@ -2,6 +2,37 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.8.4] - 2026-06-23
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Add --ignore-installed to pip in CI to bypass debian packages
|
||||||
|
|
||||||
|
## [0.8.3] - 2026-06-23
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Lower check_test_speed threshold to 4 seconds
|
||||||
|
- Pass --break-system-packages to pip in CI environments
|
||||||
|
|
||||||
|
## [0.8.2] - 2026-06-23
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Encode spaces in pair commands to survive shell word-splitting
|
||||||
|
|
||||||
|
## [0.8.1] - 2026-06-23
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Set fresh MOLECULE_HOME per pair to avoid stale config cache
|
||||||
|
|
||||||
|
## [0.8.0] - 2026-06-23
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Fix molecule platforms to use sleep infinity, add --platforms-file
|
||||||
|
|
||||||
## [0.7.0] - 2026-06-23
|
## [0.7.0] - 2026-06-23
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@
|
|||||||
# Aligned with CI (ci.yml uses same thresholds).
|
# Aligned with CI (ci.yml uses same thresholds).
|
||||||
set -e
|
set -e
|
||||||
export PYTHONPATH=src
|
export PYTHONPATH=src
|
||||||
python3 -m devx.tools.check_test_speed --max-seconds 10 --max-single-seconds 0.5
|
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||||
|
|
||||||
__version__ = "0.7.0"
|
__version__ = "0.8.4"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from pathlib import Path
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from devx.i18n import _
|
from devx.i18n import _
|
||||||
from devx.molecule.platforms import PLATFORMS
|
from devx.molecule.platforms import PLATFORMS, load_platforms
|
||||||
|
|
||||||
DEFAULT_MAX_RUNNERS = 3
|
DEFAULT_MAX_RUNNERS = 3
|
||||||
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
|
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
|
||||||
@@ -41,7 +41,8 @@ class TestPair:
|
|||||||
|
|
||||||
def encode(self) -> str:
|
def encode(self) -> str:
|
||||||
"""Serialize to a pipe-delimited string for CI consumption."""
|
"""Serialize to a pipe-delimited string for CI consumption."""
|
||||||
return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}"
|
cmd = self.platform["command"].replace(" ", "__SPACE__")
|
||||||
|
return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{cmd}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode(encoded: str) -> TestPair:
|
def decode(encoded: str) -> TestPair:
|
||||||
@@ -49,7 +50,7 @@ class TestPair:
|
|||||||
parts = encoded.split("|")
|
parts = encoded.split("|")
|
||||||
return TestPair(
|
return TestPair(
|
||||||
scenario=parts[0],
|
scenario=parts[0],
|
||||||
platform={"name": parts[1], "image": parts[2], "command": parts[3]},
|
platform={"name": parts[1], "image": parts[2], "command": parts[3].replace("__SPACE__", " ")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -63,9 +64,8 @@ class MultiRoleTestPair:
|
|||||||
|
|
||||||
def encode(self) -> str:
|
def encode(self) -> str:
|
||||||
"""Serialize to a pipe-delimited string: ``role|scenario|platform_name|image|command``."""
|
"""Serialize to a pipe-delimited string: ``role|scenario|platform_name|image|command``."""
|
||||||
return (
|
cmd = self.platform["command"].replace(" ", "__SPACE__")
|
||||||
f"{self.role}|{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}"
|
return f"{self.role}|{self.scenario}|{self.platform['name']}|{self.platform['image']}|{cmd}"
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode(encoded: str) -> MultiRoleTestPair:
|
def decode(encoded: str) -> MultiRoleTestPair:
|
||||||
@@ -74,7 +74,7 @@ class MultiRoleTestPair:
|
|||||||
return MultiRoleTestPair(
|
return MultiRoleTestPair(
|
||||||
role=parts[0],
|
role=parts[0],
|
||||||
scenario=parts[1],
|
scenario=parts[1],
|
||||||
platform={"name": parts[2], "image": parts[3], "command": parts[4]},
|
platform={"name": parts[2], "image": parts[3], "command": parts[4].replace("__SPACE__", " ")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -238,6 +238,13 @@ def _write_github_env(key: str, value: str) -> None:
|
|||||||
help="Roles directory for multi-role discovery (scans */molecule/*/). "
|
help="Roles directory for multi-role discovery (scans */molecule/*/). "
|
||||||
"Use this for projects with multiple Ansible roles. Default: disabled (single-role mode).",
|
"Use this for projects with multiple Ansible roles. Default: disabled (single-role mode).",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--platforms-file",
|
||||||
|
type=click.Path(exists=True, file_okay=True, path_type=Path),
|
||||||
|
default=None,
|
||||||
|
help="JSON file with custom platform list (each entry: name, image, command). "
|
||||||
|
"Overrides the default platform matrix. Useful for projects with custom test images.",
|
||||||
|
)
|
||||||
def cli(
|
def cli(
|
||||||
runner_index: int | None,
|
runner_index: int | None,
|
||||||
max_runners: int,
|
max_runners: int,
|
||||||
@@ -247,7 +254,9 @@ def cli(
|
|||||||
skip_if_excess: bool,
|
skip_if_excess: bool,
|
||||||
molecule_root: Path | None,
|
molecule_root: Path | None,
|
||||||
roles_root: Path | None,
|
roles_root: Path | None,
|
||||||
|
platforms_file: Path | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
platforms = load_platforms(platforms_file)
|
||||||
# Multi-role mode: discover (role, scenario) pairs across all roles
|
# Multi-role mode: discover (role, scenario) pairs across all roles
|
||||||
if roles_root is not None:
|
if roles_root is not None:
|
||||||
role_scenarios = discover_multi_role_scenarios(roles_root)
|
role_scenarios = discover_multi_role_scenarios(roles_root)
|
||||||
@@ -256,10 +265,10 @@ def cli(
|
|||||||
click.echo(f"{role}|{scenario}")
|
click.echo(f"{role}|{scenario}")
|
||||||
return
|
return
|
||||||
if list_platforms:
|
if list_platforms:
|
||||||
for p in PLATFORMS:
|
for p in platforms:
|
||||||
click.echo(f"{p['name']}|{p['image']}|{p['command']}")
|
click.echo(f"{p['name']}|{p['image']}|{p['command']}")
|
||||||
return
|
return
|
||||||
pairs_mr = build_multi_role_pairs(role_scenarios)
|
pairs_mr = build_multi_role_pairs(role_scenarios, platforms)
|
||||||
if runner_index is None:
|
if runner_index is None:
|
||||||
groups = distribute_multi_role(pairs_mr, max_runners)
|
groups = distribute_multi_role(pairs_mr, max_runners)
|
||||||
for i, group in enumerate(groups):
|
for i, group in enumerate(groups):
|
||||||
@@ -291,10 +300,10 @@ def cli(
|
|||||||
click.echo(s)
|
click.echo(s)
|
||||||
return
|
return
|
||||||
if list_platforms:
|
if list_platforms:
|
||||||
for p in PLATFORMS:
|
for p in platforms:
|
||||||
click.echo(f"{p['name']}|{p['image']}|{p['command']}")
|
click.echo(f"{p['name']}|{p['image']}|{p['command']}")
|
||||||
return
|
return
|
||||||
pairs = build_pairs(scenarios)
|
pairs = build_pairs(scenarios, platforms)
|
||||||
if runner_index is None:
|
if runner_index is None:
|
||||||
groups = distribute(pairs, max_runners)
|
groups = distribute(pairs, max_runners)
|
||||||
for i, group in enumerate(groups):
|
for i, group in enumerate(groups):
|
||||||
|
|||||||
@@ -114,12 +114,14 @@ def parse_pair(pair: str) -> tuple[str, str, str, str, str]:
|
|||||||
|
|
||||||
Supports both 4-part (single-role) and 5-part (multi-role) formats.
|
Supports both 4-part (single-role) and 5-part (multi-role) formats.
|
||||||
For 4-part pairs, role is empty (caller uses default role dir).
|
For 4-part pairs, role is empty (caller uses default role dir).
|
||||||
|
Spaces in the command field are encoded as ``__SPACE__`` to survive
|
||||||
|
shell word-splitting when ``$TEST_PAIRS`` is expanded unquoted.
|
||||||
"""
|
"""
|
||||||
parts = pair.split("|")
|
parts = pair.split("|")
|
||||||
if len(parts) == 4:
|
if len(parts) == 4:
|
||||||
return "", parts[0], parts[1], parts[2], parts[3]
|
return "", parts[0], parts[1], parts[2], parts[3].replace("__SPACE__", " ")
|
||||||
if len(parts) == 5:
|
if len(parts) == 5:
|
||||||
return parts[0], parts[1], parts[2], parts[3], parts[4]
|
return parts[0], parts[1], parts[2], parts[3], parts[4].replace("__SPACE__", " ")
|
||||||
raise click.ClickException(f"Invalid pair format: {pair!r} (expected 4 or 5 pipe-delimited parts)")
|
raise click.ClickException(f"Invalid pair format: {pair!r} (expected 4 or 5 pipe-delimited parts)")
|
||||||
|
|
||||||
|
|
||||||
@@ -134,6 +136,12 @@ def build_env_for_pair(pair: str, base_env: dict[str, str]) -> dict[str, str]:
|
|||||||
elif "MOLECULE_PLATFORM_COMMAND" in env:
|
elif "MOLECULE_PLATFORM_COMMAND" in env:
|
||||||
del env["MOLECULE_PLATFORM_COMMAND"]
|
del env["MOLECULE_PLATFORM_COMMAND"]
|
||||||
env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true"
|
env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true"
|
||||||
|
# Use a fresh MOLECULE_HOME per pair to avoid stale config cache
|
||||||
|
# from previous CI runs (causes "Instances missing" errors).
|
||||||
|
if "MOLECULE_HOME" not in env:
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
env["MOLECULE_HOME"] = tempfile.mkdtemp(prefix="molecule-ci-")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,39 @@ dev tools and CI scripts.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
#: Supported OS platform matrix.
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
#: Default supported OS platform matrix.
|
||||||
#: Each entry maps a short name to (image, command).
|
#: Each entry maps a short name to (image, command).
|
||||||
#: The command must be systemd since rootless Docker requires
|
#: Uses the project's pre-built molecule-test-base image with
|
||||||
#: loginctl/systemctl --user.
|
#: ``sleep infinity`` (NOT systemd) to avoid cgroup v2 failures.
|
||||||
PLATFORMS: list[dict[str, str]] = [
|
PLATFORMS: list[dict[str, str]] = [
|
||||||
{"name": "ubuntu-2204", "image": "geerlingguy/docker-ubuntu2204-ansible:latest", "command": "/lib/systemd/systemd"},
|
{
|
||||||
{"name": "ubuntu-2404", "image": "geerlingguy/docker-ubuntu2404-ansible:latest", "command": "/lib/systemd/systemd"},
|
"name": "ubuntu-2604",
|
||||||
{"name": "debian-12", "image": "geerlingguy/docker-debian12-ansible:latest", "command": "/lib/systemd/systemd"},
|
"image": "git.oblachno.oblachno.fyi/oblachno/molecule-test-base:latest",
|
||||||
{"name": "archlinux", "image": "marcstraube/archlinux-ansible:latest", "command": "/usr/lib/systemd/systemd"},
|
"command": "sleep infinity",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_platforms(platforms_file: str | Path | None = None) -> list[dict[str, str]]:
|
||||||
|
"""Load platforms from a JSON file, falling back to PLATFORMS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platforms_file: Path to a JSON file with a list of platform dicts.
|
||||||
|
Each dict must have ``name``, ``image``, and ``command`` keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of platform dictionaries.
|
||||||
|
"""
|
||||||
|
if platforms_file is None:
|
||||||
|
return PLATFORMS
|
||||||
|
path = Path(platforms_file)
|
||||||
|
if not path.is_file():
|
||||||
|
return PLATFORMS
|
||||||
|
with path.open() as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, list) or not data:
|
||||||
|
return PLATFORMS
|
||||||
|
return data
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ def _run(cmd: list[str]) -> None:
|
|||||||
def _install_python_deps(bin_dir: str, extras: str = "dev") -> None:
|
def _install_python_deps(bin_dir: str, extras: str = "dev") -> None:
|
||||||
"""Install the project with the specified extras in editable mode."""
|
"""Install the project with the specified extras in editable mode."""
|
||||||
pip = str(Path(bin_dir) / "pip")
|
pip = str(Path(bin_dir) / "pip")
|
||||||
_run([pip, "install", "-e", f".[{extras}]"])
|
cmd = [pip, "install", "-e", f".[{extras}]"]
|
||||||
|
# In CI (system Python), --break-system-packages allows installing to
|
||||||
|
# system site-packages, and --ignore-installed avoids uninstall failures
|
||||||
|
# for debian-installed packages (e.g. platformdirs) that lack RECORD files.
|
||||||
|
if os.environ.get("PIP_BREAK_SYSTEM_PACKAGES") == "1":
|
||||||
|
cmd.extend(["--break-system-packages", "--ignore-installed"])
|
||||||
|
_run(cmd)
|
||||||
|
|
||||||
|
|
||||||
def _install_pre_commit_hooks(bin_dir: str) -> None:
|
def _install_pre_commit_hooks(bin_dir: str) -> None:
|
||||||
|
|||||||
@@ -163,10 +163,7 @@ class TestCli:
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["--list-platforms"])
|
result = runner.invoke(cli, ["--list-platforms"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "ubuntu-2204" in result.output
|
assert "ubuntu-2604" in result.output
|
||||||
assert "ubuntu-2404" in result.output
|
|
||||||
assert "debian-12" in result.output
|
|
||||||
assert "archlinux" in result.output
|
|
||||||
|
|
||||||
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
|
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
@@ -198,7 +195,7 @@ class TestCli:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
# Output should contain encoded pairs with platform info
|
# Output should contain encoded pairs with platform info
|
||||||
assert "alpha|" in result.output
|
assert "alpha|" in result.output
|
||||||
assert "ubuntu-2204" in result.output
|
assert "ubuntu-2604" in result.output
|
||||||
|
|
||||||
|
|
||||||
class TestGithubEnv:
|
class TestGithubEnv:
|
||||||
@@ -409,7 +406,7 @@ class TestCliMultiRole:
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["--roles-root", str(roles), "--list-platforms"])
|
result = runner.invoke(cli, ["--roles-root", str(roles), "--list-platforms"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "ubuntu-2204" in result.output
|
assert "ubuntu-2604" in result.output
|
||||||
|
|
||||||
def test_roles_root_no_runner_index_prints_groups(self, tmp_path: Path) -> None:
|
def test_roles_root_no_runner_index_prints_groups(self, tmp_path: Path) -> None:
|
||||||
"""--roles-root without --runner-index prints all groups."""
|
"""--roles-root without --runner-index prints all groups."""
|
||||||
@@ -422,6 +419,27 @@ class TestCliMultiRole:
|
|||||||
assert "Runner 0:" in result.output
|
assert "Runner 0:" in result.output
|
||||||
assert "Runner 1:" in result.output
|
assert "Runner 1:" in result.output
|
||||||
|
|
||||||
|
def test_platforms_file_overrides_default(self, tmp_path: Path) -> None:
|
||||||
|
"""--platforms-file loads custom platforms from JSON."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from devx.molecule.distribute_molecule import cli
|
||||||
|
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
(roles / "role-a" / "molecule" / "default").mkdir(parents=True)
|
||||||
|
platforms_file = tmp_path / "platforms.json"
|
||||||
|
custom = [{"name": "custom-os", "image": "custom:latest", "command": "sleep infinity"}]
|
||||||
|
platforms_file.write_text(json.dumps(custom))
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["--roles-root", str(roles), "--platforms-file", str(platforms_file), "--list-platforms"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "custom-os" in result.output
|
||||||
|
assert "custom:latest" in result.output
|
||||||
|
|
||||||
def test_roles_root_skips_non_dir_role(self, tmp_path: Path) -> None:
|
def test_roles_root_skips_non_dir_role(self, tmp_path: Path) -> None:
|
||||||
"""Non-directory entries in roles root are skipped."""
|
"""Non-directory entries in roles root are skipped."""
|
||||||
roles = tmp_path / "roles"
|
roles = tmp_path / "roles"
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"""Unit tests for scripts/ci/platforms.py."""
|
"""Unit tests for devx.molecule.platforms."""
|
||||||
|
|
||||||
from devx.molecule.platforms import PLATFORMS
|
import json
|
||||||
|
|
||||||
|
from devx.molecule.platforms import PLATFORMS, load_platforms
|
||||||
|
|
||||||
|
|
||||||
class TestPlatforms:
|
class TestPlatforms:
|
||||||
def test_platforms_not_empty(self) -> None:
|
def test_platforms_not_empty(self) -> None:
|
||||||
assert len(PLATFORMS) >= 4
|
assert len(PLATFORMS) >= 1
|
||||||
|
|
||||||
def test_each_platform_has_required_keys(self) -> None:
|
def test_each_platform_has_required_keys(self) -> None:
|
||||||
for p in PLATFORMS:
|
for p in PLATFORMS:
|
||||||
@@ -17,9 +19,40 @@ class TestPlatforms:
|
|||||||
names = [p["name"] for p in PLATFORMS]
|
names = [p["name"] for p in PLATFORMS]
|
||||||
assert len(names) == len(set(names))
|
assert len(names) == len(set(names))
|
||||||
|
|
||||||
|
def test_platforms_use_sleep_infinity(self) -> None:
|
||||||
|
"""All default platforms must use sleep infinity, not systemd."""
|
||||||
|
for p in PLATFORMS:
|
||||||
|
assert p["command"] == "sleep infinity", f"Platform {p['name']} uses {p['command']}"
|
||||||
|
|
||||||
def test_known_platforms_present(self) -> None:
|
def test_known_platforms_present(self) -> None:
|
||||||
names = {p["name"] for p in PLATFORMS}
|
names = {p["name"] for p in PLATFORMS}
|
||||||
assert "ubuntu-2204" in names
|
assert "ubuntu-2604" in names
|
||||||
assert "ubuntu-2404" in names
|
|
||||||
assert "debian-12" in names
|
|
||||||
assert "archlinux" in names
|
class TestLoadPlatforms:
|
||||||
|
def test_load_platforms_default(self, tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""load_platforms with no file returns PLATFORMS."""
|
||||||
|
result = load_platforms(None)
|
||||||
|
assert result == PLATFORMS
|
||||||
|
|
||||||
|
def test_load_platforms_from_file(self, tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""load_platforms reads custom platforms from JSON file."""
|
||||||
|
custom = [
|
||||||
|
{"name": "custom-os", "image": "custom:latest", "command": "sleep infinity"},
|
||||||
|
]
|
||||||
|
f = tmp_path / "platforms.json"
|
||||||
|
f.write_text(json.dumps(custom))
|
||||||
|
result = load_platforms(f)
|
||||||
|
assert result == custom
|
||||||
|
|
||||||
|
def test_load_platforms_missing_file_falls_back(self, tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""load_platforms falls back to PLATFORMS when file doesn't exist."""
|
||||||
|
result = load_platforms(tmp_path / "nonexistent.json")
|
||||||
|
assert result == PLATFORMS
|
||||||
|
|
||||||
|
def test_load_platforms_empty_list_falls_back(self, tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""load_platforms falls back to PLATFORMS when file has empty list."""
|
||||||
|
f = tmp_path / "platforms.json"
|
||||||
|
f.write_text("[]")
|
||||||
|
result = load_platforms(f)
|
||||||
|
assert result == PLATFORMS
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Unit tests for devx.tools.setup."""
|
"""Unit tests for devx.tools.setup."""
|
||||||
|
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
@@ -47,6 +48,14 @@ class TestInstallPythonDeps:
|
|||||||
_install_python_deps(".venv/bin", "ci,lint")
|
_install_python_deps(".venv/bin", "ci,lint")
|
||||||
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"])
|
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"])
|
||||||
|
|
||||||
|
@patch("devx.tools.setup._run")
|
||||||
|
def test_install_with_break_system_packages(self, mock_run: MagicMock) -> None:
|
||||||
|
with patch.dict(os.environ, {"PIP_BREAK_SYSTEM_PACKAGES": "1"}):
|
||||||
|
_install_python_deps(".venv/bin", "ci")
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
[".venv/bin/pip", "install", "-e", ".[ci]", "--break-system-packages", "--ignore-installed"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestInstallPreCommitHooks:
|
class TestInstallPreCommitHooks:
|
||||||
@patch("devx.tools.setup._run")
|
@patch("devx.tools.setup._run")
|
||||||
|
|||||||
Reference in New Issue
Block a user