Skip to content

Commit

Permalink
Expand & test "TestingSequence" assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
viceroypenguin authored Jan 28, 2023
1 parent 5e2a031 commit d6953fc
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 35 deletions.
4 changes: 2 additions & 2 deletions MoreLinq.Test/MemoizeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
277 changes: 246 additions & 31 deletions MoreLinq.Test/TestingSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> Of<T>(params T[] elements) =>
Of(Options.None, elements);
new(elements, Options.None, maxEnumerations: 1);

internal static TestingSequence<T> Of<T>(Options options, params T[] elements) =>
elements.AsTestingSequence(options);
elements.AsTestingSequence(options, maxEnumerations: 1);

internal static TestingSequence<T> AsTestingSequence<T>(this IEnumerable<T> source,
Options options = Options.None) =>
Options options = Options.None,
int maxEnumerations = 1) =>
source != null
? new TestingSequence<T>(source) { IsReiterationAllowed = options.HasFlag(Options.AllowMultipleEnumerations) }
? new TestingSequence<T>(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,
}
}

Expand All @@ -51,59 +64,261 @@ public enum Options
/// </summary>
sealed class TestingSequence<T> : IEnumerable<T>, IDisposable
{
bool? _disposed;
IEnumerable<T>? _sequence;
readonly IEnumerable<T> _sequence;
readonly Options _options;
readonly int _maxEnumerations;

int _disposedCount;
int _enumerationCount;

internal TestingSequence(IEnumerable<T> sequence) =>
internal TestingSequence(IEnumerable<T> 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();

/// <summary>
/// Checks that the iterator was disposed, and then resets.
/// </summary>
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<T> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<int> InvalidUsage(IEnumerable<int> 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<AssertionException>().With.Message.Matches(@"^\s*" + Regex.Escape(message)));
}
}
14 changes: 12 additions & 2 deletions MoreLinq.Test/WatchableEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ sealed class WatchableEnumerator<T> : IEnumerator<T>
readonly IEnumerator<T> _source;

public event EventHandler? Disposed;
public event EventHandler? GetCurrentCalled;
public event EventHandler<bool>? MoveNextCalled;

public WatchableEnumerator(IEnumerator<T> 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()
Expand Down

0 comments on commit d6953fc

Please sign in to comment.