Working towards proper integration of elevated mocks with nsub (still much to do)

This commit is contained in:
Scott Bilas 2017-10-24 19:01:27 +02:00
Родитель 404d9c6e55
Коммит 4760208090
13 изменённых файлов: 377 добавлений и 29 удалений

Просмотреть файл

@ -0,0 +1,50 @@
using System;
using System.Linq;
using NSubstitute.Core;
using NSubstitute.Exceptions;
using NSubstitute.Proxies;
using NSubstitute.Proxies.CastleDynamicProxy;
using NSubstitute.Proxies.DelegateProxy;
namespace NSubstitute.Elevated
{
class ElevatedProxyFactory : IProxyFactory
{
readonly ElevatedProxyMapper m_ElevatedProxyMapper;
readonly IProxyFactory m_DefaultProxyFactory = new ProxyFactory(new DelegateProxyFactory(), new CastleDynamicProxyFactory());
public ElevatedProxyFactory(ElevatedProxyMapper elevatedProxyMapper) => m_ElevatedProxyMapper = elevatedProxyMapper;
object IProxyFactory.GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
{
if (!ShouldHandle(typeToProxy))
return m_DefaultProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
if (typeToProxy == typeof(SubstituteStatic.Proxy))
{
if (additionalInterfaces != null && additionalInterfaces.Any())
throw new SubstituteException("Can not substitute interfaces as static");
var actualType = (Type)constructorArguments[0];
return m_ElevatedProxyMapper.MockStatic(actualType, callRouter);
}
throw NotImplementedException();
return null;
}
static bool ShouldHandle(Type typeToProxy)
{
if (typeToProxy.IsInterface || typeToProxy.IsAbstract)
return false;
// TEMP
if (typeToProxy.FullName != "SystemUnderTest.SimpleClass")
return false;
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using NSubstitute.Core;
using NSubstitute.Elevated.Utilities;
using NSubstitute.Exceptions;
namespace NSubstitute.Elevated
{
class ElevatedProxyMapper
{
readonly ISubstitutionContext m_SubstitutionContext;
readonly CallFactory m_CallFactory;
public ElevatedProxyMapper(ISubstitutionContext substitutionContext)
{
m_SubstitutionContext = substitutionContext;
m_CallFactory = new CallFactory(m_SubstitutionContext);
}
public SubstituteStatic.Proxy MockStatic(Type type, ICallRouter callRouter)
{
var staticField = GetStaticFieldInfo(type);
if (staticField == null)
throw new SubstituteException("Can not substitute for non-patched types");
if (staticField.GetValue(null) != null)
throw new SubstituteException("Can not substitute the same type twice (did you forget to Dispose() your previous substitute?)");
staticField.SetValue(null, callRouter);
return new SubstituteStatic.Proxy(new DelegateDisposable(() =>
{
var found = staticField.GetValue(null);
if (found == null)
throw new SubstituteException("Unexpected static unmock of an already unmocked type");
if (found != callRouter)
throw new SubstituteException("Discovered unexpected call router attached in static mock context");
staticField.SetValue(null, null);
}));
}
public void Mock(Type type, object instance, ICallRouter callRouter)
{
var field = GetFieldInfo(type);
if (field == null)
throw new SubstituteException("Can not substitute for non-patched types");
field.SetValue(instance, callRouter);
}
public bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, MethodInfo method, Type[] methodGenericTypes, object[] args)
{
var field = instance == null ? GetStaticFieldInfo(actualType) : GetFieldInfo(actualType);
var callRouter = (ICallRouter)field?.GetValue(instance);
if (callRouter != null)
{
Func<object> baseResult = () => invocation.Proceed(); // $$$ need to turn this into a func reentry which goes straight to the leftover
var result = new Lazy<object>(baseResult);
Func<object> baseMethod = () => result.Value;
var mappedInvocation = m_CallFactory.Create(method, args, instance, baseMethod);
Array.Copy(mappedInvocation.GetArguments(), args, args.Length); // $$$ unsure about this..apparently need to copy back results, but our version has a bug on this
mockedReturnValue = callRouter.Route(mappedInvocation);
return true;
}
mockedReturnValue = mockedReturnType.GetDefaultValue();
return false;
}
FieldInfo GetStaticFieldInfo(Type type) => m_StaticFieldCache.GetOrAdd(type, t => t.GetField("__mockStaticRouter", BindingFlags.Static | BindingFlags.NonPublic));
FieldInfo GetFieldInfo(Type type) => m_FieldCache.GetOrAdd(type, t => t.GetField("__mockRouter", BindingFlags.Instance | BindingFlags.NonPublic));
readonly Dictionary<Type, FieldInfo> m_StaticFieldCache = new Dictionary<Type, FieldInfo>();
readonly Dictionary<Type, FieldInfo> m_FieldCache = new Dictionary<Type, FieldInfo>();
}
}

Просмотреть файл

@ -1,22 +0,0 @@
using System;
using NSubstitute.Core;
namespace NSubstitute.Elevated
{
class ElevatedSubstituteFactory : ISubstituteFactory
{
readonly ISubstituteFactory m_Forwarder;
public ElevatedSubstituteFactory(ISubstituteFactory forwarder)
=> m_Forwarder = forwarder;
object ISubstituteFactory.Create(Type[] typesToProxy, object[] constructorArguments)
=> m_Forwarder.Create(typesToProxy, constructorArguments);
object ISubstituteFactory.CreatePartial(Type[] typesToProxy, object[] constructorArguments)
=> m_Forwarder.CreatePartial(typesToProxy, constructorArguments);
ICallRouter ISubstituteFactory.GetCallRouterCreatedFor(object substitute)
=> m_Forwarder.GetCallRouterCreatedFor(substitute);
}
}

Просмотреть файл

@ -3,10 +3,13 @@ using System.Collections.Generic;
using JetBrains.Annotations;
using NSubstitute.Core;
using NSubstitute.Core.Arguments;
using NSubstitute.Elevated.Utilities;
using NSubstitute.Exceptions;
using NSubstitute.Routing;
namespace NSubstitute.Elevated
{
// this class exists solely to hook in our own proxy factory to the nsub machinery
public class ElevatedSubstitutionContext : ISubstitutionContext
{
readonly ISubstitutionContext m_Forwarder;
@ -15,9 +18,26 @@ namespace NSubstitute.Elevated
public ElevatedSubstitutionContext([NotNull] ISubstitutionContext forwarder)
{
m_Forwarder = forwarder;
m_ElevatedSubstituteFactory = new ElevatedSubstituteFactory(forwarder.SubstituteFactory);
m_ElevatedSubstituteFactory = new SubstituteFactory(this,
new CallRouterFactory(), new ElevatedProxyFactory(ElevatedProxyMapper), new CallRouterResolver());
}
public static IDisposable AutoHook()
{
var hookedContext = SubstitutionContext.Current;
var thisContext = new ElevatedSubstitutionContext(hookedContext);
SubstitutionContext.Current = thisContext;
return new DelegateDisposable(() =>
{
if (SubstitutionContext.Current != thisContext)
throw new SubstituteException("Unexpected hook in place of ours");
SubstitutionContext.Current = hookedContext;
});
}
internal ElevatedProxyMapper ElevatedProxyMapper { get; } = new ElevatedProxyMapper();
// this is the only one we're overriding for now, so we can hook our own factory in there.
ISubstituteFactory ISubstitutionContext.SubstituteFactory => m_ElevatedSubstituteFactory;

Просмотреть файл

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
namespace NSubstitute.Elevated
{
public static class Extensions
{
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> @this, TKey key, Func<TKey, TValue> createFunc)
{
if (@this.TryGetValue(key, out var found))
return found;
found = createFunc(key);
@this.Add(key, found);
return found;
}
public static object GetDefaultValue(this Type @this)
{
object defaultValue = null;
if (@this.IsValueType && @this != typeof(void))
defaultValue = Activator.CreateInstance(@this);
return defaultValue;
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using NSubstitute.Core;
// this namespace contains types that must be public in order to be usable from patched assemblies, yet
// we do not want used from normal client api
namespace NSubstitute.Elevated.WeaverInternals
{
// important: keep all non-mscorlib types out of the public surface area of this class, so as to
// avoid needing to add more references than NSubstitute.Elevated to the assembly during patching.
public static class PatchedAssemblyBridge
{
// returns true if a mock is in place and it is taking over functionality. instance may be null
// if static. mockedReturnValue is ignored in a void return func.
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, Type[] methodGenericTypes, object[] args)
{
if (!(SubstitutionContext.Current is ElevatedSubstitutionContext elevated))
{
mockedReturnValue = mockedReturnType.GetDefaultValue();
return false;
}
var method = (MethodInfo) new StackTrace(1).GetFrame(0).GetMethod();
if (method.IsGenericMethodDefinition)
method = method.MakeGenericMethod(methodGenericTypes);
return elevated.ElevatedProxyMapper.TryMock(actualType, instance, mockedReturnType, out mockedReturnValue, method, methodGenericTypes, args);
}
}
}

Просмотреть файл

@ -0,0 +1,19 @@
using System;
namespace NSubstitute.Elevated
{
public static class SubstituteStatic
{
// callers need an actual object in order to chain further arranging, so we return this placeholder for static substitutes
public class Proxy : IDisposable
{
readonly IDisposable m_Forwarder;
internal Proxy(IDisposable forwarder) => m_Forwarder = forwarder;
public void Dispose() { m_Forwarder.Dispose(); }
}
public static Proxy For<T>() => For(typeof(T));
public static Proxy For(Type staticType) => Substitute.For<Proxy>(staticType);
}
}

Просмотреть файл

@ -0,0 +1,17 @@
using System;
using JetBrains.Annotations;
namespace NSubstitute.Elevated.Utilities
{
public class DelegateDisposable : IDisposable
{
readonly Action m_DisposeAction;
public DelegateDisposable([NotNull] Action disposeAction) => m_DisposeAction = disposeAction;
public void Dispose()
{
m_DisposeAction();
}
}
}

Просмотреть файл

@ -1,6 +1,5 @@
using System;
using SystemUnderTest;
using NSubstitute;
using NSubstitute.Exceptions;
using NUnit.Framework;
using Shouldly;
@ -10,6 +9,20 @@ namespace NSubstitute.Elevated.Tests
[TestFixture]
class BasicTests
{
IDisposable m_Dispose;
[OneTimeSetUp]
public void Setup()
{
m_Dispose = ElevatedSubstitutionContext.AutoHook();
}
[OneTimeTearDown]
public void TearDown()
{
m_Dispose.Dispose();
}
[Test]
public void MockByInterface_ShouldUseNSubDefaultBehavior()
{
@ -46,7 +59,7 @@ namespace NSubstitute.Elevated.Tests
}
[Test]
public void ClassWithNoDefaultCtor_MocksWithoutError()
public void ClassWithNoDefaultCtor_Mocks()
{
var sub = Substitute.For<ClassWithNoDefaultCtor>();
@ -55,7 +68,7 @@ namespace NSubstitute.Elevated.Tests
# if TEST_ICALLS
[Test]
public void ClassWithICallInCtor_MocksWithoutError()
public void ClassWithICallInCtor_Mocks()
{
// $ TODO: make this into an actual test of the icall thing. currently just checks that doesn't throw..not that interesting
@ -67,7 +80,7 @@ namespace NSubstitute.Elevated.Tests
# endif
[Test]
public void ClassWithThrowInCtor_MocksWithoutError()
public void ClassWithThrowInCtor_Mocks()
{
var sub = Substitute.For<ClassWithCtorThrow>();
@ -102,14 +115,14 @@ namespace NSubstitute.Elevated.Tests
}
[Test]
public void NonMockedClassWithDependentTypes_LoadsWithoutError()
public void NonMockedClassWithDependentTypes_Loads()
{
// ReSharper disable once PossibleNullReferenceException
typeof(ClassWithDependency).GetMethod("Dummy").ReturnType.FullName.ShouldBe("mycodedep.DependentType");
}
[Test]
public void ClassWithDependentTypes_MocksWithoutError()
public void ClassWithDependentTypes_Mocks()
{
// simple test to ensure that we can patch methods that use types from foreign assemblies
@ -118,5 +131,37 @@ namespace NSubstitute.Elevated.Tests
// ReSharper disable once PossibleNullReferenceException
sub.GetType().GetMethod("Dummy").ReturnType.FullName.ShouldBe("mycodedep.DependentType");
}
[Test]
public void SimpleClass_FullMock_DoesNotCallDefaultImpls()
{
var sub = Substitute.For<SimpleClass>();
sub.VoidMethod(5);
sub.Modified.ShouldBe(0);
sub.ReturnMethod(5).ShouldBe(0);
sub.Modified.ShouldBe(0);
sub.ReturnMethod(5).Returns(10);
sub.ReturnMethod(5).ShouldBe(10);
sub.Modified.ShouldBe(0);
}
[Test]
public void SimpleClass_PartialMock_CallsDefaultImpls()
{
var sub = Substitute.For<SimpleClass>();
sub.VoidMethod(5);
sub.Modified.ShouldBe(5);
sub.ReturnMethod(3).ShouldBe(8);
sub.Modified.ShouldBe(8);
sub.ReturnMethod(4).Returns(10); // $$$ whats the right way to do this without triggering the method call?
sub.ReturnMethod(4).ShouldBe(10);
sub.Modified.ShouldBe(8);
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
using System;
namespace NSubstitute.Elevated.Tests
{
public static class MockWeaverTestUtils
{
}
}

Просмотреть файл

@ -0,0 +1,19 @@
using System;
using NUnit.Framework;
namespace NSubstitute.Elevated.Tests
{
[TestFixture]
public class MockWeaverTests
{
[NonSerialized]
object __mockContext;
[NonSerialized]
static object __mockStaticContext;
[Test]
public void NonParamStaticMethod()
{
}
}
}

Просмотреть файл

@ -10,6 +10,9 @@
<PackageReference Include="JetBrains.Annotations">
<Version>11.0.0</Version>
</PackageReference>
<PackageReference Include="Mono.Cecil">
<Version>0.9.6.4</Version>
</PackageReference>
<PackageReference Include="NSubstitute">
<Version>2.0.3</Version>
</PackageReference>
@ -22,6 +25,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\source\NSubstitute.Elevated\NSubstitute.Elevated.csproj" />
<ProjectReference Include="..\Support\SystemUnderTest\SystemUnderTest.csproj" />
</ItemGroup>

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using NSubstitute.Elevated.WeaverInternals;
#if TEST_ICALLS
using System.Runtime.CompilerServices;
@ -77,4 +78,50 @@ namespace SystemUnderTest
{
public DependentAssembly.DependentType Dummy => new DependentAssembly.DependentType();
}
public class SimpleClass
{
public int Modified;
// actual
//public void VoidMethod() => ++Modified;
//public int ReturnMethod() => ++Modified;
// hack until patching works
public void VoidMethod(int count)
{
if (PatchedAssemblyBridge.TryMock(this, new object[] { count }))
return;
Modified += count;
}
public int ReturnMethod(int count)
{
if (PatchedAssemblyBridge.TryMock(out var returnValue, this, new object[] { count }))
return (int)returnValue;
return Modified += count;
}
}
}
namespace NSubstitute.Elevated.WeaverInternals
{
public static class PatchedAssemblyBridge
{
public static bool TryMock(object instance, object[] methodCallArgs)
{
return false;
}
public static bool TryMock(out object returnValue, object instance, object[] methodCallArgs)
{
returnValue = null;
// $$$ use https://stackoverflow.com/a/353073 when figure out what return value type is
return false;
}
}
}