11. Testing

The solution contains a single test project, CHDStudio.Tests (xUnit, net10.0-windows), with 1116 tests across 57 test classes: 1088 unit tests plus 28 integration tests that need a local sample folder (see §11.5), plus the shared FakeHttpMessageHandler and IszImageBuilder helpers.

Expected result on a machine without the local sample folders: the 1088 unit tests pass, while the 28 integration tests fail on the missing sample data. CI excludes them with --filter "Category!=Integration"; a change that leaves exactly those 28 failing has broken nothing.

11.1 Running the Tests

dotnet test CSharp_CHDStudio.sln -c Release
# or, faster, without rebuilding:
dotnet test CHDStudio.Tests/CHDStudio.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 in Dispose.
  • HTTP-dependent tests inject an HttpClient backed by FakeHttpMessageHandler (the only shared helper): a Func<HttpRequestMessage, HttpResponseMessage> or a convenience (HttpStatusCode, string content, string contentType) constructor, plus a static WithAsyncHandler helper.
  • Internals are tested because the Avalonia project (CHDStudio.csproj) grants InternalsVisibleTo("CHDStudio.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). PbpFileIntegrationTests is the exception — see §11.5.
  • Committed fixtures live in CHDStudio.Tests/Fixtures/ and are copied to the output directory by the csproj. There are four groups: ecm-sample.ecm, so the ECM decoder can be verified against the reference implementation’s own output without that tool being installed; rar-multipart/set.part1.rar…set.part5.rar, a real WinRAR store-mode volume set holding a known 3500-byte payload, so multi-volume RAR extraction can be tested without WinRAR at test time; MdsV2/ (the MIT-licensed mdsx test images: plain, compressed, single-file .mdx, and password-encrypted), so the v2/MDX decryption pipeline is pinned to real files; and laserdisc-small.avi, a 4-frame YUY2 + PCM AVI written with CHDSharp’s own test writer, so the createld path is exercised without needing a real laserdisc dump.
  • Format fixtures are built in code rather than committed where the format allows it: IszImageBuilder writes ISZ files the way real UltraISO files are laid out (spec-conformant headers, obfuscated tables, stripped bzip2 headers, optional UltraISO checksums), and MdsTests/RawCdImageDetectorTests/SplitImageJoinerTests synthesise 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, real multi-part RAR extraction from the first and a later volume, missing-volume / missing-first-volume classification, renamed .001 RAR sets, SharpCompress decoder-crash rethrow, 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
CcdParserTests.cs / CcdModelTests.cs CloneCD .ccd parsing (disc/session/track fields, MSF formatting), track-count bounds and overflow-safe parsing, and IsoWriter output: whole-sector extraction and rejection of a partial trailing sector
ChdSharpEncoderServiceTests.cs In-process createcd/createdvd/createhd/createraw/createld round-trips verified with Chd.CheckFile, header unit sizes, per-hunk progress callbacks, extraction-command detection for CD/DVD/laserdisc CHDs, unsupported-command rejection, rejection of inputs whose size is not a whole number of units, and non-AVI createld rejection
ChdSharpProgressLoggerTests.cs 10%-step throttling for reader and encoder reports, cumulative encoder ratio, repeated percents logged once, zero-total/zero-hunk edge cases, byte-count formatting
ChdChecksumReportTests.cs Report path/name, whole-image SHA-1 equals a fresh ComputeHashes pass, per-track blocks for CD, whole-image-only blocks for DVD, all three algorithms present
ChdInfoReportTests.cs CD report (version/type/metadata/tracks/hunks/SHA-1), DVD report without a track section, unreadable-header text instead of an exception
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
RecoveredImageClassifierTests.cs Recovery layout routing: raw 2352 sniffing, 2048 DVD classification, 2336/2324 Mode 2 cues, 2448/2368 subchannel stripping down to a cued 2352 image, and the skip reason for sizes that fit no layout
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, matching is directory-scoped and case-insensitive; RemoveRarVolumeParts keeps only the first partNN volume per set (the lowest when the first is missing) and leaves lone parts and plain archives alone — and ResolveOutputCollisions keeps the first non-archive input of each colliding output group, order-independently, including three-way collisions and all-archive groups
IoThroughputCounterTests.cs Throughput sampling math (bytes/delta, zero/negative deltas), counter reset, the platform availability probe, and child-process sampling (the chdman speed fix)
LegacyCleanupServiceTests.cs Removal of the legacy logs/Resources folders and maxcso.exe/psxpackager.exe, missing paths, in-use files, and never throwing
ScreenshotServiceTests.cs Preferred/fallback directory layout (%LocalAppData%\CHDStudio\screenshots first), timestamped file names, directory creation, fallback on unwritable folders, and null when both fail
SplitImageJoinerTests.cs .001/.002 and .i00/.i01 set discovery and ordering, gaps, single-file non-sets, byte totals, and join output equality
RarVolumeSetTests.cs partNN.rar name parsing (padding, case, rejects), first-volume resolution from a later part, sets kept apart within a folder, ordered .partNN / .001 / old-style .rNN volume enumeration, total sizes, and first-volume name reconstruction
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, medium type, low-nibble mode-to-cue mapping, sector-size classification (2352 / 2448 / 2368 / 2336 / 2048), implausible session counts, .mdf lookup (declared footer names incl. UTF-16 and *.mdf wildcards, exact, decorated, ambiguous, subdirectory, split .i00, Unicode composition, multi-file join), subchannel stripping, pregap rebuild (INDEX 00 with and without .pregap.bin), MSF formatting, and the prepared shapes
MdsPregapLayoutTests.cs Per-track v2 pregap layout from StoredDataSectors: mixed stored/missing pregaps are rebuilt selectively with every INDEX 00 at the right LBA, all-stored images get INDEX 00 without a rebuild, and the first track’s pregap before LBA 0 is not materialized
MdsV2Tests.cs MDS v2 / MDX: RIPEMD-160 known vectors, descriptor decryption and parsing, encrypted/compressed detection, an MDX container larger than the v1 descriptor cap, byte-for-byte decoding of the mdsx plain / compressed / password-encrypted fixtures, and the wrong-password failure
IszHeaderTests.cs Every header field read at its documented offset (the test that catches an offset mistake), the 64-byte UltraISO checksum fields, 64-bit image-size arithmetic for dual-layer sizes, signature and short-input rejection, legal no-chunk-table headers, and each refusal in GetUnusableReason: all four encryption modes, version ≠ 1, a later segment opened directly, zero-sector headers, zero/implausible chunk sizes and unreadable pointer widths
IszDecoderTests.cs Chunk-entry bit-packing for 2/3/4-byte pointers, the table obfuscation itself, segment naming for the .i01 and both .partNN schemes, round trips for zlib / bzip2 (stripped BZh) / stored / all-zero (with and without recorded lengths) and mixed chunk types, trailing partial chunks, checksummed images (whole, split and no-table), images with no chunk table, two-segment images with a chunk straddling the boundary, and the refusals: not-an-ISZ, encrypted, truncated file, truncated chunk table, corrupt compressed data, checksum mismatch, 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) that Fixtures/ecm-sample.ecm was 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 (MDSSharp / CSOSharp / PBPSharp / ISZSharp)

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
PbpDiagnosticsTests.cs Diagnostics classification of PBP failure codes into user-data vs. app-bug conditions
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

CCDSharp is covered in the unit suite (CcdParserTests, CcdModelTests, IsoWriter whole/partial-sector tests); the library is referenced by the test project. The older note that no CCDSharp tests existed is obsolete.

11.4 Writing New Tests — Quick Conventions

  1. File-scoped namespace CHDStudio.Tests; using Xunit is global.
  2. For filesystem tests, mirror the GUID-temp-dir + IDisposable pattern.
  3. For HTTP tests, use FakeHttpMessageHandler and pass the HttpClient to the internal constructor overloads (StatsService, BugReportService, UpdateService, AppHttpClient).
  4. For chdman-dependent tests, early-return when chdman.exe is absent from AppContext.BaseDirectory.
  5. Prefer building binary fixtures in code (see IszImageBuilder) over committing them. Commit one only when the format cannot be generated trustworthily in-repo, as with ecm-sample.ecm (the reference encoder’s own output) or the WinRAR-produced RAR volume set (there is no RAR writer in the repository).
  6. 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.
  7. Run the full suite before pushing. On the maintainer’s machine a full run is 1116 passed / 0 failed; CI runs the unit tests only, via --filter "Category!=Integration" (1088 tests). The integration classes’ behaviour without samples is described 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 internal type cannot appear in a public xUnit [Theory] signature (CS0051) — use a [Fact].
  • StringBuilder.AppendLine($"...") trips MA0011; pass an IFormatProvider or build the string first.
  • Assert.SkipUnless is xUnit v3; this project is on 2.9.3, so conditional tests early-return instead.

11.5 The PBP Integration Sample Folder

PbpFileIntegrationTests reads real .pbp files from a local sample folder (D:\Emulators\...\PsxPackager on the maintainer’s machine). When that folder is absent the group fails rather than skips — the tests 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. These failures do not indicate a defect in the application; adopting the CSO tests’ early-return pattern would fix the ergonomics.


11.6 CHDBattleTest Battleground (historical)

The CHDBattleTest console harness (chdbattle) that pitted chdman (MAME) against the CHDSharp encoder — which at the time shipped as the standalone CHDSharp.exe CLI — on a corpus of real CHDs was removed from the solution in 3.5.1 — its purpose was fulfilled. Per file it ran timed decode battles (extractraw, extractcd, extractdvd, extracthd), encode battles (copy, createcd, createdvd, createhd with a chosen codec), SHA-256 product-parity checks, and 224+ cross-verifications. Results landed as results.csv, report.md, battle.log, console.log in the output root, resume-safe.

Final status (CHDSharp 1.4.3, corpus of 56 discs — 43 CD + 3 GD-ROM, 10 DVD, 3 HDD): zero mismatches. Every parity battle passed byte-identically and all cross-verifications agreed, i.e. the CHDSharp encoder produces byte-identical CHDs to chdman 0.289 on this corpus. History: 1.4.1 had 46 parity fails (createdvd 10, copy:zstd 18, createcd:cdzl 18), 1.4.2 fixed the DVD group (36 remaining), and 1.4.3’s stale work-buffer + LZMA match-finder fix closed the rest.

The CHDSharp.exe CLI itself was later removed from the repository and the release zips; the encoder now runs in-process through ChdSharpEncoderService (CHDSharpLib). ChdSharpEncoderServiceTests covers the replacement: in-process createcd, createdvd, createhd, createraw and createld round-trips, each verified with Chd.CheckFile (the laserdisc case uses the committed laserdisc-small.avi fixture).


This site uses Just the Docs, a documentation theme for Jekyll.