test: increase ECS test coverage from 67% to 98%#19
Conversation
Add comprehensive tests for previously uncovered functions: World: ArchetypeCount, ComponentCount, TickRate, LastTickDuration, Systems, OnPostTick, Run (context cancel), ComponentInfo, ComponentInfos, RLock/RUnlock, dead entity operations, missing component edge cases. Query: Query1.EachLocked, Query2.Count, Query2.Iter, Query3 (Each, EachLocked, Count), Query4 (Each, EachLocked, Count), early break for Iter on Query1 and Query2. Component: add testDamage type for Query4 tests. Closes #11
There was a problem hiding this comment.
Pull request overview
This PR increases statement coverage for the internal/ecs package by adding a comprehensive set of unit tests around World APIs (tick loop/callbacks, component reflection access, dead-entity behavior) and query iteration/counting behavior, supporting the ECS “hardening phase” before refactoring (issue #11).
Changes:
- Add extensive
Worldtests covering counts/metadata, tick timing,Runcancellation, post-tick callbacks, locking, and dead-entity operations. - Add
Query1–Query4tests coveringEach,EachLocked,Count, iterator behavior, and early-break behavior. - Introduce a
testDamagecomponent type used byQuery4tests.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| internal/ecs/world_test.go | Adds 15 World-focused tests, including tick loop and reflect-based component access edge cases. |
| internal/ecs/query_test.go | Adds 12 query tests covering iteration/counting across Query1–Query4 and early-break behavior. |
| internal/ecs/component_test.go | Adds testDamage component type for use in query tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _ = w.Run(ctx) | ||
|
|
||
| if !called { | ||
| t.Fatal("post-tick function should have been called") | ||
| } |
There was a problem hiding this comment.
TestWorld_OnPostTick discards the error from w.Run(ctx) (_ = w.Run(ctx)). If Run returns unexpectedly (e.g., nil or another error), this test could still pass as long as called is true. Please assert the returned error (typically context.Canceled) so the test validates the full contract.
|
|
||
| func TestWorld_DeadEntity_Operations(t *testing.T) { | ||
| w := NewWorld() | ||
| RegisterComponent[testPosition](w) |
There was a problem hiding this comment.
In TestWorld_DeadEntity_Operations, testPosition is registered twice in a row and the second returned ID is used. RegisterComponent returns the existing ID when already registered, so this works, but the extra call is redundant and makes the intent less clear. Consider registering once and reusing that ID.
| RegisterComponent[testPosition](w) |
| if w.LastTickDuration() <= 0 { | ||
| t.Fatal("LastTickDuration should be positive after update") |
There was a problem hiding this comment.
TestWorld_LastTickDuration asserts LastTickDuration() > 0 after Update(), but Update() stores time.Since(start).Nanoseconds() and it can legitimately be 0 if the tick completes within the same clock tick. This makes the test potentially flaky on fast machines. Consider making the system do a tiny Sleep/work to ensure measurable duration, or assert on a weaker invariant (e.g., value is non-negative and/or changes from the initial value).
| if w.LastTickDuration() <= 0 { | |
| t.Fatal("LastTickDuration should be positive after update") | |
| if w.LastTickDuration() < 0 { | |
| t.Fatal("LastTickDuration should not be negative after update") |
| called := false | ||
| w.OnPostTick(func(_ *World) { | ||
| called = true | ||
| }) | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| // Run one tick then cancel. | ||
| go func() { | ||
| time.Sleep(w.TickRate() + 10*time.Millisecond) | ||
| cancel() | ||
| }() | ||
| _ = w.Run(ctx) | ||
|
|
||
| if !called { | ||
| t.Fatal("post-tick function should have been called") | ||
| } |
There was a problem hiding this comment.
TestWorld_OnPostTick uses time.Sleep(w.TickRate() + 10*time.Millisecond) before canceling the context. That small margin can be flaky under load if Run hasn’t started its ticker yet or the scheduler delays the tick beyond the sleep window. A more deterministic approach is to cancel from inside the post-tick callback (signal on a channel, then cancel), and wrap the whole test in a context.WithTimeout to avoid hangs.
| called := false | |
| w.OnPostTick(func(_ *World) { | |
| called = true | |
| }) | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| // Run one tick then cancel. | |
| go func() { | |
| time.Sleep(w.TickRate() + 10*time.Millisecond) | |
| cancel() | |
| }() | |
| _ = w.Run(ctx) | |
| if !called { | |
| t.Fatal("post-tick function should have been called") | |
| } | |
| // Channel to signal that the post-tick callback has been invoked. | |
| calledCh := make(chan struct{}, 1) | |
| w.OnPostTick(func(_ *World) { | |
| // Signal the first time this is called; subsequent calls are ignored. | |
| select { | |
| case calledCh <- struct{}{}: | |
| default: | |
| } | |
| }) | |
| // Use a timeout to avoid hanging the test if the callback is never called. | |
| ctx, cancel := context.WithTimeout(context.Background(), time.Second) | |
| defer cancel() | |
| // Run the world in a separate goroutine so we can wait for the callback. | |
| errCh := make(chan error, 1) | |
| go func() { | |
| errCh <- w.Run(ctx) | |
| }() | |
| // Wait for the post-tick callback or for the context to time out. | |
| select { | |
| case <-calledCh: | |
| // Callback was called at least once; cancel to stop the loop. | |
| cancel() | |
| case <-ctx.Done(): | |
| t.Fatal("context timed out before post-tick callback was called") | |
| } | |
| // Ensure Run returns promptly after cancelation. | |
| select { | |
| case <-errCh: | |
| case <-time.After(500 * time.Millisecond): | |
| t.Fatal("Run did not return after context cancelation") | |
| } |
What changed and why
Increases ECS package statement coverage from 67.0% to 98.3%, well above the 80% project target.
New tests added
world_test.go — 15 new test functions:
TestWorld_ArchetypeCount,TestWorld_ComponentCount,TestWorld_TickRate,TestWorld_LastTickDurationTestWorld_Systems,TestWorld_OnPostTick,TestWorld_Run_ContextCancelTestWorld_ComponentInfo,TestWorld_ComponentInfosTestWorld_RLock_RUnlockTestWorld_DeadEntity_Operations(Get/Has/Remove/Destroy/EntityComponents/GetComponentValue/SetComponentValue on dead entities)TestWorld_GetComponentValue_MissingComponent,TestWorld_SetComponentValue_MissingComponentquery_test.go — 12 new test functions:
TestQuery1_EachLockedTestQuery2_Count,TestQuery2_IterTestQuery3_Each,TestQuery3_EachLocked,TestQuery3_CountTestQuery4_Each,TestQuery4_EachLocked,TestQuery4_CountTestQuery1_Iter_EarlyBreak,TestQuery2_Iter_EarlyBreakcomponent_test.go — Added
testDamagetype for Query4 tests.How to test
Related issue
Closes #11