diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 272a951..5eaaadc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,9 +48,11 @@ jobs: - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: junit.xml build: needs: check diff --git a/pyproject.toml b/pyproject.toml index f2ba209..3a2de18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ markers = [ "slow: mark test as slow", ] addopts = "-m 'not integration'" +filterwarnings = ["error"] [dependency-groups] dev = [ diff --git a/src/mygit.py b/src/mygit.py index 3903713..3c2a782 100644 --- a/src/mygit.py +++ b/src/mygit.py @@ -111,7 +111,7 @@ class RemoteState: @dataclass class _RemoteLsResult: remote: pygit2.Remote - refs: list[dict] + refs: list error: str | None @@ -127,7 +127,7 @@ def fetch_states(requests: list[RemoteStateRequest]) -> Iterator[RemoteState]: def ls(remote: pygit2.Remote) -> _RemoteLsResult: try: - refs = remote.ls_remotes() + refs = remote.list_heads() error = None except pygit2.GitError as exception: refs = [] @@ -148,9 +148,9 @@ def ls(remote: pygit2.Remote) -> _RemoteLsResult: else: branch_ref = f"refs/heads/{branch}" for ref in ls_result.refs: - is_head = ref["name"] == "HEAD" and ref["symref_target"] == branch_ref - if is_head or ref["name"] == branch_ref: - yield RemoteState(url, branch, str(ref["oid"]), None) + is_head = ref.name == "HEAD" and ref.symref_target == branch_ref + if is_head or ref.name == branch_ref: + yield RemoteState(url, branch, str(ref.oid), None) break diff --git a/src/snapjaw.py b/src/snapjaw.py index 5c6823b..47b14dd 100644 --- a/src/snapjaw.py +++ b/src/snapjaw.py @@ -358,12 +358,15 @@ def get_addon_states(config: Config, addons_dir: str) -> list[AddonState]: processed += 1 print(f"{processed}/{total_addons}", end="\r") comment = None + addon_dir = os.path.join(addons_dir, addon.name) if state.error is not None: status = AddonStatus.Error comment = state.error + elif not os.path.isdir(addon_dir): + status = AddonStatus.Missing elif state.head_commit_hex is None: status = AddonStatus.Unknown - elif addon.checksum is None or not signature.validate(os.path.join(addons_dir, addon.name), addon.checksum): + elif addon.checksum is None or not signature.validate(addon_dir, addon.checksum): status = AddonStatus.Modified elif state.head_commit_hex == addon.commit: status = AddonStatus.UpToDate diff --git a/tests/conftest.py b/tests/conftest.py index d528f8b..94cf68f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,9 +128,9 @@ def _mock_remote(name, url, refs=None, error=None): remote.name = name remote.url = url if error: - remote.ls_remotes.side_effect = pygit2.GitError(error) + remote.list_heads.side_effect = pygit2.GitError(error) else: - remote.ls_remotes.return_value = refs or [] + remote.list_heads.return_value = refs or [] return remote return _mock_remote diff --git a/tests/test_mygit.py b/tests/test_mygit.py index 2b8802d..df3a2a2 100644 --- a/tests/test_mygit.py +++ b/tests/test_mygit.py @@ -75,7 +75,7 @@ def test_success_returns_commit_hash(self, mock_remote, mock_pygit2_repo, fetch_ remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "refs/heads/master", "symref_target": "", "oid": "deadbeef"}], + refs=[SimpleNamespace(name="refs/heads/master", symref_target="", oid="deadbeef")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote])) @@ -92,7 +92,7 @@ def test_head_symref_resolves_branch(self, mock_remote, mock_pygit2_repo, fetch_ remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "HEAD", "symref_target": "refs/heads/main", "oid": "cafebabe"}], + refs=[SimpleNamespace(name="HEAD", symref_target="refs/heads/main", oid="cafebabe")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote])) @@ -137,7 +137,7 @@ def test_branch_not_found_in_refs(self, mock_remote, mock_pygit2_repo, fetch_sta remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "refs/heads/other-branch", "symref_target": "", "oid": "deadbeef"}], + refs=[SimpleNamespace(name="refs/heads/other-branch", symref_target="", oid="deadbeef")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote])) diff --git a/tests/test_snapjaw_states.py b/tests/test_snapjaw_states.py index 1a1996b..f8366b0 100644 --- a/tests/test_snapjaw_states.py +++ b/tests/test_snapjaw_states.py @@ -107,6 +107,9 @@ def test_error(self, tmp_path, monkeypatch, make_addon, capsys): def test_unknown(self, tmp_path, monkeypatch, make_addon, capsys): """No commit and no error sets Unknown status.""" + addon_dir = tmp_path / "TestAddon" + addon_dir.mkdir() + addon = make_addon() config = Config(addons_by_key={"testaddon": addon}) @@ -154,6 +157,29 @@ def test_missing(self, tmp_path, monkeypatch, make_addon, capsys): assert states[0].addon == "MyMissingAddon" capsys.readouterr() + def test_missing_directory_with_remote_state(self, tmp_path, monkeypatch, make_addon, capsys): + """Addon in config with valid remote state but missing directory should be Missing, not crash. + + Reproduces https://github.com/.../issues/47: when an addon directory is renamed + (e.g. adding '---' suffix), signature.validate raises ValueError instead of + gracefully reporting the addon as missing. + """ + # Addon is in config with a checksum, but its directory does NOT exist on disk + addon = make_addon(checksum="sig|2") + config = Config(addons_by_key={"testaddon": addon}) + + # Remote returns a valid state (so the code enters the signature.validate branch) + monkeypatch.setattr( + "snapjaw.mygit.fetch_states", + lambda reqs: iter([RemoteState("https://github.com/test/test.git", "master", "abc123", None)]), + ) + # Do NOT mock signature.validate — use the real one to reproduce the ValueError + + states = get_addon_states(config, str(tmp_path)) + assert len(states) == 1 + assert states[0].status == AddonStatus.Missing + capsys.readouterr() + def test_multiple_addons_different_statuses(self, tmp_path, monkeypatch, make_addon, capsys): """Multiple addons with different statuses are correctly detected.""" # Create addon directories with content