Public Access
DEVX-3: feat: pluggable change classification framework
This commit is contained in:
@@ -1,4 +1,16 @@
|
||||
"""Unit tests for scripts/ci/classify_changes.py."""
|
||||
"""Unit tests for devx.ci.classify_changes.
|
||||
|
||||
Tests cover:
|
||||
- Glob matching (``_glob_to_regex``, ``_matches_glob``)
|
||||
- Classifier config loading from pyproject.toml
|
||||
- ChangeClassifier with layered rules (overrides, patterns, default)
|
||||
- Tag system (orthogonal categories)
|
||||
- Backward-compatible API (is_user_facing, is_workflow_only, classify_changes)
|
||||
- Git helpers (get_changed_files, get_latest_tag, run_git)
|
||||
- CLI (main with --quiet, --check, --github-output)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -9,6 +21,12 @@ from click.testing import CliRunner
|
||||
|
||||
import devx.ci.classify_changes as classify_changes_mod
|
||||
from devx.ci.classify_changes import (
|
||||
ChangeClassifier,
|
||||
ClassificationResult,
|
||||
ClassifierConfig,
|
||||
FileClassification,
|
||||
_glob_to_regex,
|
||||
_matches_glob,
|
||||
classify_changes,
|
||||
get_changed_files,
|
||||
get_latest_tag,
|
||||
@@ -19,98 +37,326 @@ from devx.ci.classify_changes import (
|
||||
run_git,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Glob matching tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestIsUserFacing:
|
||||
def test_src_is_user_facing(self) -> None:
|
||||
assert is_user_facing("src/devx/cli.py") is True
|
||||
|
||||
def test_ansible_is_user_facing(self) -> None:
|
||||
assert is_user_facing("ansible/roles/gitea-runner/tasks/main.yml") is True
|
||||
class TestGlobToRegex:
|
||||
def test_double_star_matches_anything(self) -> None:
|
||||
regex = _glob_to_regex(".gitea/**")
|
||||
assert regex.match(".gitea/workflows/ci.yml")
|
||||
assert regex.match(".gitea/actionlint.yaml")
|
||||
assert regex.match(".gitea/a/b/c/d.yml")
|
||||
assert not regex.match("tests/test_foo.py")
|
||||
|
||||
def test_pyproject_is_user_facing(self) -> None:
|
||||
assert is_user_facing("pyproject.toml") is True
|
||||
def test_double_star_in_middle(self) -> None:
|
||||
"""** in the middle of a pattern matches any number of segments."""
|
||||
regex = _glob_to_regex("src/**/test_*.py")
|
||||
assert regex.match("src/test_foo.py")
|
||||
assert regex.match("src/devx/test_cli.py")
|
||||
assert regex.match("src/a/b/c/test_bar.py")
|
||||
assert not regex.match("src/cli.py")
|
||||
|
||||
def test_workflow_is_not_user_facing(self) -> None:
|
||||
assert is_user_facing(".gitea/workflows/ci.yml") is False
|
||||
def test_single_star_matches_within_segment(self) -> None:
|
||||
regex = _glob_to_regex("src/*/cli.py")
|
||||
assert regex.match("src/devx/cli.py")
|
||||
assert regex.match("src/pkg/cli.py")
|
||||
assert not regex.match("src/devx/sub/cli.py")
|
||||
|
||||
def test_ci_scripts_are_not_user_facing(self) -> None:
|
||||
assert is_user_facing("scripts/ci/release.py") is False
|
||||
def test_question_mark_matches_single_char(self) -> None:
|
||||
regex = _glob_to_regex("file?.py")
|
||||
assert regex.match("file1.py")
|
||||
assert regex.match("fileA.py")
|
||||
assert not regex.match("file12.py")
|
||||
|
||||
def test_dev_scripts_are_not_user_facing(self) -> None:
|
||||
"""All scripts under scripts/ are infrastructure (CI/CD, dev tools).
|
||||
User-facing code lives in src/devx/."""
|
||||
assert is_user_facing("scripts/check_test_speed.py") is False
|
||||
assert is_user_facing("scripts/configure_repo.py") is False
|
||||
assert is_user_facing("scripts/install_checkmake.py") is False
|
||||
def test_literal_match(self) -> None:
|
||||
regex = _glob_to_regex("Makefile")
|
||||
assert regex.match("Makefile")
|
||||
assert not regex.match("makefile")
|
||||
|
||||
def test_shell_scripts_are_not_user_facing(self) -> None:
|
||||
assert is_user_facing("scripts/setup.sh") is False
|
||||
assert is_user_facing("scripts/molecule_all.sh") is False
|
||||
def test_special_chars_escaped(self) -> None:
|
||||
regex = _glob_to_regex("file.test.py")
|
||||
assert regex.match("file.test.py")
|
||||
assert not regex.match("fileXtest.py")
|
||||
|
||||
def test_scripts_init_is_not_user_facing(self) -> None:
|
||||
assert is_user_facing("scripts/__init__.py") is False
|
||||
|
||||
def test_version_file_is_not_user_facing(self) -> None:
|
||||
"""__init__.py only contains __version__ — a release artifact,
|
||||
not user-facing code. Version bumps alone should not trigger releases."""
|
||||
assert is_user_facing("src/devx/__init__.py") is False
|
||||
class TestMatchesGlob:
|
||||
def test_double_star(self) -> None:
|
||||
assert _matches_glob(".gitea/workflows/ci.yml", ".gitea/**")
|
||||
assert _matches_glob("tests/unit/test_cli.py", "tests/**")
|
||||
assert not _matches_glob("src/devx/cli.py", "tests/**")
|
||||
|
||||
def test_api_clients_is_not_user_facing(self) -> None:
|
||||
"""api_clients.py is used only by CI/CD scripts, not by the GRM CLI."""
|
||||
assert is_user_facing("src/devx/api_clients.py") is False
|
||||
def test_exact_match(self) -> None:
|
||||
assert _matches_glob("Makefile", "Makefile")
|
||||
assert _matches_glob("src/devx/__init__.py", "src/devx/__init__.py")
|
||||
assert not _matches_glob("src/devx/cli.py", "src/devx/__init__.py")
|
||||
|
||||
def test_docs_are_not_user_facing(self) -> None:
|
||||
assert is_user_facing("docs/user/getting-started.md") is False
|
||||
def test_prefix_matching(self) -> None:
|
||||
assert _matches_glob(".gitea/workflows/ci.yml", ".gitea/")
|
||||
assert _matches_glob("scripts/ci/release.py", "scripts/")
|
||||
assert not _matches_glob("tests/test_foo.py", "scripts/")
|
||||
|
||||
def test_tests_are_not_user_facing(self) -> None:
|
||||
assert is_user_facing("tests/unit/test_cli.py") is False
|
||||
def test_single_star(self) -> None:
|
||||
assert _matches_glob("src/devx/cli.py", "src/devx/*.py")
|
||||
assert not _matches_glob("src/devx/sub/cli.py", "src/devx/*.py")
|
||||
|
||||
def test_agents_md_is_not_user_facing(self) -> None:
|
||||
assert is_user_facing("AGENTS.md") is False
|
||||
|
||||
def test_makefile_is_not_user_facing(self) -> None:
|
||||
assert is_user_facing("Makefile") is False
|
||||
# ---------------------------------------------------------------------------
|
||||
# ClassifierConfig tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestClassifierConfig:
|
||||
def test_from_pyproject_loads_config(self, tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
"[tool.devx.classify]\n"
|
||||
'infrastructure = [".gitea/**", "tests/**"]\n'
|
||||
'infrastructure_overrides = ["src/pkg/__init__.py"]\n'
|
||||
'user_facing_overrides = ["docs/important.py"]\n'
|
||||
"\n"
|
||||
"[tool.devx.classify.tags]\n"
|
||||
'ansible = ["ansible/**"]\n'
|
||||
)
|
||||
config = ClassifierConfig.from_pyproject(str(pyproject))
|
||||
assert config.infrastructure == [".gitea/**", "tests/**"]
|
||||
assert config.infrastructure_overrides == ["src/pkg/__init__.py"]
|
||||
assert config.user_facing_overrides == ["docs/important.py"]
|
||||
assert config.tags == {"ansible": ["ansible/**"]}
|
||||
|
||||
def test_from_pyproject_missing_file(self) -> None:
|
||||
config = ClassifierConfig.from_pyproject("/nonexistent/pyproject.toml")
|
||||
assert config.infrastructure == []
|
||||
assert config.infrastructure_overrides == []
|
||||
assert config.user_facing_overrides == []
|
||||
assert config.tags == {}
|
||||
|
||||
def test_from_pyproject_missing_section(self, tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text('[project]\nname = "test"\n')
|
||||
config = ClassifierConfig.from_pyproject(str(pyproject))
|
||||
assert config.infrastructure == []
|
||||
|
||||
def test_from_pyproject_partial_config(self, tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text('[tool.devx.classify]\ninfrastructure = [".gitea/**"]\n')
|
||||
config = ClassifierConfig.from_pyproject(str(pyproject))
|
||||
assert config.infrastructure == [".gitea/**"]
|
||||
assert config.infrastructure_overrides == []
|
||||
assert config.user_facing_overrides == []
|
||||
assert config.tags == {}
|
||||
|
||||
def test_defaults_are_empty(self) -> None:
|
||||
config = ClassifierConfig()
|
||||
assert config.infrastructure == []
|
||||
assert config.infrastructure_overrides == []
|
||||
assert config.user_facing_overrides == []
|
||||
assert config.tags == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ChangeClassifier tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestChangeClassifier:
|
||||
def _make_classifier(self, **kwargs: object) -> ChangeClassifier:
|
||||
"""Create a classifier with explicit config (no pyproject.toml needed)."""
|
||||
config = ClassifierConfig(**kwargs) # type: ignore[arg-type]
|
||||
return ChangeClassifier(config)
|
||||
|
||||
def test_infrastructure_pattern_matches(self) -> None:
|
||||
classifier = self._make_classifier(infrastructure=[".gitea/**", "tests/**"])
|
||||
fc = classifier.classify_file(".gitea/workflows/ci.yml")
|
||||
assert not fc.is_user_facing
|
||||
assert "infrastructure" in fc.matched_rule
|
||||
|
||||
def test_unknown_file_defaults_to_user_facing(self) -> None:
|
||||
"""Safe default: unknown files are user-facing (require release)."""
|
||||
assert is_user_facing("some/new/file.type") is True
|
||||
assert is_user_facing("new_root_file.txt") is True
|
||||
classifier = self._make_classifier(infrastructure=[".gitea/**"])
|
||||
fc = classifier.classify_file("src/devx/cli.py")
|
||||
assert fc.is_user_facing
|
||||
assert fc.matched_rule is None
|
||||
assert "default" in fc.reason.lower()
|
||||
|
||||
def test_is_workflow_only_inverse(self) -> None:
|
||||
assert is_workflow_only(".gitea/workflows/ci.yml") is True
|
||||
assert is_workflow_only("src/devx/cli.py") is False
|
||||
assert is_workflow_only("pyproject.toml") is False
|
||||
def test_infrastructure_override(self) -> None:
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=[".gitea/**"],
|
||||
infrastructure_overrides=["src/devx/__init__.py"],
|
||||
)
|
||||
fc = classifier.classify_file("src/devx/__init__.py")
|
||||
assert not fc.is_user_facing
|
||||
assert fc.matched_rule == "infrastructure_overrides"
|
||||
|
||||
def test_user_facing_override_beats_infrastructure(self) -> None:
|
||||
"""User-facing overrides have highest priority (safety)."""
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=["tests/**"],
|
||||
user_facing_overrides=["tests/test_public_api.py"],
|
||||
)
|
||||
fc = classifier.classify_file("tests/test_public_api.py")
|
||||
assert fc.is_user_facing
|
||||
assert fc.matched_rule == "user_facing_overrides"
|
||||
|
||||
class TestClassifyChanges:
|
||||
def test_all_user_facing(self) -> None:
|
||||
files = ["src/devx/cli.py", "ansible/roles/gitea-runner/tasks/main.yml"]
|
||||
result = classify_changes(files)
|
||||
assert result["user_facing"] == files
|
||||
assert result["workflow_only"] == []
|
||||
def test_user_facing_override_beats_infrastructure_override(self) -> None:
|
||||
"""User-facing overrides beat infrastructure overrides (safety first)."""
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=[".gitea/**"],
|
||||
infrastructure_overrides=["src/devx/__init__.py"],
|
||||
user_facing_overrides=["src/devx/__init__.py"],
|
||||
)
|
||||
fc = classifier.classify_file("src/devx/__init__.py")
|
||||
assert fc.is_user_facing
|
||||
|
||||
def test_all_workflow_only(self) -> None:
|
||||
files = [".gitea/workflows/ci.yml", "docs/index.md", "AGENTS.md"]
|
||||
result = classify_changes(files)
|
||||
assert result["user_facing"] == []
|
||||
assert result["workflow_only"] == files
|
||||
def test_tags_are_computed(self) -> None:
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=[".gitea/**"],
|
||||
tags={"ansible": ["ansible/**", ".ansible-lint"], "docs": ["docs/**"]},
|
||||
)
|
||||
fc = classifier.classify_file("ansible/tasks/main.yml")
|
||||
assert "ansible" in fc.tags
|
||||
assert "docs" not in fc.tags
|
||||
|
||||
def test_mixed(self) -> None:
|
||||
def test_tags_orthogonal_to_classification(self) -> None:
|
||||
"""A file can be infrastructure AND tagged."""
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=[".gitea/**", "docs/**"],
|
||||
tags={"docs": ["docs/**"]},
|
||||
)
|
||||
fc = classifier.classify_file("docs/index.md")
|
||||
assert not fc.is_user_facing # infrastructure
|
||||
assert "docs" in fc.tags # also tagged
|
||||
|
||||
def test_classify_multiple_files(self) -> None:
|
||||
classifier = self._make_classifier(
|
||||
infrastructure=[".gitea/**", "tests/**"],
|
||||
infrastructure_overrides=["src/devx/__init__.py"],
|
||||
tags={"ansible": ["ansible/**"]},
|
||||
)
|
||||
files = [
|
||||
"src/devx/cli.py",
|
||||
".gitea/workflows/ci.yml",
|
||||
"pyproject.toml",
|
||||
"docs/index.md",
|
||||
"src/devx/__init__.py",
|
||||
"ansible/tasks/main.yml",
|
||||
"tests/test_foo.py",
|
||||
]
|
||||
result = classify_changes(files)
|
||||
assert "src/devx/cli.py" in result["user_facing"]
|
||||
assert "pyproject.toml" in result["user_facing"]
|
||||
assert ".gitea/workflows/ci.yml" in result["workflow_only"]
|
||||
assert "docs/index.md" in result["workflow_only"]
|
||||
result = classifier.classify(files)
|
||||
assert "src/devx/cli.py" in result.user_facing
|
||||
assert "ansible/tasks/main.yml" in result.user_facing
|
||||
assert ".gitea/workflows/ci.yml" in result.infrastructure
|
||||
assert "src/devx/__init__.py" in result.infrastructure
|
||||
assert "tests/test_foo.py" in result.infrastructure
|
||||
assert result.has_user_facing
|
||||
assert result.has_tag("ansible")
|
||||
assert "ansible/tasks/main.yml" in result.tags["ansible"]
|
||||
|
||||
def test_empty(self) -> None:
|
||||
result = classify_changes([])
|
||||
assert result == {"user_facing": [], "workflow_only": []}
|
||||
def test_classify_empty(self) -> None:
|
||||
classifier = self._make_classifier(infrastructure=[".gitea/**"])
|
||||
result = classifier.classify([])
|
||||
assert not result.has_user_facing
|
||||
assert result.user_facing == []
|
||||
assert result.infrastructure == []
|
||||
|
||||
def test_reason_is_human_readable(self) -> None:
|
||||
classifier = self._make_classifier(infrastructure=[".gitea/**"])
|
||||
fc = classifier.classify_file(".gitea/workflows/ci.yml")
|
||||
assert ".gitea/**" in fc.reason
|
||||
fc2 = classifier.classify_file("src/devx/cli.py")
|
||||
assert "default" in fc2.reason.lower() or "user-facing" in fc2.reason.lower()
|
||||
|
||||
|
||||
class TestClassificationResult:
|
||||
def test_has_user_facing(self) -> None:
|
||||
result = ClassificationResult(user_facing=["src/cli.py"])
|
||||
assert result.has_user_facing
|
||||
|
||||
def test_has_user_facing_empty(self) -> None:
|
||||
result = ClassificationResult()
|
||||
assert not result.has_user_facing
|
||||
|
||||
def test_has_tag(self) -> None:
|
||||
result = ClassificationResult(tags={"ansible": ["ansible/tasks/main.yml"]})
|
||||
assert result.has_tag("ansible")
|
||||
assert not result.has_tag("docs")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backward-compatible API tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBackwardCompatibleAPI:
|
||||
def test_is_workflow_only_with_config(self) -> None:
|
||||
"""is_workflow_only uses the config-driven classifier by default."""
|
||||
with patch.object(classify_changes_mod, "_get_classifier") as mock:
|
||||
classifier = MagicMock()
|
||||
classifier.classify_file.return_value = FileClassification(
|
||||
path=".gitea/workflows/ci.yml",
|
||||
is_user_facing=False,
|
||||
reason="test",
|
||||
matched_rule="infrastructure: .gitea/**",
|
||||
)
|
||||
mock.return_value = classifier
|
||||
assert is_workflow_only(".gitea/workflows/ci.yml") is True
|
||||
|
||||
def test_is_user_facing_with_config(self) -> None:
|
||||
with patch.object(classify_changes_mod, "_get_classifier") as mock:
|
||||
classifier = MagicMock()
|
||||
classifier.classify_file.return_value = FileClassification(
|
||||
path="src/devx/cli.py",
|
||||
is_user_facing=True,
|
||||
reason="test",
|
||||
matched_rule=None,
|
||||
)
|
||||
mock.return_value = classifier
|
||||
assert is_user_facing("src/devx/cli.py") is True
|
||||
|
||||
def test_legacy_patterns_mode(self) -> None:
|
||||
"""is_workflow_only with explicit patterns uses legacy prefix matching."""
|
||||
patterns = frozenset([".gitea/", "tests/"])
|
||||
assert is_workflow_only(".gitea/workflows/ci.yml", patterns) is True
|
||||
assert is_workflow_only("tests/test_foo.py", patterns) is True
|
||||
assert is_workflow_only("src/devx/cli.py", patterns) is False
|
||||
|
||||
def test_classify_changes_with_config(self) -> None:
|
||||
with patch.object(classify_changes_mod, "_get_classifier") as mock:
|
||||
classifier = MagicMock()
|
||||
classifier.classify.return_value = ClassificationResult(
|
||||
user_facing=["src/devx/cli.py"],
|
||||
infrastructure=[".gitea/workflows/ci.yml"],
|
||||
)
|
||||
mock.return_value = classifier
|
||||
result = classify_changes(["src/devx/cli.py", ".gitea/workflows/ci.yml"])
|
||||
assert "src/devx/cli.py" in result["user_facing"]
|
||||
assert ".gitea/workflows/ci.yml" in result["workflow_only"]
|
||||
|
||||
def test_classify_changes_legacy_mode(self) -> None:
|
||||
patterns = frozenset([".gitea/", "tests/"])
|
||||
result = classify_changes([".gitea/ci.yml", "src/cli.py"], patterns)
|
||||
assert ".gitea/ci.yml" in result["workflow_only"]
|
||||
assert "src/cli.py" in result["user_facing"]
|
||||
|
||||
def test_has_user_facing_changes_with_config(self) -> None:
|
||||
with (
|
||||
patch.object(classify_changes_mod, "get_changed_files", return_value=["src/devx/cli.py"]),
|
||||
patch.object(classify_changes_mod, "_get_classifier") as mock,
|
||||
):
|
||||
classifier = MagicMock()
|
||||
classifier.classify.return_value = ClassificationResult(
|
||||
user_facing=["src/devx/cli.py"],
|
||||
)
|
||||
mock.return_value = classifier
|
||||
assert has_user_facing_changes("v0.1.0", "HEAD") is True
|
||||
|
||||
def test_has_user_facing_changes_legacy(self) -> None:
|
||||
with patch.object(classify_changes_mod, "get_changed_files", return_value=[".gitea/ci.yml"]):
|
||||
patterns = frozenset([".gitea/"])
|
||||
assert has_user_facing_changes("v0.1.0", "HEAD", patterns) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Git helper tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetChangedFiles:
|
||||
@@ -127,23 +373,6 @@ class TestGetChangedFiles:
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestHasUserFacingChanges:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
def test_true_when_user_facing(self, mock_get: MagicMock) -> None:
|
||||
mock_get.return_value = ["src/devx/cli.py", "docs/index.md"]
|
||||
assert has_user_facing_changes("v0.1.0", "HEAD") is True
|
||||
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
def test_false_when_workflow_only(self, mock_get: MagicMock) -> None:
|
||||
mock_get.return_value = [".gitea/workflows/ci.yml", "docs/index.md"]
|
||||
assert has_user_facing_changes("v0.1.0", "HEAD") is False
|
||||
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
def test_false_when_no_changes(self, mock_get: MagicMock) -> None:
|
||||
mock_get.return_value = []
|
||||
assert has_user_facing_changes("v0.1.0", "HEAD") is False
|
||||
|
||||
|
||||
class TestGetLatestTag:
|
||||
@patch("subprocess.run")
|
||||
def test_returns_tag(self, mock_run: MagicMock) -> None:
|
||||
@@ -170,6 +399,11 @@ class TestRunGit:
|
||||
run_git(["git", "bad-command"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="")
|
||||
def test_no_tags_outputs_true(self, mock_tag: MagicMock) -> None:
|
||||
@@ -206,7 +440,6 @@ class TestMain:
|
||||
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="")
|
||||
def test_no_tags_non_quiet(self, mock_tag: MagicMock) -> None:
|
||||
"""Non-quiet mode with no tags prints user-facing message."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
assert result.exit_code == 0
|
||||
@@ -215,7 +448,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files", return_value=[])
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_no_changes_non_quiet(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""Non-quiet mode with no changes prints message."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
assert result.exit_code == 0
|
||||
@@ -224,7 +456,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_quiet_user_facing(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""Quiet mode with user-facing changes outputs true."""
|
||||
mock_changes.return_value = ["src/devx/cli.py"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--quiet"])
|
||||
@@ -234,7 +465,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_quiet_workflow_only(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""Quiet mode with workflow-only changes outputs false."""
|
||||
mock_changes.return_value = [".gitea/workflows/ci.yml"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--quiet"])
|
||||
@@ -244,7 +474,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_with_explicit_base(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""Explicit --base overrides latest tag."""
|
||||
mock_changes.return_value = ["src/devx/cli.py"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--base", "v0.2.0", "--head", "HEAD"])
|
||||
@@ -254,7 +483,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_ansible_true(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check ansible with Ansible changes outputs true."""
|
||||
mock_changes.return_value = ["ansible/tasks/main.yml", ".gitea/workflows/ci.yml"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "ansible", "--quiet"])
|
||||
@@ -264,7 +492,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_ansible_false(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check ansible with no Ansible changes outputs false."""
|
||||
mock_changes.return_value = ["src/devx/cli.py", ".gitea/workflows/ci.yml"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "ansible", "--quiet"])
|
||||
@@ -274,7 +501,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_user_facing_true(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check user-facing with user-facing changes outputs true."""
|
||||
mock_changes.return_value = ["src/devx/cli.py", ".gitea/workflows/ci.yml"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "user-facing", "--quiet"])
|
||||
@@ -284,7 +510,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_user_facing_false(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check user-facing with only workflow changes outputs false."""
|
||||
mock_changes.return_value = [".gitea/workflows/ci.yml", "tests/test_foo.py"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "user-facing", "--quiet"])
|
||||
@@ -294,7 +519,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_ansible_non_quiet(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check ansible in non-quiet mode prints file list."""
|
||||
mock_changes.return_value = ["ansible/tasks/main.yml"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "ansible"])
|
||||
@@ -304,7 +528,6 @@ class TestMain:
|
||||
@patch("devx.ci.classify_changes.get_changed_files")
|
||||
@patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
||||
def test_check_user_facing_non_quiet(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
||||
"""--check user-facing in non-quiet mode prints file list."""
|
||||
mock_changes.return_value = ["src/devx/cli.py"]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--check", "user-facing"])
|
||||
|
||||
Reference in New Issue
Block a user