using AcDream.App.Rendering; 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"); }); GlResourceConstructionException 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); } [Fact] public void WorldRenderFoundationConstructorsPublishEveryOwnedPrefix() { string root = FindRepoRoot(); string graphicsDevice = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Wb", "OpenGLGraphicsDevice.cs")); string shader = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Wb", "GLSLShader.cs")); string uniformBuffer = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Wb", "ManagedGLUniformBuffer.cs")); AssertAppearsInOrder( graphicsDevice, "var resources = new ResourceCleanupGroup();", "InstanceVBO = CreateConstructionBuffer(resources", "WrapSampler = CreateConstructionSampler(", "ClampSampler = CreateConstructionSampler(", "_sceneDataBuffer = new ManagedGLUniformBuffer(", "resources.Add(", "\"WB scene-data uniform buffer\"", "InitializeSharedDebugResources(resources);", "resources.TransferAll();", "resources.RollbackConstructionAndThrow("); AssertAppearsInOrder( shader, "ShaderProgramConstruction.Build(", "resources.Add(\"WB shader program\"", "configure shader {Name} SceneData binding", "resources.TransferAll();", "resources.RollbackConstructionAndThrow("); AssertAppearsInOrder( uniformBuffer, "TrackedGlResource.CreateBuffer(", "resources.Add(", "TrackedGlResource.AllocateBufferStorage(", "resources.TransferAll();", "resources.RollbackConstructionAndThrow("); Assert.Contains("internal void DisposeImmediately()", uniformBuffer, StringComparison.Ordinal); } [Fact] public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "TextRenderer.cs")); AssertAppearsInOrder( source, "shader = new Shader(gl,", "resources.Add(\"text shader\", shader.Dispose);", "frameBuffers[i] = CreateFrameBufferSet(resources);", "whiteTexture = GlResourceCommand.CreateTexture(", "resources.Add(", "\"white texture\"", "GlResourceCommand.Execute(", "initialize TextRenderer white texture", "resources.RetryCleanup();", "_resources = resources;"); AssertAppearsInOrder( source, "private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)", "TrackedGlResource.CreateVertexArray(", "resources.Add(\"frame VAO\", vaoRelease.Run);", "TrackedGlResource.CreateBuffer(", "resources.Add(", "\"frame VBO\""); Assert.Contains("_resources.RetryCleanup();", source, StringComparison.Ordinal); Assert.DoesNotContain("private FrameBufferSet CreateFrameBufferSet()", source, StringComparison.Ordinal); } [Fact] public void ReturnedNameIsRetainedWhenPostconditionAndFirstRollbackFail() { bool deleteFails = true; int deleteCalls = 0; GlResourceConstructionException failure = Assert.Throws(() => GlResourceCommand.CreateNameCore( "synthetic buffer", static () => { }, () => 42, () => throw new InvalidOperationException("postcondition failed"), name => { Assert.Equal(42u, name); deleteCalls++; if (deleteFails) throw new InvalidOperationException("delete failed"); })); Assert.False(failure.IsCleanupComplete); Assert.Equal(1, deleteCalls); deleteFails = false; failure.RetryCleanup(); failure.RetryCleanup(); Assert.True(failure.IsCleanupComplete); Assert.Equal(2, deleteCalls); } private static void AssertAppearsInOrder(string source, params string[] values) { int cursor = -1; foreach (string value in values) { int next = source.IndexOf(value, cursor + 1, StringComparison.Ordinal); Assert.True(next > cursor, $"Missing or out-of-order source fragment: {value}"); cursor = next; } } private static string FindRepoRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) return directory.FullName; directory = directory.Parent; } throw new DirectoryNotFoundException("Could not find AcDream.slnx."); } }