"""Tests for SQLite-backed cache and package state.""" import datetime from pathlib import Path import debian_todo class TestLoadCache: """Tests for load_cache function.""" def test_returns_none_when_db_missing(self, tmp_path, monkeypatch): monkeypatch.setattr(debian_todo, "CACHE_PATH", tmp_path / "cache.sqlite3") result = debian_todo.load_cache([]) assert result is None def test_returns_none_on_version_mismatch(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) conn = debian_todo.get_db_connection() with conn: conn.execute( "INSERT INTO metadata (key, value) VALUES ('source_cache_version', ?)", (str(debian_todo.CACHE_VERSION - 1),), ) conn.close() 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") db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) conn = debian_todo.get_db_connection() with conn: conn.execute( "INSERT INTO metadata (key, value) VALUES ('source_cache_version', ?)", (str(debian_todo.CACHE_VERSION),), ) conn.execute( "INSERT INTO source_file_mtimes (path, mtime) VALUES (?, ?)", (str(source_file), 0.0), ) conn.close() 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)) db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) conn = debian_todo.get_db_connection() with conn: conn.execute( "INSERT INTO metadata (key, value) VALUES ('source_cache_version', ?)", (str(debian_todo.CACHE_VERSION),), ) conn.execute( "INSERT INTO source_file_mtimes (path, mtime) VALUES (?, ?)", (str(source_file), mtime), ) conn.execute( """ INSERT INTO source_info_cache (source, team, uploaders, vcs_git) VALUES (?, ?, ?, ?) """, ("foo", "python-team", "John ", "https://example.invalid/foo"), ) conn.close() result = debian_todo.load_cache([str(source_file)]) assert result == { "foo": { "team": "python-team", "uploaders": "John ", "vcs_git": "https://example.invalid/foo", } } class TestSaveCache: """Tests for save_cache function.""" def test_saves_cache_to_sqlite(self, tmp_path, monkeypatch): source_file = tmp_path / "Sources" source_file.write_text("dummy") db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) vcs_by_source = { "foo": { "team": "python-team", "uploaders": "", "vcs_git": "https://example.invalid/foo", } } debian_todo.save_cache([str(source_file)], vcs_by_source) conn = debian_todo.get_db_connection() row = conn.execute( "SELECT value FROM metadata WHERE key = 'source_cache_version'" ).fetchone() cached = conn.execute( "SELECT source, team, uploaders, vcs_git FROM source_info_cache" ).fetchone() conn.close() assert row["value"] == str(debian_todo.CACHE_VERSION) assert dict(cached) == { "source": "foo", "team": "python-team", "uploaders": "", "vcs_git": "https://example.invalid/foo", } def test_handles_missing_source_file(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) debian_todo.save_cache(["/nonexistent/file"], {}) result = debian_todo.load_cache([]) assert result is None class TestPackageState: """Tests for package discovery tracking.""" def test_sync_records_first_seen_upstream_version(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) seen_at = datetime.datetime(2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":type": "New upstream", ":details": "1.2.3 (currently in unstable: 1.2.2-1)", } ], seen_at=seen_at, ) discovery_map = debian_todo.load_package_discovery_map() assert discovery_map == { "foo": ("1.2.3", "2026-05-05T12:00:00Z", True) } def test_sync_preserves_discovery_time_for_same_version( self, tmp_path, monkeypatch ): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3", } ], seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), ) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3", } ], seen_at=datetime.datetime( 2026, 5, 6, 12, 0, tzinfo=datetime.timezone.utc ), ) discovery_map = debian_todo.load_package_discovery_map() assert discovery_map["foo"] == ("1.2.3", "2026-05-05T12:00:00Z", True) def test_sync_updates_discovery_time_when_version_changes( self, tmp_path, monkeypatch ): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3", } ], seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), ) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.4", } ], seen_at=datetime.datetime( 2026, 5, 7, 8, 30, tzinfo=datetime.timezone.utc ), ) discovery_map = debian_todo.load_package_discovery_map() assert discovery_map["foo"] == ("1.2.4", "2026-05-07T08:30:00Z", True) def test_sync_without_discovery_recording_leaves_timestamp_empty( self, tmp_path, monkeypatch ): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) debian_todo.sync_package_state( [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3", } ], seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), record_discovery=False, ) discovery_map = debian_todo.load_package_discovery_map() assert discovery_map["foo"] == (None, None, False) def test_sorts_by_oldest_discovery_date(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) debian_todo.sync_package_state( [ { ":source": "older", ":shortname": "newupstream_older", ":details": "1.0.0", }, ], seen_at=datetime.datetime( 2026, 5, 6, 12, 0, tzinfo=datetime.timezone.utc ), ) debian_todo.sync_package_state( [ { ":source": "newer", ":shortname": "newupstream_newer", ":details": "2.0.0", }, { ":source": "older", ":shortname": "newupstream_older", ":details": "1.0.0", }, ], seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), ) sorted_items = debian_todo.sort_todo_list_by_discovery( [ { ":source": "newer", ":shortname": "newupstream_newer", ":details": "2.0.0", }, { ":source": "older", ":shortname": "newupstream_older", ":details": "1.0.0", }, ] ) assert [item[":source"] for item in sorted_items] == ["newer", "older"] def test_save_watch_snapshot(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) repo_dir = tmp_path / "pkg" debian_dir = repo_dir / "debian" debian_dir.mkdir(parents=True) (debian_dir / "watch").write_text("Version: 5\n") debian_todo.save_watch_snapshot("mypkg", repo_dir) conn = debian_todo.get_db_connection() row = conn.execute( "SELECT watch_content FROM package_state WHERE source = ?", ("mypkg",), ).fetchone() conn.close() assert row["watch_content"] == "Version: 5\n" class TestListOutput: """Tests for list output using discovery timestamps.""" def test_list_shows_discovery_datetime(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" todo_path = tmp_path / "todo.json" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) monkeypatch.setattr(debian_todo, "TODO_PATH", todo_path) monkeypatch.setattr(debian_todo, "load_source_info_map", lambda: {}) monkeypatch.setattr(debian_todo, "load_notes", lambda: {}) todo_list = [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3 (currently in unstable: 1.2.2-1)", } ] debian_todo.save_todo_list(todo_list) debian_todo.sync_package_state( todo_list, seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), record_discovery=True, ) class FakeConsole: def __init__(self) -> None: self.width = 80 self.output: list[str] = [] def print(self, message) -> None: self.output.append(str(message)) fake_console = FakeConsole() monkeypatch.setattr(debian_todo, "Console", lambda: fake_console) debian_todo.list_todos(include_prerelease=False) assert any("D: 2026-05-05 12:00 UTC" in line for line in fake_console.output) def test_list_hides_pre_cutoff_discovery_datetime(self, tmp_path, monkeypatch): db_path = tmp_path / "cache.sqlite3" todo_path = tmp_path / "todo.json" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) monkeypatch.setattr(debian_todo, "TODO_PATH", todo_path) monkeypatch.setattr(debian_todo, "load_source_info_map", lambda: {}) monkeypatch.setattr(debian_todo, "load_notes", lambda: {}) todo_list = [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3 (currently in unstable: 1.2.2-1)", } ] debian_todo.save_todo_list(todo_list) debian_todo.sync_package_state( todo_list, seen_at=datetime.datetime( 2026, 5, 5, 12, 0, tzinfo=datetime.timezone.utc ), record_discovery=False, ) class FakeConsole: def __init__(self) -> None: self.width = 80 self.output: list[str] = [] def print(self, message) -> None: self.output.append(str(message)) fake_console = FakeConsole() monkeypatch.setattr(debian_todo, "Console", lambda: fake_console) debian_todo.list_todos(include_prerelease=False) assert any("D: -" in line for line in fake_console.output) def test_list_shows_post_cutoff_discovery_datetime_even_without_flag( self, tmp_path, monkeypatch ): db_path = tmp_path / "cache.sqlite3" todo_path = tmp_path / "todo.json" monkeypatch.setattr(debian_todo, "CACHE_PATH", db_path) monkeypatch.setattr(debian_todo, "TODO_PATH", todo_path) monkeypatch.setattr(debian_todo, "load_source_info_map", lambda: {}) monkeypatch.setattr(debian_todo, "load_notes", lambda: {}) todo_list = [ { ":source": "foo", ":shortname": "newupstream_foo", ":details": "1.2.3 (currently in unstable: 1.2.2-1)", } ] debian_todo.save_todo_list(todo_list) debian_todo.sync_package_state( todo_list, seen_at=datetime.datetime( 2026, 5, 5, 12, 1, tzinfo=datetime.timezone.utc ), record_discovery=False, ) conn = debian_todo.get_db_connection() with conn: conn.execute( """ UPDATE package_state SET discovered_version = ?, discovered_at = ?, discovered_by_refresh = 0 WHERE source = ? """, ("1.2.3", "2026-05-05T12:01:00Z", "foo"), ) conn.close() class FakeConsole: def __init__(self) -> None: self.width = 80 self.output: list[str] = [] def print(self, message) -> None: self.output.append(str(message)) fake_console = FakeConsole() monkeypatch.setattr(debian_todo, "Console", lambda: fake_console) debian_todo.list_todos(include_prerelease=False) assert any("D: 2026-05-05 12:01 UTC" in line for line in fake_console.output)