diff --git a/Containers.Test/RingBufferTests.cs b/Containers.Test/RingBufferTests.cs index 061d089..7faf41e 100644 --- a/Containers.Test/RingBufferTests.cs +++ b/Containers.Test/RingBufferTests.cs @@ -360,4 +360,59 @@ public void Constructor_MinimumLength_IsUsable() Assert.AreEqual(1, buffer.Count); Assert.AreEqual(2, buffer.At(0)); } + + [TestMethod] + public void Enumerate_PushBackDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.PushBack(5)); + + [TestMethod] + public void Enumerate_ClearDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Clear()); + + [TestMethod] + public void Enumerate_ResizeDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Resize(8)); + + [TestMethod] + public void Enumerate_ResampleDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Resample(8)); + + [TestMethod] + public void Enumerate_ModifiedAfterLastElement_ThrowsOnFinalMoveNext() + { + RingBuffer buffer = new([1, 2], 2); + using IEnumerator enumerator = buffer.GetEnumerator(); + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsTrue(enumerator.MoveNext()); + + buffer.PushBack(3); + + Assert.ThrowsExactly(() => enumerator.MoveNext()); + } + + [TestMethod] + public void Enumerate_ModifiedBetweenEnumerations_DoesNotThrow() + { + RingBuffer buffer = new([1, 2, 3, 4], 4); + _ = buffer.ToArray(); + buffer.PushBack(5); + + Assert.AreSequenceEqual(Enumerable.Range(2, 4), buffer); + } + + private static void AssertModificationDuringForeachThrows(Action> modify) + { + RingBuffer buffer = new([1, 2, 3, 4], 4); + + Assert.ThrowsExactly(() => + { + foreach (int item in buffer) + { + if (item == 1) + { + modify(buffer); + } + } + }); + } } diff --git a/Containers/RingBuffer.cs b/Containers/RingBuffer.cs index 635a0ea..57ed78e 100644 --- a/Containers/RingBuffer.cs +++ b/Containers/RingBuffer.cs @@ -70,6 +70,11 @@ public class RingBuffer : IReadOnlyList /// public int Count { get; private set; } // Track number of valid elements + /// + /// Incremented by every change, so an enumerator can tell the buffer changed under it. + /// + private int version; + /// /// Gets the element at the specified index in the buffer. /// @@ -141,6 +146,7 @@ private void AllocateBuffer(int length) BackIndex = 0; FrontIndex = 0; Count = 0; + version++; } /// @@ -171,6 +177,7 @@ public void PushBack(T o) // Advance back index with wraparound using bitwise AND for efficiency BackIndex = (BackIndex + 1) & (Capacity - 1); + version++; } /// @@ -285,12 +292,18 @@ public void Resample(int length) /// Returns an enumerator that iterates through the buffer. /// /// An enumerator for the buffer. - public IEnumerator GetEnumerator() + /// The buffer was modified during enumeration. + public IEnumerator GetEnumerator() => Enumerate(version); + + private IEnumerator Enumerate(int expected) { for (int i = 0; i < Count; i++) { + Enumeration.ThrowIfModified(expected, version); yield return At(i); } + + Enumeration.ThrowIfModified(expected, version); } /// @@ -320,5 +333,6 @@ public void Clear() BackIndex = 0; FrontIndex = 0; Count = 0; + version++; } }