Code review on T10-T12 bundle (commits 0cf86bb/00bb030/0405947 + audit
fix 76e1a64) found 3 Important issues:
1. LandblockStreamer.Start() had an idempotency race — the XML doc
claimed thread-safety but the implementation checked _worker != null
before assigning, allowing two callers to both pass the check and
spawn duplicate worker threads. Fixed via Interlocked.CompareExchange.
2. No test verified the worker emits Failed when buildMeshOrNull returns
null. Added Load_WhenBuildMeshReturnsNull_ReportsFailed.
3. StreamingControllerTests.cs:81 used MeshData: default! when
constructing a Loaded result. If a future test flows MeshData
through the apply callback, the null reference would NRE rather
than producing a meaningful assertion failure. Replaced with a real
empty LandblockMeshData instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.9 KiB
C#
112 lines
3.9 KiB
C#
using System.Collections.Generic;
|
||
using AcDream.App.Streaming;
|
||
using AcDream.Core.World;
|
||
using DatReaderWriter.DBObjs;
|
||
using Xunit;
|
||
|
||
namespace AcDream.Core.Tests.Streaming;
|
||
|
||
public class StreamingControllerTests
|
||
{
|
||
private sealed class FakeStreamer
|
||
{
|
||
public List<uint> Loads { get; } = new();
|
||
public List<uint> Unloads { get; } = new();
|
||
public Queue<LandblockStreamResult> Pending { get; } = new();
|
||
|
||
public void EnqueueLoad(uint id) => Loads.Add(id);
|
||
public void EnqueueUnload(uint id) => Unloads.Add(id);
|
||
public IReadOnlyList<LandblockStreamResult> DrainCompletions(int max)
|
||
{
|
||
var batch = new List<LandblockStreamResult>();
|
||
while (batch.Count < max && Pending.Count > 0)
|
||
batch.Add(Pending.Dequeue());
|
||
return batch;
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void FirstTick_EnqueuesWholeVisibleWindow()
|
||
{
|
||
var state = new GpuWorldState();
|
||
var fake = new FakeStreamer();
|
||
var controller = new StreamingController(
|
||
enqueueLoad: fake.EnqueueLoad,
|
||
enqueueUnload: fake.EnqueueUnload,
|
||
drainCompletions: fake.DrainCompletions,
|
||
applyTerrain: (_, _) => { },
|
||
state: state,
|
||
radius: 2);
|
||
|
||
// Center at (50, 50); no landblocks loaded yet.
|
||
controller.Tick(observerCx: 50, observerCy: 50);
|
||
|
||
// 5×5 window = 25 loads enqueued, 0 unloads.
|
||
Assert.Equal(25, fake.Loads.Count);
|
||
Assert.Empty(fake.Unloads);
|
||
}
|
||
|
||
[Fact]
|
||
public void SecondTick_SamePosition_EnqueuesNothing()
|
||
{
|
||
var state = new GpuWorldState();
|
||
var fake = new FakeStreamer();
|
||
var controller = new StreamingController(
|
||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||
(_, _) => { }, state, radius: 2);
|
||
|
||
controller.Tick(50, 50);
|
||
fake.Loads.Clear();
|
||
|
||
controller.Tick(50, 50);
|
||
|
||
Assert.Empty(fake.Loads);
|
||
Assert.Empty(fake.Unloads);
|
||
}
|
||
|
||
[Fact]
|
||
public void DrainingLoadedResult_AddsToState()
|
||
{
|
||
var state = new GpuWorldState();
|
||
var fake = new FakeStreamer();
|
||
var applied = new List<LoadedLandblock>();
|
||
var controller = new StreamingController(
|
||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||
(lb, _) => applied.Add(lb), state, radius: 2);
|
||
|
||
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
|
||
// Entities (positional record). Adjust if the first positional arg
|
||
// name differs.
|
||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||
// A.5 T10-T12 follow-up: use a real empty mesh instance instead of
|
||
// default! so any future test that flows MeshData through the apply
|
||
// callback gets a non-null reference to inspect rather than an NRE.
|
||
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
|
||
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
||
System.Array.Empty<uint>());
|
||
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(0x32320FFEu, LandblockStreamTier.Near, lb, stubMesh));
|
||
|
||
controller.Tick(50, 50);
|
||
|
||
Assert.Single(applied);
|
||
Assert.True(state.IsLoaded(0x32320FFEu));
|
||
}
|
||
|
||
[Fact]
|
||
public void DrainingUnloadedResult_RemovesFromState()
|
||
{
|
||
var state = new GpuWorldState();
|
||
var fake = new FakeStreamer();
|
||
var controller = new StreamingController(
|
||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||
(_, _) => { }, state, radius: 2);
|
||
|
||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||
state.AddLandblock(lb);
|
||
fake.Pending.Enqueue(new LandblockStreamResult.Unloaded(0x32320FFEu));
|
||
|
||
controller.Tick(50, 50);
|
||
|
||
Assert.False(state.IsLoaded(0x32320FFEu));
|
||
}
|
||
}
|