using AcDream.App.Rendering; namespace AcDream.App.Tests.Rendering; public sealed class ResourceShutdownTransactionTests { [Fact] public void AttemptsIndependentOperationsAndRetriesOnlyPendingOnes() { var calls = new List(); int retryAttempts = 0; var transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("owners", [ new("first", () => calls.Add("first")), new("retry", () => { calls.Add("retry"); if (retryAttempts++ == 0) throw new InvalidOperationException("synthetic transient failure"); }), new("last", () => calls.Add("last")), ]), new ResourceShutdownStage("dependent", [ new("dependent", () => calls.Add("dependent")), ])); transaction.CompleteOrThrow(); Assert.Equal(["first", "retry", "last", "retry", "dependent"], calls); Assert.True(transaction.IsComplete); } [Fact] public void PersistentFailureNeverRunsDependentStage() { int failures = 0; int dependentCalls = 0; var transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("owner", [ new("persistent", () => { failures++; throw new InvalidOperationException("persistent"); }), ]), new ResourceShutdownStage("dependent", [ new("must-not-run", () => dependentCalls++), ])); Assert.Throws(transaction.CompleteOrThrow); Assert.Equal(2, failures); Assert.Equal(0, dependentCalls); Assert.False(transaction.IsComplete); } [Fact] public void ReentrantCompletionDoesNotReplayActiveOperation() { ResourceShutdownTransaction? transaction = null; int calls = 0; transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("stage", [ new("reentrant", () => { calls++; transaction!.CompleteOrThrow(); }), ])); transaction.CompleteOrThrow(); Assert.Equal(1, calls); Assert.True(transaction.IsComplete); } [Fact] public void PersistentFailuresInOneStageAreAllAttemptedAndNamed() { int firstCalls = 0; int secondCalls = 0; var transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("owners", [ new("first", () => { firstCalls++; throw new InvalidOperationException("first failure"); }), new("second", () => { secondCalls++; throw new InvalidOperationException("second failure"); }), ])); AggregateException error = Assert.Throws( transaction.CompleteOrThrow); Assert.True(firstCalls > 0); Assert.Equal(firstCalls, secondCalls); Assert.Contains( error.InnerExceptions, exception => exception.Message.Contains("first", StringComparison.Ordinal)); Assert.Contains( error.InnerExceptions, exception => exception.Message.Contains("second", StringComparison.Ordinal)); Assert.False(transaction.IsComplete); } [Fact] public void ExplicitRetryAfterFailedCompletionDoesNotReplaySuccessfulOperations() { int completeCalls = 0; int pendingCalls = 0; bool allowPending = false; var transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("owners", [ new("complete", () => completeCalls++), new("pending", () => { pendingCalls++; if (!allowPending) throw new InvalidOperationException("not yet"); }), ])); Assert.Throws(transaction.CompleteOrThrow); int pendingCallsBeforeRetry = pendingCalls; allowPending = true; transaction.CompleteOrThrow(); Assert.Equal(1, completeCalls); Assert.Equal(pendingCallsBeforeRetry + 1, pendingCalls); Assert.True(transaction.IsComplete); } [Fact] public void EmptyStagesConvergeWithoutSkippingLaterOperations() { int calls = 0; var transaction = new ResourceShutdownTransaction( new ResourceShutdownStage("empty", []), new ResourceShutdownStage("owner", [ new("owner", () => calls++), ]), new ResourceShutdownStage("empty tail", [])); transaction.CompleteOrThrow(); Assert.Equal(1, calls); Assert.True(transaction.IsComplete); } }