diff --git a/.gitignore b/.gitignore index b60f6e619..4b5e338c3 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ backend/romm_test/logs backend/romm_test/config .pytest_cache /romm_test +.coverage # service worker frontend/dev-dist diff --git a/backend/.coveragerc b/backend/.coveragerc new file mode 100644 index 000000000..2b89a8e4f --- /dev/null +++ b/backend/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + alembic/* + */tests/* diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 86a20ee41..4a29fe600 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -57,7 +57,10 @@ class ScanStats: def __add__(self, other: Any) -> ScanStats: if not isinstance(other, ScanStats): - return NotImplemented + raise NotImplementedError( + f"Addition not implemented between ScanStats and {type(other)}" + ) + return ScanStats( scanned_platforms=self.scanned_platforms + other.scanned_platforms, added_platforms=self.added_platforms + other.added_platforms, diff --git a/backend/endpoints/sockets/tests/test_scan.py b/backend/endpoints/sockets/tests/test_scan.py new file mode 100644 index 000000000..80222c7ec --- /dev/null +++ b/backend/endpoints/sockets/tests/test_scan.py @@ -0,0 +1,250 @@ +from unittest.mock import Mock + +import pytest +from handler.scan_handler import ScanType +from models.rom import Rom + +from ..scan import ScanStats, _should_scan_rom + + +def test_scan_stats(): + stats = ScanStats() + assert stats.scanned_platforms == 0 + assert stats.added_platforms == 0 + assert stats.metadata_platforms == 0 + assert stats.scanned_roms == 0 + assert stats.added_roms == 0 + assert stats.metadata_roms == 0 + assert stats.scanned_firmware == 0 + assert stats.added_firmware == 0 + + stats.scanned_platforms += 1 + stats.added_platforms += 1 + stats.metadata_platforms += 1 + stats.scanned_roms += 1 + stats.added_roms += 1 + stats.metadata_roms += 1 + stats.scanned_firmware += 1 + stats.added_firmware += 1 + + assert stats.scanned_platforms == 1 + assert stats.added_platforms == 1 + assert stats.metadata_platforms == 1 + assert stats.scanned_roms == 1 + assert stats.added_roms == 1 + assert stats.metadata_roms == 1 + assert stats.scanned_firmware == 1 + assert stats.added_firmware == 1 + + +def test_merging_scan_stats(): + stats = ScanStats( + scanned_platforms=1, + added_platforms=2, + metadata_platforms=3, + scanned_roms=4, + added_roms=5, + metadata_roms=6, + scanned_firmware=7, + added_firmware=8, + ) + + stats2 = ScanStats( + scanned_platforms=10, + added_platforms=11, + metadata_platforms=12, + scanned_roms=13, + added_roms=14, + metadata_roms=15, + scanned_firmware=16, + added_firmware=17, + ) + + stats += stats2 + + assert stats.scanned_platforms == 11 + assert stats.added_platforms == 13 + assert stats.metadata_platforms == 15 + assert stats.scanned_roms == 17 + assert stats.added_roms == 19 + assert stats.metadata_roms == 21 + assert stats.scanned_firmware == 23 + assert stats.added_firmware == 25 + + stats3: dict = {} + with pytest.raises(NotImplementedError): + stats += stats3 + + +class TestShouldScanRom: + def test_new_platforms_scan_with_no_rom(self): + """NEW_PLATFORMS should scan when rom is None""" + result = _should_scan_rom(ScanType.NEW_PLATFORMS, None, []) + assert result is True + + def test_new_platforms_scan_with_existing_rom(self, rom: Rom): + """NEW_PLATFORMS should not scan when rom exists""" + result = _should_scan_rom(ScanType.NEW_PLATFORMS, rom, []) + assert result is False + + # Test QUICK scan type + def test_quick_scan_with_no_rom(self): + """QUICK should scan when rom is None""" + result = _should_scan_rom(ScanType.QUICK, None, []) + assert result is True + + def test_quick_scan_with_existing_rom(self, rom: Rom): + """QUICK should not scan when rom exists""" + result = _should_scan_rom(ScanType.QUICK, rom, []) + assert result is False + + # Test COMPLETE scan type + def test_complete_scan_always_scans(self, rom: Rom): + """COMPLETE should always scan regardless of rom state""" + assert _should_scan_rom(ScanType.COMPLETE, None, []) is True + assert _should_scan_rom(ScanType.COMPLETE, rom, []) is True + assert _should_scan_rom(ScanType.COMPLETE, rom, ["2", "3"]) is True + + # Test HASHES scan type + def test_hashes_scan_always_scans(self, rom: Rom): + """HASHES should always scan regardless of rom state""" + assert _should_scan_rom(ScanType.HASHES, None, []) is True + assert _should_scan_rom(ScanType.HASHES, rom, []) is True + assert _should_scan_rom(ScanType.HASHES, rom, ["2", "3"]) is True + + # Test UNIDENTIFIED scan type + def test_unidentified_scan_with_no_rom(self): + """UNIDENTIFIED should not scan when rom is None""" + result = _should_scan_rom(ScanType.UNIDENTIFIED, None, []) + assert result is False + + def test_unidentified_scan_with_unidentified_rom(self, rom: Rom): + """UNIDENTIFIED should scan when rom is unidentified""" + rom.igdb_id = None + rom.moby_id = None + rom.ss_id = None + rom.ra_id = None + rom.launchbox_id = None + result = _should_scan_rom(ScanType.UNIDENTIFIED, rom, []) + assert result is True + + def test_unidentified_scan_with_identified_rom(self, rom: Rom): + """UNIDENTIFIED should not scan when rom is identified""" + rom.igdb_id = 1 + result = _should_scan_rom(ScanType.UNIDENTIFIED, rom, []) + assert result is False + + # Test PARTIAL scan type + def test_partial_scan_with_no_rom(self): + """PARTIAL should not scan when rom is None""" + result = _should_scan_rom(ScanType.PARTIAL, None, []) + assert result is False + + def test_partial_scan_with_identified_rom(self, rom: Rom): + """PARTIAL should scan when rom is identified""" + rom.igdb_id = 1 + result = _should_scan_rom(ScanType.PARTIAL, rom, []) + assert result is True + + def test_partial_scan_with_unidentified_rom(self, rom: Rom): + """PARTIAL should not scan when rom is not identified""" + rom.igdb_id = None + rom.moby_id = None + rom.ss_id = None + rom.ra_id = None + rom.launchbox_id = None + result = _should_scan_rom(ScanType.PARTIAL, rom, []) + assert result is False + + # Test rom_ids parameter + def test_scan_when_rom_id_in_list(self, rom: Rom): + """Should scan when rom.id is in roms_ids list regardless of scan type""" + rom.id = 1 + roms_ids = ["1", "2", "3"] + + # Test with different scan types + for scan_type in [ + ScanType.NEW_PLATFORMS, + ScanType.QUICK, + ScanType.UNIDENTIFIED, + ScanType.PARTIAL, + ]: + result = _should_scan_rom(scan_type, rom, roms_ids) + assert result is True + + def test_no_scan_when_rom_id_not_in_list(self, rom: Rom): + """Should follow normal rules when rom.id is not in roms_ids list""" + rom.id = 4 + roms_ids = ["1", "2", "3"] + + # These should not scan because rom exists and id not in list + assert _should_scan_rom(ScanType.NEW_PLATFORMS, rom, roms_ids) is False + assert _should_scan_rom(ScanType.QUICK, rom, roms_ids) is False + assert _should_scan_rom(ScanType.UNIDENTIFIED, rom, roms_ids) is False + assert _should_scan_rom(ScanType.PARTIAL, rom, roms_ids) is False + + # Edge cases + def test_empty_roms_ids_list(self, rom: Rom): + """Test behavior with empty roms_ids list""" + rom.id = 1 + rom.igdb_id = 1 + + assert _should_scan_rom(ScanType.PARTIAL, rom, []) is True + assert _should_scan_rom(ScanType.NEW_PLATFORMS, rom, []) is False + + def test_rom_id_type_conversion(self, rom: Rom): + """Test that rom.id (int) is properly compared with roms_ids (list of strings)""" + rom.id = 123 + roms_ids = ["123", "456"] + + # This should scan because 123 should match "123" + result = _should_scan_rom(ScanType.NEW_PLATFORMS, rom, roms_ids) + assert result is True + + @pytest.mark.parametrize( + "scan_type,rom_exists,is_identified,rom_in_list,expected", + [ + # Comprehensive test matrix + (ScanType.NEW_PLATFORMS, False, None, False, True), + (ScanType.NEW_PLATFORMS, True, True, False, False), + (ScanType.NEW_PLATFORMS, True, True, True, True), + (ScanType.QUICK, False, None, False, True), + (ScanType.QUICK, True, True, False, False), + (ScanType.COMPLETE, False, None, False, True), + (ScanType.COMPLETE, True, False, False, True), + (ScanType.HASHES, False, None, False, True), + (ScanType.HASHES, True, False, False, True), + (ScanType.UNIDENTIFIED, True, False, False, True), + (ScanType.UNIDENTIFIED, True, True, False, False), + (ScanType.PARTIAL, True, True, False, True), + (ScanType.PARTIAL, True, True, False, False), + ], + ) + def test_comprehensive_scenarios( + self, + scan_type, + rom_exists, + is_identified, + rom_in_list, + expected, + ): + """Test comprehensive scenarios with different combinations""" + rom: Rom = Mock(spec=Rom) + roms_ids = [] + + if rom_exists: + rom.id = 1 + if is_identified: + rom.igdb_id = 1 + else: + rom.igdb_id = None + rom.moby_id = None + rom.ss_id = None + rom.ra_id = None + rom.launchbox_id = None + + if rom_in_list: + roms_ids = ["1"] + + result = _should_scan_rom(scan_type, rom, roms_ids) + assert result is expected diff --git a/pyproject.toml b/pyproject.toml index f039f27eb..5c6bcd7ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "anyio ~= 4.4", "authlib ~= 1.3", "colorama ~= 0.4", + "coverage>=7.9.2", "defusedxml ~= 0.7.1", "emoji == 2.10.1", "fastapi-pagination[sqlalchemy] ~= 0.12", diff --git a/uv.lock b/uv.lock index b7c19d688..c132e5435 100644 --- a/uv.lock +++ b/uv.lock @@ -296,6 +296,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, ] +[[package]] +name = "coverage" +version = "7.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, + { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, + { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +] + [[package]] name = "crontab" version = "1.0.4" @@ -1657,6 +1688,7 @@ dependencies = [ { name = "anyio" }, { name = "authlib" }, { name = "colorama" }, + { name = "coverage" }, { name = "defusedxml" }, { name = "emoji" }, { name = "fastapi", extra = ["standard"] }, @@ -1717,6 +1749,7 @@ requires-dist = [ { name = "anyio", specifier = "~=4.4" }, { name = "authlib", specifier = "~=1.3" }, { name = "colorama", specifier = "~=0.4" }, + { name = "coverage", specifier = ">=7.9.2" }, { name = "defusedxml", specifier = "~=0.7.1" }, { name = "emoji", specifier = "==2.10.1" }, { name = "fakeredis", marker = "extra == 'test'", specifier = "~=2.21" },