Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/Emit/Moq.DynamicProxy/DynamicMockFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using Castle.DynamicProxy;
using Stunts;
using Stunts.Sdk;

namespace Moq.Sdk
{
/// <summary>
/// Provides an <see cref="IMockFactory"/> that creates types at run-time using Castle DynamicProxy.
/// </summary>
public class DynamicMockFactory : DynamicStuntFactory, IMockFactory
{
/// <inheritdoc />
public object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments)
=> CreateStunt(mocksAssembly, baseType, implementedInterfaces, constructorArguments);

/// <summary>
/// Creates the mock proxy.
/// </summary>
protected override object CreateProxy(Type baseType, Type[] implementedInterfaces, object[] constructorArguments, bool notImplemented)
{
if (!implementedInterfaces.Contains(typeof(IMocked)))
{
var fixedInterfaces = new Type[implementedInterfaces.Length + 1];
fixedInterfaces[0] = typeof(IMocked);
implementedInterfaces.CopyTo(fixedInterfaces, 1);
implementedInterfaces = fixedInterfaces;
}

var mocked = (IMocked)Generator.CreateClassProxy(baseType, implementedInterfaces, Options, constructorArguments, new DynamicMockInterceptor(notImplemented));

// Save for cloning purposes. We opened a generated proxy from DP to figure out the ctor signature it creates.
// The lazy-calculated value allows us to provide a new interceptor for every retrieval.
// Add first-class support in statebag for this pattern of either Func<T> for values, or
// Lazy<T>, since both could be quite useful for expensive state that may be needed lazily.
mocked.Mock.State.Set(".ctor", () => new object[] { new IInterceptor[] { new DynamicMockInterceptor(notImplemented) } }.Concat(constructorArguments).ToArray());

return mocked;
}

class DynamicMockInterceptor : DynamicStuntInterceptor
{
IMock mock;

public DynamicMockInterceptor(bool notImplemented) : base(notImplemented) { }

public override void Intercept(IInvocation invocation)
{
if (invocation.Method.DeclaringType == typeof(IMocked))
{
invocation.ReturnValue = LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock((IStunt)invocation.Proxy));
return;
}

base.Intercept(invocation);
}
}
}
}
13 changes: 13 additions & 0 deletions src/Emit/Moq.DynamicProxy/Moq.DynamicProxy.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Moq.Sdk</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Moq\Moq.Sdk\Moq.Sdk.csproj" />
<ProjectReference Include="..\Stunts.DynamicProxy\Stunts.DynamicProxy.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions src/Emit/Stunts.DynamicProxy/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Moq.DynamicProxy")]
80 changes: 80 additions & 0 deletions src/Emit/Stunts.DynamicProxy/DynamicStuntFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;
using Castle.DynamicProxy;
using Castle.DynamicProxy.Generators;

namespace Stunts.Sdk
{
/// <summary>
/// Provides a <see cref="IStuntFactory"/> that creates types at run-time using Castle DynamicProxy.
/// </summary>
public class DynamicStuntFactory : IStuntFactory
{
static readonly ProxyGenerator generator;
static readonly ProxyGenerationOptions options;

static DynamicStuntFactory()
{
#pragma warning disable 618
AttributesToAvoidReplicating.Add<SecurityPermissionAttribute>();
#pragma warning restore 618

AttributesToAvoidReplicating.Add<System.Runtime.InteropServices.MarshalAsAttribute>();
AttributesToAvoidReplicating.Add<System.Runtime.InteropServices.TypeIdentifierAttribute>();

options = new ProxyGenerationOptions { Hook = new ToStringMethodHook() };
#if DEBUG
// This allows invoking generator.ProxyBuilder.ModuleScope.SaveAssembly() for troubleshooting.
generator = new ProxyGenerator(new DefaultProxyBuilder(new ModuleScope(true)));
#else
generator = new ProxyGenerator();
#endif
}

/// <inheritdoc />
public object CreateStunt(Assembly stuntsAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments)
{
var notImplemented = false;

if (baseType.IsInterface)
{
var fixedInterfaces = new Type[implementedInterfaces.Length + 1];
fixedInterfaces[0] = baseType;
implementedInterfaces.CopyTo(fixedInterfaces, 1);
implementedInterfaces = fixedInterfaces;
baseType = typeof(object);
notImplemented = true;
}

if (!implementedInterfaces.Contains(typeof(IStunt)))
{
var fixedInterfaces = new Type[implementedInterfaces.Length + 1];
fixedInterfaces[0] = typeof(IStunt);
implementedInterfaces.CopyTo(fixedInterfaces, 1);
implementedInterfaces = fixedInterfaces;
}

// TODO: do delegate proxies via interfaces like moq4 does.

return CreateProxy(baseType, implementedInterfaces, constructorArguments, notImplemented);
}

/// <summary>
/// Creates the proxy with the <see cref="Generator"/>, adding interceptors to implement its behavior.
/// </summary>
protected virtual object CreateProxy(Type baseType, Type[] implementedInterfaces, object[] constructorArguments, bool notImplemented)
=> generator.CreateClassProxy(baseType, implementedInterfaces, options, constructorArguments, new DynamicStuntInterceptor(notImplemented));

/// <summary>
/// The <see cref="ProxyGenerator"/> used to create proxy types.
/// </summary>
protected ProxyGenerator Generator => generator;

/// <summary>
/// The <see cref="ProxyGenerationOptions"/> used when creating proxy types.
/// </summary>
protected ProxyGenerationOptions Options => options;
}
}
58 changes: 58 additions & 0 deletions src/Emit/Stunts.DynamicProxy/DynamicStuntInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.ObjectModel;
using Castle.DynamicProxy;

namespace Stunts.Sdk
{
internal class DynamicStuntInterceptor : IInterceptor, IStunt // Implemented to detect breaking changes in Stunts
{
readonly bool notImplemented;
readonly BehaviorPipeline pipeline;

internal DynamicStuntInterceptor(bool notImplemented)
{
this.notImplemented = notImplemented;
pipeline = new BehaviorPipeline();
}

public ObservableCollection<IStuntBehavior> Behaviors => pipeline.Behaviors;

public virtual void Intercept(IInvocation invocation)
{
if (invocation.Method.DeclaringType == typeof(IStunt))
{
invocation.ReturnValue = Behaviors;
return;
}

var input = new MethodInvocation(invocation.Proxy, invocation.Method, invocation.Arguments);
var returns = pipeline.Invoke(input, (i, next) => {
try
{
if (notImplemented)
throw new NotImplementedException();

invocation.Proceed();
var returnValue = invocation.ReturnValue;
return input.CreateValueReturn(returnValue, invocation.Arguments);
}
catch (Exception ex)
{
return input.CreateExceptionReturn(ex);
}
});

var exception = returns.Exception;
if (exception != null)
throw exception;

invocation.ReturnValue = returns.ReturnValue;
for (var i = 0; i < returns.Outputs.Count; i++)
{
var name = returns.Outputs.GetInfo(i).Name;
var index = input.Arguments.IndexOf(name);
invocation.SetArgumentValue(index, returns.Outputs[i]);
}
}
}
}
16 changes: 16 additions & 0 deletions src/Emit/Stunts.DynamicProxy/Stunts.DynamicProxy.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Stunts.Sdk</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Stunts\Stunts\Stunts.csproj" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions src/Emit/Stunts.DynamicProxy/ToStringMethodHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Reflection;
using Castle.DynamicProxy;

namespace Stunts.Sdk
{
/// <summary>
/// Hook used to tells Castle which methods to proxy in mocked classes.
///
/// Here we proxy the default methods Castle suggests (everything Object's methods)
/// plus Object.ToString(), so we can give mocks useful default names.
///
/// This is required to allow Moq to mock ToString on proxy *class* implementations.
/// </summary>
[Serializable]
internal class ToStringMethodHook : AllMethodsHook
{
/// <summary>
/// Extends AllMethodsHook.ShouldInterceptMethod to also intercept Object.ToString().
/// </summary>
public override bool ShouldInterceptMethod(Type type, MethodInfo methodInfo)
{
var isObjectToString = methodInfo.DeclaringType == typeof(object) && methodInfo.Name == "ToString";

return base.ShouldInterceptMethod(type, methodInfo) || isObjectToString;
}
}
}
13 changes: 13 additions & 0 deletions src/Moq.sln
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Stunts.Package", "Stunts\St
EndProject
Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Moq.Package", "Moq\Moq.Package\Moq.Package.msbuildproj", "{09C65B0B-9206-4682-9837-6B98936553C1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.DynamicProxy", "Emit\Stunts.DynamicProxy\Stunts.DynamicProxy.csproj", "{2FC48D20-CB36-4606-A9DC-22981DD13A3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.DynamicProxy", "Emit\Moq.DynamicProxy\Moq.DynamicProxy.csproj", "{BA43EF2F-6CA8-49A5-A4E7-C2FF71928F05}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
Testing\Moq.Testing.projitems*{1bd7dfde-2fc8-4899-9903-6ca5724cc185}*SharedItemsImports = 13
Expand Down Expand Up @@ -131,6 +135,14 @@ Global
{09C65B0B-9206-4682-9837-6B98936553C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09C65B0B-9206-4682-9837-6B98936553C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09C65B0B-9206-4682-9837-6B98936553C1}.Release|Any CPU.Build.0 = Release|Any CPU
{2FC48D20-CB36-4606-A9DC-22981DD13A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2FC48D20-CB36-4606-A9DC-22981DD13A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FC48D20-CB36-4606-A9DC-22981DD13A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FC48D20-CB36-4606-A9DC-22981DD13A3D}.Release|Any CPU.Build.0 = Release|Any CPU
{BA43EF2F-6CA8-49A5-A4E7-C2FF71928F05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA43EF2F-6CA8-49A5-A4E7-C2FF71928F05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA43EF2F-6CA8-49A5-A4E7-C2FF71928F05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA43EF2F-6CA8-49A5-A4E7-C2FF71928F05}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -143,6 +155,7 @@ Global
{EDBDA217-CA42-4C82-826F-7D990185EC0F} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45}
{A4D96CA3-75ED-43E4-B172-872EF2BCF128} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45}
{336A513F-BEC7-44EE-A720-F61E3A65410F} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45}
{2FC48D20-CB36-4606-A9DC-22981DD13A3D} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DA4CFD03-827D-482B-9304-83456D2A8115}
Expand Down
2 changes: 1 addition & 1 deletion src/Moq/Moq.Sdk.Tests/MockFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void CanReplaceMockFactory()

class MyMockFactory : IMockFactory
{
public object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments)
public object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments)
=> throw new NotImplementedException();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Moq/Moq.Sdk/IMockFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ public interface IMockFactory
/// <param name="mocksAssembly">Assembly where compile-time generated mocks exist.</param>
/// <param name="baseType">The base type (or main interface) of the mock.</param>
/// <param name="implementedInterfaces">Additional interfaces implemented by the mock, or an empty array.</param>
/// <param name="construtorArguments">
/// <param name="constructorArguments">
/// Constructor arguments if the <paramref name="baseType" /> is a class, rather than an interface, or an empty array.
/// </param>
/// <returns>A mock that implements <see cref="IMocked"/> in addition to the specified interfaces (if any).</returns>
object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments);
object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments);
}
}
3 changes: 3 additions & 0 deletions src/Moq/Moq.Sdk/MockExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static IMock<T> Clone<T>(this IMock<T> mock) where T : class
if (!mock.State.TryGetValue<object[]>(".ctor", out var ctor))
throw new ArgumentException("No constructor state found for cloning.");

// TODO: THIS DOESN'T WORK WITH DYNAMIC PROXIES, SINCE WE'RE MISSING THE INTERCEPTORS
// This is what it looks like in a DP: public BaseWithCtorProxy(IInterceptor[] interceptorArray, string value) : base(value)
// So we need to persist the interceptors as part of the ctor array, maybe?
var clone = ((IMocked)Activator.CreateInstance(mock.Object.GetType(), ctor)).Mock;
clone.State = mock.State.Clone();

Expand Down
6 changes: 4 additions & 2 deletions src/Moq/Moq.Sdk/MockFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Reflection;
using Stunts;

namespace Moq.Sdk
{
Expand All @@ -16,7 +15,10 @@ public class MockFactory : IMockFactory
/// </summary>
public static IMockFactory Default { get; set; } = new MockFactory();

MockFactory() { }
/// <summary>
/// Initializes a new <see cref="IMockFactory"/>.
/// </summary>
public MockFactory() { }

/// <summary>
/// See <see cref="IMockFactory.CreateMock(Assembly, Type, Type[], object[])"/>
Expand Down
Loading