11. Testing
The solution contains a single test project, BatchConvertToCHD.Tests (xUnit, net10.0-windows), with 777 tests across 44 test classes, plus the shared FakeHttpMessageHandler and IszImageBuilder helpers.
Expected result on a clean machine: 762 passed, 15 failed. The 15 failures are a fixture problem, not a regression — see §11.5. A change that leaves exactly those 15 failing has broken nothing.
11.1 Running the Tests
dotnet test CSharp_BatchConvertToCHD.sln -c Release
# or, faster, without rebuilding:
dotnet test BatchConvertToCHD.Tests/BatchConvertToCHD.Tests.csproj --no-build
Requirements: the tests are run on Windows (the app project is net10.0-windows). Some tests need the app’s output directory to contain chdman.exe (it is copied by the build).
11.2 How Tests Are Structured
- Plain xUnit
[Fact]/[Theory]+[InlineData]. - Filesystem-dependent tests create a GUID temp directory per test class (
Path.GetTempPath() + $"{ClassName}_{Guid:N}") and clean it up inDispose. - HTTP-dependent tests inject an
HttpClientbacked byFakeHttpMessageHandler(the only shared helper): aFunc<HttpRequestMessage, HttpResponseMessage>or a convenience(HttpStatusCode, string content, string contentType)constructor, plus a staticWithAsyncHandlerhelper. - Internals are tested because
BatchConvertToCHD.csprojgrantsInternalsVisibleTo("BatchConvertToCHD.Tests"). - Integration tests are tagged
[Trait("Category", "Integration")]and read real sample files from fixed absolute directories (D:\Emulators\...). Most early-return when the samples are absent, so on machines without the sample folders they are effectively skipped (reported as passed).PbpFileIntegrationTestsis the exception — see §11.5. - Committed fixtures live in
BatchConvertToCHD.Tests/Fixtures/and are copied to the output directory by the csproj. There is one today,ecm-sample.ecm; it exists so the ECM decoder can be verified against the reference implementation’s own output without that tool being installed. - Format fixtures are built in code rather than committed where the format allows it:
IszImageBuilderwrites spec-conformant ISZ files, andMdsTests/RawCdImageDetectorTests/SplitImageJoinerTestssynthesise their descriptors and sector data. This keeps the repository free of disc-sized binaries.
11.3 Coverage by File
Application-level tests
| File | Focus |
|---|---|
AppConfigTests.cs |
Arm64 detection, chdman/7za exe names, API URLs/keys, app name, interval/timeout constants |
AppHttpClientTests.cs |
Singleton behavior, Accept header, TLS 1.2+1.3, dispose semantics, thread safety |
ArchiveServiceTests.cs |
ZIP extraction (real ZIPs built in-test), corrupt/unsupported/missing archives, bin-only archives → auto-cue, ExtractCsoAsync failure/cancellation, 7za fallback matrix, multi-part RAR / network detection, disk-full errors |
BinCueGeneratorTests.cs |
Auto-cue marker, cue content, mode alternation, read/rewrite |
BugReportApiSinkTests.cs |
Sink forwards Warning/Error/Fatal, ignores Debug/Info |
BugReportServiceTests.cs |
Report formatting (inner exceptions, depth), HTTP method/header/body, success/failure mapping, the full exclusion-pattern list (incl. case-insensitivity), no-HTTP-call for excluded messages |
CancellationHandlingTests.cs |
IsCancellationException, IsDiskSpaceException, IsCorruptionException, IsCrcErrorException and their mutual exclusivity |
CueNormalizerTests.cs |
Encoding detection (CP949/CP1251/CP932/UTF-8/UTF-32LE BOM), canonicalization, zero-padding resolution, unresolved names, MP3 transform hook, canonical write format |
CueWorkDirectoryTests.cs |
Work-dir creation rules, in-place BOM fast path, MP3→WAV decoding (fake + real NAudio decoders), end-to-end tests running real chdman.exe (BOM regression, cue/bin/mp3, cue/iso/mp3; skipped when chdman is absent) |
FileExtensionsTests.cs |
All extension constants and sets via reflection, cross-consistency, no duplicates |
FileItemTests.cs |
INotifyPropertyChanged, DisplaySize formatting (0 B … 1.5 TB) |
FileWatcherServiceTests.cs |
Start/Stop/Dispose, GetContextForMissingFile diagnostics with a real FileSystemWatcher, history eviction at 1000 entries, buffer-overflow clearing |
GameFileParserTests.cs |
cue/gdi/toc referenced-file extraction (quoted/unquoted/spaces/multi-file), encoding detection |
GitHubReleaseTests.cs |
Model defaults, JSON (de)serialization |
IsoSectorValidatorTests.cs |
Sector-size alignment warnings; descriptors/empty/missing not validated |
MainWindowHelperTests.cs |
StripUtf8BomIfPresentAsync, SelectChdmanErrorLine (skips progress lines, picks last real error) |
PathUtilsTests.cs |
SanitizeFileName, GetSafeTempFileName, path validation, relative paths, best-temp-directory selection, and CreateTempDirectoryOnSameVolume — including the property that actually matters: a cue written in the returned directory can reach the image by a non-rooted relative path, checked for every ready fixed volume |
PbpExtractionResultTests.cs |
Result-model defaults/setters |
RetryingFileOperationsTests.cs |
TryDeleteAsync/TryMoveAsync with real file locks (FileShare.None), read-only attribute clearing, retry-then-give-up, success-after-lock-release, missing-source/missing-destination semantics |
StatsServiceTests.cs |
POST method/URL/Bearer header/body, no-throw on 429/401/400/500/network errors |
UpdateServiceTests.cs |
Version parsing/normalization theories, new/older/minor/major comparisons, draft/prerelease skip, rate-limit and 5xx handling (no bug report), bug-report paths, invalid tags |
Format detection and recovery tests
| File | Focus |
|---|---|
DiscImageSignatureTests.cs |
Magic-byte identification of every DiscImageKind, IsArchive grouping, Describe phrasing, unknown/short/missing files |
RawCdImageDetectorTests.cs |
Sync-mark and mode-byte sniffing (MODE1/MODE2), rejection of cooked 2048-byte images and non-sector-aligned files, candidate extensions, generated cue content, and the cross-volume refusal that returns null |
InputFileFilterTests.cs |
A raw image is dropped when a sibling descriptor covers it (by base name and by cue text), kept when nothing covers it, and matching is directory-scoped and case-insensitive |
SplitImageJoinerTests.cs |
.001/.002 and .i00/.i01 set discovery and ordering, gaps, single-file non-sets, byte totals, and join output equality |
TrackBinCueBuilderTests.cs |
(Track N) set recognition and ordering, multi-FILE cue content, data track mode vs. AUDIO tracks, non-track-set rejection |
MdsTests.cs |
.mds header/session/track parsing, mode-to-cue-track mapping, sector-size classification (2352 / 2448 / 2368 / 2048), implausible session counts, .mdf lookup, subchannel stripping, MSF formatting, and the three MdsInputPreparer shapes |
IszHeaderTests.cs |
Every header field read at its documented offset (the test that catches an offset mistake), 64-bit image-size arithmetic for dual-layer sizes, signature and short-input rejection, and each refusal in GetUnusableReason including all four encryption modes |
IszDecoderTests.cs |
Chunk-entry bit-packing for 2/3/4-byte pointers, segment naming, round trips for zlib / bzip2 / stored / all-zero and mixed chunk types, trailing partial chunks, two-segment images with a chunk straddling the boundary, and the refusals: not-an-ISZ, encrypted, truncated file, truncated chunk table, corrupt compressed data, missing segment, and a segment from a different image |
CdSectorEccEdcTests.cs |
Sync/mode layout, EDC accumulation equivalence whole vs. in pieces, and the parity distinction that matters: Mode 1 parity covers the address, Mode 2 Form 1 parity does not and restores it afterwards; Form 2 gets an EDC and no parity |
EcmImageDecoderTests.cs |
Decoding the committed reference fixture to an expected SHA1, a guard asserting the fixture still contains all four block kinds, repeatability, output naming, and the refusals: no signature, truncated, wrong trailing checksum, corrupt data, missing file |
CueNormalizerFallbackTests.cs |
FILE-line fallbacks: bare name beside the cue, extension swap, single-FILE match by elimination — and that audio tracks and multi-FILE cues are deliberately not guessed |
How the ECM decoder is verified.
EcmImageDecoderTests.BuildReferenceImage()rebuilds, in code, the 12-sector image (4 Mode 1, 4 Mode 2 Form 1, 4 Mode 2 Form 2) thatFixtures/ecm-sample.ecmwas encoded from by Neill Corlett’s own encoder. Decoding the fixture and comparing proves both the block parsing and the regenerated EDC/parity match the reference implementation. The fixture was produced in a run that also confirmed the reverse direction: the real encoder reported stripping the parity from all twelve sectors — which it only does for sectors whose parity it agrees with — and the real decoder produced the identical SHA1. Regenerating the fixture requires that tool and is documented in the repository’s working notes; the guard test exists so a regeneration from a simpler image cannot quietly stop exercising the Mode 2 branches.
Library tests (CSOSharp / PBPSharp)
| File | Focus |
|---|---|
CsoFileTests.cs |
Open-error mapping, v1/v2 open, dispose behavior, block reads |
CsoStreamTests.cs |
Full stream contract: seek/read/zero-length, cross-block reads, throw semantics |
CsoHeaderTests.cs |
Header constants, v1/v2 validity, total blocks, index offset shift |
CsoFileIntegrationTests.cs |
Real .cso files: byte-for-byte block comparison vs. paired .iso, full extraction equality, stream parity |
PbpFileTests.cs |
Open errors, header/SFO/disc parsing, PbpError enum ordinal assertions, synthetic PBP+SFO builders |
PbpHeaderTests.cs |
Magic, size (0x28), defaults, validity |
SfoDataTests.cs / SfoEntryTests.cs / TocEntryTests.cs |
SFO lookups (incl. type mismatch), entry formats, TOC/track types |
CueSheetWriterTests.cs |
Generated CUE content: data/audio tracks, INDEX 00 with 150-frame lead-in, zero-clamp, padding |
PbpFileIntegrationTests.cs |
Real .pbp files: header/SFO/TOC, ExtractToBinCue byte-equality vs. original BIN, normalized CUE equality |
Gap: there are currently no CCDSharp unit tests — the test project does not reference CCDSharp (
BatchConvertToCHD.Tests.csproj:36–38); the only touch-point is the"CCDSharp: Conversion error"exclusion pattern. CCDSharp behavior is exercised indirectly only if a real.ccdfile flows through the app.
11.4 Writing New Tests — Quick Conventions
- File-scoped namespace
BatchConvertToCHD.Tests;using Xunitis global. - For filesystem tests, mirror the GUID-temp-dir +
IDisposablepattern. - For HTTP tests, use
FakeHttpMessageHandlerand pass theHttpClientto the internal constructor overloads (StatsService,BugReportService,UpdateService,AppHttpClient). - For chdman-dependent tests, early-return when
chdman.exeis absent fromAppContext.BaseDirectory. - Prefer building binary fixtures in code (see
IszImageBuilder) over committing them. Commit one only when the format cannot be generated trustworthily in-repo, as withecm-sample.ecm. - When a fixture asserts agreement with an outside implementation, add a guard test that the fixture still covers the cases it is meant to. A fixture can be regenerated more simply and silently stop testing anything.
- Run the full suite before pushing. A good run is 762 passed / 15 failed, with the failures being exactly the ones in §11.5.
Analyzer constraints worth knowing
The test project runs Meziantou.Analyzer too, and a few rules bite:
- One top-level type per file (nested types are fine).
- An
internaltype cannot appear in a public xUnit[Theory]signature (CS0051) — use a[Fact]. StringBuilder.AppendLine($"...")trips MA0011; pass anIFormatProvideror build the string first.Assert.SkipUnlessis xUnit v3; this project is on 2.9.3, so conditional tests early-return instead.
11.5 The 15 Expected Failures
On a machine without the integration sample folders, these 15 fail rather than skip. They are pre-existing and unrelated to the conversion pipeline:
| Count | Tests | Why |
|---|---|---|
| 15 | PbpFileIntegrationTests.* |
They need a real .pbp sample that is not in the repository. Unlike the CSO integration tests, they assert on the discovered-sample collection before checking whether it is empty, so an absent sample surfaces as Assert.NotEmpty() Failure: Collection was empty instead of an early return. |
The PBP group is worth fixing — by adopting the CSO tests’ early-return pattern — but it does not indicate a defect in the application.