mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 22:01:12 +00:00
13 KiB
13 KiB
tests/AGENTS.md
ccm_core_tests — doctest unit tests for ccm_core. Hermetic, fast, no real network or disk. Read the root AGENTS.md first.
File pointers
main.cpp— doctest entry point withDOCTEST_CONFIG_IMPLEMENT_WITH_MAIN. Do not put tests here.fakes/InMemoryFileSystem.{hpp,cpp}—IFileSystemimplementation backed bystd::map. Normalizes paths vialexically_normal().generic_string()(always/separators).fs_names_tests.cpp—formatTextForFs+parseIndexFromFilename. Both are compatibility ports of the original Rust path rules.domain_json_tests.cpp— JSON round-trip tests for every domain type. Update this file whenever a domain type changes.image_service_tests.cpp—ImageService(uses inlineRecordingImageStorefake).collection_service_tests.cpp—CollectionService<MagicCard>(uses inlineInMemoryRepo+StubImageStore).config_service_tests.cpp—ConfigServiceagainstInMemoryFileSystem.json_collection_repository_tests.cpp,json_set_repository_tests.cpp— repository round-trips againstInMemoryFileSystem. Set-repo cases also pin Pokemonsets-west.json/sets-asia.jsonpaths and migrate-on-load from legacypokemon/sets.json/pokemonjp/sets.json.local_image_store_tests.cpp—LocalImageStoreagainstInMemoryFileSystem+ConfigService:copyIn(extension preserved, missing source errors),remove(existing file deleted; absent path is a no-op),resolvePathlayout underdataStorage/<game>/images/.set_service_tests.cpp—SetServicewithFakeSetSource+InMemSetRepo.magic_set_source_tests.cpp—MagicSetSource::parseResponse(Scryfall mapping). DrivesfetchAllviaFixedHttpClientfake.magic_card_preview_source_tests.cpp—MagicCardPreviewSource::buildSearchUrlURL-encoding rules +parseResponse(data[0].image_uris.normal). DrivesfetchImageUrlviaFixedHttpClient.card_preview_service_tests.cpp—CardPreviewServiceregistry/orchestration throughregisterModule(IGameModule&)with an inlineFakeGameModulereturning aFakeSource : ICardPreviewSource(which carries aPreviewLookupError::Kindknob so tests can drive both transient and not-found paths) and aFixedHttpClient. Both fakes countcallsso cache-hit assertions are precise. Pin-downs include: "module returning nullptr is silently skipped", the per-gamedetectFirstPrint/detectPrintVariantsopt-in guards, and the LRU bytes cache (repeatfetchPreviewBytesfor the same(game, name, setId, setNo)returns the cached payload without touching the source or HTTP; different cards get separate cache slots; transient errors are not cached so a flaky connection recovers;fetchImageBytesByUrlis keyed by URL and serves the per-game card-back fallback from the same LRU). ProductionfetchAndCacherejects empty HTTP bodies (not exercised by these fakes unless a test sets an emptybodydeliberately). The negative-cache behavior is also pinned down: aNotFoundsource error writes through to the persistent cache and short-circuits the next lookup (source not re-invoked); editing a lookup-relevant field invalidates the negative entry automatically; warm-restart (a fresh service over the same cache fake) honors a previously stored negative entry; and a later positive result for the same key replaces the negative entry. The persistent-tier wiring uses an inlineInMemoryByteCache : IPreviewByteCachefake whoseEntry { negative, payload }carries the kind explicitly.local_preview_byte_cache_tests.cpp—LocalPreviewByteCacheadapter againstStdFileSystem(real disk under a uniquetemp_directory_path()/ccm_preview_cache_test_*per case, RAIITempDircleanup; see alsostd_file_system_tests.cpp). Pin-downs: store/load round-trips bytes verbatim; missing key is a clean miss; empty payload is silently skipped; sidecar mismatch (faked hash collision) is treated as a miss so we never serve the wrong card's bytes (or wrong card's negative verdict); the cache survives an adapter restart over the same directory; total-size eviction drops the oldest.binby mtime when astorewould exceed the cap; aloadtouches the entry's mtime so frequently-viewed cards survive eviction. Negative-entry coverage:storeNegativeround-trips asNegativeHit(not a miss, not a payload, and not counted against the byte cap); negatives survive an adapter restart; a later positivestoreoverwrites a previous negative and a laterstoreNegativeoverwrites a previous positive (releasing its bytes from the cap); and the sidecar collision check applies to negative entries too.std_file_system_tests.cpp—StdFileSystemdirectly (exists,isDirectory,ensureDirectory,readText,writeText,copyFile,remove,listDirectory) under a uniquetemp_directory_path()/ccm_std_fs_test_*directory per case; scope matches the real-disk exception documented for preview-cache tests.pokemon_west_set_id_tests.cpp—canonicalizeWestSetIdidentity + legacy pokemontcg → TCGdex EN mappings (sv1→sv01,pgo→swsh10.5, …) and unknown passthrough.pokemon_collection_set_sync_tests.cpp—syncPokemonCollectionSetsWest id migration + name/date refresh; Asia metadata-only refresh.pokemon_set_source_tests.cpp—PokemonSetSource::parseListResponse(TCGdex EN/v2/en/setstop-level array) +parseReleaseDate/parseCatalogPackFromSetDetail. DrivesfetchAll/fetchAllWithCatalogvia routing HTTP fakes and asserts EN endpoints.pokemon_card_preview_source_tests.cpp—PokemonCardPreviewSource::buildSearchUrl/buildCardByIdUrl(canonicalization +localIdfilters),parseSearchResponse/parseCardByIdResponse(image+/high.png), andfetchImageUrl/ auto-detect viaFixedHttpClient.digibattle99_set_source_tests.cpp—DigiBattle99SetSource::parseResponsederives unique packs from digimoncard.io search arrays, slugifiesSet.id, applies curated release dates, and sorts chronologically.parseCatalog/fetchAllWithCatalogpin the set-completion checklist (multi-pack membership, setNo dedupe). DrivesfetchAllviaFixedHttpClient.digibattle99_set_completion_tests.cpp—computeDigiBattle99SetCompletion/digiBattle99ChecklistForSetownership rules +DigiBattle99SetCatalogServiceround-trip againstInMemoryFileSystem.yugioh_set_completion_tests.cpp—computeYuGiOhSetCompletion/yuGiOhChecklistForSetownership rules (printing-slot match) +YuGiOhSetCatalogServiceround-trip againstInMemoryFileSystem.pokemon_set_completion_tests.cpp—computePokemonSetCompletion/pokemonChecklistForSetWest/Asia ownership isolation + region/language filters +PokemonSetCatalogServicedual-path FS round-trip.digibattle99_card_preview_source_tests.cpp— CDN image URL fromsetNo, search URL encoding (series/n/pack/card),parseImageUrlFromSearchNotFound vs Transient, and auto-detect print variants. DrivesfetchImageUrl/detectPrintVariantsviaFixedHttpClient.yugioh_set_source_tests.cpp—YuGiOhSetSource::parseResponsefor YGOPRODeckcardsets.php(set_code,set_name,tcg_date) includingYYYY-MM-DD->YYYY/MM/DDrewrite and chronological sort checks. AlsoparseCatalog/fetchAllWithCatalogfor the set-completion checklist fromcardinfo.php.yugioh_set_lookup_tests.cpp—lookupYuGiOhSetByShorthand/ helpers inccm/util/YuGiOhSetLookup.hpp(trim, ASCII case-fold, exactSet.idmatch, not-found vs ambiguous).game_module_tests.cpp— smoke tests that each concreteIGameModule(Magic / Pokemon / Yu-Gi-Oh / DigiBattle99) reports stableid(),dirName(),displayName(), and a non-nullcardPreviewSource()when constructed with a noopIHttpClient.yugioh_card_preview_source_tests.cpp—YuGiOhCardPreviewSourceYugipedia + YGOPRODeck unit coverage. Helper-level tests pin downnormalizeName(whitespace + Yugipedia-policy punctuation stripping),ygoRarityShortCode+rarityCodeFor(CCM3 dialog rarity names → canonical short codes used by both the YGO overview table and Yugipedia filename generation; unknown rarity falls through),extractSetCode(LOB-005/LOB-DE005→LOB),buildCandidateFilenames(printed-edition first, EN/NA/EU/AU + png/jpg, rarity-less fallback round, empty list when slug or set code is missing),buildYugipediaQueryUrl(singletitles=File:A|File:Bbatch, percent-encoded), andparseYugipediaResponse(returns the URL of the highest-priority filename that resolved, errors when every candidate ismissing). End-to-endfetchImageUrlcases use aRoutingHttpClientto verify Yugipedia is queried first and the per-printing scan is returned when found, that empty/error Yugipedia responses fall through to the YGOPRODeckcard_images[0]fallback, that the YGOPRODeck error is propagated when both upstreams fail, and that an emptysetNoskips Yugipedia entirely.parseFirstPrintpreferred-set_namelookup is also covered for the auto-detect path.parsePrintVariantsincludes synthetic scenarios aligned with theyugioh_same_card_set_variant_testsfixture (dual-rarity vs multi-code within one display set, duplicate suppression, and no merge across unrelatedset_namerows when the picker label matches nothing).card_sorter_tests.cpp—sortMagicCards/sortPokemonCardsper-column behavior. Pin-down tests forbyField-equivalent semantics: case-insensitive strings, chronological set sort viaset.releaseDate, numericamount,false < trueboolean order, stable composition (sort by name then by set keeps inner-name order). Update this file whenever you add a new column / sort key.card_filter_tests.cpp—matchesMagicFilter/matchesPokemonFilter/matchesYuGiOhFilterrow-matcher behavior. Pin-down tests forapplyFilter-equivalent semantics: case-insensitive substring match acrosstableFieldsvalueKeys (name, set.name, language, condition, amount-as-string, note; Pokemon addssetNo; Yu-Gi-Oh addssetNo+rarity), boolean flag columns intentionally excluded, empty filter matches everything. Update this file whenever you add a new searchable column.CMakeLists.txt— explicit list of every.cpp(no glob).
Conventions
- Framework: doctest. Each test file
#include <doctest/doctest.h>and usesTEST_SUITE("...")+TEST_CASE("..."). Asserts:CHECK,REQUIRE,CHECK_THROWS. - No real I/O. Everything goes through
ccm::testing::InMemoryFileSystemor an inline test-local fake. If you need HTTP, write a fakeIHttpClientlikeFixedHttpClientinmagic_set_source_tests.cpp. Narrow exceptions (unique temp dirs + RAII cleanup):local_preview_byte_cache_tests.cpp(mtime/size semantics tied to realstd::filesystem) andstd_file_system_tests.cpp(StdFileSystemintegration). Do not add further real-disk suites without the same cleanup guarantees. - Fakes for narrow concerns stay in the test file as anonymous-namespace classes (e.g.
RecordingImageStore,InMemoryRepo). Promote a fake totests/fakes/only when more than one test file needs it. - Path strings in expectations must use forward slashes. The fake normalizes everything to
generic_string(). Do not hard-code\separators. - Test names describe behavior, not implementation. Prefer "missing file is created with defaults" over "test_init_no_file".
- Add a
.cppto theadd_executablecall intests/CMakeLists.txt. No glob.
Required follow-ups
- After modifying any domain type field or alias you must extend the matching test in
domain_json_tests.cpp. - After modifying
formatTextForFsorparseIndexFromFilenameyou must extendfs_names_tests.cpp— these are byte-compatibility shims with the original Rust code. - After adding a new service in
core/you must add a corresponding<name>_service_tests.cppwith at least the happy-path and one error-path test. - After adding a new game's set source / card preview source you must add
tests/<name>_set_source_tests.cppand (if applicable)tests/<name>_card_preview_source_tests.cppmirroring the Magic and Pokemon files. Add them totests/CMakeLists.txt. - After adding or changing
ICardPreviewSourceoptional capabilities (for exampledetectFirstPrint/detectPrintVariants), you must extendcard_preview_service_tests.cppand the corresponding game source tests to cover both supported and unsupported paths.
Commands
- Configure with tests on:
cmake -S . -B build -DCCM_BUILD_TESTS=ON - Build the suite:
cmake --build build --target ccm_core_tests - Run all tests:
ctest --test-dir build --output-on-failure - Run a single test by name pattern:
./build/bin/ccm_core_tests --test-case="*nextImageIndex*" - Run a single suite:
./build/bin/ccm_core_tests --test-suite="MagicSetSource::parseResponse"
Anti-patterns
- Don't depend on
ccm_ui_wxfrom tests. UI is out of scope here. - Don't rely on file paths existing on the host (no
/tmp, noC:\Users\...). UseInMemoryFileSystem. - Don't add tests that require network access. The Pokemon stub test checks the message, not a real API call.