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(); 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(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(() => 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(() => 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); } /// /// 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. /// [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; } }