Public Access
DEVX-61: optimise slow unit tests and handle missing tea binary in TeaCLI
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Failing after 14s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 1m6s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 1m12s
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Failing after 14s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 1m6s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 1m12s
This commit was merged in pull request #101.
This commit is contained in:
@@ -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 5 --max-single-seconds 0.5
|
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
|
||||||
|
|||||||
@@ -82,12 +82,15 @@ class TeaCLI:
|
|||||||
cmd = [self._tea, *args]
|
cmd = [self._tea, *args]
|
||||||
if json_output:
|
if json_output:
|
||||||
cmd.extend(["--output", "json"])
|
cmd.extend(["--output", "json"])
|
||||||
result = subprocess.run( # nosec B603
|
try:
|
||||||
cmd,
|
result = subprocess.run( # nosec B603
|
||||||
capture_output=True,
|
cmd,
|
||||||
text=True,
|
capture_output=True,
|
||||||
check=False,
|
text=True,
|
||||||
)
|
check=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise TeaCLIError(f"tea binary not found ('{self._tea}'). Install tea or add it to PATH.") from e
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise TeaCLIError(
|
raise TeaCLIError(
|
||||||
f"tea command failed (rc={result.returncode}): {' '.join(args)}\nstderr: {result.stderr.strip()}"
|
f"tea command failed (rc={result.returncode}): {' '.join(args)}\nstderr: {result.stderr.strip()}"
|
||||||
|
|||||||
@@ -317,9 +317,9 @@ class TestCollectKeys:
|
|||||||
assert "completed" in keys
|
assert "completed" in keys
|
||||||
assert "pending" in keys
|
assert "pending" in keys
|
||||||
|
|
||||||
def test_default_dir_includes_dynamic_keys(self) -> None:
|
def test_default_dir_includes_dynamic_keys(self, tmp_path: Path) -> None:
|
||||||
"""The default source dir should include DYNAMIC_KEYS."""
|
"""collect_keys includes DYNAMIC_KEYS even with an empty source dir."""
|
||||||
keys = check_translations.collect_keys(check_translations.DEFAULT_SRC_DIR)
|
keys = check_translations.collect_keys(tmp_path)
|
||||||
assert "completed" in keys
|
assert "completed" in keys
|
||||||
assert "pending" in keys
|
assert "pending" in keys
|
||||||
assert "in_progress" in keys
|
assert "in_progress" in keys
|
||||||
|
|||||||
@@ -113,6 +113,28 @@ class TestPyprojectReading:
|
|||||||
assert cfg.VIKUNJA_PROJECT_ID == 6
|
assert cfg.VIKUNJA_PROJECT_ID == 6
|
||||||
importlib.reload(cfg)
|
importlib.reload(cfg)
|
||||||
|
|
||||||
|
def test_pyproject_int_value_used(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||||
|
"""When pyproject.toml has an int value, it is used (covers _get_int return)."""
|
||||||
|
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n[tool.devx]\nvikunja_project_id = 42\n')
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False)
|
||||||
|
import devx.config as cfg
|
||||||
|
|
||||||
|
importlib.reload(cfg)
|
||||||
|
assert cfg.VIKUNJA_PROJECT_ID == 42
|
||||||
|
importlib.reload(cfg)
|
||||||
|
|
||||||
|
def test_env_int_override(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||||
|
"""Env var override for int config takes priority over pyproject.toml."""
|
||||||
|
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n[tool.devx]\nvikunja_project_id = 42\n')
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.setenv("DEVX_VIKUNJA_PROJECT_ID", "99")
|
||||||
|
import devx.config as cfg
|
||||||
|
|
||||||
|
importlib.reload(cfg)
|
||||||
|
assert cfg.VIKUNJA_PROJECT_ID == 99
|
||||||
|
importlib.reload(cfg)
|
||||||
|
|
||||||
def test_tool_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
def test_tool_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||||
"""When [tool] is not a dict, defaults are used."""
|
"""When [tool] is not a dict, defaults are used."""
|
||||||
(tmp_path / "pyproject.toml").write_text('tool = "not a dict"\n')
|
(tmp_path / "pyproject.toml").write_text('tool = "not a dict"\n')
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ class TestTeaCLIRun:
|
|||||||
with pytest.raises(TeaCLIError, match="auth error"):
|
with pytest.raises(TeaCLIError, match="auth error"):
|
||||||
cli._run(["labels", "list"])
|
cli._run(["labels", "list"])
|
||||||
|
|
||||||
|
def test_run_tea_not_found_raises_tea_error(self) -> None:
|
||||||
|
cli = TeaCLI(tea_bin="tea")
|
||||||
|
with patch("subprocess.run", side_effect=FileNotFoundError("tea not found")):
|
||||||
|
with pytest.raises(TeaCLIError, match="tea binary not found"):
|
||||||
|
cli._run(["labels", "list"])
|
||||||
|
|
||||||
def test_run_includes_json_flag(self) -> None:
|
def test_run_includes_json_flag(self) -> None:
|
||||||
cli = TeaCLI(tea_bin="/fake/tea")
|
cli = TeaCLI(tea_bin="/fake/tea")
|
||||||
mock_result = MagicMock(returncode=0, stdout="[]", stderr="")
|
mock_result = MagicMock(returncode=0, stdout="[]", stderr="")
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
proc = MagicMock()
|
proc = MagicMock()
|
||||||
@@ -163,7 +163,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
proc = MagicMock()
|
proc = MagicMock()
|
||||||
@@ -208,7 +208,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
proc = MagicMock()
|
proc = MagicMock()
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
proc = MagicMock()
|
proc = MagicMock()
|
||||||
@@ -338,7 +338,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.subprocess.Popen") as mock_popen,
|
patch("devx.molecule.molecule_ci_guard.subprocess.Popen") as mock_popen,
|
||||||
patch("devx.molecule.molecule_ci_guard.subprocess.run") as mock_run,
|
patch("devx.molecule.molecule_ci_guard.subprocess.run") as mock_run,
|
||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs") as mock_get_jobs,
|
patch("devx.molecule.molecule_ci_guard.get_running_jobs") as mock_get_jobs,
|
||||||
patch("time.sleep", side_effect=lambda x: real_sleep(0.05)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_get_jobs.return_value = [{"name": "molecule-tests (1)", "conclusion": "success"}]
|
mock_get_jobs.return_value = [{"name": "molecule-tests (1)", "conclusion": "success"}]
|
||||||
proc = MagicMock()
|
proc = MagicMock()
|
||||||
@@ -385,7 +385,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
mock_killpg.side_effect = ProcessLookupError("no such process")
|
mock_killpg.side_effect = ProcessLookupError("no such process")
|
||||||
@@ -432,7 +432,7 @@ class TestCli:
|
|||||||
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
|
patch("devx.molecule.molecule_ci_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.1)),
|
patch("time.sleep", side_effect=lambda x: real_sleep(0)),
|
||||||
):
|
):
|
||||||
mock_getpgid.return_value = 123
|
mock_getpgid.return_value = 123
|
||||||
mock_killpg.side_effect = [None, ProcessLookupError("no such process")]
|
mock_killpg.side_effect = [None, ProcessLookupError("no such process")]
|
||||||
|
|||||||
@@ -398,10 +398,16 @@ class TestMain:
|
|||||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
|
||||||
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
||||||
@patch("devx.ci.publish.TeaCLI")
|
@patch("devx.ci.publish.TeaCLI")
|
||||||
|
@patch("devx.ci.publish.publish_to_gitea_registry")
|
||||||
@patch("devx.ci.publish.publish_to_pypi")
|
@patch("devx.ci.publish.publish_to_pypi")
|
||||||
@patch("devx.ci.publish.build_package")
|
@patch("devx.ci.publish.build_package")
|
||||||
def test_create_release_already_exists_is_idempotent(
|
def test_create_release_already_exists_is_idempotent(
|
||||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
self,
|
||||||
|
mock_build: MagicMock,
|
||||||
|
mock_publish: MagicMock,
|
||||||
|
mock_gitea_pub: MagicMock,
|
||||||
|
mock_tea_cls: MagicMock,
|
||||||
|
mock_notes: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""If create_release fails with 'already exists', treat as success."""
|
"""If create_release fails with 'already exists', treat as success."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
@@ -416,10 +422,16 @@ class TestMain:
|
|||||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
|
||||||
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
||||||
@patch("devx.ci.publish.TeaCLI")
|
@patch("devx.ci.publish.TeaCLI")
|
||||||
|
@patch("devx.ci.publish.publish_to_gitea_registry")
|
||||||
@patch("devx.ci.publish.publish_to_pypi")
|
@patch("devx.ci.publish.publish_to_pypi")
|
||||||
@patch("devx.ci.publish.build_package")
|
@patch("devx.ci.publish.build_package")
|
||||||
def test_create_release_other_error_raises(
|
def test_create_release_other_error_raises(
|
||||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
self,
|
||||||
|
mock_build: MagicMock,
|
||||||
|
mock_publish: MagicMock,
|
||||||
|
mock_gitea_pub: MagicMock,
|
||||||
|
mock_tea_cls: MagicMock,
|
||||||
|
mock_notes: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""If create_release fails with a non-'already exists' error, raise."""
|
"""If create_release fails with a non-'already exists' error, raise."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
|||||||
Reference in New Issue
Block a user