242 lines
10 KiB
Python
242 lines
10 KiB
Python
"""Unit tests for scripts/ci/classify_changes.py."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import click
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from scripts.ci.classify_changes import (
|
|
classify_changes,
|
|
get_changed_files,
|
|
get_latest_tag,
|
|
has_user_facing_changes,
|
|
is_user_facing,
|
|
is_workflow_only,
|
|
main,
|
|
run_git,
|
|
)
|
|
|
|
|
|
class TestIsUserFacing:
|
|
def test_src_is_user_facing(self) -> None:
|
|
assert is_user_facing("src/gitea_runner_manager/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
|
|
|
|
def test_pyproject_is_user_facing(self) -> None:
|
|
assert is_user_facing("pyproject.toml") is True
|
|
|
|
def test_workflow_is_not_user_facing(self) -> None:
|
|
assert is_user_facing(".gitea/workflows/ci.yml") is False
|
|
|
|
def test_ci_scripts_are_not_user_facing(self) -> None:
|
|
assert is_user_facing("scripts/ci/release.py") is False
|
|
|
|
def test_dev_scripts_are_user_facing(self) -> None:
|
|
"""Dev scripts (check_test_speed, configure_repo) are NOT in the
|
|
workflow-only allowlist, so they default to user-facing."""
|
|
assert is_user_facing("scripts/check_test_speed.py") is True
|
|
assert is_user_facing("scripts/configure_repo.py") is True
|
|
assert is_user_facing("scripts/install_checkmake.py") is True
|
|
|
|
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_scripts_init_is_not_user_facing(self) -> None:
|
|
assert is_user_facing("scripts/__init__.py") is False
|
|
|
|
def test_docs_are_not_user_facing(self) -> None:
|
|
assert is_user_facing("docs/user/getting-started.md") is False
|
|
|
|
def test_tests_are_not_user_facing(self) -> None:
|
|
assert is_user_facing("tests/unit/test_cli.py") is False
|
|
|
|
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
|
|
|
|
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
|
|
|
|
def test_is_workflow_only_inverse(self) -> None:
|
|
assert is_workflow_only(".gitea/workflows/ci.yml") is True
|
|
assert is_workflow_only("src/gitea_runner_manager/cli.py") is False
|
|
assert is_workflow_only("pyproject.toml") is False
|
|
|
|
|
|
class TestClassifyChanges:
|
|
def test_all_user_facing(self) -> None:
|
|
files = ["src/gitea_runner_manager/cli.py", "ansible/roles/gitea-runner/tasks/main.yml"]
|
|
result = classify_changes(files)
|
|
assert result["user_facing"] == files
|
|
assert result["workflow_only"] == []
|
|
|
|
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_mixed(self) -> None:
|
|
files = [
|
|
"src/gitea_runner_manager/cli.py",
|
|
".gitea/workflows/ci.yml",
|
|
"pyproject.toml",
|
|
"docs/index.md",
|
|
]
|
|
result = classify_changes(files)
|
|
assert "src/gitea_runner_manager/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"]
|
|
|
|
def test_empty(self) -> None:
|
|
result = classify_changes([])
|
|
assert result == {"user_facing": [], "workflow_only": []}
|
|
|
|
|
|
class TestGetChangedFiles:
|
|
@patch("scripts.ci.classify_changes.run_git")
|
|
def test_returns_file_list(self, mock_run_git: MagicMock) -> None:
|
|
mock_run_git.return_value = "file1.py\nfile2.py\nfile3.md"
|
|
result = get_changed_files("v0.1.0", "HEAD")
|
|
assert result == ["file1.py", "file2.py", "file3.md"]
|
|
|
|
@patch("scripts.ci.classify_changes.run_git")
|
|
def test_empty_when_no_changes(self, mock_run_git: MagicMock) -> None:
|
|
mock_run_git.return_value = ""
|
|
result = get_changed_files("v0.1.0", "HEAD")
|
|
assert result == []
|
|
|
|
|
|
class TestHasUserFacingChanges:
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
def test_true_when_user_facing(self, mock_get: MagicMock) -> None:
|
|
mock_get.return_value = ["src/gitea_runner_manager/cli.py", "docs/index.md"]
|
|
assert has_user_facing_changes("v0.1.0", "HEAD") is True
|
|
|
|
@patch("scripts.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("scripts.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:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="v0.3.0\n", stderr="")
|
|
assert get_latest_tag() == "v0.3.0"
|
|
|
|
@patch("subprocess.run")
|
|
def test_returns_empty_when_no_tags(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error")
|
|
assert get_latest_tag() == ""
|
|
|
|
|
|
class TestRunGit:
|
|
@patch("scripts.ci.classify_changes.subprocess.run")
|
|
def test_success(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="file1.py\n", stderr="")
|
|
result = run_git(["git", "diff", "--name-only", "v0.1.0", "HEAD"])
|
|
assert result == "file1.py"
|
|
|
|
@patch("scripts.ci.classify_changes.subprocess.run")
|
|
def test_failure_raises(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="git error")
|
|
with pytest.raises(click.ClickException):
|
|
run_git(["git", "bad-command"])
|
|
|
|
|
|
class TestMain:
|
|
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="")
|
|
def test_no_tags_outputs_true(self, mock_tag: MagicMock) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["--quiet"])
|
|
assert result.exit_code == 0
|
|
assert "true" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files", return_value=[])
|
|
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
|
def test_no_changes_outputs_false(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["--quiet"])
|
|
assert result.exit_code == 0
|
|
assert "false" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
|
def test_workflow_only_exits_2(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
|
mock_changes.return_value = [".gitea/workflows/ci.yml", "docs/index.md"]
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [])
|
|
assert result.exit_code == 2
|
|
assert "no release needed" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
|
|
def test_user_facing_exits_0(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
|
|
mock_changes.return_value = ["src/gitea_runner_manager/cli.py", "docs/index.md"]
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [])
|
|
assert result.exit_code == 0
|
|
assert "release needed" in result.output
|
|
|
|
@patch("scripts.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
|
|
assert "No tags found" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files", return_value=[])
|
|
@patch("scripts.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
|
|
assert "No changes" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
@patch("scripts.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/gitea_runner_manager/cli.py"]
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["--quiet"])
|
|
assert result.exit_code == 0
|
|
assert "true" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
@patch("scripts.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"])
|
|
assert result.exit_code == 0
|
|
assert "false" in result.output
|
|
|
|
@patch("scripts.ci.classify_changes.get_changed_files")
|
|
@patch("scripts.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/gitea_runner_manager/cli.py"]
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["--base", "v0.2.0", "--head", "HEAD"])
|
|
assert result.exit_code == 0
|
|
assert "release needed" in result.output
|