From d6953fc3beb084b9275eac8eeeafdafbae38b119 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Sat, 28 Jan 2023 05:49:49 -0600 Subject: [PATCH] Expand & test "TestingSequence" assertions --- MoreLinq.Test/MemoizeTest.cs | 4 +- MoreLinq.Test/TestingSequence.cs | 277 ++++++++++++++++++++++++--- MoreLinq.Test/WatchableEnumerator.cs | 14 +- 3 files changed, 260 insertions(+), 35 deletions(-) diff --git a/MoreLinq.Test/MemoizeTest.cs b/MoreLinq.Test/MemoizeTest.cs index f2bed5c5e..31ce2aeca 100644 --- a/MoreLinq.Test/MemoizeTest.cs +++ b/MoreLinq.Test/MemoizeTest.cs @@ -246,7 +246,7 @@ public void MemoizeRethrowsErrorDuringIterationToAllIteratorsUntilDisposed() var error = new TestException("This is a test exception."); using var xs = MoreEnumerable.From(() => 123, () => throw error) - .AsTestingSequence(TestingSequence.Options.AllowMultipleEnumerations); + .AsTestingSequence(maxEnumerations: 2); var memoized = xs.Memoize(); using ((IDisposable)memoized) using (var r1 = memoized.Read()) @@ -274,7 +274,7 @@ public void MemoizeRethrowsErrorDuringIterationStartToAllIteratorsUntilDisposed( using var xs = MoreEnumerable.From(() => 0 == i++ ? throw error // throw at start for first iteration only : 42) - .AsTestingSequence(TestingSequence.Options.AllowMultipleEnumerations); + .AsTestingSequence(maxEnumerations: 2); var memoized = xs.Memoize(); using ((IDisposable)memoized) using (var r1 = memoized.Read()) diff --git a/MoreLinq.Test/TestingSequence.cs b/MoreLinq.Test/TestingSequence.cs index 98572d7cc..479459a9b 100644 --- a/MoreLinq.Test/TestingSequence.cs +++ b/MoreLinq.Test/TestingSequence.cs @@ -20,27 +20,40 @@ namespace MoreLinq.Test using System; using System.Collections; using System.Collections.Generic; + using System.Text.RegularExpressions; using NUnit.Framework; + using static TestingSequence; static class TestingSequence { internal static TestingSequence Of(params T[] elements) => - Of(Options.None, elements); + new(elements, Options.None, maxEnumerations: 1); internal static TestingSequence Of(Options options, params T[] elements) => - elements.AsTestingSequence(options); + elements.AsTestingSequence(options, maxEnumerations: 1); internal static TestingSequence AsTestingSequence(this IEnumerable source, - Options options = Options.None) => + Options options = Options.None, + int maxEnumerations = 1) => source != null - ? new TestingSequence(source) { IsReiterationAllowed = options.HasFlag(Options.AllowMultipleEnumerations) } + ? new TestingSequence(source, options, maxEnumerations) : throw new ArgumentNullException(nameof(source)); + internal const string ExpectedDisposal = "Expected sequence to be disposed."; + internal const string TooManyEnumerations = "Sequence should not be enumerated more than expected."; + internal const string TooManyDisposals = "Sequence should not be disposed more than once per enumeration."; + internal const string SimultaneousEnumerations = "Sequence should not have simultaneous enumeration."; + internal const string MoveNextPostDisposal = "LINQ operators should not call MoveNext() on a disposed sequence."; + internal const string MoveNextPostEnumeration = "LINQ operators should not continue iterating a sequence that has terminated."; + internal const string CurrentPostDisposal = "LINQ operators should not attempt to get the Current value on a disposed sequence."; + internal const string CurrentPostEnumeration = "LINQ operators should not attempt to get the Current value on a completed sequence."; + [Flags] public enum Options { None, - AllowMultipleEnumerations + AllowRepeatedDisposals = 0x2, + AllowRepeatedMoveNexts = 0x4, } } @@ -51,59 +64,261 @@ public enum Options /// sealed class TestingSequence : IEnumerable, IDisposable { - bool? _disposed; - IEnumerable? _sequence; + readonly IEnumerable _sequence; + readonly Options _options; + readonly int _maxEnumerations; + + int _disposedCount; + int _enumerationCount; - internal TestingSequence(IEnumerable sequence) => + internal TestingSequence(IEnumerable sequence, Options options, int maxEnumerations) + { _sequence = sequence; + _maxEnumerations = maxEnumerations; + _options = options; + } - public bool IsDisposed => _disposed ?? false; - public bool IsReiterationAllowed { get; init; } public int MoveNextCallCount { get; private set; } + public bool IsDisposed => _enumerationCount > 0 && _disposedCount == _enumerationCount; - void IDisposable.Dispose() => - AssertDisposed(); - - /// - /// Checks that the iterator was disposed, and then resets. - /// - void AssertDisposed() + void IDisposable.Dispose() { - if (_disposed == null) - return; - Assert.That(_disposed, Is.True, "Expected sequence to be disposed."); - _disposed = null; + if (_enumerationCount > 0) + Assert.That(_disposedCount, Is.EqualTo(_enumerationCount), ExpectedDisposal); } public IEnumerator GetEnumerator() { - if (!IsReiterationAllowed) - Assert.That(_sequence, Is.Not.Null, "LINQ operators should not enumerate a sequence more than once."); - - Debug.Assert(_sequence is not null); + Assert.That(_enumerationCount, Is.LessThan(_maxEnumerations), TooManyEnumerations); + Assert.That(_enumerationCount, Is.EqualTo(_disposedCount), SimultaneousEnumerations); + _enumerationCount++; var enumerator = _sequence.GetEnumerator().AsWatchable(); - _disposed = false; + var disposed = false; enumerator.Disposed += delegate { - Assert.That(_disposed, Is.False, "LINQ operators should not dispose a sequence more than once."); - _disposed = true; + if (!disposed) + { + _disposedCount++; + disposed = true; + } + else if (!_options.HasFlag(Options.AllowRepeatedDisposals)) + { + Assert.Fail(TooManyDisposals); + } }; + var ended = false; enumerator.MoveNextCalled += (_, moved) => { - Assert.That(ended, Is.False, "LINQ operators should not continue iterating a sequence that has terminated."); + Assert.That(disposed, Is.False, MoveNextPostDisposal); + if (!_options.HasFlag(Options.AllowRepeatedMoveNexts)) + Assert.That(ended, Is.False, MoveNextPostEnumeration); + ended = !moved; MoveNextCallCount++; }; - if (!IsReiterationAllowed) - _sequence = null; + enumerator.GetCurrentCalled += delegate + { + Assert.That(disposed, Is.False, CurrentPostDisposal); + Assert.That(ended, Is.False, CurrentPostEnumeration); + }; return enumerator; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [TestFixture] + public class TestingSequenceTest + { + [Test] + public void TestingSequencePublicPropertiesTest() + { + using var sequence = Of(1, 2, 3, 4); + Assert.That(sequence.IsDisposed, Is.False); + Assert.That(sequence.MoveNextCallCount, Is.EqualTo(0)); + + var iter = sequence.GetEnumerator(); + Assert.That(sequence.IsDisposed, Is.False); + Assert.That(sequence.MoveNextCallCount, Is.EqualTo(0)); + + for (var i = 1; i <= 4; i++) + { + _ = iter.MoveNext(); + Assert.That(sequence.IsDisposed, Is.False); + Assert.That(sequence.MoveNextCallCount, Is.EqualTo(i)); + } + + iter.Dispose(); + Assert.That(sequence.IsDisposed, Is.True); + } + + [Test] + public void TestingSequenceShouldValidateDisposal() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + var _ = enumerable.GetEnumerator(); + + yield break; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, ExpectedDisposal); + } + + [Test] + public void TestingSequenceShouldValidateNumberOfUsages() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + using (enumerable.GetEnumerator()) + yield return 1; + using (enumerable.GetEnumerator()) + yield return 2; + using (enumerable.GetEnumerator()) + yield return 3; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(maxEnumerations: 2); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, TooManyEnumerations); + } + + [Test] + public void TestingSequenceShouldValidateDisposeOnDisposedSequence() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + var enumerator = enumerable.GetEnumerator(); + enumerator.Dispose(); + enumerator.Dispose(); + + yield break; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, TooManyDisposals); + } + + [Test] + public void TestingSequenceShouldValidateMoveNextOnDisposedSequence() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + var enumerator = enumerable.GetEnumerator(); + enumerator.Dispose(); + _ = enumerator.MoveNext(); + + yield break; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, MoveNextPostDisposal); + } + + [Test] + public void TestingSequenceShouldValidateMoveNextOnCompletedSequence() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + using var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) + yield return enumerator.Current; + _ = enumerator.MoveNext(); + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, MoveNextPostEnumeration); + } + + [Test] + public void TestingSequenceShouldValidateCurrentOnDisposedSequence() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + var enumerator = enumerable.GetEnumerator(); + enumerator.Dispose(); + yield return enumerator.Current; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, CurrentPostDisposal); + } + + [Test] + public void TestingSequenceShouldValidateCurrentOnEndedSequence() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + using var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) + yield return enumerator.Current; + yield return enumerator.Current; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, CurrentPostEnumeration); + } + + [Test] + public void TestingSequenceShouldValidateSimultaneousEnumeration() + { + static IEnumerable InvalidUsage(IEnumerable enumerable) + { + using var enum1 = enumerable.GetEnumerator(); + using var enum2 = enumerable.GetEnumerator(); + + yield break; + } + + static void Act() + { + using var xs = Enumerable.Range(1, 10).AsTestingSequence(maxEnumerations: 2); + InvalidUsage(xs).Consume(); + } + + AssertTestingSequenceException(Act, SimultaneousEnumerations); + } + static void AssertTestingSequenceException(TestDelegate code, string message) => + Assert.That(code, Throws.InstanceOf().With.Message.Matches(@"^\s*" + Regex.Escape(message))); } } diff --git a/MoreLinq.Test/WatchableEnumerator.cs b/MoreLinq.Test/WatchableEnumerator.cs index cb6b14bef..4cb3d4221 100644 --- a/MoreLinq.Test/WatchableEnumerator.cs +++ b/MoreLinq.Test/WatchableEnumerator.cs @@ -31,13 +31,23 @@ sealed class WatchableEnumerator : IEnumerator readonly IEnumerator _source; public event EventHandler? Disposed; + public event EventHandler? GetCurrentCalled; public event EventHandler? MoveNextCalled; public WatchableEnumerator(IEnumerator source) => _source = source ?? throw new ArgumentNullException(nameof(source)); - public T Current => _source.Current; - object? IEnumerator.Current => Current; + public T Current + { + get + { + GetCurrentCalled?.Invoke(this, EventArgs.Empty); + return _source.Current; + } + } + + object? IEnumerator.Current => this.Current; + public void Reset() => _source.Reset(); public bool MoveNext()