Add tests.

This commit is contained in:
Edward Betts 2026-02-01 14:02:53 +00:00
parent e80f511155
commit a7d4bc4ae9
10 changed files with 915 additions and 418 deletions

0
tests/__init__.py Normal file
View file

120
tests/test_cache.py Normal file
View file

@ -0,0 +1,120 @@
"""Tests for cache loading and saving."""
import json
import pytest
from pathlib import Path
import debian_todo
class TestLoadCache:
"""Tests for load_cache function."""
def test_returns_none_when_file_missing(self, tmp_path, monkeypatch):
cache_file = tmp_path / "cache.json"
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([])
assert result is None
def test_returns_none_on_invalid_json(self, tmp_path, monkeypatch):
cache_file = tmp_path / "cache.json"
cache_file.write_text("not valid json")
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([])
assert result is None
def test_returns_none_on_version_mismatch(self, tmp_path, monkeypatch):
cache_file = tmp_path / "cache.json"
cache_file.write_text(json.dumps({
"cache_version": debian_todo.CACHE_VERSION - 1,
"sources_mtimes": {},
"vcs_by_source": {},
}))
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([])
assert result is None
def test_returns_none_when_mtime_mismatch(self, tmp_path, monkeypatch):
source_file = tmp_path / "Sources"
source_file.write_text("dummy")
cache_file = tmp_path / "cache.json"
cache_file.write_text(json.dumps({
"cache_version": debian_todo.CACHE_VERSION,
"sources_mtimes": {str(source_file): 0.0},
"vcs_by_source": {},
}))
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([str(source_file)])
assert result is None
def test_returns_cache_when_valid(self, tmp_path, monkeypatch):
import os
source_file = tmp_path / "Sources"
source_file.write_text("dummy")
mtime = os.path.getmtime(str(source_file))
cache_file = tmp_path / "cache.json"
cache_file.write_text(json.dumps({
"cache_version": debian_todo.CACHE_VERSION,
"sources_mtimes": {str(source_file): mtime},
"vcs_by_source": {
"foo": {"vcs_git": "python-team", "uploaders": "John <j@e.com>"}
},
}))
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([str(source_file)])
assert result == {"foo": {"vcs_git": "python-team", "uploaders": "John <j@e.com>"}}
def test_normalizes_legacy_string_format(self, tmp_path, monkeypatch):
import os
source_file = tmp_path / "Sources"
source_file.write_text("dummy")
mtime = os.path.getmtime(str(source_file))
cache_file = tmp_path / "cache.json"
cache_file.write_text(json.dumps({
"cache_version": debian_todo.CACHE_VERSION,
"sources_mtimes": {str(source_file): mtime},
"vcs_by_source": {"foo": "python-team"}, # legacy string format
}))
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
result = debian_todo.load_cache([str(source_file)])
assert result == {"foo": {"vcs_git": "python-team", "uploaders": ""}}
class TestSaveCache:
"""Tests for save_cache function."""
def test_saves_cache_file(self, tmp_path, monkeypatch):
import os
source_file = tmp_path / "Sources"
source_file.write_text("dummy")
cache_file = tmp_path / "cache.json"
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
vcs_by_source = {"foo": {"vcs_git": "python-team", "uploaders": ""}}
debian_todo.save_cache([str(source_file)], vcs_by_source)
assert cache_file.exists()
data = json.loads(cache_file.read_text())
assert data["cache_version"] == debian_todo.CACHE_VERSION
assert data["vcs_by_source"] == vcs_by_source
assert str(source_file) in data["sources_mtimes"]
def test_handles_missing_source_file(self, tmp_path, monkeypatch):
cache_file = tmp_path / "cache.json"
monkeypatch.setattr(debian_todo, "CACHE_PATH", cache_file)
debian_todo.save_cache(["/nonexistent/file"], {})
# Should not create cache file if source file is missing
assert not cache_file.exists()

117
tests/test_filter.py Normal file
View file

@ -0,0 +1,117 @@
"""Tests for TODO list filtering functions."""
import pytest
from debian_todo import filter_todo_list, summarize_sources
class TestFilterTodoList:
"""Tests for filter_todo_list function."""
def test_filters_non_upstream_items(self):
todo_list = [
{":shortname": "newupstream_foo", ":details": "1.0.0", ":source": "foo"},
{":shortname": "rc_bug", ":details": "bug info", ":source": "bar"},
{":shortname": "other_type", ":details": "other", ":source": "baz"},
]
result = filter_todo_list(todo_list)
assert len(result) == 1
assert result[0][":source"] == "foo"
def test_filters_prerelease_by_default(self):
todo_list = [
{":shortname": "newupstream_foo", ":details": "1.0.0", ":source": "foo"},
{":shortname": "newupstream_bar", ":details": "2.0.0rc1", ":source": "bar"},
]
result = filter_todo_list(todo_list)
assert len(result) == 1
assert result[0][":source"] == "foo"
def test_includes_prerelease_when_requested(self):
todo_list = [
{":shortname": "newupstream_foo", ":details": "1.0.0", ":source": "foo"},
{":shortname": "newupstream_bar", ":details": "2.0.0rc1", ":source": "bar"},
]
result = filter_todo_list(todo_list, include_prerelease=True)
assert len(result) == 2
def test_filters_matching_versions(self):
# When normalized versions match, the item should be filtered out
todo_list = [
{
":shortname": "newupstream_foo",
":details": "1.0.0 (currently in unstable: 1.0.0-1)",
":source": "foo",
},
]
result = filter_todo_list(todo_list)
assert len(result) == 0
def test_keeps_different_versions(self):
todo_list = [
{
":shortname": "newupstream_foo",
":details": "1.1.0 (currently in unstable: 1.0.0-1)",
":source": "foo",
},
]
result = filter_todo_list(todo_list)
assert len(result) == 1
def test_handles_epoch_in_current(self):
# Epoch should be stripped for comparison
todo_list = [
{
":shortname": "newupstream_foo",
":details": "1.0.0 (currently in unstable: 1:1.0.0-1)",
":source": "foo",
},
]
result = filter_todo_list(todo_list)
assert len(result) == 0
def test_skips_invalid_items(self):
todo_list = [
{":shortname": "newupstream_foo", ":source": "foo"}, # missing :details
{":details": "1.0.0", ":source": "bar"}, # missing :shortname
{":shortname": 123, ":details": "1.0.0", ":source": "baz"}, # wrong type
]
result = filter_todo_list(todo_list)
assert len(result) == 0
def test_empty_list(self):
assert filter_todo_list([]) == []
class TestSummarizeSources:
"""Tests for summarize_sources function."""
def test_extracts_sources(self):
todo_list = [
{":source": "foo", ":shortname": "newupstream_foo"},
{":source": "bar", ":shortname": "newupstream_bar"},
{":source": "baz", ":shortname": "rc_bug"},
]
result = summarize_sources(todo_list)
assert result == {"foo", "bar", "baz"}
def test_deduplicates_sources(self):
todo_list = [
{":source": "foo", ":shortname": "type1"},
{":source": "foo", ":shortname": "type2"},
]
result = summarize_sources(todo_list)
assert result == {"foo"}
def test_skips_non_string_sources(self):
todo_list = [
{":source": "foo"},
{":source": 123},
{":source": None},
{},
]
result = summarize_sources(todo_list)
assert result == {"foo"}
def test_empty_list(self):
assert summarize_sources([]) == set()

57
tests/test_notes.py Normal file
View file

@ -0,0 +1,57 @@
"""Tests for notes loading."""
import pytest
from pathlib import Path
import debian_todo
class TestLoadNotes:
"""Tests for load_notes function."""
def test_loads_notes_from_file(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
notes_file.write_text("foo some note\nbar another note\n")
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {"foo": "some note", "bar": "another note"}
def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {}
def test_handles_package_without_note(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
notes_file.write_text("foo\n")
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {"foo": ""}
def test_joins_multiple_notes(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
notes_file.write_text("foo first note\nfoo second note\n")
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {"foo": "first note; second note"}
def test_skips_blank_lines(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
notes_file.write_text("foo note one\n\nbar note two\n\n")
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {"foo": "note one", "bar": "note two"}
def test_handles_multiword_notes(self, tmp_path, monkeypatch):
notes_file = tmp_path / "notes"
notes_file.write_text("foo this is a long note with many words\n")
monkeypatch.setattr(debian_todo, "NOTES_PATH", notes_file)
result = debian_todo.load_notes()
assert result == {"foo": "this is a long note with many words"}

71
tests/test_vcs.py Normal file
View file

@ -0,0 +1,71 @@
"""Tests for VCS and uploader related functions."""
import pytest
from debian_todo import vcs_git_to_team, normalize_uploaders, HIDE_UPLOADER
class TestVcsGitToTeam:
"""Tests for vcs_git_to_team function."""
def test_salsa_python_team(self):
url = "https://salsa.debian.org/python-team/packages/python-foo.git"
assert vcs_git_to_team(url) == "python-team"
def test_salsa_homeassistant_team(self):
url = "https://salsa.debian.org/homeassistant-team/python-bar.git"
assert vcs_git_to_team(url) == "homeassistant-team"
def test_salsa_personal_repo(self):
url = "https://salsa.debian.org/username/package.git"
assert vcs_git_to_team(url) == "username"
def test_non_salsa_url(self):
url = "https://github.com/user/repo.git"
assert vcs_git_to_team(url) == url
def test_none_input(self):
assert vcs_git_to_team(None) is None
def test_empty_string(self):
assert vcs_git_to_team("") is None
def test_salsa_with_branch(self):
url = "https://salsa.debian.org/python-team/packages/foo.git -b debian/main"
assert vcs_git_to_team(url) == "python-team"
class TestNormalizeUploaders:
"""Tests for normalize_uploaders function."""
def test_single_uploader(self):
uploaders = "John Doe <john@example.com>"
assert normalize_uploaders(uploaders) == "John Doe <john@example.com>"
def test_multiple_uploaders(self):
uploaders = "John Doe <john@example.com>, Jane Doe <jane@example.com>"
result = normalize_uploaders(uploaders)
assert result == "John Doe <john@example.com>\nJane Doe <jane@example.com>"
def test_hides_configured_uploader(self):
uploaders = f"John Doe <john@example.com>, {HIDE_UPLOADER}"
result = normalize_uploaders(uploaders)
assert result == "John Doe <john@example.com>"
assert HIDE_UPLOADER not in result
def test_only_hidden_uploader(self):
uploaders = HIDE_UPLOADER
assert normalize_uploaders(uploaders) == ""
def test_strips_whitespace(self):
uploaders = " John Doe <john@example.com> , Jane Doe <jane@example.com> "
result = normalize_uploaders(uploaders)
assert result == "John Doe <john@example.com>\nJane Doe <jane@example.com>"
def test_empty_string(self):
assert normalize_uploaders("") == ""
def test_extra_commas(self):
uploaders = "John Doe <john@example.com>,, Jane Doe <jane@example.com>,"
result = normalize_uploaders(uploaders)
assert result == "John Doe <john@example.com>\nJane Doe <jane@example.com>"

104
tests/test_version.py Normal file
View file

@ -0,0 +1,104 @@
"""Tests for version parsing and normalization functions."""
import pytest
from debian_todo import (
parse_details,
is_prerelease_version,
normalize_upstream_version,
)
class TestParseDetails:
"""Tests for parse_details function."""
def test_with_current_version(self):
details = "1.2.3 (currently in unstable: 1.2.2-1)"
new, current = parse_details(details)
assert new == "1.2.3"
assert current == "1.2.2-1"
def test_without_current_version(self):
details = "1.2.3"
new, current = parse_details(details)
assert new == "1.2.3"
assert current is None
def test_with_epoch_in_current(self):
details = "2.0.0 (currently in unstable: 1:1.9.0-2)"
new, current = parse_details(details)
assert new == "2.0.0"
assert current == "1:1.9.0-2"
def test_strips_whitespace(self):
details = " 1.2.3 (currently in unstable: 1.2.2-1 ) "
new, current = parse_details(details)
assert new == "1.2.3"
assert current == "1.2.2-1"
def test_empty_string(self):
new, current = parse_details("")
assert new == ""
assert current is None
class TestIsPrereleaseVersion:
"""Tests for is_prerelease_version function."""
def test_alpha_version(self):
assert is_prerelease_version("1.0.0alpha1") is True
assert is_prerelease_version("1.0.0-alpha2") is True
assert is_prerelease_version("1.0.0.alpha") is True
def test_beta_version(self):
assert is_prerelease_version("1.0.0beta1") is True
assert is_prerelease_version("1.0.0-beta2") is True
assert is_prerelease_version("2.0b1") is True
def test_rc_version(self):
assert is_prerelease_version("1.0.0rc1") is True
assert is_prerelease_version("1.0.0-rc2") is True
assert is_prerelease_version("1.0.0.rc3") is True
def test_short_prerelease(self):
assert is_prerelease_version("1.0a1") is True
assert is_prerelease_version("1.0b2") is True
def test_stable_version(self):
assert is_prerelease_version("1.0.0") is False
assert is_prerelease_version("2.3.4") is False
assert is_prerelease_version("1.0.0-1") is False
def test_version_with_current(self):
assert is_prerelease_version("1.0.0rc1 (currently in unstable: 0.9.0-1)") is True
assert is_prerelease_version("1.0.0 (currently in unstable: 0.9.0-1)") is False
def test_case_insensitive(self):
assert is_prerelease_version("1.0.0Alpha1") is True
assert is_prerelease_version("1.0.0BETA1") is True
assert is_prerelease_version("1.0.0RC1") is True
class TestNormalizeUpstreamVersion:
"""Tests for normalize_upstream_version function."""
def test_simple_version(self):
assert normalize_upstream_version("1.2.3") == "1.2.3"
def test_strips_debian_revision(self):
assert normalize_upstream_version("1.2.3-1") == "1.2.3"
assert normalize_upstream_version("1.2.3-2ubuntu1") == "1.2.3"
def test_strips_epoch(self):
assert normalize_upstream_version("1:1.2.3") == "1.2.3"
assert normalize_upstream_version("2:1.2.3") == "1.2.3"
def test_strips_epoch_and_revision(self):
assert normalize_upstream_version("1:1.2.3-4") == "1.2.3"
def test_strips_whitespace(self):
assert normalize_upstream_version(" 1.2.3 ") == "1.2.3"
def test_version_with_hyphen_in_upstream(self):
# Only the last hyphen is treated as revision separator
assert normalize_upstream_version("1.2.3-beta-1") == "1.2.3-beta"