From 238cec40fdc6cd24304c6c0f73f4abc591d5f197 Mon Sep 17 00:00:00 2001 From: Orinks Date: Wed, 4 Feb 2026 21:04:00 +0000 Subject: [PATCH] test: add edge case tests for clock_pack_loader (#12) Add 9 new tests covering uncovered lines in clock_pack_loader: - discover_packs with non-existent directory - discover_packs skipping non-directory items - discover_packs handling ClockPackError from invalid packs - discover_packs handling generic exceptions (PermissionError) - load_pack with invalid JSON manifest - load_pack with missing version field - validate_pack with unsupported audio format - get_pack returning None for missing pack - refresh clearing cache and re-discovering packs Closes #12 --- tests/test_clock_pack.py | 185 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/tests/test_clock_pack.py b/tests/test_clock_pack.py index 53baed6..0b17d4f 100644 --- a/tests/test_clock_pack.py +++ b/tests/test_clock_pack.py @@ -13,6 +13,18 @@ class TestClockPackLoader: """Test clock pack discovery and loading.""" + def test_discover_packs_missing_directory(self): + """Should return empty dict when clocks directory does not exist.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + clocks_dir = Path(tmpdir) / "missing" + loader = ClockPackLoader(clocks_dir) + + packs = loader.discover_packs() + + assert packs == {} + def test_discover_packs_in_directory(self): """Should find all clock packs in a directory.""" from accessiclock.services.clock_pack_loader import ClockPackLoader @@ -62,6 +74,79 @@ def test_ignore_directories_without_manifest(self): assert "valid" in packs assert "invalid" not in packs + def test_discover_packs_skips_non_directories(self): + """Should skip non-directory items like files.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + clocks_dir = Path(tmpdir) + + (clocks_dir / "not_a_pack.txt").write_text("just a file") + + (clocks_dir / "pack1").mkdir() + (clocks_dir / "pack1" / "clock.json").write_text( + json.dumps({"name": "Pack 1", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + loader = ClockPackLoader(clocks_dir) + packs = loader.discover_packs() + + assert "pack1" in packs + assert "not_a_pack.txt" not in packs + + def test_discover_packs_skips_invalid_packs(self): + """Should skip invalid packs that raise ClockPackError.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + clocks_dir = Path(tmpdir) + + (clocks_dir / "good").mkdir() + (clocks_dir / "good" / "clock.json").write_text( + json.dumps({"name": "Good", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + (clocks_dir / "bad").mkdir() + (clocks_dir / "bad" / "clock.json").write_text("{invalid json") + + loader = ClockPackLoader(clocks_dir) + packs = loader.discover_packs() + + assert "good" in packs + assert "bad" not in packs + + def test_discover_packs_handles_generic_exception(self, monkeypatch): + """Should continue when a pack load raises a generic exception.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + clocks_dir = Path(tmpdir) + + (clocks_dir / "good").mkdir() + (clocks_dir / "good" / "clock.json").write_text( + json.dumps({"name": "Good", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + (clocks_dir / "bad").mkdir() + (clocks_dir / "bad" / "clock.json").write_text( + json.dumps({"name": "Bad", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + loader = ClockPackLoader(clocks_dir) + original_load_pack = loader.load_pack + + def fake_load_pack(pack_id: str): + if pack_id == "bad": + raise PermissionError("no access") + return original_load_pack(pack_id) + + monkeypatch.setattr(loader, "load_pack", fake_load_pack) + + packs = loader.discover_packs() + + assert "good" in packs + assert "bad" not in packs + class TestClockPackManifest: """Test clock pack manifest parsing.""" @@ -109,6 +194,36 @@ def test_manifest_missing_required_fields(self): with pytest.raises(ClockPackError): loader.load_pack("incomplete") + def test_manifest_invalid_json(self): + """Should raise error for manifest with invalid JSON.""" + from accessiclock.services.clock_pack_loader import ClockPackError, ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + pack_dir = Path(tmpdir) / "broken" + pack_dir.mkdir() + (pack_dir / "clock.json").write_text("{invalid json") + + loader = ClockPackLoader(Path(tmpdir)) + + with pytest.raises(ClockPackError): + loader.load_pack("broken") + + def test_manifest_missing_version(self): + """Should raise error for manifest missing version field.""" + from accessiclock.services.clock_pack_loader import ClockPackError, ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + pack_dir = Path(tmpdir) / "missing_version" + pack_dir.mkdir() + + manifest = {"name": "Incomplete", "sounds": {}} + (pack_dir / "clock.json").write_text(json.dumps(manifest)) + + loader = ClockPackLoader(Path(tmpdir)) + + with pytest.raises(ClockPackError): + loader.load_pack("missing_version") + class TestClockPackValidation: """Test clock pack validation.""" @@ -168,6 +283,76 @@ def test_validation_fails_for_missing_sounds(self): assert is_valid is False assert any("hour.wav" in str(e) for e in errors) + def test_validation_fails_for_unsupported_audio_format(self): + """Should fail validation for unsupported audio formats.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + pack_dir = Path(tmpdir) / "test_pack" + pack_dir.mkdir() + + manifest = { + "name": "Test", + "version": "1.0.0", + "author": "Test", + "sounds": { + "hour": "hour.xyz", + }, + } + (pack_dir / "clock.json").write_text(json.dumps(manifest)) + (pack_dir / "hour.xyz").touch() + + loader = ClockPackLoader(Path(tmpdir)) + pack_info = loader.load_pack("test_pack") + + is_valid, errors = loader.validate_pack(pack_info) + assert is_valid is False + assert any("hour.xyz" in str(e) for e in errors) + + +class TestClockPackCache: + """Test cache behavior for clock pack loader.""" + + def test_get_pack_returns_none_for_missing_pack(self): + """Should return None when pack is not in cache.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + loader = ClockPackLoader(Path(tmpdir)) + assert loader.get_pack("missing") is None + + def test_refresh_clears_cache_and_rediscovers(self): + """Should clear cache and repopulate with rediscovered packs.""" + from accessiclock.services.clock_pack_loader import ClockPackLoader + + with tempfile.TemporaryDirectory() as tmpdir: + clocks_dir = Path(tmpdir) + + (clocks_dir / "pack1").mkdir() + (clocks_dir / "pack1" / "clock.json").write_text( + json.dumps({"name": "Pack 1", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + loader = ClockPackLoader(clocks_dir) + loader.discover_packs() + + assert loader.get_pack("pack1") is not None + + (clocks_dir / "pack1" / "clock.json").unlink() + (clocks_dir / "pack1").rmdir() + + (clocks_dir / "pack2").mkdir() + (clocks_dir / "pack2" / "clock.json").write_text( + json.dumps({"name": "Pack 2", "version": "1.0.0", "author": "Test", "sounds": {}}) + ) + + refreshed = loader.refresh() + + assert "pack1" not in refreshed + assert "pack2" in refreshed + assert loader.get_pack("pack1") is None + assert loader.get_pack("pack2") is not None + class TestClockPackInfo: """Test ClockPackInfo data class."""