Skip to content

test: increase ECS test coverage from 67% to 98%#19

Open
gauvainw wants to merge 1 commit into
mainfrom
test/ecs-coverage
Open

test: increase ECS test coverage from 67% to 98%#19
gauvainw wants to merge 1 commit into
mainfrom
test/ecs-coverage

Conversation

@gauvainw

Copy link
Copy Markdown
Owner

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_LastTickDuration
  • TestWorld_Systems, TestWorld_OnPostTick, TestWorld_Run_ContextCancel
  • TestWorld_ComponentInfo, TestWorld_ComponentInfos
  • TestWorld_RLock_RUnlock
  • TestWorld_DeadEntity_Operations (Get/Has/Remove/Destroy/EntityComponents/GetComponentValue/SetComponentValue on dead entities)
  • TestWorld_GetComponentValue_MissingComponent, TestWorld_SetComponentValue_MissingComponent

query_test.go — 12 new test functions:

  • TestQuery1_EachLocked
  • TestQuery2_Count, TestQuery2_Iter
  • TestQuery3_Each, TestQuery3_EachLocked, TestQuery3_Count
  • TestQuery4_Each, TestQuery4_EachLocked, TestQuery4_Count
  • TestQuery1_Iter_EarlyBreak, TestQuery2_Iter_EarlyBreak

component_test.go — Added testDamage type for Query4 tests.

How to test

go test -race -count=1 ./internal/ecs/
go test -coverprofile=cover.out ./internal/ecs/ && go tool cover -func=cover.out | tail -1

Related issue

Closes #11

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 World tests covering counts/metadata, tick timing, Run cancellation, post-tick callbacks, locking, and dead-entity operations.
  • Add Query1Query4 tests covering Each, EachLocked, Count, iterator behavior, and early-break behavior.
  • Introduce a testDamage component type used by Query4 tests.

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.

Comment on lines +309 to +313
_ = w.Run(ctx)

if !called {
t.Fatal("post-tick function should have been called")
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

func TestWorld_DeadEntity_Operations(t *testing.T) {
w := NewWorld()
RegisterComponent[testPosition](w)

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
RegisterComponent[testPosition](w)

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +278
if w.LastTickDuration() <= 0 {
t.Fatal("LastTickDuration should be positive after update")

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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")

Copilot uses AI. Check for mistakes.
Comment on lines +298 to +313
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")
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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")
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: increase ECS test coverage

2 participants