Skip to content

Commit

Permalink
Store information about last call in thread local storage
Browse files Browse the repository at this point in the history
NSubstitute relies on information about last call to set return value.
In order to better support concurrent usage of substitution during its
configuration implementation was switched to use thread local storage for
last call info.

The following changes were made:
 - PendingSpecification is now stored in ThreadLocal context.
 - PendingSpecification stores both pending specification and information
   about the last call.
 - CallStack was reworked to be a CallCollection.
 - Add new kind of exception to indicate that last router is known, but
   information about last call is missing.
  • Loading branch information
zvirja committed Dec 16, 2016
1 parent ed904ca commit 0507eb8
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 0507eb8

Please sign in to comment.