Skip to content

Testing

Valkerran edited this page Oct 12, 2026 · 8 revisions

Testing

Three xUnit projects. All must be green before any commit — CI runs them on every push and pull request, on Linux, Windows and macOS.

dotnet test PCEdit.SaveFileHandler.Tests/PCEdit.SaveFileHandler.Tests.csproj
dotnet test PCEdit.App.Core.Tests/PCEdit.App.Core.Tests.csproj
dotnet test PCEdit.Desktop.Tests/PCEdit.Desktop.Tests.csproj    # headless UI tests
Project Covers
PCEdit.SaveFileHandler.Tests Framing, JSON records, key order, number format, BOM behaviour, planet hash
PCEdit.App.Core.Tests Workspace, inventory editor, codecs, catalogs, ViewModels, localization parity

The load-save invariant

The most important assertions in the repo. An unedited load → save must produce the same bytes, so that a diff after an edit shows only the edit:

Test What it pins
RoundTrip_RealSampleSaveFile_ReserializesCharacterForCharacter Whole-file, string-exact
RoundTrip_RealSampleSaveFile_PreservesEveryKeyAndValue No key or value lost, ordering aside
SaveThenLoad_OfAnUnchangedSave_IsByteIdenticalOnDisk The same, through real file I/O
RoundTrip_PreservesKeyOrderCharacterForCharacter WorldObject key order replayed exactly
RoundTrip_PlayerTypedText_IsWrittenWithoutEscapes Accented, CJK, emoji and HTML-sensitive text is not turned into escapes (v1.7.1)
Serialize_ReproducesTheGameFraming The \r, \r@\r, \r@ framing

Byte-order-mark tests

The Game Pass corruption bug (v1.1.1) has a test per branch:

  • Save_ToANewPath_WritesUtf8WithBom — "Save As" matches the Steam game's own output.
  • Save_OverAFileThatHasABom_KeepsTheBom
  • Save_OverAFileThatHasNoBom_KeepsItBomLess
  • Save_OverARealBomLessGamePassSave_KeepsItBomLess — against the raw WGS fixture, on real bytes.

Fixtures

Fixture Game Platform BOM Role
Standard-2.json 2.008 Steam yes The backward-compatibility regression; single planet
mini-save.json 2.008 hand-authored yes Rare object shapes (count, linkedWo, container text, a logistics container, a deliberately unknown key)
Humble-2.102.json 2.102 Steam yes Single planet (Humble)
Humble-2.103.json 2.103 Steam yes Current game version: the same Humble world, re-saved by 2.103
Interplanetary-2.102.json 2.102 Xbox / Game Pass no The BOM-less path, and the only multi-planet save

Rules for handling them:

  • All are byte-exact and marked -text in .gitattributes. Nothing may normalise their line endings.
  • Never let a load → save round-trip overwrite one. Tests copy to a temp path first.
  • Interplanetary-2.102.json must never gain a BOM. It is the only proof the Game Pass path works on real bytes.
  • Interplanetary-2.102.json cannot join SaveThenLoad_OfAnUnchangedSave_IsByteIdenticalOnDisk — that theory saves to a path that does not exist yet, which is "Save As", and correctly emits a BOM.
  • mini-save.json has no trailing newline. Regenerate it with a byte-writing script, never an editor.

SaveFileFixtures (in the handler tests) builds a save with a distinct, greppable marker value in every property, so a round trip can assert each value lands back in the exact property it started in — that is what pins the section-order contract.

What else is covered

Format details GameNumberFormatTests (an unedited number keeps its exact text; an edited whole number serializes as N.0), GameFormatKeyMappingTests (the abbreviated key spellings a symmetric model round-trip cannot catch — demandGrps, supplyGrps, linkedWo, count, text, and logisticsPaused being written only when the save had it), WorldObjectConverterTests, JsonRecordSerializerTests (camelCase, null omission, and an InvalidDataException that names the offending section, record and key; duplicate keys, a null in a modelled key and a missing always-written key are each refused by name), ModelRequiredKeyTests (every model property is either required or nullable).

Planet identity PlanetHashTests checks the hash against the integers the game actually wrote, including resolving every placed world object on the multi-planet fixture.

App layer SaveFileWorkspaceTests, InventoryEditorTests (moves, full-destination refusal, grouping, the Move dialog's options), PlanetIndexTests, the codec tests, ItemCatalogTests / LogisticsGroupCatalogTests (the embedded JSON parses and every item's category exists), IdSearchTests and InventoryGroupTests (id-prefix matching, text vs id search, narrowing a card to its matching items while keeping the true fill), and ViewModel tests for Overview, Inventories, the Move dialog (SelectInventoryViewModelTests), Teleport and the logistics editor. Since v1.8.0 also JsonSettingsStoreTests and FileSaveBackupServiceTests (both work in a throwaway folder under the temp directory), UnexpectedErrorReporterTests and QuietTraceListenerTests.

Atomic writes AtomicFileTests (handler tests, v1.8.0) pin the write itself: no temp file left behind, a failed write leaves the old bytes, a read-only file is refused, a symbolic link survives and a chain of links ends where it really ends, permissions are kept. The link and permission cases return early on Windows, where creating a link needs a privilege; CI runs the tests on Linux.

Localization LocalizationCatalogTests enforce key parity across all 15 catalogs, non-empty values, matching {0} placeholders, and that every LocKeys constant exists. See Localization.

Test doubles

PCEdit.App.Core.Tests/Fakes/ holds hand-written fakes for the platform interfaces (FakeNavigationService, FakeDialogService, FakeSaveFileStore, FakeSaveBackupService, FakeScreenReaderAnnouncer) — no mocking framework. The test stack is xUnit 2 with Microsoft.NET.Test.Sdk 18 and xunit.runner.visualstudio 4; there is no coverage collector. Dependabot keeps the test projects on the same versions through one grouped update. PCEdit.Desktop.Tests is the exception to "xUnit 2": it is on xUnit v3, because Avalonia's headless test package is built on it and the two cannot be mixed in one project. Fixtures/WorkspaceFixtures.cs builds a loaded workspace to drive ViewModels against.

UI tests

PCEdit.Desktop.Tests runs the real main window and dialogs on Avalonia's headless platform, with no display. Each test builds a UiHarness, which gives it:

  • fresh services and a real MainWindow;
  • a private folder for settings, backups, the log and a copy of a fixture save, so a test never reads or writes your real PCEdit files;
  • helpers to load the save, make an edit, press Save, and read or click the dialogs that opened.

They cover what only exists in the desktop head: the disclaimer gate, the Save button and its status colour, the "save file has changed" prompt, the "Settings not saved" and "Unexpected error" dialogs, and the Move dialog's status line being readable by assistive technology.

Two things to know when adding one:

  • The app keeps running after an unexpected error, which would let a broken test pass. The harness records any exception that reaches the UI thread and fails the test, unless the test sets ExpectUnexpectedError.
  • Each test has its own localizer. Text a view model provides follows it; text bound in XAML with {m:Loc} stays in the start-up language, so assert language changes on view-model text.

What is not covered

Layout, the theme and colours are not asserted: the UI tests check that the right control has the right text and style class, not how it is drawn. Those are verified by running the app, and on Linux by the manual checklist and WSL distro matrix in Building & Packaging. The Inventories, Teleport, Terra Tokens and Repair pages have view-model tests but no UI tests yet.

Clone this wiki locally