v0.7.0
Added
-
[AtlasScenario(RestartWorld = true)]: restart-same-world isolation (issue #54), completing
the isolation trilogy:FreshWorld = truerecycles the host for a brand-new world (strongest
isolation, one full boot);RollbackWorld = truerestores the same host's world snapshot
without a reboot (fastest, no restart);RestartWorld = truerestarts 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 asFreshWorld: 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);StrictIsolationwithRestartWorldis 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 anAtlasSetupExceptionand 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 reportsN restart(s). -
atlas --version(oratlas version): prints the package version (the informational version
without the+shabuild 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 underatlas 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 normaldotnet testverbosity, 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 anAtlasIsolationExceptioncarrying 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. SettingStrictIsolationwithoutRollbackWorldis 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):--scenariomust 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 asatlas 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 anEnumReplaceModeoverload: 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,PlaceSchematicstamps 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
withAtlasSetupExceptioncarrying 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.dllcopy (issue #49, option 1):
before the embedded server boots, Atlas compares the test output's copy byte-for-byte
(size + SHA-256) against theVINTAGE_STORYinstall's copy and fails with a clear
AtlasSetupExceptionnaming 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: repointingVINTAGE_STORYat a different
install without rebuilding loads the target'sVintagestoryLibagainst the stale local
VintagestoryAPIand dies deep into boot with a crypticMissingFieldException. 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
AssemblyRunnerdisposal race (issue #59): everywhere Atlas drives xunit's
AssemblyRunnerin-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 reportIdleplus 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 finalWaitOne()(AssemblyRunner.cs:263); the worker then dies with an unhandled
ObjectDisposedExceptionon 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.