Skip to content

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 15:16
· 68 commits to main since this release
bdc84a6

Added

  • [AtlasScenario(RestartWorld = true)]: restart-same-world isolation (issue #54), completing
    the isolation trilogy: FreshWorld = true recycles the host for a brand-new world (strongest
    isolation, one full boot); RollbackWorld = true restores the same host's world snapshot
    without a reboot (fastest, no restart); RestartWorld = true restarts the server for real
    and carries the world over, for scenarios asserting on what actually persists. Before the
    scenario runs, the class host is shut down gracefully (the engine's shutdown persists the
    world save), the save is harvested, and a replacement host boots against it in a fresh
    scratch directory (the harvested file is deleted once the replacement is up). The scenario
    then runs on a genuinely restarted server whose world survived a real save/load round trip,
    so persistence scenarios (SaveGame moddata, manifests, whatever a mod writes for reload) are
    finally writable. Costs one full boot, same as FreshWorld: the boot IS the round trip under
    test. Semantics: the three world flags are mutually exclusive (any combination is a setup
    error resolved before any boot); StrictIsolation with RestartWorld is a setup error too
    (a restart either works or fails the scenario hard, so there is no silent degrade to be
    strict about); a failed harvest fails the scenario with an AtlasSetupException and a
    replacement boot crash surfaces as-is, never a silent fallback; a class-level
    [AtlasWorld(SaveFile = ...)] composes naturally, the restart carrying forward the CURRENT
    world state (mutations included), not the original fixture; joined test players do not
    survive a restart (their connections die with the host), so requesting one with players
    joined fails the scenario instead of silently dropping them. The per-class isolation summary
    line now also reports N restart(s).

  • atlas --version (or atlas version): prints the package version (the informational version
    without the +sha build metadata) and exits 0; needs no scenario assembly and no VINTAGE_STORY.

  • Rollback degrades are now visible in the standard workflow (issue #53). When a
    [AtlasScenario(RollbackWorld = true)] request cannot be honored and falls back to a full
    host recycle, the degrade is attached to the scenario's own test output, with the classified
    reason (players joined, mini-dimension chunks loaded, engine drift, or a generic
    capture/restore failure), the one-line failure detail and the measured cost of the fallback
    recycle. That output travels inside the test result, so it shows up in the IDE test explorer,
    in the TRX report's per-test StdOut and under atlas run (which now prints non-empty test
    output indented beneath the PASS/FAIL line); the one-line stderr warning remains. Previously
    the stderr line was the only signal, invisible at normal dotnet test verbosity, so a suite
    could silently pay full recycles everywhere (e.g. a fixture pregenerating mini-dimensions at
    boot poisoning every rollback) while the author believed rollback was active.

  • [AtlasScenario(RollbackWorld = true, StrictIsolation = true)]: opt-in strict mode for
    suites that treat the rollback speedup as a contract. A degraded rollback FAILS the scenario
    with an AtlasIsolationException carrying the degrade reason instead of silently recycling.
    The host is still recycled before the failure surfaces, so later scenarios of the class keep
    running on a clean world: strictness changes visibility, not safety. A genuine server crash
    during the rollback attempt is never re-labelled and keeps surfacing as
    ServerCrashedException. Setting StrictIsolation without RollbackWorld is a setup error
    (only a rollback request can degrade, so there is nothing to be strict about).

  • Per-class isolation summary: when a scenario class hands its host off (to the next class, the
    fixture harvest or process exit), Atlas prints one stderr line with the class's isolation
    outcomes, e.g. [Atlas] isolation summary for MyMod.Tests.MyScenarios: 2 rollback(s) succeeded, 1 degraded to a full host recycle (mini-dimension chunks loaded x1), 0 FreshWorld recycle(s). Classes that never requested rollback isolation stay silent. This is the honest
    place to see the isolation cost, which per-test durations hide (the restore or recycle happens
    outside the timed test body).

  • atlas fixture <Scenarios.dll> --scenario <substring> --out <fixture.vcdbs> [--force]: builds
    the prebuilt world save that [AtlasWorld(SaveFile = "fixtures/myworld.vcdbs")] boots against,
    turning what used to be folklore (run a builder scenario, then harvest the save its graceful
    teardown wrote from the host's scratch data path) into a first-class command. The builder is an
    ordinary [AtlasScenario] whose side effect is building the world (place blocks, run commands,
    seed data): --scenario must select exactly ONE scenario by display-name substring (zero or
    several matches is a usage error listing the candidates, exit 2), the run uses the same
    in-process mechanics as atlas run --filter, and after the scenario passes and the host tears
    down gracefully the persisted save is copied to --out (parent directories created; an
    existing file is only overwritten with --force). A failing builder writes nothing and exits 1,
    so a broken builder can never silently produce a half-built fixture.

  • IWorldSession.PlaceSchematic(path, origin), plus an EnumReplaceMode overload: loads a
    block schematic (.json, e.g. a worldedit export) and places it with its minimum X/Y/Z
    corner at the given position, returning the placed block count. This makes "how do I load a
    prebuilt structure in a test" first-class next to the world-fixtures story:
    [AtlasWorld(SaveFile = ...)] loads a whole prebuilt world, PlaceSchematic stamps a single
    prebuilt structure into the running one. Paths resolve like every other fixture path
    (absolute, or relative to the test assembly's directory), a missing or malformed file fails
    with AtlasSetupException carrying the resolved path and the engine's error, and placement
    mirrors the engine's worldedit import: blocks, decors, block entities (with their saved
    data) and any entities stored in the schematic.

  • Fail-fast preflight for a stale test-output VintagestoryAPI.dll copy (issue #49, option 1):
    before the embedded server boots, Atlas compares the test output's copy byte-for-byte
    (size + SHA-256) against the VINTAGE_STORY install's copy and fails with a clear
    AtlasSetupException naming both files (size, short hash, assembly version) and both remedies
    (rebuild against the target install, or copy the install's dll AND pdb over the local ones).
    This is the multi-install trap of differential runs: repointing VINTAGE_STORY at a different
    install without rebuilding loads the target's VintagestoryLib against the stale local
    VintagestoryAPI and dies deep into boot with a cryptic MissingFieldException. A version
    check would not catch it (forks rebuild the API at the same assembly version), so the
    comparison is content-based. Consumers that do not copy the dll (Private=false) are
    unaffected: the check skips silently when no local copy exists.

Fixed

  • In-process AssemblyRunner disposal race (issue #59): everywhere Atlas drives xunit's
    AssemblyRunner in-process (atlas run, the worker mode and the engine's own nested-runner
    E2E test), disposal now waits (bounded, 30 s) for the runner to report Idle plus a short
    grace, and LEAKS the runner instead of disposing it if it never idles. xunit.runner.utility
    2.x disposes the runner's completion events while its worker thread can still be heading into
    its final WaitOne() (AssemblyRunner.cs:263); the worker then dies with an unhandled
    ObjectDisposedException on a pool thread, which kills the whole process. That is how one
    flaky nested run sank an entire green CI leg: a leaked event in a finishing process is
    harmless, a disposed-while-awaited event is a process kill.