using AcDream.App.Rendering; using Silk.NET.OpenGL; namespace AcDream.App.Tests.Rendering; public sealed class ShaderProgramConstructionTests { [Theory] [InlineData("ShaderSource:VertexShader", 1, 0)] [InlineData("CompileShader:VertexShader", 1, 0)] [InlineData("CreateShader:FragmentShader", 1, 0)] [InlineData("ShaderSource:FragmentShader", 2, 0)] [InlineData("CompileShader:FragmentShader", 2, 0)] [InlineData("CreateProgram", 2, 0)] [InlineData("AttachShader:VertexShader", 2, 1)] [InlineData("AttachShader:FragmentShader", 2, 1)] [InlineData("LinkProgram", 2, 1)] public void ThrowAfterEachAllocatedNameRollsBackEveryPublishedName( string failingOperation, int expectedShaderDeletes, int expectedProgramDeletes) { var api = new FakeShaderApi { ThrowOn = failingOperation }; Assert.Throws(() => ShaderProgramConstruction.Build(api, "vertex", "fragment")); Assert.Equal(expectedShaderDeletes, api.DeletedShaders.Count); Assert.Equal(expectedProgramDeletes, api.DeletedPrograms.Count); Assert.Equal(api.CreatedShaders, api.DeletedShaders.Order().ToArray()); Assert.Equal(api.CreatedPrograms, api.DeletedPrograms.Order().ToArray()); } [Fact] public void CompileFailurePreservesOriginalFailureWhenRollbackIsClean() { var api = new FakeShaderApi { VertexCompiles = false }; InvalidOperationException failure = Assert.Throws(() => ShaderProgramConstruction.Build(api, "vertex", "fragment")); Assert.Contains("VertexShader compile failed", failure.Message); Assert.Equal([1u], api.DeletedShaders); Assert.Empty(api.CreatedPrograms); } [Fact] public void LinkFailureDetachesAndDeletesBothShadersAndProgram() { var api = new FakeShaderApi { ProgramLinks = false }; InvalidOperationException failure = Assert.Throws(() => ShaderProgramConstruction.Build(api, "vertex", "fragment")); Assert.Contains("program link failed", failure.Message); Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); Assert.Equal([1u, 2u], api.DeletedShaders); Assert.Equal([3u], api.DeletedPrograms); } [Fact] public void SuccessfulBuildCommitsOnlyProgramAndReleasesTemporaryShaders() { var api = new FakeShaderApi(); uint program = ShaderProgramConstruction.Build(api, "vertex", "fragment"); Assert.Equal(3u, program); Assert.Equal([1u, 2u], api.DeletedShaders); Assert.Empty(api.DeletedPrograms); Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); } [Fact] public void CleanupFailureStillAttemptsEveryOtherReleaseAndProgramRollback() { var api = new FakeShaderApi { ThrowOn = "DetachShader:VertexShader" }; AggregateException failure = Assert.Throws(() => ShaderProgramConstruction.Build(api, "vertex", "fragment")); Assert.Single(failure.InnerExceptions); Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); Assert.Equal([1u, 2u], api.DeletedShaders); Assert.Equal([3u], api.DeletedPrograms); } [Fact] public void ConstructionAndRollbackFailuresAreReportedTogether() { var api = new FakeShaderApi { ProgramLinks = false, ThrowOn = "DeleteShader:VertexShader", }; GlResourceConstructionException failure = Assert.Throws(() => ShaderProgramConstruction.Build(api, "vertex", "fragment")); Assert.Equal(2, failure.InnerExceptions.Count); Assert.Contains("program link failed", failure.InnerExceptions[0].Message); Assert.Equal([1u, 2u], api.DeleteShaderAttempts); Assert.Equal([2u], api.DeletedShaders); Assert.Equal([3u], api.DeletedPrograms); api.ThrowOn = null; failure.RetryCleanup(); Assert.True(failure.IsCleanupComplete); Assert.Equal([2u, 1u], api.DeletedShaders); Assert.Equal([1u, 2u, 1u], api.DeleteShaderAttempts); } private sealed class FakeShaderApi : IShaderProgramBuildApi { private readonly Dictionary _shaderTypes = []; private uint _nextName = 1; public string? ThrowOn { get; set; } public bool VertexCompiles { get; init; } = true; public bool FragmentCompiles { get; init; } = true; public bool ProgramLinks { get; init; } = true; public List CreatedShaders { get; } = []; public List CreatedPrograms { get; } = []; public List DeleteShaderAttempts { get; } = []; public List DeletedShaders { get; } = []; public List DeletedPrograms { get; } = []; public List DetachCalls { get; } = []; public uint CreateShader(ShaderType type) { Fail($"CreateShader:{type}"); uint name = _nextName++; CreatedShaders.Add(name); _shaderTypes.Add(name, type); return name; } public void ShaderSource(uint shader, string source) => Fail($"ShaderSource:{TypeOf(shader)}"); public void CompileShader(uint shader) => Fail($"CompileShader:{TypeOf(shader)}"); public int GetShaderCompileStatus(uint shader) => TypeOf(shader) switch { ShaderType.VertexShader => VertexCompiles ? 1 : 0, ShaderType.FragmentShader => FragmentCompiles ? 1 : 0, _ => 0, }; public string GetShaderInfoLog(uint shader) => $"bad {TypeOf(shader)}"; public uint CreateProgram() { Fail("CreateProgram"); uint name = _nextName++; CreatedPrograms.Add(name); return name; } public void AttachShader(uint program, uint shader) => Fail($"AttachShader:{TypeOf(shader)}"); public void LinkProgram(uint program) => Fail("LinkProgram"); public int GetProgramLinkStatus(uint program) => ProgramLinks ? 1 : 0; public string GetProgramInfoLog(uint program) => "bad link"; public void DetachShader(uint program, uint shader) { string operation = $"DetachShader:{TypeOf(shader)}"; DetachCalls.Add(operation); Fail(operation); } public void DeleteShader(uint shader) { DeleteShaderAttempts.Add(shader); Fail($"DeleteShader:{TypeOf(shader)}"); DeletedShaders.Add(shader); } public void DeleteProgram(uint program) { Fail("DeleteProgram"); DeletedPrograms.Add(program); } private ShaderType TypeOf(uint shader) => _shaderTypes[shader]; private void Fail(string operation) { if (ThrowOn == operation) throw new InvalidOperationException(operation + " failed"); } } }