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 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 the Avalonia project (
CHDStudio.csproj) grantsInternalsVisibleTo("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).PbpFileIntegrationTestsis 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; andlaserdisc-small.avi, a 4-frame YUY2 + PCM AVI written with CHDSharp’s own test writer, so thecreateldpath is exercised without needing a real laserdisc dump. - Format fixtures are built in code rather than committed where the format allows it:
IszImageBuilderwrites ISZ files the way real UltraISO files are laid out (spec-conformant headers, obfuscated tables, stripped bzip2 headers, optional UltraISO checksums), 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, 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) 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 (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,IsoWriterwhole/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
- File-scoped namespace
CHDStudio.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(the reference encoder’s own output) or the WinRAR-produced RAR volume set (there is no RAR writer in the repository). - 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. 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
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 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).