DEVX-152: fix: remove dead translation keys and add missing one
CI / validate (pull_request) Successful in 50s
CI / auto-merge (pull_request) Failing after 11s

Co-authored-by: emil User <emil.simeonov@tutanota.com>
This commit is contained in:
2026-08-09 03:04:46 +02:00
committed by emil
parent 5cacbdec09
commit 74ee1830e5
12 changed files with 190 additions and 131 deletions
-1
View File
@@ -141,7 +141,6 @@ src/devx/
└── molecule/ # Optional molecule testing helpers (for Ansible projects) └── molecule/ # Optional molecule testing helpers (for Ansible projects)
├── discover_runners.py # Dynamic Gitea runner discovery ├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role) ├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role)
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast (--roles-root)
├── molecule_all.py # Run all molecule scenarios locally ├── molecule_all.py # Run all molecule scenarios locally
├── start_docker.py # Ensure Docker daemon is running for molecule tests ├── start_docker.py # Ensure Docker daemon is running for molecule tests
└── platforms.py # Supported molecule platforms └── platforms.py # Supported molecule platforms
-5
View File
@@ -226,10 +226,6 @@ python -m devx.molecule.distribute_molecule --runner-index 1 --max-runners 3
python -m devx.molecule.distribute_molecule --list # list all scenarios python -m devx.molecule.distribute_molecule --list # list all scenarios
python -m devx.molecule.distribute_molecule --list-platforms # list platforms python -m devx.molecule.distribute_molecule --list-platforms # list platforms
# Run molecule tests with cross-runner fail-fast
python -m devx.molecule.molecule_ci_guard pair1 pair2
python -m devx.molecule.molecule_ci_guard --roles-root ansible/roles pair1 pair2
# Run all molecule scenarios locally (sequential) # Run all molecule scenarios locally (sequential)
python -m devx.molecule.molecule_all python -m devx.molecule.molecule_all
python -m devx.molecule.molecule_all --bin .venv/bin python -m devx.molecule.molecule_all --bin .venv/bin
@@ -303,7 +299,6 @@ devx --version
| `devx molecule all` | Run all molecule scenarios on all supported platforms | | `devx molecule all` | Run all molecule scenarios on all supported platforms |
| `devx molecule discover-runners` | Discover available Gitea Actions runners | | `devx molecule discover-runners` | Discover available Gitea Actions runners |
| `devx molecule distribute` | Distribute molecule test pairs across parallel runners | | `devx molecule distribute` | Distribute molecule test pairs across parallel runners |
| `devx molecule guard` | Run molecule tests with CI failure polling |
See [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki/CLI-Commands) See [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki/CLI-Commands)
in the wiki for full command documentation with examples. in the wiki for full command documentation with examples.
+3 -3
View File
@@ -104,8 +104,8 @@ devx is a self-contained Python package under `src/devx/`:
- **Dev tools** (`devx.tools`) — setup, install_tools, check_test_speed, - **Dev tools** (`devx.tools`) — setup, install_tools, check_test_speed,
configure_repo, generate_badges, generate_cliff_config, install_checkmake configure_repo, generate_badges, generate_cliff_config, install_checkmake
- **Molecule tools** (`devx.molecule`) — Optional, for projects with Ansible - **Molecule tools** (`devx.molecule`) — Optional, for projects with Ansible
roles: distribute_molecule, molecule_ci_guard, molecule_all, discover_runners, roles: distribute_molecule, molecule_all, discover_runners, start_docker,
start_docker, platforms platforms
See [Architecture](Architecture) for the full package structure, module See [Architecture](Architecture) for the full package structure, module
descriptions, design principles, and data flow diagrams. descriptions, design principles, and data flow diagrams.
@@ -132,7 +132,7 @@ devx provides a `devx` CLI with three command groups:
- `devx ci <command>` — CI/CD automation (17 commands) - `devx ci <command>` — CI/CD automation (17 commands)
- `devx tools <command>` — Developer tools (9 commands) - `devx tools <command>` — Developer tools (9 commands)
- `devx molecule <command>` — Molecule testing (4 commands, optional) - `devx molecule <command>` — Molecule testing (3 commands, optional)
See [CLI Commands](CLI-Commands) for full command documentation with examples. See [CLI Commands](CLI-Commands) for full command documentation with examples.
+3 -11
View File
@@ -50,7 +50,6 @@ src/devx/
├── __init__.py ├── __init__.py
├── discover_runners.py # Dynamic Gitea runner discovery ├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute scenarios across runners ├── distribute_molecule.py # Distribute scenarios across runners
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast
├── molecule_all.py # Run all molecule scenarios locally ├── molecule_all.py # Run all molecule scenarios locally
├── start_docker.py # Ensure Docker is available for molecule ├── start_docker.py # Ensure Docker is available for molecule
└── platforms.py # Supported molecule platforms └── platforms.py # Supported molecule platforms
@@ -300,9 +299,9 @@ Distributes files matching a glob pattern across N parallel runners
### `integration_guard.py` ### `integration_guard.py`
Runs pytest with the same cross-runner failure detection mechanism used by Runs pytest with cross-runner failure detection. A background thread polls
`molecule_ci_guard`. If any other integration-tests matrix runner reports the Gitea API. If any other integration-tests matrix runner reports failure,
failure, the current pytest subprocess is killed and this runner exits early. the current pytest subprocess is killed and this runner exits early.
## Developer tools (`devx.tools`) ## Developer tools (`devx.tools`)
@@ -382,13 +381,6 @@ the supported OS platform matrix. Supports `--roles-root` for multi-role
repositories, `--list` to list scenarios, and `--list-platforms` to list repositories, `--list` to list scenarios, and `--list-platforms` to list
platforms. platforms.
### `molecule_ci_guard.py`
Runs molecule tests sequentially while polling the Gitea API for other runner
failures. If any other molecule matrix runner reports failure, the current
molecule subprocess is killed and this runner exits early. Supports both
single-role (4-part) and multi-role (5-part) pair encoding.
### `molecule_all.py` ### `molecule_all.py`
Runs all molecule scenarios on all supported OS platforms sequentially. Runs all molecule scenarios on all supported OS platforms sequentially.
-9
View File
@@ -478,15 +478,6 @@ python -m devx.molecule.distribute_molecule --list
python -m devx.molecule.distribute_molecule --list-platforms python -m devx.molecule.distribute_molecule --list-platforms
``` ```
### `molecule_ci_guard.py`
Runs molecule tests sequentially while polling the Gitea API for other runner
failures. Aborts early if another runner fails the same job.
```bash
python -m devx.molecule.molecule_ci_guard [--roles-root <dir>] pair1 pair2 ...
```
### `validate_commit_msg.py` ### `validate_commit_msg.py`
Validates commit messages. On feature branches: conventional commits only Validates commit messages. On feature branches: conventional commits only
-26
View File
@@ -631,29 +631,3 @@ Options:
- `--list-platforms` — list all platforms, one per line - `--list-platforms` — list all platforms, one per line
- `--roles-root <dir>` — roles root directory for multi-role repos (default: - `--roles-root <dir>` — roles root directory for multi-role repos (default:
`ansible/roles`) `ansible/roles`)
### `devx molecule guard`
Run molecule tests sequentially with CI failure polling. A background thread
polls the Gitea API. If any other molecule matrix runner reports failure, the
current molecule subprocess is killed and this runner exits early with code 1.
```bash
devx molecule guard pair1 pair2 pair3
devx molecule guard --roles-root ansible/roles pair1 pair2
```
Each pair is encoded as:
- **Single-role (4-part):** `scenario|platform_name|platform_image|platform_command`
- **Multi-role (5-part):** `role|scenario|platform_name|platform_image|platform_command`
Options:
- `--roles-root <dir>` — roles root directory for multi-role repos
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
- `GITEA_REPOSITORY` — repository in `owner/repo` format
-1
View File
@@ -52,7 +52,6 @@ REQUIRED_SCRIPTS = [
"detect_release_commit.py", "detect_release_commit.py",
"push_badges.py", "push_badges.py",
"distribute_molecule.py", "distribute_molecule.py",
"molecule_ci_guard.py",
"validate_commit_msg.py", "validate_commit_msg.py",
] ]
+51 -7
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Run integration tests with cross-runner failure detection. """Run integration tests with cross-runner failure detection.
Wraps ``pytest`` with the same Gitea API polling mechanism used by Wraps ``pytest`` with Gitea API polling. If any other integration-tests
``molecule_ci_guard``. If any other integration-tests matrix runner matrix runner reports failure, the current pytest subprocess is killed
reports failure, the current pytest subprocess is killed and this runner and this runner exits early with code 1.
exits early with code 1.
Usage:: Usage::
@@ -35,17 +34,62 @@ import threading
import time import time
import click import click
import requests
from devx.config import REPO_NAME, REPO_OWNER from devx.config import REPO_NAME, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.molecule.molecule_ci_guard import (
poll_for_other_failures,
)
from devx.tokens import get_ci_token from devx.tokens import get_ci_token
POLL_INTERVAL = 10 POLL_INTERVAL = 10
def get_running_jobs(gitea_url: str, owner: str, repo: str, token: str, run_id: int) -> list[dict]:
"""Return jobs for the given workflow run."""
url = f"{gitea_url}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/jobs"
headers = {"Authorization": f"token {token}"}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
return data.get("jobs", [])
def any_other_runner_failed(jobs: list[dict], current_job_name: str, current_index: int) -> bool:
"""Return True if any other matrix job has failed."""
for job in jobs:
name = job.get("name", "")
if not name.startswith(current_job_name):
continue
if name == f"{current_job_name} ({current_index})" or name == current_job_name:
continue
if job.get("conclusion") == "failure":
return True
return False
def poll_for_other_failures(
gitea_url: str,
owner: str,
repo: str,
token: str,
run_id: int,
job_name: str,
current_index: int,
stop_event: threading.Event,
failed_event: threading.Event,
) -> None:
"""Background thread: poll API and signal if another runner fails."""
while not stop_event.is_set():
try:
jobs = get_running_jobs(gitea_url, owner, repo, token, run_id)
if any_other_runner_failed(jobs, job_name, current_index):
click.echo(_("Another runner failed. Stopping this runner early."))
failed_event.set()
return
except requests.RequestException as exc:
click.echo(_("API poll warning: {exc}", exc=exc))
stop_event.wait(POLL_INTERVAL)
@click.command(context_settings={"ignore_unknown_options": True}) @click.command(context_settings={"ignore_unknown_options": True})
@click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED, required=True) @click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED, required=True)
def cli(pytest_args: tuple[str, ...]) -> None: def cli(pytest_args: tuple[str, ...]) -> None:
-7
View File
@@ -301,13 +301,6 @@ def molecule_discover_runners(args: tuple[str, ...]) -> None:
_run_module("devx.molecule.discover_runners", list(args)) _run_module("devx.molecule.discover_runners", list(args))
@molecule.command("guard")
@click.argument("args", nargs=-1)
def molecule_guard(args: tuple[str, ...]) -> None:
"""Run molecule tests sequentially with CI failure polling."""
_run_module("devx.molecule.molecule_ci_guard", list(args))
@molecule.command("all") @molecule.command("all")
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def molecule_all(args: tuple[str, ...]) -> None: def molecule_all(args: tuple[str, ...]) -> None:
+7 -47
View File
@@ -783,14 +783,6 @@
"ru": "Additional directory to scan (default: scripts, tests). Can be repeated.", "ru": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"zh": "Additional directory to scan (default: scripts, tests). Can be repeated." "zh": "Additional directory to scan (default: scripts, tests). Can be repeated."
}, },
"All molecule tests passed.": {
"bg": "All molecule tests passed.",
"de": "All molecule tests passed.",
"en": "All molecule tests passed.",
"pl": "Wszystkie testy molecule zakończone pomyślnie.",
"ru": "All molecule tests passed.",
"zh": "All molecule tests passed."
},
"Allow empty tag (PR mode where SHA is concrete).": { "Allow empty tag (PR mode where SHA is concrete).": {
"bg": "Позволи празен таг (PR режим, където SHA е конкретен).", "bg": "Позволи празен таг (PR режим, където SHA е конкретен).",
"de": "Leeren Tag zulassen (PR-Modus, in dem SHA konkret ist).", "de": "Leeren Tag zulassen (PR-Modus, in dem SHA konkret ist).",
@@ -799,13 +791,13 @@
"ru": "Разрешить пустой тег (режим PR, где SHA конкретен).", "ru": "Разрешить пустой тег (режим PR, где SHA конкретен).",
"zh": "允许空标签(SHA 为具体值的 PR 模式)。" "zh": "允许空标签(SHA 为具体值的 PR 模式)。"
}, },
"Another molecule runner failed. Stopping this runner early.": { "Another runner failed. Stopping this runner early.": {
"bg": "Another molecule runner failed. Stopping this runner early.", "bg": "Друг runner се провали. Спиране на този runner по-рано.",
"de": "Another molecule runner failed. Stopping this runner early.", "de": "Ein anderer Runner ist fehlgeschlagen. Dieser Runner wird vorzeitig gestoppt.",
"en": "Another molecule runner failed. Stopping this runner early.", "en": "Another runner failed. Stopping this runner early.",
"pl": "Inny runner molecule zakończył się niepowodzeniem. Wczesne zatrzymanie tego runnera.", "pl": "Inny runner zakończył się niepowodzeniem. Wczesne zatrzymanie tego runnera.",
"ru": "Another molecule runner failed. Stopping this runner early.", "ru": "Другой runner завершился с ошибкой. Останавливаю этот runner досрочно.",
"zh": "Another molecule runner failed. Stopping this runner early." "zh": "另一个 runner 失败。提前停止此 runner。"
}, },
"Assigned {count} files to runner {runner_index}": { "Assigned {count} files to runner {runner_index}": {
"bg": "Assigned {count} files to runner {runner_index}", "bg": "Assigned {count} files to runner {runner_index}",
@@ -1495,14 +1487,6 @@
"ru": "FAILED: {count} undocumented dependency/ies", "ru": "FAILED: {count} undocumented dependency/ies",
"zh": "FAILED: {count} undocumented dependency/ies" "zh": "FAILED: {count} undocumented dependency/ies"
}, },
"FAILED: {pair} exited with code {code}": {
"bg": "FAILED: {pair} exited with code {code}",
"de": "FAILED: {pair} exited with code {code}",
"en": "FAILED: {pair} exited with code {code}",
"pl": "NIEUDANE: {pair} zakończone kodem {code}",
"ru": "FAILED: {pair} exited with code {code}",
"zh": "FAILED: {pair} exited with code {code}"
},
"Failed images: {names}": { "Failed images: {names}": {
"bg": "Failed images: {names}", "bg": "Failed images: {names}",
"de": "Failed images: {names}", "de": "Failed images: {names}",
@@ -2263,14 +2247,6 @@
"ru": "PASS: All documentation checks passed!", "ru": "PASS: All documentation checks passed!",
"zh": "PASS: All documentation checks passed!" "zh": "PASS: All documentation checks passed!"
}, },
"PASSED: {pair}": {
"bg": "PASSED: {pair}",
"de": "PASSED: {pair}",
"en": "PASSED: {pair}",
"pl": "UDANE: {pair}",
"ru": "PASSED: {pair}",
"zh": "PASSED: {pair}"
},
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": { "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.", "bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.", "de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
@@ -2743,14 +2719,6 @@
"ru": "Running: {cmd}", "ru": "Running: {cmd}",
"zh": "Running: {cmd}" "zh": "Running: {cmd}"
}, },
"Running: {scenario} on {platform}": {
"bg": "Running: {scenario} on {platform}",
"de": "Running: {scenario} on {platform}",
"en": "Running: {scenario} on {platform}",
"pl": "Uruchamianie: {scenario} na {platform}",
"ru": "Running: {scenario} on {platform}",
"zh": "Running: {scenario} on {platform}"
},
"SSH key set up successfully": { "SSH key set up successfully": {
"bg": "SSH ключът е настроен успешно", "bg": "SSH ключът е настроен успешно",
"de": "SSH-Schlüssel erfolgreich eingerichtet", "de": "SSH-Schlüssel erfolgreich eingerichtet",
@@ -3830,13 +3798,5 @@
"pl": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)", "pl": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"ru": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)", "ru": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"zh": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)" "zh": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)"
},
"Cleaning up: running molecule destroy for {scenario}": {
"en": "Cleaning up: running molecule destroy for {scenario}",
"bg": "Изчистване: изпълнение на molecule destroy за {scenario}",
"de": "Aufräumen: molecule destroy wird ausgeführt für {scenario}",
"pl": "Czyszczenie: uruchamianie molecule destroy dla {scenario}",
"ru": "Очистка: запуск molecule destroy для {scenario}",
"zh": "清理:正在为 {scenario} 运行 molecule destroy"
} }
} }
-8
View File
@@ -40,7 +40,6 @@ class TestCliGroups:
result = runner.invoke(cli, ["molecule", "--help"]) result = runner.invoke(cli, ["molecule", "--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "distribute" in result.output assert "distribute" in result.output
assert "guard" in result.output
assert "all" in result.output assert "all" in result.output
@@ -231,13 +230,6 @@ class TestMoleculeCommands:
assert result.exit_code == 0 assert result.exit_code == 0
mock_run.assert_called_once_with("devx.molecule.discover_runners", []) mock_run.assert_called_once_with("devx.molecule.discover_runners", [])
@patch("devx.cli._run_module")
def test_molecule_guard(self, mock_run: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["molecule", "guard"])
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.molecule.molecule_ci_guard", [])
@patch("devx.cli._run_module") @patch("devx.cli._run_module")
def test_molecule_all(self, mock_run: MagicMock) -> None: def test_molecule_all(self, mock_run: MagicMock) -> None:
runner = CliRunner() runner = CliRunner()
+126 -6
View File
@@ -7,6 +7,7 @@ import subprocess # nosec B404
import time import time
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner from click.testing import CliRunner
from devx.ci.integration_guard import cli from devx.ci.integration_guard import cli
@@ -111,9 +112,8 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.ci.integration_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg") as mock_killpg, patch("os.killpg") as mock_killpg,
patch("os.getpgid") as mock_getpgid, patch("os.getpgid") as mock_getpgid,
patch("time.sleep", side_effect=lambda x: real_sleep(0)), patch("time.sleep", side_effect=lambda x: real_sleep(0)),
@@ -158,9 +158,8 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.ci.integration_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg", side_effect=ProcessLookupError("no such process")), patch("os.killpg", side_effect=ProcessLookupError("no such process")),
patch("os.getpgid") as mock_getpgid, patch("os.getpgid") as mock_getpgid,
patch("time.sleep", side_effect=lambda x: real_sleep(0)), patch("time.sleep", side_effect=lambda x: real_sleep(0)),
@@ -203,9 +202,8 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.ci.integration_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg") as mock_killpg, patch("os.killpg") as mock_killpg,
patch("os.getpgid") as mock_getpgid, patch("os.getpgid") as mock_getpgid,
patch("time.sleep", side_effect=lambda x: real_sleep(0)), patch("time.sleep", side_effect=lambda x: real_sleep(0)),
@@ -289,3 +287,125 @@ def test_main_module_block() -> None:
namespace = dict(ig.__dict__) namespace = dict(ig.__dict__)
exec(compile(source, ig.__file__, "exec"), namespace) exec(compile(source, ig.__file__, "exec"), namespace)
assert callable(namespace["cli"]) assert callable(namespace["cli"])
class TestGetRunningJobs:
def test_returns_jobs(self) -> None:
with patch("devx.ci.integration_guard.requests.get") as mock_get:
mock_response = MagicMock()
mock_response.json.return_value = {
"jobs": [
{"name": "integration-tests (0)", "conclusion": "success"},
{"name": "integration-tests (1)", "conclusion": "failure"},
]
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
from devx.ci.integration_guard import get_running_jobs
jobs = get_running_jobs("https://gitea.example", "owner", "repo", "token", 123)
assert len(jobs) == 2
mock_get.assert_called_once()
def test_raises_on_request_error(self) -> None:
import requests
with patch("devx.ci.integration_guard.requests.get") as mock_get:
mock_get.side_effect = requests.RequestException("boom")
with pytest.raises(requests.RequestException):
from devx.ci.integration_guard import get_running_jobs
get_running_jobs("https://gitea.example", "owner", "repo", "token", 123)
class TestAnyOtherRunnerFailed:
def test_detects_other_failure(self) -> None:
from devx.ci.integration_guard import any_other_runner_failed
jobs = [
{"name": "integration-tests (0)", "conclusion": "success"},
{"name": "integration-tests (1)", "conclusion": "failure"},
{"name": "integration-tests (2)", "conclusion": "running"},
]
assert any_other_runner_failed(jobs, "integration-tests", 0) is True
def test_ignores_current_runner(self) -> None:
from devx.ci.integration_guard import any_other_runner_failed
jobs = [
{"name": "integration-tests (0)", "conclusion": "failure"},
{"name": "integration-tests (1)", "conclusion": "success"},
]
assert any_other_runner_failed(jobs, "integration-tests", 0) is False
def test_ignores_non_matching_jobs(self) -> None:
from devx.ci.integration_guard import any_other_runner_failed
jobs = [
{"name": "quality", "conclusion": "failure"},
{"name": "integration-tests (1)", "conclusion": "success"},
]
assert any_other_runner_failed(jobs, "integration-tests", 0) is False
class TestPollForOtherFailures:
def test_sets_failed_event_when_other_runner_fails(self) -> None:
from devx.ci.integration_guard import poll_for_other_failures
stop_event = MagicMock()
failed_event = MagicMock()
def side_effect(*args, **kwargs):
if stop_event.wait.call_count < 1:
return [
{"name": "integration-tests (0)", "conclusion": "success"},
{"name": "integration-tests (1)", "conclusion": "failure"},
]
return []
with patch("devx.ci.integration_guard.get_running_jobs") as mock_get_jobs:
mock_get_jobs.side_effect = side_effect
stop_event.is_set.side_effect = [False, False]
stop_event.wait.return_value = True
poll_for_other_failures(
"https://gitea.example",
"owner",
"repo",
"token",
123,
"integration-tests",
0,
stop_event,
failed_event,
)
failed_event.set.assert_called_once()
def test_poll_warns_on_api_error(self) -> None:
import requests
from devx.ci.integration_guard import poll_for_other_failures
stop_event = MagicMock()
failed_event = MagicMock()
with patch("devx.ci.integration_guard.get_running_jobs") as mock_get_jobs:
mock_get_jobs.side_effect = requests.RequestException("boom")
stop_event.is_set.side_effect = [False, True]
stop_event.wait.return_value = True
poll_for_other_failures(
"https://gitea.example",
"owner",
"repo",
"token",
123,
"integration-tests",
0,
stop_event,
failed_event,
)
failed_event.set.assert_not_called()