acdream/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs
2026-08-18 14:58:25 +02:00

123 lines
4.3 KiB
C#

using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Rendering.Gpu;
namespace AcDream.App.Tests.Rendering;
public sealed class ResourceCleanupGroupTests
{
[Fact]
public void CleanupRunsInReverseOrderAndNeverReplaysSuccess()
{
var calls = new List<string>();
var resources = new ResourceCleanupGroup();
int middleFailures = 1;
resources.Add("first", () => calls.Add("first"));
resources.Add("middle", () =>
{
calls.Add("middle");
if (middleFailures-- > 0)
throw new InvalidOperationException("middle failed");
});
resources.Add("last", () => calls.Add("last"));
Assert.Throws<AggregateException>(resources.RetryCleanup);
Assert.Equal(["last", "middle", "first"], calls);
resources.RetryCleanup();
resources.RetryCleanup();
Assert.True(resources.IsCleanupComplete);
Assert.Equal(["last", "middle", "first", "middle"], calls);
}
[Fact]
public void TransferAllLeavesPublishedResourcesUntouched()
{
int releaseCalls = 0;
var resources = new ResourceCleanupGroup();
resources.Add("published", () => releaseCalls++);
resources.TransferAll();
resources.RetryCleanup();
Assert.True(resources.IsCleanupComplete);
Assert.Equal(0, releaseCalls);
Assert.Throws<InvalidOperationException>(() =>
resources.Add("late", () => { }));
}
[Fact]
public void FailedConstructionRollbackRetainsOnlyUnreleasedOwnership()
{
int releases = 0;
bool releaseFails = true;
var resources = new ResourceCleanupGroup();
resources.Add("retryable", () =>
{
releases++;
if (releaseFails)
throw new InvalidOperationException("release failed");
});
ResourceConstructionException failure =
Assert.Throws<ResourceConstructionException>(() =>
resources.RollbackConstructionAndThrow(
"construction failed",
new InvalidOperationException("original failure")));
Assert.False(failure.IsCleanupComplete);
Assert.Equal(1, releases);
releaseFails = false;
failure.RetryCleanup();
failure.RetryCleanup();
Assert.True(failure.IsCleanupComplete);
Assert.Equal(2, releases);
}
/// <summary>
/// Campaign V slice V4a moved TextRenderer's constructor off raw
/// VAO/VBO/texture GL names and onto two device-owned resources — a
/// pipeline and a 1x1 white fill texture — with a catch that disposed
/// whichever already existed when the other threw.
///
/// Slice V6d removed the white texture: the shader gained an untextured
/// branch that produces what white-times-colour produced, so the fill needs
/// no texture at all. That leaves exactly ONE owned resource, which is a
/// stronger property than correct rollback — with nothing to orphan there is
/// no partial-construction window to get wrong. This test pins that, so
/// re-growing a second resource without re-growing the rollback fails here.
/// </summary>
[Fact]
public void TextRendererConstructionCreatesAndDisposesOnlyOnePipeline()
{
using var device = new RecordingGpuDevice();
int buffersBefore = device.CreatedBuffers.Count;
int pipelinesBefore = device.CreatedPipelines.Count;
int samplersBefore = device.CreatedSamplers.Count;
int texturesBefore = device.CreatedTextures.Count;
int slotsBefore = device.LiveTextureSlotCount;
var renderer = new TextRenderer(
device,
new NullGpuFrameSource(),
shaderDir: "unused");
RecordingGpuPipeline pipeline = Assert.Single(
device.CreatedPipelines.Skip(pipelinesBefore));
Assert.Equal(buffersBefore, device.CreatedBuffers.Count);
Assert.Equal(samplersBefore, device.CreatedSamplers.Count);
Assert.Equal(texturesBefore, device.CreatedTextures.Count);
Assert.Equal(slotsBefore, device.LiveTextureSlotCount);
renderer.Dispose();
Assert.True(pipeline.IsDisposed);
}
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;
}
}