using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; namespace AcDream.App.Tests.Rendering; public sealed class PortalMeshReferenceOwnerTests { [Fact] public void PartialAcquireRollsBackEveryCommittedSibling() { var adapter = new RecordingMeshAdapter(); adapter.FailIncrementBeforeCommit(0x01000001u, attempts: 1); var owner = new PortalMeshReferenceOwner( adapter, [0x01000001u, 0x01000002u]); Assert.Throws(owner.Acquire); Assert.Equal(0, adapter.TotalReferences); owner.Acquire(); Assert.Equal(2, adapter.IncrementAttempts(0x01000001u)); Assert.Equal(2, adapter.IncrementAttempts(0x01000002u)); owner.Dispose(); Assert.Equal(0, adapter.TotalReferences); } [Fact] public void CommittedAcquireFailureIsRolledBackBeforeRetry() { var adapter = new RecordingMeshAdapter(); adapter.FailIncrementAfterCommit(0x01000001u, attempts: 1); var owner = new PortalMeshReferenceOwner(adapter, [0x01000001u]); Assert.Throws(owner.Acquire); Assert.Equal(0, adapter.TotalReferences); owner.Acquire(); Assert.Equal(2, adapter.IncrementAttempts(0x01000001u)); Assert.Equal(1, adapter.ReferenceCount(0x01000001u)); owner.Dispose(); Assert.Equal(0, adapter.TotalReferences); } [Fact] public void FailedAcquireRollbackRetainsExactReferenceForDisposeRetry() { var adapter = new RecordingMeshAdapter(); adapter.FailIncrementBeforeCommit(0x01000001u, attempts: 1); adapter.FailDecrementBeforeCommit(0x01000002u, attempts: 1); var owner = new PortalMeshReferenceOwner( adapter, [0x01000001u, 0x01000002u]); AggregateException failure = Assert.Throws(owner.Acquire); Assert.Contains("rollback", failure.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal(0, adapter.ReferenceCount(0x01000001u)); Assert.Equal(1, adapter.ReferenceCount(0x01000002u)); owner.Dispose(); Assert.True(owner.IsDisposed); Assert.Equal(2, adapter.DecrementAttempts(0x01000002u)); Assert.Equal(0, adapter.TotalReferences); } [Fact] public void DisposeAttemptsEveryReferenceAndRetriesOnlyPendingRelease() { var adapter = new RecordingMeshAdapter(); var owner = new PortalMeshReferenceOwner( adapter, [0x01000001u, 0x01000002u]); owner.Acquire(); adapter.FailDecrementBeforeCommit(0x01000001u, attempts: 1); Assert.Throws(owner.Dispose); Assert.False(owner.IsDisposed); Assert.Equal(0, adapter.ReferenceCount(0x01000002u)); owner.Dispose(); owner.Dispose(); Assert.True(owner.IsDisposed); Assert.Equal(2, adapter.DecrementAttempts(0x01000001u)); Assert.Equal(1, adapter.DecrementAttempts(0x01000002u)); Assert.Equal(0, adapter.TotalReferences); } [Fact] public void CommittedReleaseFailureCompletesOwnershipWithoutDoubleDecrement() { var adapter = new RecordingMeshAdapter(); var owner = new PortalMeshReferenceOwner(adapter, [0x01000001u]); owner.Acquire(); adapter.FailDecrementAfterCommit(0x01000001u, attempts: 1); Assert.Throws(owner.Dispose); Assert.True(owner.IsDisposed); owner.Dispose(); Assert.Equal(1, adapter.DecrementAttempts(0x01000001u)); Assert.Equal(0, adapter.TotalReferences); } [Fact] public void DisposeReentryDuringAcquireReconcilesBeforeReturning() { PortalMeshReferenceOwner? owner = null; var adapter = new RecordingMeshAdapter { AfterIncrement = _ => owner!.Dispose(), }; owner = new PortalMeshReferenceOwner(adapter, [0x01000001u]); Assert.Throws(owner.Acquire); owner.Dispose(); Assert.True(owner.IsDisposed); Assert.Equal(1, adapter.IncrementAttempts(0x01000001u)); Assert.Equal(1, adapter.DecrementAttempts(0x01000001u)); Assert.Equal(0, adapter.TotalReferences); } private sealed class RecordingMeshAdapter : IWbMeshAdapter { private readonly Dictionary _references = []; private readonly Dictionary _incrementAttempts = []; private readonly Dictionary _decrementAttempts = []; private readonly Dictionary _incrementFailuresBeforeCommit = []; private readonly Dictionary _incrementFailuresAfterCommit = []; private readonly Dictionary _decrementFailuresBeforeCommit = []; private readonly Dictionary _decrementFailuresAfterCommit = []; public Action? AfterIncrement { get; init; } public int TotalReferences => _references.Values.Sum(); public int ReferenceCount(ulong id) => _references.GetValueOrDefault(id); public int IncrementAttempts(ulong id) => _incrementAttempts.GetValueOrDefault(id); public int DecrementAttempts(ulong id) => _decrementAttempts.GetValueOrDefault(id); public void FailIncrementBeforeCommit(ulong id, int attempts) => _incrementFailuresBeforeCommit[id] = attempts; public void FailIncrementAfterCommit(ulong id, int attempts) => _incrementFailuresAfterCommit[id] = attempts; public void FailDecrementBeforeCommit(ulong id, int attempts) => _decrementFailuresBeforeCommit[id] = attempts; public void FailDecrementAfterCommit(ulong id, int attempts) => _decrementFailuresAfterCommit[id] = attempts; public void IncrementRefCount(ulong id) { _incrementAttempts[id] = IncrementAttempts(id) + 1; if (Consume(_incrementFailuresBeforeCommit, id)) throw new InvalidOperationException("synthetic pre-commit acquire failure"); _references[id] = ReferenceCount(id) + 1; AfterIncrement?.Invoke(id); if (Consume(_incrementFailuresAfterCommit, id)) { throw new MeshReferenceMutationException( "synthetic committed acquire failure", mutationCommitted: true, new InvalidOperationException()); } } public void DecrementRefCount(ulong id) { _decrementAttempts[id] = DecrementAttempts(id) + 1; if (Consume(_decrementFailuresBeforeCommit, id)) throw new InvalidOperationException("synthetic pre-commit release failure"); int references = ReferenceCount(id); if (references <= 0) throw new InvalidOperationException("reference underflow"); _references[id] = references - 1; if (Consume(_decrementFailuresAfterCommit, id)) { throw new MeshReferenceMutationException( "synthetic committed release failure", mutationCommitted: true, new InvalidOperationException()); } } private static bool Consume(Dictionary remaining, ulong id) { int count = remaining.GetValueOrDefault(id); if (count <= 0) return false; remaining[id] = count - 1; return true; } } }