Namespace Serenity.Tests.Shared.Editor
Classes
AssetFileOnDisk
Reads an asset the way a game project finds it after the editor is closed: from its file, never from the AssetDatabase's in-memory object. A test that only reads the loaded object cannot tell an edit that was saved from one that is still waiting for a save.
AssetFileSnapshot
One asset file's bytes and timestamp, taken before a test drives code that must not write it, so the test can tell whether it was written — marked dirty, or changed on disk — and put it back exactly as it was when it was. Putting the timestamp back too keeps every assembly's shipped-content residue guard seeing the file as this test found it, so a red run never leaves the package modified.
CutsceneDialogTestData
The names, durations and message fragments the editor tests of the cutscene dialog stage share, so that a wizard test, an Outline test and a recipe test all talk about the same dialog.
EmptyFolderChainPruner
Deletes an ancestor folder chain from a leaf upward, one folder at a time, stopping at the first
folder that still holds something (or at PruneUpward(string, string)'s own stop boundary) — the shared
cleanup a test fixture uses after removing the one asset it created into a shared conventional folder
it owns no redirect seam for (InstallTaskAuthoring.ResolveDefaultTaskFolder's real-project
fallback, for instance). Order-independent by design: it prunes whatever is EMPTY right now rather
than tracking which folder a specific test call created, so two independent test fixtures sharing the
same conventional folder never leave residue depending on which one happens to run last in a suite.
Deliberately its own type rather than a test-side call into the production
Serenity.Global.Infrastructure.Editor.Utils.SerenityEmptyFolderPruner: that pruner (a) only
deletes a folder from a caller-supplied "folders THIS run created" set, which is exactly the
order-dependent tracking this type exists to avoid, and (b) tests emptiness via
AssetDatabase.FindAssets(string.Empty, ...), which still matches a folder's own just-emptied
index entry within the same synchronous pass — the real filesystem check in Serenity.Tests.Shared.Editor.EmptyFolderChainPruner.IsEmpty(System.String)
exists specifically because that AssetDatabase-based check reported a folder "occupied" immediately
after this exact kind of test-cleanup emptied it.
FixtureOwnedFolders
A fixture's hold on folders outside its own private Assets/Fixture root. For each folder it
records the topmost folder on that path which did not exist yet, so Release() removes what
the fixture created and nothing the project already held.
That distinction is the whole point. A project copy lands under the REAL Assets/Game/Media
whatever folder the test ran in, and a customer project — like a persistent CI workspace — already
keeps its own content under Assets/Game. A cleanup that removes the user-content root only "when
the fixture created it" therefore passes on a pristine checkout and leaks everything below that root
the moment it was already there.
ImmediateModeGuiPass
Runs a piece of IMGUI code — an inspector's OnInspectorGUI, a window's draw — through one real
frame, with no window, no GUI view and no graphics device. EditorWindow.SendEvent is the
usual way to drive OnGUI from a test, but a headless run never delivers it, which is exactly the
run a CI or a batch verification is; this opens the same container the UI Toolkit host opens around an
IMGUIContainer, so every GUILayout call finds the state it expects.
A frame is what IMGUI runs for every repaint: a UnityEngine.EventType.Layout event, the layout calculation, then a UnityEngine.EventType.Repaint event over the rects it produced. Both events are sent, because code that writes only while it paints is as real a write-on-draw as code that writes during layout. A layout pass that ended early on an UnityEngine.ExitGUIException is not repainted: a real host abandons that frame too.
One error Unity logs during a headless repaint describes the host, not the code under test: a number field registers its drag-cursor zone while it repaints, and Unity reports that no GUI view is there to receive it (Serenity.Tests.Shared.Editor.ImmediateModeGuiPass.CursorRectOutsideGuiViewMessage). That one message is let through; every other error, assert or exception logged while repainting still fails the test that drew.
InMemoryEditorSessionStateStore
Serenity.Global.Infrastructure.Editor.IEditorSessionStateStore that lives in a test's own memory. Every test that exercises code reading or writing editor session scratch builds one of these, so no test can reach — let alone wipe — the real editor session the author is working in. Share one instance between two objects to model the session surviving a domain reload.
InspectorDrawPass
Draws an editor the way the Inspector window draws it, headlessly, so a contract over "drawing never writes" exercises the same entry points a selection does. The Inspector asks UnityEditor.Editor.CreateInspectorGUI first and binds what it returns; only when that is null does it host UnityEditor.Editor.OnInspectorGUI, which it then draws frame by frame (ImmediateModeGuiPass).
InspectorDrawPassTests
InspectorDrawPass is what the "drawing never writes" contracts —
ShippedContentInspectorNoWriteOnDrawTests and FoundationEditorNoWriteOnReadTests — draw
every inspector through, so a write it cannot reach is a write those contracts silently pass. Each probe
editor here writes at exactly one of the entry points the Inspector window drives, and the pass must
reach every one of them.
InspectorDrawProbeAsset
The asset InspectorDrawPassTests draws its probe editors over. It only counts the writes a probe editor makes, so a test can tell a caught write from one the pass never reached.
ObjectFieldDropAssert
The undo half of an inspector object-field test: the drop lands in the field, and one Undo takes it back out, which only happens when the field recorded the object that owns it before it wrote.
ObjectFieldDropRecorder
Stands in for IMGUI's object field in an inspector test: the drawer it hands out reports an object as
dropped onto every field it draws, and remembers whether that field was told to accept scene objects.
A batch run has no OnGUI pass to drive the real field through, so an inspector exposes a seam
that takes this drawer in place of IMGUI's own.
PendingEditorDelayCalls
Runs every UnityEditor.EditorApplication.delayCall queued so far, right now — the deferred work an EditMode test would otherwise never see, because a synchronous test finishes before the editor's next tick. The passive Addressables labeler registers imported assets from exactly such a callback, so a test that checks what a run left in the Addressables settings drains the queue first.
ProjectFileBytesSnapshot
The bytes of every file under a set of project folders, and of any single project files named beside them,
taken at one moment — .meta files included — so a test can name exactly the files a run wrote. Every
pending asset write is flushed before the files are read, so an unsaved change is never missed and a change an
earlier test left unsaved is never blamed on this one.
Each file is kept as its length and a 64-bit FNV-1a fingerprint of its bytes — enough to catch an accidental change, without a copy of the tree in memory or a cryptographic hash, which under the editor's runtime made one snapshot of the package tree take over ten seconds. Opening every file is the remaining cost, so a fingerprint is remembered with the length and last write time it was read at and reused while both are unchanged: only a write that kept a file's length and restored its timestamp could slip past, and no tool a test drives does that. A file written within Serenity.Tests.Shared.Editor.ProjectFileBytesSnapshot.RecentWriteWindow of being read is never remembered, because a second write inside the file system's timestamp resolution would keep the same timestamp.
ProjectTreeAndShippedContentResidueGuardBase
Reusable EditMode safety net an editor test assembly adopts by declaring one namespace-less
[SetUpFixture] subclass of this base — NUnit scopes a SetUpFixture to the
assembly holding it, keyed by the namespace of the class that carries the attribute, so a single
shared fixture in this assembly would only ever watch Serenity.Tests.Shared.Editor itself.
Every subclass gets two independent guards for free:
- Project-tree residue: a before/after snapshot of every top-level entry under
Assets/plus everything under Serenity.Global.Infrastructure.Editor.SerenityUserContentPaths.GameRoot, the default output root every wizard's own creator falls back to when a test drives it without redirecting its output. - Shipped-content tampering: a before/after snapshot of (path, length, last-write-time) for every file under Serenity.Tests.Shared.Editor.ProjectTreeAndShippedContentResidueGuardBase.WatchedShippedContentRoots — cheap and deterministic on purpose, so content is never hashed and never restored; the guard only ever reports.
A full EditMode run can trigger a mid-suite domain reload (a test that generates or recompiles a script), which discards a fixture instance and constructs a fresh one for whatever tests remain — a baseline held in an instance field would then be captured again and already include an EARLIER segment's leak, hiding it. UnityEditor.SessionState survives a domain reload within the same Unity process, so the baseline captured at the true start of the run is the one the final teardown compares against. The teardown erases that baseline, so the next run in the same editor session starts from the tree as it is then — a folder the author creates between two runs is never reported as residue. Every key is namespaced by AssemblyDisplayName so two guard subclasses running in the same Unity process (every editor test assembly gets one) never share, or clobber, each other's baseline.
The setup snapshot is silent (a leftover may predate this guard or come from an aborted run); the teardown snapshot fails loudly, naming the owning assembly and every offending entry, so a fixture that stops cleaning up — or a product code path that writes into shipped content — turns into a suite failure instead of silent working-tree pollution that a CI runner without workspace cleanup carries into its next run.
The on-disk comparison alone has a real blind spot: EditorUtility.SetDirty only marks an
object dirty in memory, and AssetDatabase.SaveAssets is a GLOBAL flush — any assembly's call
to it writes every currently-dirty object project-wide, not only the ones that assembly itself
touched. An assembly whose own tests dirty a shipped object but never explicitly save it reports
clean at its own teardown (the file on disk has not changed YET); the object then stays dirty
across the domain until something else flushes it — a later assembly's own routine
AssetDatabase.SaveAssets() hygiene call, or the editor process's own shutdown after every
guard has already run its teardown — at which point no assembly's on-disk snapshot ever sees the
write inside its own window. Serenity.Tests.Shared.Editor.ProjectTreeAndShippedContentResidueGuardBase.FindDirtyShippedContentPaths(System.Collections.Generic.IEnumerable{System.String},System.Collections.Generic.IReadOnlyCollection{System.String}) closes that gap: it checks
EditorUtility.IsDirty directly against every ALREADY-LOADED, natively-persistable object under
the watched roots, which names the assembly that actually caused the dirty state at ITS OWN teardown,
before anything flushes it — the on-disk check remains for the write that already lands inside one
assembly's own window. A dirty imported (foreign) asset — a dynamic Font rebuilding its glyph
atlas is the recurring real-world case — is excluded: AssetDatabase.SaveAssets can never write
a foreign asset's source file, so its in-memory dirtiness can never become the disk write this guard
exists to catch.
The guard never changes editor state: it only ever reads UnityEditor.EditorUtility.IsDirty(UnityEngine.Object)
and never calls EditorUtility.ClearDirty or writes anything back. In the INTERACTIVE editor's own
Test Runner this matters, because Serenity.Tests.Shared.Editor.ProjectTreeAndShippedContentResidueGuardBase.FindDirtyShippedContentPaths(System.Collections.Generic.IEnumerable{System.String},System.Collections.Generic.IReadOnlyCollection{System.String}) cannot otherwise tell a
developer's own unsaved edit to a shipped asset — made before the run even started — from dirtiness the
run itself caused. OneTimeSetUp() closes that gap by recording, alongside the tree and
shipped-content baselines in the same UnityEditor.SessionState entry, every path already dirty at that
moment; OneTimeTearDown() then reports only a path that is dirty NOW and was absent from
that baseline. A developer's pre-existing dirty asset is therefore never reported, and — because nothing
here ever clears a dirty flag — it stays exactly as dirty as the developer left it. An object one
assembly's OWN tests dirty during the run IS reported by that assembly, since it was clean at that
assembly's own set-up; the next assembly's set-up then captures it as already dirty, so it is reported
once, by the assembly that actually caused it, and never again downstream.
ProjectTreeAndShippedContentResidueGuardBaseDirtyAssetTests
Drives Serenity.Tests.Shared.Editor.ProjectTreeAndShippedContentResidueGuardBase.FindDirtyShippedContentPaths(System.Collections.Generic.IEnumerable{System.String},System.Collections.Generic.IReadOnlyCollection{System.String}) and Serenity.Tests.Shared.Editor.ProjectTreeAndShippedContentResidueGuardBase.FindNewlyDirtyShippedContentPaths(System.Collections.Generic.IEnumerable{System.String},System.Collections.Generic.IReadOnlyCollection{System.String},System.Collections.Generic.IReadOnlyCollection{System.String}) against real AssetDatabase-backed assets under a disposable scratch folder — the mechanism only inspects already-LOADED UnityEngine.Object instances via UnityEngine.Resources.FindObjectsOfTypeAll``1, so a file merely written to disk without going through the AssetDatabase proves nothing about the native/imported distinction the mechanism exists to draw. The baseline-attribution cases also prove the guard never mutates editor state: every asset this fixture dirties is still dirty after the mechanism runs, whether or not it gets reported.
ProjectTreeAndShippedContentResidueGuardBaseTests
Drives ProjectTreeAndShippedContentResidueGuardBase's shipped-content snapshot/compare functions directly against a disposable temp root standing in for a watched shipped root — proof the mechanism itself works, independent of whether any real shipped-content bug currently exists for the real per-assembly guards to catch. A non-vacuous self-test: every case below asserts both what MUST be reported and, where relevant, what must NOT be.
StepWizardWindowCatalog
Every concrete stepped wizard window in the project, found through UnityEditor.TypeCache so a contract covers a new wizard the moment it is written rather than when someone remembers to register it. Shared by the contracts that drive all of them at once, so the enumeration and its ordering are stated once.
UpdateInPlaceRoundTripContract
Reusable EditMode contract for the rule that an editor tool managing an existing asset owns everything
that asset already has: confirming an update-in-place wizard run with ZERO edits must leave every
already-set field on every asset it touches
untouched — name, hide flags, and every serialized field alike. Guards against a wizard's own
SeedFromTarget seeding only a handful of fields and
silently reverting everything else to its static template default the moment Confirm is pressed.
A wizard registers by implementing UpdateInPlaceRoundTripContract.IAdapter<TAsset> — seed from a SET of existing assets, confirm with no further edits — a single asset is the trivial case of a one-element set, for the multi-asset adopters (Wave Set, Player Input, Character) coming in later slices.
Built entirely on the existing UnitySerializedFieldReflection / UnitySerializedFieldSeeds /
UnitySerializedFieldAssertions trio (Serenity.Tests.Shared) rather than a new
UnityEditor.SerializedObject-based walker of its own — the same machinery
SystemConfigurationSettingsCreatorTests already exercises for its own field-walk regression test,
so an Object-reference field is mutated (via MutateEveryFieldToNonDefault(Object, Object, string)'s
alternateObjectReference) and compared by asset identity, never silently skipped.
WizardAnotherRunReseedContractTests
A wizard never offers to update an asset its fields were not loaded from. The seed guard of Serenity.Global.Infrastructure.Editor.SerenityWizardTargetAsset remembers which asset a run last loaded so that finishing a half-retyped Id does not reload over the author's edits — but "Create Another" resets those fields, so a guard that still remembers the previous run's asset would let the next run resolve the SAME asset, read "Update", and write the reset defaults over it. Every stepped wizard is covered: the contract finds each window's targets by walking its fields rather than through a registration list a new wizard can be written without, and starts the new run through the base's own reset.
WizardDoneStepReloadContractTests
The Done-step half of WizardDraftReloadContractTests: whatever a stepped wizard's last run
reported must still be there after a domain reload, because the primary action's own script generation
or asset import can trigger one on its normal, successful path. Every stepped wizard is covered
automatically through StepWizardWindowCatalog: the run is written through the base's own
outcome write — the one CompletePrimaryAction uses — and read back off a reloaded copy. The
per-window reload assertions that drive a real primary action live beside each window's own tests
(WizardDoneStepReloadSimulator), and the ratchet in the CodeHygiene suite keeps any window
from holding a non-serialized copy of the creator's result the Done step would read instead.
WizardDoneStepReloadSimulator
Simulates the domain reload a script-generating or asset-importing primary action can trigger on its normal path, for a test proving a wizard's Done step still reads the run that just finished: a fresh instance receives only what the window's serializer actually persists, so state the window keeps in a non-serialized field comes back at its default, exactly as a real reload leaves it.
WizardDraftReloadContractTests
The reload half of UpdateInPlaceRoundTripContract's "tools are management tools" rule: an edit the author has made but not yet confirmed must still be there after a script recompile. Every stepped wizard is covered automatically — the contract finds each window's scratch-draft holders by Serenity.Global.Infrastructure.Editor.ISerenityWizardDraftHolder rather than a registration list a new wizard can be written without.
WizardDraftReloadSimulator
Drives a wizard window through the messages Unity fires around a domain reload — OnDisable, the
reload itself in its harshest shape, then OnEnable — so a test proves what the window's OWN
messages do rather than restating them through a per-window seam. Shared by the contract that sweeps
every stepped wizard and by the per-window tests that pin one window's own drafts.
WizardFinishedRunSummaryContractTests
Once a run has created what it set out to create, the run is over. Going back from the Done step to the Summary must show what the run wrote and offer what comes after the run — never a pending "Will create" for an asset that now exists, never the creator's own "already exists" collision against it, and never a primary button that would run the same create a second time.
Every stepped wizard with a Summary step is covered one of three ways, and the sweep prints which. A wizard with
a WizardRealRunDriverBase is driven for real: its real primary action writes into a fixture
folder, then Done → Back → Summary. Where that real run latches the run finished, it is proven END TO END. Where
it does not — the window finishes through PingAndGoTo or a plain GoTo and keeps its run open —
the window is named in Serenity.Tests.Shared.Editor.WizardFinishedRunSummaryContractTests.RealRunLeavesTheRunOpen and the sweep pins that it still does, so the day
it latches it moves to the end-to-end list instead of passing unnoticed. A wizard whose real run cannot be built
headless is proven only through the base's own completion path (CompletePrimaryAction) and named in
Serenity.Tests.Shared.Editor.WizardFinishedRunSummaryContractTests.BasePlumbingOnlyReasons with the reason. A new wizard must take one of the three.
WizardRealRunDriverBase
One stepped wizard's real run, arranged so that pressing its primary action performs the create or update an author would get — the window's own creator, its own completion path — with every asset the run writes landing under a fixture folder the contract owns. WizardFinishedRunSummaryContractTests finds every driver by type and drives its window for real; a window without one is proven only through the base's completion plumbing, and the contract names it with the reason. The non-generic root the contract discovers and holds; a driver derives from WizardRealRunDriver<TWindow> in its module's own test assembly, beside the tests whose fixture it reuses, so it can reach the window's internal test seams.
WizardRealRunDriver<TWindow>
A WizardRealRunDriverBase typed to its window, so a driver arranges and presses through the window's own internal test seams. It opens the window either unshown or through its own menu item — the two ways the window's existing tests open it — and closes it the same way.
WizardRealRunIdentityContractTests
Every foundation asset a wizard's real run creates, and the asset the run targets, carry both halves of their identity: the Id services resolve them by and the Guid references name them by. A creator that stamps the Guid but leaves the Id blank hands the author an asset the Health Check reports as "Empty Id" the moment the run finishes — the defect Create Prefetch Policy shipped with. Every wizard with a WizardRealRunDriverBase is driven for real, and every IFoundationSettings object in those files is read the way the Health Check reads it. What a driver staged for the run to work on and the run only touched belongs to the fixture, not to the creator, so it is not judged here.
WizardStepErrorContractTests
The rule a stepped wizard's breadcrumb makes unforgiving: it asks EVERY step ahead of the reader for its blocked reason on every repaint, so a step check that dereferences a field the author has not filled in yet throws many times a second on a window that has only just been opened. A missing value is a validation message, never an exception — this contract states that for every wizard StepWizardWindowCatalog finds, on a freshly created window with nothing configured, which is exactly the state an author sees first.
WritesOnCreateInspectorGuiProbeEditor
A UI Toolkit editor that writes its target while it builds its inspector — the write a pass that
only ever calls OnInspectorGUI never reaches, because the Inspector window never calls it for an
editor that builds a UI Toolkit inspector.
WritesOnRepaintProbeEditor
An editor that writes its target only while a Repaint event is being drawn — the write a pass that sends nothing but Layout never sees.
Interfaces
UpdateInPlaceRoundTripContract.IAdapter<TAsset>
What a wizard-specific test adapter supplies to run this contract against its own window and creator.