Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,19 @@ jobs:
run: uv run mypy src/

- name: Tests with coverage
run: uv run pytest --cov=src --cov-report=term-missing --cov-fail-under=100
run: uv run pytest -m "" --cov=src --cov-branch --cov-report=term-missing --cov-report=xml --cov-fail-under=100 --junitxml=junit.xml -o junit_family=legacy

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml

- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}

build:
needs: check
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# snapjaw: Vanilla World of Warcraft AddOn manager
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![CodeQL](https://github.com/refaim/snapjaw/actions/workflows/codeql.yml/badge.svg?branch=master)](https://github.com/refaim/snapjaw/actions/workflows/codeql.yml) [![Package](https://github.com/refaim/snapjaw/actions/workflows/package.yml/badge.svg)](https://github.com/refaim/snapjaw/actions/workflows/package.yml)
[![CI](https://github.com/refaim/snapjaw/actions/workflows/ci.yml/badge.svg)](https://github.com/refaim/snapjaw/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/refaim/snapjaw/graph/badge.svg)](https://codecov.io/gh/refaim/snapjaw)
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![GitHub release](https://img.shields.io/github/v/release/refaim/snapjaw)](https://github.com/refaim/snapjaw/releases/latest)

## Features
- Support for Git repositories as addon sources
Expand Down Expand Up @@ -70,5 +74,5 @@ snapjaw update ShaguTweaks
```

## Requirements for developers
- [Python 3.10](https://www.python.org)
- [Python 3.12](https://www.python.org)
- [uv](https://docs.astral.sh/uv/)
12 changes: 7 additions & 5 deletions src/snapjaw.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ def cmd_install(config: Config, args):
author = path.pop(0)
repository = path.pop(0)
if path:
if path[0] == "-" and path[1] == "tree":
path = path[2:]
if path[0] == "-" and len(path) > 1 and path[1] == "tree":
branch_from_url = "/".join(path[2:])
elif path[0] == "tree":
path = path[1:]
branch_from_url = "/".join(path)
branch_from_url = "/".join(path[1:])
# Ignore non-tree paths (blob, commits, etc.)
path_string = "/".join([author, repository])
if not path_string.endswith(".git"):
path_string += ".git"
Expand Down Expand Up @@ -318,7 +318,9 @@ def format_dt(dt: datetime | None) -> str:
if not args.verbose:
num_updated = Counter(s.status for s in addon_states)[AddonStatus.UpToDate]
if num_updated > 0:
msg = f"{num_updated}{' other' if table else ''} addons are up to date"
other = " other" if table else ""
noun = "addon is" if num_updated == 1 else "addons are"
msg = f"{num_updated}{other} {noun} up to date"
print(cr.Fore.GREEN + msg + cr.Fore.RESET)
cr.deinit()

Expand Down
86 changes: 85 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Shared fixtures for snapjaw tests."""

from datetime import datetime
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

import pygit2
import pytest
Expand Down Expand Up @@ -78,6 +78,37 @@ def _make_toc_addon(name, interface_version):
return _make_toc_addon


@pytest.fixture
def mock_tmpdir_context():
"""Context manager patch for mocking TemporaryDirectory."""
mock = MagicMock()
mock.__enter__ = MagicMock(return_value="/tmp/repo")
mock.__exit__ = MagicMock(return_value=False)
return patch("mygit.TemporaryDirectory", return_value=mock)


@pytest.fixture
def fetch_states_patches(mock_pygit2_repo, mock_tmpdir_context):
"""Combined patches for fetch_states tests.

Returns a context manager with mocked pygit2 repo, GitError, sha1, and tmpdir.
The sha1 mock is configured to return "abc123" as hexdigest.
"""
from contextlib import ExitStack, contextmanager

@contextmanager
def _patches():
with ExitStack() as stack:
stack.enter_context(patch("mygit.pygit2.init_repository", return_value=mock_pygit2_repo))
stack.enter_context(patch("mygit.pygit2.GitError", pygit2.GitError))
mock_sha1 = stack.enter_context(patch("mygit.sha1"))
mock_sha1.return_value.hexdigest.return_value = "abc123"
stack.enter_context(mock_tmpdir_context)
yield

return _patches


@pytest.fixture
def mock_pygit2_repo():
"""Create a mocked pygit2.Repository."""
Expand All @@ -103,3 +134,56 @@ def _mock_remote(name, url, refs=None, error=None):
return remote

return _mock_remote


@pytest.fixture
def mock_install_env(tmp_path, monkeypatch, fixed_now):
"""Setup mocked environment for install_addon tests.

Returns a context manager that sets up repo_dir, mocks clone/TemporaryDirectory/signature,
and provides addons_dir and config.
"""
from contextlib import contextmanager

from mygit import RepositoryInfo
from snapjaw import Config

@contextmanager
def _setup(*, trailing_slash=True):
addons_dir = tmp_path / "Addons"
addons_dir.mkdir(exist_ok=True)

repo_dir = tmp_path / "repo"
repo_dir.mkdir(exist_ok=True)

workdir = str(repo_dir) + ("/" if trailing_slash else "")

repo_info = RepositoryInfo(
workdir=workdir,
branch="master",
head_commit_hex="abc123",
head_commit_time=fixed_now,
)
monkeypatch.setattr("snapjaw.mygit.clone", lambda url, branch, path: repo_info)

mock_tmpdir = MagicMock()
mock_tmpdir.__enter__ = MagicMock(return_value=str(repo_dir))
mock_tmpdir.__exit__ = MagicMock(return_value=False)
monkeypatch.setattr("snapjaw.TemporaryDirectory", lambda: mock_tmpdir)
monkeypatch.setattr("snapjaw.signature.calculate", lambda path: "sig|2")

config = Config(addons_by_key={})
config._loaded_from = str(tmp_path / "snapjaw.json")

class Env:
pass

env = Env()
env.addons_dir = addons_dir
env.repo_dir = repo_dir
env.config = config
env.repo_info = repo_info

yield env

return _setup
Loading
Loading