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
8 changes: 8 additions & 0 deletions backend/handler/scan_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,14 @@ async def fetch_hltb_rom() -> HLTBRom:
)
)
):
# A refresh keeps the ID already on the ROM, often a manual match,
# rather than trading it for a filename guess. COMPLETE rematches.
if rom.hltb_id and (
scan_type == ScanType.UPDATE
or (scan_type == ScanType.UNMATCHED and not rom.hltb_metadata)
):
return await meta_hltb_handler.get_rom_by_id(rom.hltb_id)

return await meta_hltb_handler.get_rom(rom_attrs["fs_name"], platform.slug)

return HLTBRom(hltb_id=None)
Expand Down
143 changes: 143 additions & 0 deletions backend/tests/handler/test_scan_hltb_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""How a scan picks a ROM's HowLongToBeat match.

The filename lookup matches fuzzily, so re-running it on a ROM that already
carries an ID lets a different game outscore a match the user pinned by hand.
"""

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

import pytest

from handler.database import db_platform_handler, db_rom_handler
from handler.metadata.hltb_handler import HLTBMetadata, HLTBRom
from handler.scan_handler import MetadataSource, ScanType, scan_rom
from models.platform import Platform
from models.rom import Rom
from utils.context import initialize_context

FS_NAME = "Mario Kart 64 (USA).z64"
PINNED = HLTBRom(hltb_id=2255, name="Mario Kart 64")
GUESSED = HLTBRom(hltb_id=9999, name="Mario Kart 8")
NO_MATCH = HLTBRom(hltb_id=None)


@pytest.fixture
def lookups():
"""Patch both HowLongToBeat lookups, defaulting each to a miss."""
with (
patch(
"handler.scan_handler.meta_hltb_handler.get_rom_by_id",
new=AsyncMock(return_value=NO_MATCH),
) as by_id,
patch(
"handler.scan_handler.meta_hltb_handler.get_rom",
new=AsyncMock(return_value=NO_MATCH),
) as by_name,
):
yield SimpleNamespace(by_id=by_id, by_name=by_name)


async def _scan(
scan_type: ScanType,
hltb_id: int | None = None,
hltb_metadata: HLTBMetadata | None = None,
) -> Rom:
platform = db_platform_handler.add_platform(
Platform(id=1, slug="n64", fs_slug="n64", name="Nintendo 64")
)
rom = db_rom_handler.add_rom(
Rom(
platform_id=platform.id,
fs_name=FS_NAME,
fs_name_no_tags="Mario Kart 64",
fs_name_no_ext="Mario Kart 64 (USA)",
fs_extension="z64",
fs_path="n64",
name="Mario Kart 64",
hltb_id=hltb_id,
hltb_metadata=dict(hltb_metadata or {}),
fs_size_bytes=1024,
tags=[],
)
)

async with initialize_context():
return await scan_rom(
platform=platform,
scan_type=scan_type,
rom=rom,
fs_rom={
"fs_name": FS_NAME,
"flat": True,
"nested": False,
"files": [],
"crc_hash": "",
"md5_hash": "",
"sha1_hash": "",
"ra_hash": "",
},
metadata_sources=[MetadataSource.HLTB],
newly_added=False,
)


async def test_no_stored_id_uses_the_file_name_lookup(lookups):
lookups.by_name.return_value = GUESSED

result = await _scan(ScanType.COMPLETE)

lookups.by_id.assert_not_awaited()
lookups.by_name.assert_awaited_once_with(FS_NAME, "n64")
assert result.hltb_id == 9999


async def test_complete_rescan_rematches_a_stored_id(lookups):
"""A complete rescan wipes external IDs by design, so it re-runs the search."""
lookups.by_name.return_value = GUESSED

result = await _scan(ScanType.COMPLETE, hltb_id=2255)

lookups.by_id.assert_not_awaited()
lookups.by_name.assert_awaited_once_with(FS_NAME, "n64")
assert result.hltb_id == 9999


async def test_update_scan_refetches_a_stored_id(lookups):
"""A pinned match is refreshed by ID, never re-guessed from the filename."""
lookups.by_id.return_value = PINNED

result = await _scan(
ScanType.UPDATE, hltb_id=2255, hltb_metadata=HLTBMetadata(release_year=1997)
)

lookups.by_id.assert_awaited_once_with(2255)
lookups.by_name.assert_not_awaited()
assert result.hltb_id == 2255


async def test_stored_id_is_not_replaced_by_a_file_name_guess(lookups):
"""An ID the provider can't resolve stays put rather than being re-guessed."""
await _scan(ScanType.UPDATE, hltb_id=2255)

lookups.by_id.assert_awaited_once_with(2255)
lookups.by_name.assert_not_awaited()


async def test_unmatched_scan_refetches_metadata_for_a_stored_id(lookups):
lookups.by_id.return_value = PINNED

result = await _scan(ScanType.UNMATCHED, hltb_id=2255)

lookups.by_id.assert_awaited_once_with(2255)
lookups.by_name.assert_not_awaited()
assert result.hltb_id == 2255


async def test_unmatched_scan_without_a_stored_id_uses_the_file_name_lookup(lookups):
lookups.by_name.return_value = GUESSED

await _scan(ScanType.UNMATCHED)

lookups.by_id.assert_not_awaited()
lookups.by_name.assert_awaited_once_with(FS_NAME, "n64")
Loading