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"); }); 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 TextRendererConstructorOwnsExactlyOneDeviceResource() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "TextRenderer.cs")); Assert.Equal(1, CountOccurrences(source, "device.CreatePipeline(")); Assert.Equal(0, CountOccurrences(source, "device.CreateTexture(")); Assert.Equal(0, CountOccurrences(source, "device.CreateBuffer(")); Assert.Equal(0, CountOccurrences(source, "device.CreateSampler(")); Assert.Equal(0, CountOccurrences(source, "device.RegisterTexture(")); // And the one resource is released. Assert.Contains("public void Dispose() => _pipeline.Dispose();", source, StringComparison.Ordinal); } private static int CountOccurrences(string source, string needle) { int count = 0; for (int i = source.IndexOf(needle, StringComparison.Ordinal); i >= 0; i = source.IndexOf(needle, i + needle.Length, StringComparison.Ordinal)) { count++; } return count; } 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."); } }