Skip to content

Troubleshooting

Pixnop edited this page Jul 13, 2026 · 5 revisions

Troubleshooting

AtlasSetupException

Thrown when Atlas cannot prepare the test environment. Common causes:

  • VINTAGE_STORY is unset, or does not point at a folder containing VintagestoryAPI.dll. Atlas needs this to locate the game's binaries and libraries for assembly resolution.
  • VintagestoryAPI.dll is present in the test output without VintagestoryAPI.pdb. The boot preflight fails fast because the game cannot initialize its logger without the pdb; see the dedicated section below for the root cause and fixes.
  • A mod path in [assembly: AtlasMods(...)] (or [AtlasWorld(Mods = ...)]) does not resolve to an existing folder, .zip, or .dll relative to the test assembly's output directory. ModStager.Stage names every missing path in the exception message. See Mod Staging for path resolution rules.
  • Bridge staging copy failure. ModStager.StageBridge wraps any file system failure (locked file, permissions, missing source) copying AtlasBridge.dll into the scratch mods folder, naming both the source and destination paths and carrying the underlying file system error as the inner exception.
  • "Bridge mod did not start" or a mod loader rejection (bad modinfo.json, dependency resolution failure): the exception message includes the mod loader's own report. Check the embedded server's own logs, which land in the scenario's scratch data path (printed on failure; look for a temp directory created per test class under the OS temp folder).
  • Two hosts requested concurrently: HostRegistry.GetOrCreateAsync throws this if a second scenario class tries to get a host while one is already in flight. This should only happen if [assembly: CollectionBehavior(DisableTestParallelization = true)] is missing; see the one-live-server section below.
  • "Test player 'X' did not finish joining the world": the server rejected the synthetic join. Most likely the player name breaks the engine's rule (letters, digits, underscores and dashes only, 16 characters at most - the number one cause in practice); otherwise a game network-version drift relative to the Atlas build. The server's own log (in the scratch data path named by the message) states the exact reason.
  • "A test player named 'X' already joined this class's world": the class host's world is shared by every scenario in the class, and a player joined by an earlier scenario is still connected. Reuse that ITestPlayer (share it via a field), join under a different name (several players per world are supported), or isolate the scenario with [AtlasScenario(FreshWorld = true)].

TypeInitializationException in LoggerBase at server boot (missing VintagestoryAPI.pdb)

Symptom (on Atlas versions without the preflight): every scenario fails during the first server boot with

TypeInitializationException: The type initializer for 'Vintagestory.API.Common.LoggerBase' threw an exception.
 ---> NullReferenceException

surfacing from ServerHost.ConfigureEngineStatics (the first ServerLogger construction). Current Atlas versions detect the condition up front and throw an AtlasSetupException naming the offending directory instead.

Root cause (verified by decompiling Vintage Story 1.22.0): LoggerBase's static constructor deliberately throws a dummy exception and derives its SourcePath from new StackTrace(e, fNeedFileInfo: true).GetFrame(0).GetFileName(). That file name only exists when the pdb sits next to the loaded VintagestoryAPI.dll; without it, GetFileName() returns null and the subsequent .Split(...) throws. A VintagestoryAPI.dll copy in your test output wins default assembly probing over the game install's copy, so if that copy shipped without VintagestoryAPI.pdb, the boot dies.

How you get there: a vendored dll (checked into the repo without its pdb), a custom copy step, or a <Reference> whose HintPath points at a location holding only the dll. A plain <Reference> with a HintPath into the game install is safe - MSBuild copies related files (pdb, xml) along with the dll.

Fix, either:

  • ship the matching VintagestoryAPI.pdb next to the dll (it must be the pdb produced with that exact dll - a pdb from a different game version has the wrong debug GUID and the crash comes back), or
  • stop copying the dll (<Private>false</Private> on the reference) so probing falls through to Atlas's resolve hook, which loads the game install's copy with its pdb beside it.

Atlas.E2E.targets (shipped buildTransitive in the NuGet package) also emits a build warning when it sees VintagestoryAPI.dll land in the output directory without its pdb.

MissingMethodException (often inside Newtonsoft.Json or ServerConfig code)

If you installed Pixnop.Atlas.XUnit from NuGet, this almost always means VINTAGE_STORY was unset at build time: the package's buildTransitive target needs it to find the game's own Newtonsoft.Json.dll and copy it over the test SDK's transitive copy. For NuGet consumers this target runs automatically, no <Import> needed; set VINTAGE_STORY and rebuild.

If you are building Atlas from source instead (a ProjectReference rather than a PackageReference), the buildTransitive packaging step never runs, so you additionally need <Import Project=".../build/Atlas.E2E.targets" /> in the test project. Verify $(VintageStoryPath) resolves (it defaults from the VINTAGE_STORY environment variable), add the import if missing, rebuild, and re-run. See Getting Started.

VINTAGE_STORY unset

Both the build (compiling against VintagestoryAPI.dll) and the test run (booting the embedded server) need this environment variable. Set it once in your shell profile or CI environment to the folder containing VintagestoryAPI.dll:

export VINTAGE_STORY=/opt/vintagestory

ScenarioTimeoutException vs ServerCrashedException

  • ScenarioTimeoutException: thrown either by World.Until(...) when timeoutTicks elapses (tick-based, carries TicksWaited), or by the off-thread Watchdog when TimeoutMs elapses (wall-clock, independent of the server's own ticking). In the watchdog case, HostRegistry.MarkDead also marks the class host dead, since the game thread may still be running the abandoned scenario.
  • ServerCrashedException: thrown when the embedded server itself dies mid-scenario. The original crash is captured as the exception's inner exception and rethrown into the owning xUnit test; the class host is marked dead the same way, so remaining scenarios in the class fail fast with a clear message rather than cascading into opaque timeouts.

Since 0.9.0, ServerCrashedException also covers ENGINE-initiated shutdowns. When the engine stops itself (its reaction to an unhandled exception in one of its own server threads: "Caught unhandled exception in thread '...'", stop reason "Exception during Process"), ServerMain.Process() becomes a silent sleep loop; earlier Atlas versions spun on it forever, so any engine crash surfaced as a TimeoutMs hang instead of a red test. The game-thread pump now notices the engine's stop, faults pending tick waiters with the real cause, and fails the scenario promptly with a ServerCrashedException whose message points at the server's own logs (server-main.log in the scenario's scratch data path): the engine keeps the stop reason and the failing thread's stack only there, so that log is where the diagnosis lives. Atlas's own stop paths are unaffected: they cancel the pump before ever calling the engine's Stop.

The VS shutdown NRE flake

Vintage Story (confirmed on 1.22.2 and 1.22.3) has a known shutdown flake in the embedded server: it occasionally throws a NullReferenceException from ServerSystemMonitor.Dispose() while tearing down. Atlas catches and swallows this specific failure at teardown; it does not affect scenario results and is not something a test author needs to work around. Tracked upstream as issue #8 for visibility.

The swallow is instrumented rather than silent: when it happens, the run's stderr carries

[Atlas] server.Dispose() threw the known Vintage Story shutdown NRE (issue #8): ...

followed by the stack. The suspected trigger is a previous class host whose game thread never exited within the join bound (its late teardown nulls process-wide engine statics under the next host); that case is logged too, as an [Atlas] game thread did not exit within ...s and was abandoned warning on stderr, so an occurrence of the NRE can be correlated back to the abandoned teardown that likely caused it.

Scenario hangs instead of failing

await World.Until(...) timeouts are tick-based, which only elapses while the server is actually ticking. If the server itself is stuck, the scenario's TimeoutMs watchdog (default 60 seconds of wall-clock time, set via [AtlasScenario(TimeoutMs = ...)]) is what actually fails the test. If a run hangs for substantially longer than that, check for a ConfigureAwait(false) in the scenario body: it detaches the continuation from the game thread's queue, which can produce hangs that the watchdog cannot observe correctly because the continuation itself never reaches the awaited task in the expected way. See Writing Scenarios for the full game-thread rule.

One-live-server violations

Atlas hosts at most one live server per process. If a second scenario class requests a host while another is still in flight, HostRegistry.GetOrCreateAsync throws AtlasSetupException with a message pointing at the missing [assembly: CollectionBehavior(DisableTestParallelization = true)] declaration. This attribute is required in every Atlas test assembly; without it, xUnit may attempt to run scenario classes concurrently, which Atlas cannot support (see Architecture for why: the embedded server relies on process-wide statics).

Clone this wiki locally