|
5 | 5 | import re |
6 | 6 | import shutil |
7 | 7 | import sys |
| 8 | +from dataclasses import dataclass |
8 | 9 | from pathlib import Path |
9 | 10 | from typing import Protocol, cast |
10 | 11 |
|
@@ -43,6 +44,119 @@ def _load_fixer() -> FixerModule: |
43 | 44 |
|
44 | 45 | fixer = _load_fixer() |
45 | 46 |
|
| 47 | +CASE_ROOT = Path(__file__).with_name("fixtures") / "dependabot-grpc-fixer" |
| 48 | + |
| 49 | + |
| 50 | +@dataclass(frozen=True) |
| 51 | +class Case: |
| 52 | + """A single file-based test case.""" |
| 53 | + |
| 54 | + path: Path |
| 55 | + stdout_exact: str | None |
| 56 | + stdout_contains: str | None |
| 57 | + stderr_exact: str | None |
| 58 | + stderr_contains: str | None |
| 59 | + |
| 60 | + @property |
| 61 | + def expected_pyproject(self) -> Path: |
| 62 | + """Return the expected `pyproject.toml` path for the case.""" |
| 63 | + return self.path / "expected" / "pyproject.toml" |
| 64 | + |
| 65 | + |
| 66 | +def _case_dirs() -> list[Path]: |
| 67 | + """Return all fixture case directories.""" |
| 68 | + case_dirs = sorted(path for path in CASE_ROOT.iterdir() if path.is_dir()) |
| 69 | + if not case_dirs: |
| 70 | + raise AssertionError(f"no fixture cases found in {CASE_ROOT}") |
| 71 | + return case_dirs |
| 72 | + |
| 73 | + |
| 74 | +def _read_optional(path: Path) -> str | None: |
| 75 | + """Read a file if it exists.""" |
| 76 | + if path.exists(): |
| 77 | + return path.read_text(encoding="utf-8") |
| 78 | + return None |
| 79 | + |
| 80 | + |
| 81 | +def _read_contains(path: Path) -> str | None: |
| 82 | + """Read a partial-match expectation if it exists.""" |
| 83 | + if path.exists(): |
| 84 | + return path.read_text(encoding="utf-8").rstrip("\n") |
| 85 | + return None |
| 86 | + |
| 87 | + |
| 88 | +def _load_case(case_dir: Path) -> Case: |
| 89 | + """Load a case and its optional output expectations.""" |
| 90 | + expected_dir = case_dir / "expected" |
| 91 | + return Case( |
| 92 | + path=case_dir, |
| 93 | + stdout_exact=_read_optional(expected_dir / "stdout.txt"), |
| 94 | + stdout_contains=_read_contains(expected_dir / "stdout.contains.txt"), |
| 95 | + stderr_exact=_read_optional(expected_dir / "stderr.txt"), |
| 96 | + stderr_contains=_read_contains(expected_dir / "stderr.contains.txt"), |
| 97 | + ) |
| 98 | + |
| 99 | + |
| 100 | +def _prepare_workspace( |
| 101 | + case: Case, |
| 102 | + tmp_path: Path, |
| 103 | + monkeypatch: pytest.MonkeyPatch, |
| 104 | +) -> Path: |
| 105 | + """Copy fixture inputs into a temp workspace and set env vars.""" |
| 106 | + input_dir = case.path / "input" |
| 107 | + input_pyproject = input_dir / "pyproject.toml" |
| 108 | + if input_pyproject.exists(): |
| 109 | + shutil.copyfile(input_pyproject, tmp_path / fixer.PYPROJECT.name) |
| 110 | + monkeypatch.chdir(tmp_path) |
| 111 | + monkeypatch.setattr(sys, "argv", [str(FIXER_PATH)]) |
| 112 | + input_metadata = input_dir / "dependabot-metadata.json" |
| 113 | + monkeypatch.setenv( |
| 114 | + "UPDATED_DEPENDENCIES_JSON", |
| 115 | + input_metadata.read_text(encoding="utf-8") if input_metadata.exists() else "", |
| 116 | + ) |
| 117 | + return tmp_path / fixer.PYPROJECT.name |
| 118 | + |
| 119 | + |
| 120 | +@pytest.mark.parametrize( |
| 121 | + "case", map(_load_case, _case_dirs()), ids=lambda case: case.path.name |
| 122 | +) |
| 123 | +def test_fixer_cases( |
| 124 | + case: Case, |
| 125 | + tmp_path: Path, |
| 126 | + monkeypatch: pytest.MonkeyPatch, |
| 127 | + capsys: pytest.CaptureFixture[str], |
| 128 | +) -> None: |
| 129 | + """Apply each case and compare the rewritten files and output.""" |
| 130 | + workspace = _prepare_workspace(case, tmp_path, monkeypatch) |
| 131 | + if case.stderr_exact is not None or case.stderr_contains is not None: |
| 132 | + with pytest.raises(SystemExit) as excinfo: |
| 133 | + fixer.main() |
| 134 | + assert excinfo.value.code == 1 |
| 135 | + else: |
| 136 | + fixer.main() |
| 137 | + |
| 138 | + captured = capsys.readouterr() |
| 139 | + |
| 140 | + if case.stdout_exact is not None: |
| 141 | + assert captured.out == case.stdout_exact |
| 142 | + elif case.stdout_contains is not None: |
| 143 | + assert case.stdout_contains in captured.out |
| 144 | + else: |
| 145 | + assert captured.out == "" |
| 146 | + |
| 147 | + if case.stderr_exact is not None: |
| 148 | + assert captured.err == case.stderr_exact |
| 149 | + elif case.stderr_contains is not None: |
| 150 | + assert case.stderr_contains in captured.err |
| 151 | + else: |
| 152 | + assert captured.err == "" |
| 153 | + |
| 154 | + expected_pyproject = case.expected_pyproject |
| 155 | + if expected_pyproject.exists(): |
| 156 | + assert workspace.read_text(encoding="utf-8") == expected_pyproject.read_text( |
| 157 | + encoding="utf-8" |
| 158 | + ) |
| 159 | + |
46 | 160 |
|
47 | 161 | @pytest.mark.parametrize( |
48 | 162 | ("dependency", "version", "original", "expected"), |
|
0 commit comments