Skip to content

Commit

Permalink
Track last call and spec in thread-safe manner #264
Browse files Browse the repository at this point in the history
Rename ICallStack to ICallCollection. That is because the Pop()
method is not required more (and was deleted), and Delete()
method was added instead. That is much more collection that stack.

IPendingSpecification now holds both information about previous
call and pending specification. That is encapsulated to the
PendingSpecificationInfo. The reasons why I added one more
responsibility to this class are following:
- We always either use pending specification or information about
previous call. This data is mutually exclusive (we delete other
one when store current one). Therefore, I decided that information
about previous call is also a specification to some degree.
- It's simpler to track sources of specifications because now we
have a single place only. In my previous implementation we needed
to clear pending specification and information about last call - more
chances to fail somewhere.
- Data is stored in SubstitutionContext, so we have single field
instead of two (one for pending specification, second for last
call information).

PendingSpecificationInfo stores thread-local on SubstitutionContext
(i.e. per substitute).

GetCallSpec now uses IPendingSpecification only to resolve
specification. Therefore, I renamed the FromLastCall method
to the FromPendingSpecification because it better reflects the
method implementation.

TrackLastCallHandler introduced to set `IPendingSpecification`.
  • Loading branch information
zvirja authored and dtchepak committed Dec 16, 2016
1 parent ed904ca commit bb06d33
Show file tree
Hide file tree
Showing 39 changed files with 677 additions and 302 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ _ReSharper*
.DS_Store
project.lock.json
.vs/
.fake/
.fake/
.idea/
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
<Compile Include="..\NSubstitute.Acceptance.Specs\ClearSubstitute.cs">
<Link>ClearSubstitute.cs</Link>
</Compile>
<Compile Include="..\NSubstitute.Acceptance.Specs\ConcurrencyTests.cs">
<Link>ConcurrencyTests.cs</Link>
</Compile>
<Compile Include="..\NSubstitute.Acceptance.Specs\CustomHandlersSpecs.cs">
<Link>CustomHandlersSpecs.cs</Link>
</Compile>
Expand Down
123 changes: 123 additions & 0 deletions Source/NSubstitute.Acceptance.Specs/ConcurrencyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Threading;
using NSubstitute.Acceptance.Specs.Infrastructure;
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs
{
public class ConcurrencyTests
{
[Test]
public void Call_between_invocation_and_received_doesnt_cause_issue()
{
//arrange
var subs = Substitute.For<ISomething>();

var backgroundReady = new AutoResetEvent(false);

//act
var dummy = subs.Say("ping");

RunInOtherThread(() =>
{
subs.Echo(42);
backgroundReady.Set();
});

backgroundReady.WaitOne();

dummy.Returns("pong");

//assert
var actualResult = subs.Say("ping");

Assert.That(actualResult, Is.EqualTo("pong"));
}

[Test]
public void Background_invocation_doesnt_delete_specification()
{
//arrange
var subs = Substitute.For<ISomething>();

var backgroundReady = new AutoResetEvent(false);

//act
var dummy = subs.Say(Arg.Any<string>());

RunInOtherThread(() =>
{
subs.Say("hello");
backgroundReady.Set();
});

backgroundReady.WaitOne();
dummy.Returns("42");

//assert
Assert.That(subs.Say("Alex"), Is.EqualTo("42"));
}

[Test]
public void Both_threads_can_configure_returns_concurrently()
{
//arrange
var subs = Substitute.For<ISomething>();

var foregroundReady = new AutoResetEvent(false);
var backgroundReady = new AutoResetEvent(false);

//act
//1
var dummy = subs.Say("ping");

RunInOtherThread(() =>
{
//2
var d = subs.Echo(42);
SignalAndWait(backgroundReady, foregroundReady);
//4
d.Returns("42");
backgroundReady.Set();
});

backgroundReady.WaitOne();

//3
dummy.Returns("pong");
SignalAndWait(foregroundReady, backgroundReady);

//assert
Assert.That(subs.Say("ping"), Is.EqualTo("pong"));
Assert.That(subs.Echo(42), Is.EqualTo("42"));
}

#if (NET45 || NET4 || NETSTANDARD1_5)
[Test]
public void Configuration_works_fine_for_async_methods()
{
//arrange
var subs = Substitute.For<ISomething>();

//act
subs.EchoAsync(42).Returns("42");

//assert
var result = subs.EchoAsync(42).Result;
Assert.That(result, Is.EqualTo("42"));
}
#endif

private static void RunInOtherThread(Action action)
{
new Thread(action.Invoke) {IsBackground = true}.Start();
}

private static void SignalAndWait(EventWaitHandle toSignal, EventWaitHandle toWait)
{
toSignal.Set();
toWait.WaitOne();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>

<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
33 changes: 33 additions & 0 deletions Source/NSubstitute.Acceptance.Specs/PerfTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs
Expand Down Expand Up @@ -54,7 +55,39 @@ public void TimeBasicOperationsWithGenerics()
Console.WriteLine("{0}", watch.ElapsedMilliseconds);
}

[Test]
[Ignore("It's a stress test. It might take a lot of time and it not optimized for frequent execution.")]
public void Multiple_return_configuration_dont_leak_memory_for_any_args()
{
const int bufferSize = 100000000; //100 MB

var subs = Substitute.For<IByteArrayConsumer>();

//1000 chunks each 100 MB will require 100GB. If leak is present - OOM should be thrown.
for (var i = 0; i < 1000; i++)
{
subs.ConsumeArray(new byte[bufferSize]).ReturnsForAnyArgs(true);
}
}

[Test]
[Ignore("FAILS because of CallResults leak")]
public void Muiltiple_return_configurations_dont_lead_to_memory_leak()
{
const int bufferSize = 100000000; //100 MB

var subs = Substitute.For<IByteArraySource>();

//1000 chunks each 100 MB will require 100GB. If leak is present - OOM should be thrown.
for (int i = 0; i < 1000; i++)
{
subs.GetArray().Returns(new byte[bufferSize]);
}
}

public interface IFoo { int GetInt(string s); }
public interface IBar { int GetInt<T>(T t); }
public interface IByteArraySource { byte[] GetArray(); }
public interface IByteArrayConsumer { bool ConsumeArray(byte[] array); }
}
}
19 changes: 12 additions & 7 deletions Source/NSubstitute.NET/NSubstitute.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@
<Compile Include="..\NSubstitute\Core\CallBaseExclusions.cs">
<Link>Core\CallBaseExclusions.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\CallCollection.cs">
<Link>Core\CallCollection.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\CallFactory.cs">
<Link>Core\CallFactory.cs</Link>
</Compile>
Expand Down Expand Up @@ -285,9 +288,6 @@
<Compile Include="..\NSubstitute\Core\CallSpecificationFactoryFactoryYesThatsRight.cs">
<Link>Core\CallSpecificationFactoryFactoryYesThatsRight.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\CallStack.cs">
<Link>Core\CallStack.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\ConfigureCall.cs">
<Link>Core\ConfigureCall.cs</Link>
</Compile>
Expand Down Expand Up @@ -330,6 +330,9 @@
<Compile Include="..\NSubstitute\Core\ICallBaseExclusions.cs">
<Link>Core\ICallBaseExclusions.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\ICallCollection.cs">
<Link>Core\ICallCollection.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\ICallHandler.cs">
<Link>Core\ICallHandler.cs</Link>
</Compile>
Expand Down Expand Up @@ -357,9 +360,6 @@
<Compile Include="..\NSubstitute\Core\ICallSpecificationFactory.cs">
<Link>Core\ICallSpecificationFactory.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\ICallStack.cs">
<Link>Core\ICallStack.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\IConfigureCall.cs">
<Link>Core\IConfigureCall.cs</Link>
</Compile>
Expand Down Expand Up @@ -435,6 +435,9 @@
<Compile Include="..\NSubstitute\Core\PendingSpecification.cs">
<Link>Core\PendingSpecification.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\PendingSpecificationInfo.cs">
<Link>Core\PendingSpecificationInfo.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Core\PropertyCallFormatter.cs">
<Link>Core\PropertyCallFormatter.cs</Link>
</Compile>
Expand Down Expand Up @@ -666,6 +669,9 @@
<Compile Include="..\NSubstitute\Routing\Handlers\SetActionForCallHandler.cs">
<Link>Routing\Handlers\SetActionForCallHandler.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Routing\Handlers\TrackLastCallHandler.cs">
<Link>Routing\Handlers\TrackLastCallHandler.cs</Link>
</Compile>
<Compile Include="..\NSubstitute\Routing\IRoute.cs">
<Link>Routing\IRoute.cs</Link>
</Compile>
Expand All @@ -689,7 +695,6 @@
</None>
<None Include="ilmerge.exclude" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" Condition="($(MSBuildTargets) == '') Or ($(MSBuildTargets) == 'CSharp')" />
<Target Name="AfterBuild" Condition="(($(MSBuildTargets) == '') Or ($(MSBuildTargets) == 'CSharp')) And '$(OS)' == 'Windows_NT'">
<CreateItem Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)'=='.dll'">
Expand Down
82 changes: 82 additions & 0 deletions Source/NSubstitute.Specs/CallCollectionSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using NSubstitute.Core;
using NSubstitute.Specs.Infrastructure;
using NUnit.Framework;

namespace NSubstitute.Specs
{
public class CallCollectionSpecs : ConcernFor<CallCollection>
{
public override CallCollection CreateSubjectUnderTest() => new CallCollection();

[Test]
public void Should_add_call()
{
//arrange
var call = mock<ICall>();

//act
sut.Add(call);

//assert
CollectionAssert.Contains(sut.AllCalls(), call);
}

[Test]
public void Should_delete_call_when_deleted()
{
//arrange
var call = mock<ICall>();

//act
sut.Add(call);
sut.Delete(call);

//assert
CollectionAssert.DoesNotContain(sut.AllCalls(), call);
}

[Test]
public void Should_fail_when_delete_nonexisting_call()
{
//arrange
var call = mock<ICall>();

//act/assert
var exception = Assert.Throws<InvalidOperationException>(() => sut.Delete(call));
Assert.That(exception.Message, Is.StringContaining("Collection doesn't contain the call."));
}

[Test]
public void Should_delete_all_calls_on_clear()
{
//arrange
var call1 = mock<ICall>();
var call2 = mock<ICall>();

//act
sut.Add(call1);
sut.Add(call2);

sut.Clear();

//assert
CollectionAssert.IsEmpty(sut.AllCalls());
}

[Test]
public void Should_return_all_calls_in_the_order_they_were_received()
{
//arrange
var firstCall = mock<ICall>();
var secondCall = mock<ICall>();

//act
sut.Add(firstCall);
sut.Add(secondCall);

//assert
CollectionAssert.AreEqual(sut.AllCalls(), new[] { firstCall, secondCall });
}
}
}
Loading

0 comments on commit bb06d33

Please sign in to comment.