Repository navigation
test: add unit test suite and pull_request CI - #183
Draft
claude[bot] wants to merge 8 commits into
Draft
claude[bot] wants to merge 8 commits into
claude[bot] wants to merge 8 commits into
Conversation
Adds Tests~/DotNet/ArenaUnity.PureTests, a plain net8.0 NUnit project that
compiles the real package sources via <Compile Include> links rather than
copies, so the tests always exercise the shipping code. It needs no Unity
installation and no Unity license, which makes it usable as a pull-request
gate.
Only sources that are genuinely free of UnityEngine are linked in:
Runtime/ArenaTopics.cs
Runtime/Components/ArenaSceneEnvPresetsDefaults.cs
Runtime/Schemas/Converter/ArenaColorJsonConverter.cs
Runtime/Schemas/Converter/ArenaBooleanObjectJsonConverter.cs
Runtime/Schemas/Converter/ArenaStringObjectJsonConverter.cs
272 tests covering:
- all 29 ArenaTopics getters as exact strings, plus the positional
invariants the inbound router in ArenaClientScene.Messaging.cs relies on
(ArenaTopicTokens slot indices and the ArenaTopicSceneMsgTypes alphabet).
Backs CONTRIBUTING.md rule #1 and the wire protocol shared with arena-py.
- ArenaCssColors.Normalize and the integrity of its 148-entry table,
including the CSS gray/grey and fuchsia/magenta synonym pairs.
- ArenaColorJsonConverter and ArenaColorArrayJsonConverter round trips.
- the full legacy-tolerance matrix of ArenaBooleanObjectJsonConverter<T>
and ArenaStringObjectJsonConverter.
- all 19 environment presets: valid JSON, the complete documented key set,
A-Frame enum vocabularies, and colour values that normalize to hex.
Tests that need UnityEngine or the Editor are not here; they belong in the
Unity Test Runner suite.
Three pre-existing faults are pinned rather than fixed, each with a
// PINS CURRENT BEHAVIOUR (bug) comment naming file:line and the assertion to
flip once the fault is corrected:
- ArenaColorJsonConverter.cs:274 ArenaColorArrayJsonConverter.WriteJson
throws on a null array, unlike its scalar sibling.
- ArenaBooleanObjectJsonConverter.cs:58 WriteJson throws on null, although
ReadJson returns null for both JSON null and boolean false.
- ArenaStringObjectJsonConverter.cs:36 formats numbers with CurrentCulture,
so under a comma-decimal locale an animation to/from value goes out as
"1,5" and the A-Frame client cannot parse it. The same line emits .NET
"True"/"False" casing for booleans instead of JavaScript's lowercase.
The .gitignore blanket *.csproj rule exists for the project files Unity
generates and discards; this one is hand-maintained, so it is exempted, and
the test project's bin/ is ignored instead.
Adds Tests/Editor with an isolated test assembly following the same shape as
Editor/VersionCheck: Editor-only platform, autoReferenced off, and gated on
UNITY_INCLUDE_TESTS so it never enters a player build. overrideReferences is
on, so both nunit.framework.dll and Newtonsoft.Json.dll are named explicitly
in precompiledReferences.
Pins com.unity.test-framework to 1.4.6, the current stable release for
Unity 6; it brings com.unity.ext.nunit 2.0.3 (NUnit 3.5), so the assertions
here stay within the NUnit 3.0 API surface. A consuming project also needs
"testables": ["io.conix.arena.unity"] in its Packages/manifest.json for the
Test Runner to discover these tests; see Documentation~/Testing.md.
Covers only what genuinely needs UnityEngine or the Editor, with no overlap
with the plain .NET suite:
- ArenaMqttClient.MqttTopicMatch: the MQTT wildcard matrix, the JWT
publish-permission scenarios it actually serves in HasPerms, and the
cross-class invariants that tie it to ArenaTopics - every SUB_* filter
against its PUB_* topics, including the privacy direction where the
public filter must NOT match an eight-segment private topic.
- ArenaMqttClient.Base64UrlDecode.
- ArenaUnity coordinate conversions: explicit Z-negation expectations,
position/rotation/scale round trips, ArenaFloat, TimeSpanToString,
MergeRawJson including array replacement, IsGltfType and primitives.
- ArenaMessageJson's [OnDeserialized] attributes-to-data migration.
- a corpus suite over all 8 Tests/JSON Scenes fixtures: deserialization,
no unexpected log warnings, stable re-serialization, and a guard that the
three legacy attributes-shaped fixtures stay legacy.
- a reflection contract suite over every schema DTO: unique [JsonProperty]
names, a ShouldSerialize for every wire member, def-backed defaults that
match a real member, no undeclared members emitted, and [EnumMember]
round trips. Rather than asserting a flat "{}" - which 62 DTOs correctly
violate by hardcoding "required in json schema" - a fresh instance is
required to emit exactly the members its own ShouldSerialize methods ask
for.
- the deterministic, publicly reachable ArenaMesh builders.
Behaviours pinned as currently-buggy, each with a comment naming file:line
and the assertion to flip:
- Base64UrlDecode never maps '-' to '+' or '_' to '/', so a JWT payload
segment in the base64url alphabet throws FormatException. Every caller
feeds it a JWT. Refs #180. The test that the fix should make pass is
already written and marked [Ignore].
- SphereBuilder over-allocates its index buffer and leaves a tail of
degenerate (0,0,0) triangles - 36 at the default segmentation.
- ToUnityPositionString throws on a one- or two-component coordinate
string, where ArenaVector3JsonConverter tolerates both.
- TimeSpanToString tests each component's absolute value but formats the
signed one, so a negative span renders negative numbers.
These tests cannot be executed without a Unity Editor. They were verified by
compiling every file against the real NUnit and Newtonsoft.Json assemblies,
leaving only UnityEngine, UnityEditor and conix.arena.unity unresolved: no
syntax errors and no NUnit or Newtonsoft API misuse. Assertions that could
not be derived with certainty from the source were left out rather than
guessed.
This repository had nothing triggered by pull_request before now. The new
.github/workflows/test.yaml runs on every pull request and on pushes to main,
with three jobs:
- dotnet-tests: the real gate. Runs the plain .NET suite in Tests~/DotNet
with NuGet caching. Uses no secrets, so it works identically for pull
requests from forks, and it needs no Unity licence.
- check-unity-license: probes for Unity licence secrets and publishes the
answer as a job output. A ${{ secrets.* }} expression is not evaluated in
a job-level if:, so the check has to happen in a step.
- unity-editmode-tests: gated on that output, so a secret-less or fork run
skips it instead of failing the pull request. Since arena-unity is a UPM
package with no Assets/ or ProjectSettings/, the job creates a throwaway
Unity project, adds the checked-out package to it by relative path, and
sets "testables" so Tests/Editor is discovered.
Every action is pinned to a full commit SHA with the mandatory inline
# vX.Y.Z comment, per the CI conventions in CONTRIBUTING.md. The Unity editor
version is pinned to a patch release that has a published unityci/editor
image and is hoisted into a workflow-level env so it can be bumped in one
place.
Also adds Documentation~/Testing.md covering, for a developer on their own
machine: prerequisites and the exact command for each suite, how to filter
down to a single fixture or test, how to get the package into a project with
"testables" set, the Test Runner and -runTests -batchmode paths, which job CI
actually gates on and which secrets a maintainer must add to enable the other,
where to put a new test in each suite, and the PINS CURRENT BEHAVIOUR (bug)
convention so that whoever fixes one of those faults flips the assertion
instead of deleting the test.
Linked from the README library reference, the AGENTS.md index and
CONTRIBUTING.md. RELEASE.md's prerequisites previously listed only manual
verification; running the tests is now the first item.
The EditMode suite had never been executed. A real Editor run fails to compile it, and once it compiles 73 of its test cases fail. 1. ArenaSchemaContractTests referenced ArenaUnity.Schemas.ArenaMessageJson from inside namespace ArenaUnity.Tests. `ArenaUnity` is both a namespace and a static class (Runtime/ArenaUnity.cs), so the name binds to the class and the compiler reports CS0426: "The type name 'Schemas' does not exist in the type 'ArenaUnity'". Use `using ArenaUnity.Schemas;` and the unqualified type name, as ArenaMessageJsonTests already does. 2. Dto_EmitsNoUndeclaredMembers and Dto_FreshInstanceSerializesExactlyItsRequiredMembers derived their expectations from [JsonProperty] fields only. Newtonsoft also serializes unattributed public members, and this schema set relies on that for the `object_type` discriminator and for the url/dep auto-properties on the hand DTOs. Both tests failed for 36 and 40 DTOs respectively. Widen the member query to what the default contract resolver actually emits. 3. UnknownMembers_SurviveARoundTrip asserted that an unknown "createdAt" member survives verbatim. It does not: Newtonsoft's default DateParseHandling converts the extension-data value to a DateTime, so the milliseconds are dropped on re-emit and reading the token back as a string yields a culture-formatted date. Pin the real behaviour, keep a non-date member to cover verbatim preservation, and say what the fix would be. Also pin the hand DTOs emitting url and dep as explicit JSON nulls on every publish, which matters because ARENA reads a null attribute as a delete. Verified by compiling all seven EditMode files against real NUnit 3.14 and Newtonsoft 13.0.3 with the ArenaUnity class/namespace collision reproduced (0 errors, 0 warnings), and by executing the five files that need no Unity math: 626 passed, 1 ignored, 0 failed.
Independent mutation testing found two surviving mutants in the colour table: changing peachpuff from #FFDAB9 to #FFDAB8, and swapping the values of olive and olivedrab, both left all 272 tests green. The existing tests check the count, the hex format, key casing, synonym agreement and eight spot values, which leaves 131 of the 148 entries with no assertion on their value at all - and Normalize_IsIdempotentOverTheWholeTable uses the table as its own oracle, so it cannot notice. Add the complete CSS Color Module Level 4 table as an independent expectation, cross-checked against the color-name reference table. Both mutants are now caught by NameToHex_MatchesTheCssLevel4NamedColourTableEntryForEntry. Also stop spelling ArenaCssColors as ArenaUnity.Schemas.Converter.ArenaCssColors in the presets fixture. `ArenaUnity` is a static class as well as a namespace, so that spelling only compiles because Runtime/ArenaUnity.cs is not a linked source of this project - it is the same construct that failed to compile in the EditMode suite. 273 passed, 0 failed.
…sertions
Preset_HasNoDuplicateKeys could not fail. Its own comment says JObject.Parse
silently keeps only the last of a duplicated key, then the test asserts
uniqueness on the parsed properties - which a JObject enforces by
construction. Adding a duplicated "seed" to the tron preset left all 272
tests green. Count the property-name tokens in the raw literal instead, and
also compare that count against the parsed property count so a silently
collapsed key is reported. The duplicate-key mutation now fails Preset_tron.
ToUnityRotationQuat_QuaternionOverload_IsItsOwnInverse asserted only that
applying the overload twice restores the original, which an implementation
that negated the wrong components - or the identity function - would also
satisfy. Assert the single application's component signs, both with and
without invertY, and keep the involution check.
Drop the redundant step-level `env: UNITY_VERSION: ${{ env.UNITY_VERSION }}`
in the Unity job. It re-declares the name it reads inside its own env block,
which is the one place the env context is documented not to resolve; the
workflow-level value is already visible to the script, and this step writes
ProjectVersion.txt, so an empty value would fail the run confusingly.
Correct three source line references in pin comments (SphereBuilder.cs:20
not :21, PolyhedronBuilder.cs:39-51, ArenaMqttClient.cs:240-246) and say
[Ignore] attribute rather than Assert.Ignore where the base64url pin
explains how to enable the post-fix test.
dotnet suite: 273 passed. The five EditMode files that need no Unity math:
626 passed, 1 ignored. All seven still typecheck with 0 warnings.
The Unity Test Framework is only needed to run the EditMode suite in Tests/Editor, never to consume the library, so shipping it as a runtime dependency forced it on every consumer of the package. A project that runs the tests now installs it itself, pinned, alongside its "testables" entry. The CI harness manifest in test.yaml already did exactly that, so the EditMode job is unaffected; Documentation~/Testing.md and CONTRIBUTING.md now tell a developer to do the same locally. Tests/Editor/conix.arena.unity.Tests.Editor.asmdef is unchanged: its UNITY_INCLUDE_TESTS define constraint plus the "testables" gate mean a consumer that has not opted into the tests never compiles that assembly, so its UnityEngine.TestRunner and nunit.framework.dll references are never resolved.
Documentation~/Testing.md said the package "has to be listed in
testables, because Unity only discovers tests inside a package when it
is named there". Discovery is only half of it: testables decides whether
the test assembly is compiled at all, and it is the only thing that
keeps Tests/Editor out of a project that merely consumes the package.
Verified against UnityCsReference. CustomScriptAssembly.IsCompatibleWith
treats an asmdef as a test assembly when its defineConstraints contain
UNITY_INCLUDE_TESTS, and then rejects it in two separate places:
if (!buildingForEditor && (isTestAssembly || isTestFrameworkAssembly)
&& !isBuildingWithTestAssemblies)
return false;
if (isTestAssembly && AssetPathMetaData != null
&& !AssetPathMetaData.IsTestable)
return false;
The first only fires outside the Editor, because
EditorCompilationInterface.GetAdditionalEditorScriptCompilationOptions
sets both BuildingForEditor and BuildingIncludingTestAssemblies
unconditionally for AssembliesType.Editor - so it earns its keep in
player builds, where UNITY_INCLUDE_TESTS is also simply not defined. The
second is the testables gate: AssetPathMetaData.IsTestable is filled in
from the Package Manager over [RequiredByNativeCode], and is false for a
non-embedded package the project does not name in testables.
So the constraint is what marks the assembly as a test assembly - which
is what subjects it to the testables check and what drops it from player
builds - while testables is what a consuming project's Editor actually
gates on. Sharpen the testables sentence and spell the mechanism out so
the shorter, wrong version does not grow back.
Documentation only; the asmdef's defineConstraints is correct as it
stands and is unchanged. dotnet suite: 273 passed, 0 failed.
This was referenced Aug 20, 2026
test: environment presets have no golden value assertions, so A-Frame parity can drift silently
#186
Open
Open
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by mwfarb · Slack thread
WIP / draft.
Before
There are no C# tests anywhere in the repository, and nothing at all is triggered by
pull_request—grep -rn pull_request .github/returns nothing onmain. The only automated checks are the two native-plugin builds (pinned to feature branches) andrelease-please.RELEASE.md's prerequisites are entirely manual: "ensure the project compiles" and "hit Play and verify".Tests/exists but holds two shell scripts and eight JSON scene dumps — no test code.After
Two suites, split by what they need to run rather than by what they cover, and a
pull_requestworkflow that gates on the half which needs no licence.Tests~/DotNet/ArenaUnity.PureTestsTests/EditorWhat the .NET job gates today: 273 tests, ~150 ms, no secrets, so it runs identically on fork pull requests. It covers all 29
ArenaTopicsgetters as exact strings plus the positional invariants the inbound router depends on,ArenaCssColors.Normalizeand its 148-entry table, the colour JSON converters, the full legacy-tolerance matrix of the boolean-object and string-object converters, and all 19 environment presets.The Unity EditMode job stays skipped until licence secrets exist. A
${{ secrets.* }}expression is not evaluated in a job-levelif:, so a smallcheck-unity-licensejob probes for the secrets and publishes a job output thatunity-editmode-testsgates on withneeds:/if:. A skipped job does not fail the pull request, and secrets are never exposed to fork pull requests, so fork contributions skip it by design. To enable it, add under Settings > Secrets and variables > Actions eitherUNITY_LICENSE, orUNITY_EMAIL+UNITY_PASSWORD+UNITY_SERIAL.How
The .NET project never copies package source — it links the real files out of
Runtime/as MSBuildCompileitems, so the tests always compile the shipping code. Only the five sources verified free ofUnityEngineare linked in. No UnityEngine shim, stub, or fake exists anywhere in this branch; a test that exercised a reimplementation ofVector3would prove nothing about the real one, so anything needing Unity went to the EditMode suite instead.ArenaVectorJsonConverterwas a candidate but itsArenaVector3Json/ArenaVector2Jsontypes use[Tooltip]andDebug.LogWarning, so it is covered from the Unity side.Tests~/uses the same~convention asSamples~/andDocumentation~/, so UPM and Unity ignore it and nothing under it needs a.meta.Tests/Editoris imported, so every file there has one. The test assembly mirrors theEditor/VersionCheckisolation pattern: Editor-only,autoReferenced: false, gated onUNITY_INCLUDE_TESTS.Because
arena-unityis a UPM package with noAssets/orProjectSettings/, the Unity job creates a throwaway project, adds the checked-out package from disk, and sets"testables": ["io.conix.arena.unity"].com.unity.test-frameworkis pinned to1.4.6per the pin-all-versions rule; it bringscom.unity.ext.nunit(NUnit 3.5), so EditMode assertions stay inside the NUnit 3.0 API surface. Every action is SHA-pinned with the mandatory inline# vX.Y.Zcomment.com.unity.test-frameworkis not added to the package's dependencies by this branch. It is needed to run the tests, not to use the library, so forcing it on every consumer would be wrong.mainnever listed it: an earlier commit here (dfff1fe) added it topackage.jsonand a later one (8f0bcec) took it back out, sopackage.jsonis unchanged relative tomainand is not among this branch's changed files. The project that runs the tests declares the dependency itself, exactly pinned, next to itstestablesentry. The CI harness manifest above already did exactly that, so the EditMode job is unaffected, andDocumentation~/Testing.mdandCONTRIBUTING.mdnow tell a developer to do the same locally.Tests/Editor's asmdef is unchanged. What keeps that assembly out of a consumer's project is thetestablesgate, not itsUNITY_INCLUDE_TESTSdefine constraint:UNITY_INCLUDE_TESTSis defined whenever the Editor compiles, so the constraint is satisfied there. What the constraint does is mark the assembly as a test assembly, which is both what subjects it to thetestablescheck (CustomScriptAssembly.IsCompatibleWithreturns false for a test assembly whose package metadata is notIsTestable) and what drops it from player builds, where the symbol is not defined. So a project that adds the package without naming it intestablesnever compiles that assembly, and itsUnityEngine.TestRunnerandnunit.framework.dllreferences are never resolved there.New doc at
Documentation~/Testing.md, linked from the README reference list, theAGENTS.mdindex, andCONTRIBUTING.md.RELEASE.md's prerequisites now start with running the tests.Tests I have not run myself
The EditMode suite. There is no Unity Editor in the environment that produced this branch, so those ~1,900 lines are unexecuted. To keep that honest:
UnityEngine,UnityEditorandconix.arena.unityleft unresolved: no syntax errors, no NUnit or Newtonsoft API misuse.eulerAngles,ColorUtilityinternal representation,Shader.Findpaths,ParametricPlaneRandom, the internal mesh builders, and anything touching AR Foundation.LogAssert.NoUnexpectedReceived()in the scene-corpus tests, which would be genuine signal: it would mean a real persisted scene trips an[OnError]handler.The .NET suite was mutation-tested — 21 mutations across all five fixtures (a changed topic string, a deleted getter, a dropped dictionary entry, inverted branches, a broken comparer, corrupted preset values), each confirmed to fail a specific named test, each reverted, with
git diffonRuntime/empty afterwards. No mutation survived.Behaviours pinned as currently-buggy
These tests assert behaviour that is wrong, so a known fault cannot change silently. Each carries a
// PINS CURRENT BEHAVIOUR (bug)comment naming file:line, the correct behaviour, and the assertion to flip. Fixing one of these means flipping its assertion, not deleting the test.Filed separately (this branch pins them, it does not fix them):
Base64UrlDecodenever maps-to+or_to/, so a JWT payload segment in the base64url alphabet throwsFormatException; all three callers feed it a JWT. Pinned in the EditMode suite, with the post-fix assertion already written and[Ignore]d. Refs fix: Base64UrlDecode does not decode base64url, breaking JWT parsing #180ArenaClientScene.Export.cs:158. Not pinned by any test — it sits inside apublic IEnumeratorinstance method that authenticates and uploads; it needs the sanitization extracted to a static helper first. Refs fix: filename sanitization in UploadStoreFile uses a JavaScript regex literal #181PlayerPrefsfloat inEditor/ArenaVersion.cs:147. Not pinned by any test —ArenaVersionis aninternalEditor class whose throttle sits in a static constructor and a coroutine that calls the GitHub API. Refs fix: version-check throttle loses precision storing DateTime ticks in a float #182Found while writing these tests, not yet filed:
ArenaColorArrayJsonConverter.WriteJson(ArenaColorJsonConverter.cs:274) throws on a null array, unlike its scalar sibling. Newtonsoft's null-member short-circuit hides it on the common path.ArenaBooleanObjectJsonConverter.WriteJson(ArenaBooleanObjectJsonConverter.cs:58) throws on null, althoughReadJsonreturns null for both JSONnulland booleanfalse.ArenaStringObjectJsonConverter.cs:36formats numbers withCurrentCulture, so under a comma-decimal locale an animationto/fromvalue goes out as1,5and the A-Frame client cannot parse it. The same line emits .NETTrue/Falsecasing for booleans instead of JavaScript's lowercase. Pinned with the culture set explicitly so the test does not depend on runner locale.SphereBuilder.cs:54over-allocates its index buffer and leaves a tail of degenerate zero-area triangles — 36 at the default 24x16 segmentation.new ArenaTopics()zero-initializes the struct rather than running the all-optional-argument constructor, so every segment property isnull, not empty string. The topic strings still come out well-shaped because interpolation rendersnullas empty, buttopics.nameSpace.Lengththrows.PlaneBuilder.Builddivides bysegments - 1after clamping to 1, so a segment count below 1 yields NaN vertices. Not tested, to avoid a NaN-producing mesh in the suite.meeting.jsonusesobject_type: "prism", which is handled nowhere in the package.Generated by Claude Code