diff --git a/Moq.Tests/InvocationsFixture.cs b/Moq.Tests/InvocationsFixture.cs new file mode 100644 index 000000000..fd22eea23 --- /dev/null +++ b/Moq.Tests/InvocationsFixture.cs @@ -0,0 +1,108 @@ +using System; +using Xunit; + +namespace Moq.Tests +{ + public class InvocationsFixture + { + [Fact] + public void MockInvocationsAreRecorded() + { + var mock = new Mock(); + + mock.Object.CompareTo(new object()); + + Assert.Equal(1, mock.Invocations.Count); + } + + [Fact] + public void MockInvocationsIncludeInvokedMethod() + { + var mock = new Mock(); + + mock.Object.CompareTo(new object()); + + var invocation = mock.Invocations[0]; + + var expectedMethod = typeof(IComparable).GetMethod(nameof(mock.Object.CompareTo)); + + Assert.Equal(expectedMethod, invocation.Method); + } + + [Fact] + public void MockInvocationsIncludeArguments() + { + var mock = new Mock(); + + var obj = new object(); + + mock.Object.CompareTo(obj); + + var invocation = mock.Invocations[0]; + + var expectedArguments = new[] {obj}; + + Assert.Equal(expectedArguments, invocation.Arguments); + } + + [Fact] + public void MockInvocationsCanBeEnumerated() + { + var mock = new Mock(); + + mock.Object.CompareTo(-1); + mock.Object.CompareTo(0); + mock.Object.CompareTo(1); + + var count = 0; + + using (var enumerator = mock.Invocations.GetEnumerator()) + { + while (enumerator.MoveNext()) + { + Assert.NotNull(enumerator.Current); + + count++; + } + } + + Assert.Equal(3, count); + } + + [Fact] + public void MockInvocationsCanBeCleared() + { + var mock = new Mock(); + + mock.Object.CompareTo(new object()); + + mock.ResetCalls(); + + Assert.Equal(0, mock.Invocations.Count); + } + + [Fact] + public void MockInvocationsCanBeRetrievedByIndex() + { + var mock = new Mock(); + + mock.Object.CompareTo(-1); + mock.Object.CompareTo(0); + mock.Object.CompareTo(1); + + var invocation = mock.Invocations[1]; + + var arg = invocation.Arguments[0]; + + Assert.Equal(0, arg); + } + + [Fact] + public void MockInvocationsIndexerThrowsIndexOutOfRangeWhenCollectionIsEmpty() + { + var mock = new Mock(); + + Assert.Throws(() => mock.Invocations[0]); + } + } +} diff --git a/Source/InvocationCollection.cs b/Source/InvocationCollection.cs index dcbf35564..c75c85225 100644 --- a/Source/InvocationCollection.cs +++ b/Source/InvocationCollection.cs @@ -47,20 +47,20 @@ namespace Moq { internal sealed class InvocationCollection : IReadOnlyList { - private List invocations; + private Invocation[] invocations; - public InvocationCollection() - { - this.invocations = new List(); - } + private int capacity = 0; + private int count = 0; + + private readonly object invocationsLock = new object(); public int Count { get { - lock (this.invocations) + lock (this.invocationsLock) { - return this.invocations.Count; + return count; } } } @@ -69,8 +69,13 @@ public IReadOnlyInvocation this[int index] { get { - lock (this.invocations) + lock (this.invocationsLock) { + if (this.count <= index || index < 0) + { + throw new IndexOutOfRangeException(); + } + return this.invocations[index]; } } @@ -78,46 +83,87 @@ public IReadOnlyInvocation this[int index] public void Add(Invocation invocation) { - lock (this.invocations) + lock (this.invocationsLock) { - this.invocations.Add(invocation); - } - } + if (this.count == this.capacity) + { + var targetCapacity = this.capacity == 0 ? 4 : (this.capacity * 2); + Array.Resize(ref this.invocations, targetCapacity); + this.capacity = targetCapacity; + } - public bool Any() - { - return this.invocations.Count > 0; + this.invocations[this.count] = invocation; + this.count++; + } } public void Clear() { - lock (this.invocations) + lock (this.invocationsLock) { - this.invocations.Clear(); + // Replace the collection so readers with a reference to the old collection aren't interrupted + this.invocations = null; + this.count = 0; + this.capacity = 0; } } public Invocation[] ToArray() { - lock (this.invocations) + lock (this.invocationsLock) { - return this.invocations.ToArray(); + if (this.count == 0) + { + return new Invocation[0]; + } + + var result = new Invocation[this.count]; + + Array.Copy(this.invocations, result, this.count); + + return result; } } public Invocation[] ToArray(Func predicate) { - lock (this.invocations) + lock (this.invocationsLock) { - return this.invocations.Where(predicate).ToArray(); + if (this.count == 0) + { + return new Invocation[0]; + } + + var result = new List(this.count); + + for (var i = 0; i < this.count; i++) + { + var invocation = this.invocations[i]; + if (predicate(invocation)) + { + result.Add(invocation); + } + } + + return result.ToArray(); } } public IEnumerator GetEnumerator() { - lock (this.invocations) + // Take local copies of collection and count so they are isolated from changes by other threads. + Invocation[] collection; + int count; + + lock (this.invocationsLock) + { + collection = this.invocations; + count = this.count; + } + + for (var i = 0; i < count; i++) { - return this.invocations.ToList().GetEnumerator(); + yield return collection[i]; } }