diff --git a/README.md b/README.md index 0efe0fd..8597f4d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -[![Build status](https://dev.azure.com/palmund/Pose/_apis/build/status/Pose-CI?branchName=master)](https://dev.azure.com/palmund/Pose/_build/latest?definitionId=12) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![NuGet version](https://badge.fury.io/nu/Poser.svg)](https://www.nuget.org/packages/Poser) +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +[![Build status](https://dev.azure.com/palmund/Pose/_apis/build/status/Pose-CI?branchName=master&Label=build)](https://dev.azure.com/palmund/Pose/_build/latest?definitionId=12) +[![NuGet version](https://img.shields.io/nuget/v/Poser?logo=nuget)](https://www.nuget.org/packages/Poser) +[![NuGet preview version](https://img.shields.io/nuget/vpre/Poser?logo=nuget)](https://www.nuget.org/packages/Poser) + # Poser Poser allows you to replace any .NET method (including static and non-virtual) with a delegate. It is similar to [Microsoft Fakes](https://msdn.microsoft.com/en-us/library/hh549175.aspx) but unlike it Poser is implemented _entirely_ in managed code (Reflection Emit API). Everything occurs at runtime and in-memory, no unmanaged Profiling APIs and no file system pollution with re-written assemblies. @@ -115,6 +117,12 @@ Shim structShim = Shim.Replace(() => Is.A().DoSomething()).With( _Note: You cannot shim methods on specific instances of Value Types_ +### Shim operators + +```csharp +var operatorShim = Shim.Replace(() => Is.A() + Is.A()).With( + delegate(TimeSpan l, TimeSpan r) { return TimeSpan.Zero; }); +``` ### Isolating your code ```csharp @@ -135,10 +143,48 @@ PoseContext.Isolate(() => // Outputs "doing someting else with myClass" myClass.DoSomething(); + + // Outputs '00:00:00' + Console.WriteLine(TimeSpan.FromDays(1) + TimeSpan.FromSeconds(2)); -}, consoleShim, dateTimeShim, classPropShim, classShim, myClassShim, structShim); +}, consoleShim, dateTimeShim, classPropShim, classShim, myClassShim, structShim, operatorShim); ``` +## Shimming operators +Operator shimming requires that the class/struct overloads the operator in question. + +Poser supports shimming operators of the following kind: +* Arithmetic + * `+x` + * `-x` + * `!x` + * `~x` + * `x + y` + * `x - y` + * `x / y` + * `x % y` + * `x & y` + * `x | y` + * `x ^ y` + * `x << y` + * `x >> y` +* Equality + * `x == y` + * `x != y` +* Comparison + * `x < y` + * `x > y` + * `x <= y` + * `x >= y` + +In addition to this, both implicit and explicit conversion operators are supported. + +### Unsupported operators +Shimming of the following operators is not supported: +- `true` and `false` because I cannot find a good way to express the operation in an expression tree. +- `x >>> y` because expression trees cannot contain this operator. This is a limitation on the part of the compiler. +- `++` and `--` because these cannot be expressed in an expression tree. + ## Caveats & Limitations * **Breakpoints** - At this time any breakpoints set anywhere in the isolated code and its execution path will not be hit. However, breakpoints set within a shim replacement delegate are hit. diff --git a/nuget/Poser.nuspec b/nuget/Poser.nuspec index 8036f6c..587b5dd 100644 --- a/nuget/Poser.nuspec +++ b/nuget/Poser.nuspec @@ -2,7 +2,7 @@ Poser - 2.0.0 + 2.0.1 Pose Søren Guldmund Søren Guldmund @@ -16,7 +16,7 @@ Copyright 2024 docs\README.md - Provide better exception message when we cannot create instance. + Fix bug where `Enum.IsDefined` could not be called from within `PoseContext.Isolate`. diff --git a/src/Pose/Exceptions/InvalidShimSignatureException.cs b/src/Pose/Exceptions/InvalidShimSignatureException.cs index 567efc5..ca77dfc 100644 --- a/src/Pose/Exceptions/InvalidShimSignatureException.cs +++ b/src/Pose/Exceptions/InvalidShimSignatureException.cs @@ -1,17 +1,9 @@ namespace Pose.Exceptions { using System; - using System.Runtime.Serialization; - [Serializable] - internal class InvalidShimSignatureException : Exception + public class InvalidShimSignatureException : Exception { - public InvalidShimSignatureException() { } public InvalidShimSignatureException(string message) : base(message) { } - public InvalidShimSignatureException(string message, Exception inner) : base(message, inner) { } - -#if !NET8_0_OR_GREATER - protected InvalidShimSignatureException(SerializationInfo info, StreamingContext context) : base(info, context) { } -#endif } } \ No newline at end of file diff --git a/src/Pose/Exceptions/MethodRewriteException.cs b/src/Pose/Exceptions/MethodRewriteException.cs index 500bb0e..abf3715 100644 --- a/src/Pose/Exceptions/MethodRewriteException.cs +++ b/src/Pose/Exceptions/MethodRewriteException.cs @@ -1,19 +1,9 @@ namespace Pose.Exceptions { using System; - using System.Runtime.Serialization; - [Serializable] public class MethodRewriteException : Exception { - public MethodRewriteException() { } - -#if !NET8_0_OR_GREATER - protected MethodRewriteException(SerializationInfo info, StreamingContext context) : base(info, context) { } -#endif - public MethodRewriteException(string message) : base(message) { } - - public MethodRewriteException(string message, Exception innerException) : base(message, innerException) { } } } \ No newline at end of file diff --git a/src/Pose/Exceptions/UnsupportedExpressionException.cs b/src/Pose/Exceptions/UnsupportedExpressionException.cs new file mode 100644 index 0000000..3e27c03 --- /dev/null +++ b/src/Pose/Exceptions/UnsupportedExpressionException.cs @@ -0,0 +1,9 @@ +namespace Pose.Exceptions +{ + using System; + + public class UnsupportedExpressionException : Exception + { + public UnsupportedExpressionException(string message) : base(message) { } + } +} \ No newline at end of file diff --git a/src/Pose/Extensions/ExpressionTypeExtensions.cs b/src/Pose/Extensions/ExpressionTypeExtensions.cs new file mode 100644 index 0000000..657186a --- /dev/null +++ b/src/Pose/Extensions/ExpressionTypeExtensions.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; + +namespace Pose.Extensions +{ + internal static class ExpressionTypeExtensions + { + public static bool IsOverloadableOperator(this ExpressionType expressionType) + { + switch (expressionType) + { + case ExpressionType.Add: + case ExpressionType.UnaryPlus: + case ExpressionType.Negate: + case ExpressionType.Not: + case ExpressionType.Subtract: + case ExpressionType.Multiply: + case ExpressionType.Divide: + case ExpressionType.LeftShift: + case ExpressionType.RightShift: + case ExpressionType.Modulo: + case ExpressionType.ExclusiveOr: + case ExpressionType.And: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.LessThan: + case ExpressionType.GreaterThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Or: + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Pose/Extensions/ILGeneratorExtensions.cs b/src/Pose/Extensions/ILGeneratorExtensions.cs index d9f62e6..6fdacfe 100644 --- a/src/Pose/Extensions/ILGeneratorExtensions.cs +++ b/src/Pose/Extensions/ILGeneratorExtensions.cs @@ -1,9 +1,11 @@ namespace Pose.Extensions { using System; + using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Emit; + [ExcludeFromCodeCoverage(Justification = "Used only internally when printing out IL instructions")] internal static class ILGeneratorExtensions { public static byte[] GetILBytes(this ILGenerator ilGenerator) diff --git a/src/Pose/Helpers/ShimHelper.cs b/src/Pose/Helpers/ShimHelper.cs index 0ef6cb2..0520f8b 100644 --- a/src/Pose/Helpers/ShimHelper.cs +++ b/src/Pose/Helpers/ShimHelper.cs @@ -4,6 +4,7 @@ using System.Reflection; using Pose.Exceptions; +using Pose.Extensions; namespace Pose.Helpers { @@ -25,7 +26,7 @@ public static MethodBase GetMethodFromExpression(Expression expression, bool set } else { - throw new NotImplementedException("Unsupported expression"); + throw new UnsupportedExpressionException($"Expression (of type {expression.GetType()}) with NodeType '{expression.NodeType}' is not supported"); } } case ExpressionType.Call: @@ -36,9 +37,46 @@ public static MethodBase GetMethodFromExpression(Expression expression, bool set var newExpression = expression as NewExpression ?? throw new Exception($"Cannot cast expression to {nameof(NewExpression)}"); instanceOrType = null; return newExpression.Constructor; + case ExpressionType.Convert: + case ExpressionType.Not: + case ExpressionType.Negate: + case ExpressionType.UnaryPlus: + var unaryExpression = expression as UnaryExpression ?? throw new Exception($"Cannot cast expression to {nameof(UnaryExpression)}"); + instanceOrType = null; + return unaryExpression.Method ?? throw new Exception(GetExceptionMessage(expression)); + case ExpressionType.Add: + case ExpressionType.Subtract: + case ExpressionType.Multiply: + case ExpressionType.Divide: + case ExpressionType.LeftShift: + case ExpressionType.RightShift: + case ExpressionType.Modulo: + case ExpressionType.ExclusiveOr: + case ExpressionType.And: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.LessThan: + case ExpressionType.GreaterThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Or: + var binaryExpression = expression as BinaryExpression ?? throw new Exception($"Cannot cast expression to {nameof(BinaryExpression)}"); + instanceOrType = null; + return binaryExpression.Method ?? throw new Exception(GetExceptionMessage(expression)); default: - throw new NotImplementedException("Unsupported expression"); + throw new UnsupportedExpressionException($"Expression (of type {expression.GetType()}) with NodeType '{expression.NodeType}' is not supported"); + } + } + + private static string GetExceptionMessage(Expression expression) + { + if (expression.NodeType.IsOverloadableOperator()) + { + return + $"Cannot shim the {expression.NodeType} operator on {expression.Type} because the type itself does not overload this operator."; } + + return $"The expression for node type {expression.NodeType} could not be mapped to a method"; } public static void ValidateReplacementMethodSignature(MethodBase original, MethodInfo replacement, Type type, bool setter) @@ -74,17 +112,20 @@ public static void ValidateReplacementMethodSignature(MethodBase original, Metho throw new InvalidShimSignatureException("ValueType instances must be passed by ref"); } - var expectedType = (isValueType && !isStaticOrConstructor ? validOwningType.MakeByRefType() : validOwningType); - if (expectedType != shimOwningType) - throw new InvalidShimSignatureException($"Mismatched instance types. Expected {expectedType.FullName}. Got {shimOwningType.FullName}"); + var expectedOwningType = (isValueType && !isStaticOrConstructor ? validOwningType.MakeByRefType() : validOwningType); + if (expectedOwningType != shimOwningType) + throw new InvalidShimSignatureException($"Mismatched instance types. Expected {expectedOwningType.FullName}. Got {shimOwningType.FullName}. If you are shimming an instance method, then the first parameter to the replacement must be an instance of the type."); if (validParameterTypes.Length != shimParameterTypes.Length) throw new InvalidShimSignatureException($"Parameters count do not match. Expected {validParameterTypes.Length}. Got {shimParameterTypes.Length}"); for (var i = 0; i < validParameterTypes.Length; i++) { - if (validParameterTypes.ElementAt(i) != shimParameterTypes.ElementAt(i)) - throw new InvalidShimSignatureException($"Parameter types at {i} do not match"); + var expectedType = validParameterTypes.ElementAt(i); + var actualType = shimParameterTypes.ElementAt(i); + + if (expectedType != actualType) + throw new InvalidShimSignatureException($"Parameter types at {i} do not match. Expected '{expectedType}' but found {actualType}'"); } } diff --git a/src/Pose/IL/MethodRewriter.cs b/src/Pose/IL/MethodRewriter.cs index 9847b15..787264f 100644 --- a/src/Pose/IL/MethodRewriter.cs +++ b/src/Pose/IL/MethodRewriter.cs @@ -110,7 +110,7 @@ public MethodBase Rewrite() var switchTargets = instructions .Where(i => i.Operand is Instruction[]) .Select(i => i.Operand as Instruction[]); - + foreach (var switchInstructions in switchTargets) { if (switchInstructions == null) throw new Exception("The impossible happened"); @@ -119,13 +119,13 @@ public MethodBase Rewrite() targetInstructions.TryAdd(instruction.Offset, ilGenerator.DefineLabel()); } -#if DEBUG +#if TRACE Console.WriteLine("\n" + _method); #endif foreach (var instruction in instructions) { -#if DEBUG +#if TRACE Console.WriteLine(instruction); #endif @@ -181,7 +181,7 @@ public MethodBase Rewrite() } } -#if DEBUG +#if TRACE var ilBytes = ilGenerator.GetILBytes(); var browsableDynamicMethod = new BrowsableDynamicMethod(dynamicMethod, new DynamicMethodBody(ilBytes, locals)); Console.WriteLine("\n" + dynamicMethod); diff --git a/src/Pose/IL/Stubs.cs b/src/Pose/IL/Stubs.cs index 48b2483..093dc85 100644 --- a/src/Pose/IL/Stubs.cs +++ b/src/Pose/IL/Stubs.cs @@ -82,8 +82,10 @@ public static DynamicMethod GenerateStubForDirectCall(MethodBase method) signatureParamTypes.ToArray(), StubHelper.GetOwningModule(), true); - + +#if TRACE Console.WriteLine("\n" + method); +#endif var ilGenerator = stub.GetILGenerator(); @@ -173,14 +175,19 @@ public static DynamicMethod GenerateStubForDirectCall(MethodBase method) ilGenerator.MarkLabel(returnLabel); ilGenerator.Emit(OpCodes.Ret); - + return stub; } public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method, TypeInfo constrainedType) { + if (method == null) throw new ArgumentNullException(nameof(method)); + if (constrainedType == null) throw new ArgumentNullException(nameof(constrainedType)); + var thisType = constrainedType.MakeByRefType(); + var methodDeclaringType = method.DeclaringType ?? throw new Exception($"Method {method.Name} does not have a {nameof(MethodBase.DeclaringType)}"); var actualMethod = StubHelper.DeVirtualizeMethod(constrainedType, method); + var actualMethodDeclaringType = actualMethod.DeclaringType ?? throw new Exception($"Method {actualMethod.Name} does not have a {nameof(MethodBase.DeclaringType)}"); var signatureParamTypes = new List(); signatureParamTypes.Add(thisType); @@ -193,6 +200,10 @@ public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method, TypeIn StubHelper.GetOwningModule(), true); +#if TRACE + Console.WriteLine("\n" + method); +#endif + var ilGenerator = stub.GetILGenerator(); if ((actualMethod.GetMethodBody() == null && !actualMethod.IsAbstract) || StubHelper.IsIntrinsic(actualMethod)) @@ -209,6 +220,8 @@ public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method, TypeIn return stub; } + ilGenerator.DeclareLocal(typeof(MethodInfo)); + ilGenerator.DeclareLocal(typeof(int)); ilGenerator.DeclareLocal(typeof(IntPtr)); var rewriteLabel = ilGenerator.DefineLabel(); @@ -216,19 +229,40 @@ public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method, TypeIn // Inject method info into instruction stream ilGenerator.Emit(OpCodes.Ldtoken, actualMethod); - ilGenerator.Emit(OpCodes.Ldtoken, actualMethod.DeclaringType); + ilGenerator.Emit(OpCodes.Ldtoken, actualMethodDeclaringType); ilGenerator.Emit(OpCodes.Call, GetMethodFromHandle); ilGenerator.Emit(OpCodes.Castclass, typeof(MethodInfo)); + ilGenerator.Emit(OpCodes.Stloc_0); + ilGenerator.Emit(OpCodes.Ldloc_0); + ilGenerator.Emit(method.IsForValueType() ? OpCodes.Ldnull : OpCodes.Ldarg_0); + ilGenerator.Emit(OpCodes.Call, GetIndexOfMatchingShim); + ilGenerator.Emit(OpCodes.Stloc_1); + ilGenerator.Emit(OpCodes.Ldloc_1); + ilGenerator.Emit(OpCodes.Ldc_I4_M1); + ilGenerator.Emit(OpCodes.Ceq); + ilGenerator.Emit(OpCodes.Brtrue_S, rewriteLabel); + ilGenerator.Emit(OpCodes.Ldloc_1); + ilGenerator.Emit(OpCodes.Call, GetShimReplacementMethod); + ilGenerator.Emit(OpCodes.Stloc_0); + ilGenerator.Emit(OpCodes.Ldloc_0); + ilGenerator.Emit(OpCodes.Call, GetMethodPointer); + ilGenerator.Emit(OpCodes.Stloc_2); + ilGenerator.Emit(OpCodes.Ldloc_1); + ilGenerator.Emit(OpCodes.Call, GetShimDelegateTarget); + for (var i = 0; i < signatureParamTypes.Count; i++) + ilGenerator.Emit(OpCodes.Ldarg, i); + ilGenerator.Emit(OpCodes.Ldloc_2); + ilGenerator.EmitCalli(OpCodes.Calli, CallingConventions.HasThis, method.ReturnType, signatureParamTypes.ToArray(), null); + ilGenerator.Emit(OpCodes.Br_S, returnLabel); + // Rewrite method ilGenerator.MarkLabel(rewriteLabel); - ilGenerator.Emit(OpCodes.Ldc_I4_0); + ilGenerator.Emit(OpCodes.Ldloc_0); + ilGenerator.Emit(methodDeclaringType.IsInterface ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); ilGenerator.Emit(OpCodes.Call, CreateRewriter); ilGenerator.Emit(OpCodes.Call, Rewrite); ilGenerator.Emit(OpCodes.Castclass, typeof(MethodInfo)); - - // Retrieve pointer to rewritten method - ilGenerator.Emit(OpCodes.Call, GetMethodPointer); ilGenerator.Emit(OpCodes.Stloc_0); // Setup stack and make indirect call @@ -244,15 +278,22 @@ public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method, TypeIn } else { - if (actualMethod.DeclaringType != constrainedType) + if (actualMethodDeclaringType != constrainedType) { ilGenerator.Emit(OpCodes.Ldobj, constrainedType); ilGenerator.Emit(OpCodes.Box, constrainedType); - signatureParamTypes[i] = actualMethod.DeclaringType; + signatureParamTypes[i] = actualMethodDeclaringType; } } } } + + ilGenerator.Emit(OpCodes.Ldloc_0); + + // Retrieve pointer to rewritten method + ilGenerator.Emit(OpCodes.Call, GetMethodPointer); + ilGenerator.Emit(OpCodes.Stloc_0); + ilGenerator.Emit(OpCodes.Ldloc_0); ilGenerator.EmitCalli(OpCodes.Calli, CallingConventions.Standard, method.ReturnType, signatureParamTypes.ToArray(), null); @@ -280,8 +321,10 @@ public static DynamicMethod GenerateStubForVirtualCall(MethodInfo method) StubHelper.GetOwningModule(), true); +#if TRACE Console.WriteLine("\n" + method); - +#endif + var ilGenerator = stub.GetILGenerator(); if ((method.GetMethodBody() == null && !method.IsAbstract) || StubHelper.IsIntrinsic(method)) @@ -451,7 +494,7 @@ public static DynamicMethod GenerateStubForObjectInitialization(ConstructorInfo ilGenerator.MarkLabel(rewriteLabel); // ++ - if (thisType.IsValueType) + if (constructor.DeclaringType.IsValueType) { ilGenerator.Emit(OpCodes.Ldloca_S, (byte)1); // ilGenerator.Emit(OpCodes.Dup); diff --git a/src/Pose/Infrastructure/ExcludeFromCodeCoverageAttribute.cs b/src/Pose/Infrastructure/ExcludeFromCodeCoverageAttribute.cs new file mode 100644 index 0000000..4aa3502 --- /dev/null +++ b/src/Pose/Infrastructure/ExcludeFromCodeCoverageAttribute.cs @@ -0,0 +1,18 @@ +#if !NET5_0_OR_GREATER + +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property + | AttributeTargets.Event, + Inherited = false, + AllowMultiple = false + )] + public sealed class ExcludeFromCodeCoverageAttribute : Attribute + { + public string Justification { get; set; } + } +} + +#endif \ No newline at end of file diff --git a/src/Pose/Is.cs b/src/Pose/Is.cs index 762acf9..d6d94cd 100644 --- a/src/Pose/Is.cs +++ b/src/Pose/Is.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace Pose { + [ExcludeFromCodeCoverage(Justification = "A simple wrapper")] public static class Is { public static T A() => default(T); diff --git a/src/Pose/Pose.csproj b/src/Pose/Pose.csproj index 8ff0417..283666e 100644 --- a/src/Pose/Pose.csproj +++ b/src/Pose/Pose.csproj @@ -1,10 +1,13 @@ - netstandard2.0;netcoreapp2.0;netcoreapp3.0;net48;net7.0;net8.0 + netstandard2.0;netcoreapp2.0;netcoreapp3.0;net48;net5.0;net7.0;net8.0 portable Pose true + + + diff --git a/src/Pose/PoseContext.cs b/src/Pose/PoseContext.cs index 4e2b8c2..1514bea 100644 --- a/src/Pose/PoseContext.cs +++ b/src/Pose/PoseContext.cs @@ -9,7 +9,6 @@ namespace Pose public static class PoseContext { internal static Shim[] Shims { private set; get; } - internal static Dictionary StubCache { private set; get; } public static void Isolate(Action entryPoint, params Shim[] shims) { @@ -20,14 +19,19 @@ public static void Isolate(Action entryPoint, params Shim[] shims) } Shims = shims; - StubCache = new Dictionary(); var delegateType = typeof(Action<>).MakeGenericType(entryPoint.Target.GetType()); var rewriter = MethodRewriter.CreateRewriter(entryPoint.Method, false); + +#if TRACE Console.WriteLine("----------------------------- Rewriting ----------------------------- "); +#endif var methodInfo = (MethodInfo)(rewriter.Rewrite()); +#if TRACE Console.WriteLine("----------------------------- Invoking ----------------------------- "); +#endif + methodInfo.CreateDelegate(delegateType).DynamicInvoke(entryPoint.Target); } } diff --git a/src/Pose/Shim.Delegates.cs b/src/Pose/Shim.Delegates.cs index a629c91..97b171a 100644 --- a/src/Pose/Shim.Delegates.cs +++ b/src/Pose/Shim.Delegates.cs @@ -1,121 +1,165 @@ using System; +using System.Diagnostics.CodeAnalysis; using Pose.Delegates; namespace Pose { public partial class Shim { + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Delegate replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Action replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(ActionRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(Func replacement) => WithImpl(replacement); + [ExcludeFromCodeCoverage(Justification = "Forwards to WithImpl")] public Shim With(FuncRef replacement) => WithImpl(replacement); } diff --git a/src/Pose/Shim.cs b/src/Pose/Shim.cs index 37ca2a0..54245e9 100644 --- a/src/Pose/Shim.cs +++ b/src/Pose/Shim.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; @@ -55,9 +56,11 @@ private Shim(MethodBase original, object instanceOrType) _instance = instanceOrType; } + [ExcludeFromCodeCoverage(Justification = "Forwards to ReplaceImpl")] public static Shim Replace(Expression expression, bool setter = false) => ReplaceImpl(expression, setter); + [ExcludeFromCodeCoverage(Justification = "Forwards to ReplaceImpl")] public static Shim Replace(Expression> expression, bool setter = false) => ReplaceImpl(expression, setter); diff --git a/src/Sandbox/Program.cs b/src/Sandbox/Program.cs index 78bf603..b6472f3 100644 --- a/src/Sandbox/Program.cs +++ b/src/Sandbox/Program.cs @@ -6,6 +6,53 @@ namespace Pose.Sandbox { public class Program { + static void Constrain(TT a) where TT : IA{ + Console.WriteLine(a.GetString()); + } + + static void ConstrainD(TT a) where TT : D{ + Console.WriteLine(a.GetString2()); + } + + static void Box(TT a) where TT : B{ + Console.WriteLine(a.GetInt()); + } + + static void BoxD(TT a) where TT : B{ + Console.WriteLine(a.GetString2()); + } + + interface IA { + string GetString(); + } + + abstract class B : D { + public int GetInt(){return 0;} + } + + abstract class D + { + public string GetString2() => "Wuu?"; + } + + class A : B, IA { + public string GetString() => "Hello, World"; + } + + struct C : IA + { + public string GetString() => "Wee"; + } + + public class OverridenOperatorClass + { + public static explicit operator bool(OverridenOperatorClass c) => false; + + public static implicit operator int(OverridenOperatorClass c) => int.MinValue; + + public static OverridenOperatorClass operator +(OverridenOperatorClass l, OverridenOperatorClass r) => default(OverridenOperatorClass); + } + public static void Main(string[] args) { #if NET48 @@ -18,12 +65,54 @@ public static void Main(string[] args) }, dateTimeShim); #elif NETCOREAPP2_0 Console.WriteLine("2.0"); - var dateTimeShim = Shim.Replace(() => DateTime.Now).With(() => new DateTime(2004, 1, 1)); + + var with = Shim.Replace(() => Is.A().GetInt()).With(delegate(B x) { return 5;}); + var with1 = Shim.Replace(() => Is.A().GetString2()).With(delegate(D x) { return "HeyD";}); + var with2 = Shim.Replace(() => Is.A().GetString()).With(delegate(A x) { return "Hey";}); + var shim = Shim.Replace(() => Is.A().GetString()).With(delegate(ref C @this) { return "Hey2"; }); + + // var with2 = Shim.Replace(() => Is.A().GetString()).With(delegate(IA x) { return "Hey";}); PoseContext.Isolate( () => { - Console.WriteLine(DateTime.Now); - }, dateTimeShim); + var a = new A(); + // Box(a); + Console.WriteLine(a.GetString()); + Constrain(a); + ConstrainD(a); + BoxD(a); + + var c = new C(); + Console.WriteLine(c.GetString()); + Constrain(c); + }, with, with1, shim, with2); + + // var sut1 = new OverridenOperatorClass(); + // int s = sut1; + // Shim.Replace(() => Is.A() + Is.A()) + // .With(delegate(OverridenOperatorClass l, int r) { return default(OverridenOperatorClass); }); + // var operatorShim = Shim.Replace(() => (bool) sut1) + // .With(delegate (OverridenOperatorClass c) { return true; }); + // var dateTimeAddShim = Shim.Replace(() => Is.A() + Is.A()) + // .With(delegate(DateTime dt, TimeSpan ts) { return new DateTime(2004, 01, 01); }); + // var dateTimeSubtractShim = Shim.Replace(() => Is.A() - Is.A()) + // .With(delegate(DateTime dt, TimeSpan ts) { return new DateTime(1990, 01, 01); }); + // + // PoseContext.Isolate( + // () => + // { + // var dateTime = DateTime.Now; + // Console.WriteLine($"Date: {dateTime}"); + // var ts = TimeSpan.FromSeconds(1); + // Console.WriteLine($"Time: {ts}"); + // + // var time = dateTime + ts; + // Console.WriteLine($"Result1: {time}"); + // + // var time2 = dateTime - ts; + // Console.WriteLine($"Result2: {time2}"); + // }, dateTimeAddShim, dateTimeSubtractShim + // ); #elif NET6_0 Console.WriteLine("6.0"); var dateTimeShim = Shim.Replace(() => DateTime.Now).With(() => new DateTime(2004, 1, 1)); diff --git a/src/Sandbox/Sandbox.csproj b/src/Sandbox/Sandbox.csproj index 14ae089..284f179 100644 --- a/src/Sandbox/Sandbox.csproj +++ b/src/Sandbox/Sandbox.csproj @@ -7,6 +7,10 @@ netcoreapp2.0;netcoreapp3.0;net6.0;net7.0;net8.0 + + + + diff --git a/test/Pose.Tests/Helpers/ShimHelperTests.cs b/test/Pose.Tests/Helpers/ShimHelperTests.cs index a0856fe..dc60b5b 100644 --- a/test/Pose.Tests/Helpers/ShimHelperTests.cs +++ b/test/Pose.Tests/Helpers/ShimHelperTests.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using FluentAssertions; +using Pose.Exceptions; using Pose.Helpers; using Xunit; // ReSharper disable PossibleNullReferenceException @@ -19,7 +20,7 @@ public void Throws_NotImplementedException(Expression> expression, st Action act = () => ShimHelper.GetMethodFromExpression(expression.Body, false, out _); // Assert - act.Should().Throw(because: reason); + act.Should().Throw(because: reason); } // ReSharper disable once InconsistentNaming diff --git a/test/Pose.Tests/IL/MethodRewriterTests.cs b/test/Pose.Tests/IL/MethodRewriterTests.cs index 060decb..7bf3788 100644 --- a/test/Pose.Tests/IL/MethodRewriterTests.cs +++ b/test/Pose.Tests/IL/MethodRewriterTests.cs @@ -102,7 +102,7 @@ public void Can_rewrite_try_catch_returning_from_try() // Assert result.Should().Be(1, because: "that is what the method returns from the try block"); } - + public static int TryCatch_ReturnsFromTry() { try @@ -187,16 +187,178 @@ public void Can_rewrite_try_catch_blocks() var called = false; var enteredCatchBlock = false; + // A shim is necessary for the entry point to be rewritten + var shim = Shim.Replace(() => Console.WriteLine(Is.A())).With(delegate(string s) { Console.WriteLine(s); }); + Action act = () => PoseContext.Isolate( () => { try { called = true; } catch (Exception) { enteredCatchBlock = true; } - }); + }, shim); act.Should().NotThrow(); called.Should().BeTrue(); enteredCatchBlock.Should().BeFalse(); } + + private int Switch(int value) + { + return value switch + { + 0 => 1, + 1 => 2, + _ => -1 + }; + } + + [Fact] + public void Can_handle_switch_statements() + { + var value = 1; + var result = default(int); + + // A shim is necessary for the entry point to be rewritten + var shim = Shim.Replace(() => Console.WriteLine(Is.A())).With(delegate(string s) { Console.WriteLine(s); }); + + Action act = () => PoseContext.Isolate( + () => + { + result = Switch(value); + }, shim); + + act.Should().NotThrow(); + result.Should().Be(2, because: "that is the value assigned in the given switch branch"); + } + +#if NET47 || NET48 + [Fact(Skip = "Not supported on .NET Framework 4.7+")] +#else + [Fact] +#endif + public void Can_handle_exception_filters() + { + var value = 1; + var result = default(int); + + // A shim is necessary for the entry point to be rewritten + var shim = Shim.Replace(() => Console.WriteLine(Is.A())).With(delegate(string s) { Console.WriteLine(s); }); + + Action act = () => PoseContext.Isolate( + () => + { + try + { + throw new Exception("Hello"); + } + catch (Exception e) when (e.Message == "Hello") + { + result = 1; + } + catch (Exception) + { + result = -1; + } + }, shim); + + act.Should().NotThrow(); + result.Should().Be(1, because: "that is the value assigned in the matched catch block"); + } + + public class OpCodes + { + private static readonly Shim DummyShim = Shim.Replace(() => Console.WriteLine(Is.A())).With(delegate(string s) { Console.WriteLine(s); }); + + [Fact] + public void Can_handle_InlineI8() + { + var value = default(long); + Action act = () => PoseContext.Isolate( + () => + { + value = long.MaxValue; + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(long.MaxValue, because: "that is the value assigned"); + } + + [Fact] + public void Can_handle_InlineI() + { + var value = default(int); + Action act = () => PoseContext.Isolate( + () => + { + value = int.MaxValue; + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(int.MaxValue, because: "that is the value assigned"); + } + + [Fact] + public void Can_handle_ShortInlineI() + { + var value = default(sbyte); + Action act = () => PoseContext.Isolate( + () => + { + value = sbyte.MaxValue; + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(sbyte.MaxValue, because: "that is the value assigned"); + } + + [Fact] + public void Can_handle_ShortInlineR() + { + var value = default(Single); + Action act = () => PoseContext.Isolate( + () => + { + value = Single.MaxValue; + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(Single.MaxValue, because: "that is the value assigned"); + } + + [Fact] + public void Can_handle_InlineR() + { + var value = default(double); + Action act = () => PoseContext.Isolate( + () => + { + value = double.MaxValue; + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(double.MaxValue, because: "that is the value assigned"); + } + + [Fact] + public void Can_handle_Switch() + { + var value = default(int); + Action act = () => PoseContext.Isolate( + () => + { + var a = int.MaxValue; + switch(a) + { + case 1: value = 1; break; + case 2: value = 2; break; + case 3: value = 3; break; + default: value = int.MinValue; break; + } + }, DummyShim); + + act.Should().NotThrow(); + value.Should().Be(int.MinValue, because: "that is the value assigned"); + } + } } } \ No newline at end of file diff --git a/test/Pose.Tests/IL/StubsTests.cs b/test/Pose.Tests/IL/StubsTests.cs index 944d169..e95dd5b 100644 --- a/test/Pose.Tests/IL/StubsTests.cs +++ b/test/Pose.Tests/IL/StubsTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using FluentAssertions; using Pose.IL; using Xunit; @@ -87,6 +88,34 @@ public void Can_generate_stub_for_virtual_call() valueParameter.ParameterType.Should().Be(typeof(string), because: "the second parameter is the value to be added"); } + private interface IB + { + int GetInt(); + } + + private class B : IB + { + public int GetInt() => 10; + } + + [Fact] + public void Can_generate_stub_for_virtual_constrained_call() + { + // Arrange + var thisType = typeof(IB); + var methodInfo = thisType.GetMethod(nameof(IB.GetInt)); + + // Act + var dynamicMethod = Stubs.GenerateStubForVirtualCall(methodInfo, typeof(B).GetTypeInfo()); + + // Assert + var dynamicParameters = dynamicMethod.GetParameters(); + dynamicParameters.Should().HaveCount(1, because: "the dynamic method takes just the instance parameter"); + + var instanceParameter = dynamicParameters[0]; + instanceParameter.ParameterType.Should().Be(typeof(B).MakeByRefType(), because: "the first parameter is the instance"); + } + [Fact] public void Can_generate_stub_for_reference_type_constructor() { diff --git a/test/Pose.Tests/OperatorTests+Exceptions.cs b/test/Pose.Tests/OperatorTests+Exceptions.cs new file mode 100644 index 0000000..67248c2 --- /dev/null +++ b/test/Pose.Tests/OperatorTests+Exceptions.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FluentAssertions; +using Xunit; + +// ReSharper disable UnusedParameter.Local +// ReSharper disable ConvertToLambdaExpression + +namespace Pose.Tests +{ + public partial class OperatorTests + { + public class OperatorExceptionsData : IEnumerable + { + public IEnumerator GetEnumerator() + { + var arithmeticOperators = ArithmeticOperators + .Concat(BitwiseAndShit) + .Concat(Equality) + .Concat(Boolean) + .Concat(Conversion); + + return arithmeticOperators.GetEnumerator(); + } + + private static IEnumerable ArithmeticOperators + { + get + { + // Addition + yield return TestCase( + () => Shim.Replace(() => Is.A() + Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Subtraction + yield return TestCase( + () => Shim.Replace(() => Is.A() - Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Multiplication + yield return TestCase( + () => Shim.Replace(() => Is.A() * Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Division + yield return TestCase( + () => Shim.Replace(() => Is.A() / Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Modulus + yield return TestCase( + () => Shim.Replace(() => ~Is.A()) + .With(delegate(int l) { return int.MinValue; }) + ); + + // Unary plus + yield return TestCase( + () => Shim.Replace(() => +Is.A()) + .With(delegate(int l) { return int.MinValue; }) + ); + + // Unary minus + yield return TestCase( + () => Shim.Replace(() => -Is.A()) + .With(delegate(int l) { return int.MinValue; }) + ); + } + } + + private static IEnumerable BitwiseAndShit + { + get + { + // Left shift + yield return TestCase( + () => Shim.Replace(() => Is.A() << Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Right shift + yield return TestCase( + () => Shim.Replace(() => Is.A() >> Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + } + } + + [SuppressMessage("ReSharper", "EqualExpressionComparison")] + private static IEnumerable Equality + { + get + { + // Equal + yield return TestCase( + () => Shim.Replace(() => Is.A() == Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Not equal + yield return TestCase( + () => Shim.Replace(() => Is.A() != Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Less than + yield return TestCase( + () => Shim.Replace(() => Is.A() < Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Greater than + yield return TestCase( + () => Shim.Replace(() => Is.A() > Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Less than or equal to + yield return TestCase( + () => Shim.Replace(() => Is.A() <= Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + + // Greater than or equal to + yield return TestCase( + () => Shim.Replace(() => Is.A() >= Is.A()) + .With(delegate(int l, int r) { return int.MinValue; }) + ); + } + } + + private static IEnumerable Conversion + { + get + { + yield return TestCase( + () => Shim.Replace(() => (long) Is.A()) + .With(delegate(int l) { return default(long); }) + ); + } + } + + private static IEnumerable Boolean + { + get + { + // Logical negation + yield return TestCase( + () => Shim.Replace(() => !Is.A()) + .With(delegate(bool b) { return false; }) + ); + + // Logical AND + yield return TestCase( + () => Shim.Replace(() => Is.A() & Is.A()) + .With(delegate(bool l, bool r) { return default(bool); }) + ); + + // Exclusive OR + yield return TestCase( + () => Shim.Replace(() => Is.A() ^ Is.A()) + .With(delegate(bool l, bool r) { return default(bool); }) + ); + + // Logical OR + yield return TestCase( + () => Shim.Replace(() => Is.A() | Is.A()) + .With(delegate(bool l, bool r) { return default(bool); }) + ); + } + } + private static object[] TestCase(Func func) + { + return new object[] { func }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Theory] + [ClassData(typeof(OperatorExceptionsData))] + public void Throws_exception_if_the_operator_cannot_be_shimmed(Func shimFactory) + { + shimFactory.Should().Throw(because: "the operator cannot be shimmed"); + } + } +} \ No newline at end of file diff --git a/test/Pose.Tests/OperatorTests.cs b/test/Pose.Tests/OperatorTests.cs new file mode 100644 index 0000000..e3de875 --- /dev/null +++ b/test/Pose.Tests/OperatorTests.cs @@ -0,0 +1,650 @@ +using System; +using FluentAssertions; +using Xunit; + +// ReSharper disable EqualExpressionComparison +// ReSharper disable ConvertToLambdaExpression +// ReSharper disable ConditionIsAlwaysTrueOrFalse +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable UnusedParameter.Local + +namespace Pose.Tests +{ + public partial class OperatorTests + { + // ReSharper disable once ClassNeverInstantiated.Global + public class Shimming + { + internal class OperatorsClass + { + public string Value { get; set; } + + public static OperatorsClass operator +(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator +(OperatorsClass l) => null; + public static OperatorsClass operator -(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator -(OperatorsClass l) => null; + public static OperatorsClass operator ~(OperatorsClass l) => null; + public static OperatorsClass operator !(OperatorsClass l) => null; + public static OperatorsClass operator *(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator |(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator /(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator %(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator &(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator ^(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator <<(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator >>(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator ==(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator !=(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator <(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator >(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator <=(OperatorsClass l, OperatorsClass r) => null; + public static bool? operator >=(OperatorsClass l, OperatorsClass r) => null; + public static explicit operator int(OperatorsClass c) => int.MinValue; + public static implicit operator double(OperatorsClass c) => 42.0; + + // The following operators are overloadable, but they cannot be expressed in an expression tree + public static bool operator true(OperatorsClass l) => false; + public static bool operator false(OperatorsClass r) => true; + public static OperatorsClass operator >>>(OperatorsClass l, OperatorsClass r) => null; + public static OperatorsClass operator ++(OperatorsClass l) => null; + public static OperatorsClass operator --(OperatorsClass l) => null; + } + + public class Arithmetic + { + [Fact] + public void Can_shim_addition_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() + Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left + right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left + right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_addition_operator_for_TimeSpan() + { + // Arrange + var shimmedValue = TimeSpan.FromSeconds(2); + var shim = Shim.Replace(() => Is.A() + Is.A()) + .With(delegate(TimeSpan l, TimeSpan r) { return shimmedValue; }); + + var now = TimeSpan.Zero; + var zeroSeconds = TimeSpan.Zero; + var result = default(TimeSpan); + + // Act + PoseContext.Isolate(() => result = now + zeroSeconds, shim); + + // Assert + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(now + zeroSeconds, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_subtraction_operator_for_TimeSpan() + { + // Arrange + var shimmedValue = TimeSpan.FromDays(2); + var shim = Shim.Replace(() => Is.A() - Is.A()) + .With(delegate(TimeSpan dt, TimeSpan ts) { return shimmedValue; }); + + var now = TimeSpan.Zero; + var zeroSeconds = TimeSpan.Zero; + var result = default(TimeSpan); + + // Act + PoseContext.Isolate(() => result = now - zeroSeconds, shim); + + // Assert + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(now - zeroSeconds, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_subtraction_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() - Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left - right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left - right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_multiplication_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() * Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left * right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left * right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_division_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() / Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left / right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left / right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_modulus_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() % Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left % right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left % right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_bitwise_complement_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => ~Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = ~sut, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(~sut, because: "the implementation has been shimmed"); + } + + [Fact(Skip = "How to get the operator method from expression?")] + public void Can_shim_true_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = sut ? shimmedValue : null, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + } + + [Fact(Skip = "How to get the operator method from expression?")] + public void Can_shim_false_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = sut ? null : shimmedValue, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + } + + [Fact] + public void Can_shim_unary_plus_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => +Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = +sut, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(+sut, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_unary_minus_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => -Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = -sut, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(-sut, because: "the implementation has been shimmed"); + } + } + + public class BitwiseAndShift + { + [Fact] + public void Can_shim_left_shift_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() << Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left << right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left << right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_right_shift_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() >> Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left >> right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left >> right, because: "the implementation has been shimmed"); + } + } + + public class Equality + { + [Fact] + public void Can_shim_equal_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() == Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left == right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left == right).Should().BeNull(because: "that is the actual implementation"); + } + + [Fact] + public void Can_shim_not_equal_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() != Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left != right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left != right).Should().BeNull(because: "that is the actual implementation"); + } + + [Fact] + public void Can_shim_less_than_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() < Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left < right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left < right).Should().BeNull(because: "that is the actual implementation"); + } + + [Fact] + public void Can_shim_greater_than_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() > Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left > right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left > right).Should().BeNull(because: "that is the actual implementation"); + } + + [Fact] + public void Can_shim_less_than_or_equal_to_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() <= Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left <= right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left >= right).Should().BeNull(because: "that is the actual implementation"); + } + + [Fact] + public void Can_shim_greater_than_or_equal_to_operator() + { + // Arrange + bool? shimmedValue = false; + var shim = Shim.Replace(() => Is.A() >= Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + // Act + var result = default(bool?); + PoseContext.Isolate( + () => + { + var left = new OperatorsClass(); + var right = new OperatorsClass(); + + result = left >= right; + }, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + + // Verify actual implementation + var left = new OperatorsClass(); + var right = new OperatorsClass(); + (left <= right).Should().BeNull(because: "that is the actual implementation"); + } + } + + public class Conversion + { + [Fact] + public void Can_shim_explicit_cast_operator() + { + // Arrange + var shimmedValue = int.MaxValue; + var shim = Shim.Replace(() => (int) Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = int.MinValue; + + // Act + PoseContext.Isolate(() => result = (int) sut, shim); + + // Assert + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe((int)sut, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_implicit_cast_operator() + { + // Arrange + var shimmedValue = double.MaxValue; + // While this is in fact *NOT* the implicit operator, it does replace the correct method. + var shim = Shim.Replace(() => (double) Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = 42.0; + + // Act + PoseContext.Isolate(() => result = sut, shim); + + // Assert + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe((double)sut, because: "the implementation has been shimmed"); + } + } + + public class BooleanLogic + { + [Fact] + public void Can_shim_logical_negation_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => !Is.A()) + .With(delegate(OperatorsClass l) { return shimmedValue; }); + + var sut = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = !sut, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(~sut, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_logical_AND_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() & Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left & right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left & result, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_logical_exclusive_OR_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() ^ Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left ^ right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left ^ right, because: "the implementation has been shimmed"); + } + + [Fact] + public void Can_shim_logical_OR_operator() + { + // Arrange + var shimmedValue = new OperatorsClass { Value = "Hello, World" }; + var shim = Shim.Replace(() => Is.A() | Is.A()) + .With(delegate(OperatorsClass l, OperatorsClass r) { return shimmedValue; }); + + var left = new OperatorsClass(); + var right = new OperatorsClass(); + var result = default(OperatorsClass); + + // Act + PoseContext.Isolate(() => result = left | right, shim); + + // Assert + result.Should().NotBeNull(because: "the shim is configured to return a non-null value"); + result.Should().Be(shimmedValue, because: "that is the value the shim is configured to return"); + result.Should().NotBe(left | right, because: "the implementation has been shimmed"); + } + } + } + } +} \ No newline at end of file diff --git a/test/Pose.Tests/Pose.Tests.csproj b/test/Pose.Tests/Pose.Tests.csproj index ae5e80c..c0e8c86 100644 --- a/test/Pose.Tests/Pose.Tests.csproj +++ b/test/Pose.Tests/Pose.Tests.csproj @@ -1,8 +1,9 @@ - netcoreapp2.0;netcoreapp3.0;netcoreapp3.1;net47;net48;net6.0;net8.0 + net6.0;net8.0;netcoreapp2.0;netcoreapp3.0;netcoreapp3.1;net47;net48;net7.0 false + 11 diff --git a/test/Pose.Tests/RegressionTests.cs b/test/Pose.Tests/RegressionTests.cs new file mode 100644 index 0000000..7718ccd --- /dev/null +++ b/test/Pose.Tests/RegressionTests.cs @@ -0,0 +1,32 @@ +using System; +using FluentAssertions; +using Xunit; +using DateTime = System.DateTime; + +namespace Pose.Tests +{ + public class RegressionTests + { + private enum TestEnum { A } + + [Fact(DisplayName = "Enum.IsDefined cannot be called from within PoseContext.Isolate #26")] + public void Can_call_EnumIsDefined_from_Isolate() + { + // Arrange + var shim = Shim + .Replace(() => new DateTime(2024, 2, 2)) + .With((int year, int month, int day) => new DateTime(2004, 1, 1)); + var isDefined = false; + + // Act + PoseContext.Isolate( + () => + { + isDefined = Enum.IsDefined(typeof(TestEnum), nameof(TestEnum.A)); + }, shim); + + // Assert + isDefined.Should().BeTrue(because: "Enum.IsDefined can be called from Isolate"); + } + } +} \ No newline at end of file diff --git a/test/Pose.Tests/ShimTests.cs b/test/Pose.Tests/ShimTests.cs index 7e411eb..ba816fc 100644 --- a/test/Pose.Tests/ShimTests.cs +++ b/test/Pose.Tests/ShimTests.cs @@ -48,15 +48,27 @@ public void Can_shim_static_method() public class ReferenceTypes { - private class Instance + private interface IBase + { + public double GetDouble(); + } + + private abstract class Base + { + public virtual int GetInt() => 0; + } + + private class Instance : Base, IBase { // ReSharper disable once MemberCanBeMadeStatic.Local public string GetString() { return "!"; } + + public double GetDouble() => default(double); } - + [Fact] public void Can_shim_method_of_any_instance() { @@ -77,6 +89,84 @@ public void Can_shim_method_of_any_instance() dt.Should().BeEquivalentTo("String", because: "that is what the shim is configured to return"); } + /// + /// This method has the following IL code: + ///
+                ///     IL_0001: ldarg.0      // 'instance'
+                ///     IL_0002: box          !!0/*T*/
+                ///     IL_0007: callvirt     instance int32 Pose.Tests.ShimTests/Methods/ReferenceTypes/Base::GetInt()
+                ///     IL_000c: stloc.0      // V_0
+                ///     IL_000d: br.s         IL_000f
+                /// 
+ ///
+ /// + /// + /// + private static int Box(T instance) where T : Base + { + return instance.GetInt(); + } + + [Fact] + public void Can_shim_boxed_virtual_method_of_any_instance() + { + // Arrange + var shim = Shim.Replace(() => Is.A().GetInt()).With(delegate(Base @base) { return int.MinValue; }); + + // Act + int dt = default; + PoseContext.Isolate( + () => + { + var instance = new Instance(); + dt = Box(instance); + }, shim); + + // Assert + dt.Should().Be(int.MinValue, because: "that is what the shim is configured to return"); + } + + /// + /// This method has the following IL code: + ///
+                ///     IL_0001: ldarga.s     'instance'
+                ///     IL_0003: constrained. !!0/*T*/
+                ///     IL_0009: callvirt     instance float64 Pose.Tests.ShimTests/Methods/ReferenceTypes/IBase::GetDouble()
+                ///     IL_000e: stloc.0      // V_0
+                ///     IL_000f: br.s         IL_0011
+                /// 
+ ///
+ /// + /// + /// + private static double Constrain(T instance) where T : IBase + { + return instance.GetDouble(); + } + +#if NET6_0_OR_GREATER + [Fact(Skip = "Not supported on .NET 6+ (for some reason). Will need to investigate.")] +#else + [Fact] +#endif + public void Can_shim_constrained_virtual_method_of_any_instance() + { + // Arrange + var shim = Shim.Replace(() => Is.A().GetDouble()).With(delegate(Instance @base) { return double.MinValue; }); + + // Act + double dt = default; + PoseContext.Isolate( + () => + { + var instance = new Instance(); + dt = Constrain(instance); + }, shim); + + // Assert + dt.Should().Be(double.MinValue, because: "that is what the shim is configured to return"); + } + [Fact] public void Can_shim_method_of_specific_instance() { @@ -98,7 +188,7 @@ public void Can_shim_method_of_specific_instance() // Assert value.Should().BeEquivalentTo(configuredValue, because: "that is what the shim is configured to return"); } - + [Fact] public void Shims_only_the_method_of_the_specified_instance() { @@ -161,12 +251,15 @@ private abstract class AbstractBase { public virtual string GetStringFromAbstractBase() => "!"; + public virtual string GetTFromAbstractBase(string input) => "?"; + public abstract string GetAbstractString(); } private class DerivedFromAbstractBase : AbstractBase { public override string GetAbstractString() => throw new NotImplementedException(); + } private class ShadowsMethodFromAbstractBase : AbstractBase @@ -199,6 +292,29 @@ public void Can_shim_instance_method_of_abstract_type() dt.Should().BeEquivalentTo("Hello", because: "the shim configured the base class"); } + [Fact] + public void Can_shim_instance_method_with_parameters_declared_on_abstract_type() + { + // Arrange + var shim = Shim + .Replace(() => Is.A().GetTFromAbstractBase(Is.A())) + .With((AbstractBase @this, string @string) => "Hello"); + + // Act + string dt = default; + PoseContext.Isolate( + () => + { + var instance = new DerivedFromAbstractBase(); + dt = instance.GetTFromAbstractBase(""); + }, + shim + ); + + // Assert + dt.Should().BeEquivalentTo("Hello", because: "the shim configured the base class"); + } + [Fact] public void Can_shim_abstract_method_of_abstract_type() { @@ -795,6 +911,8 @@ public class ShimSignatureValidation private class Instance { public string GetString() => null; + + public int GetInt(string someValue) => int.MinValue; } [Fact] @@ -812,6 +930,21 @@ public void Throws_InvalidShimSignatureException_if_the_signature_of_the_replace act1.Should().Throw(because: "the signature of the replacement method does not match the original"); } + [Fact] + public void Throws_InvalidShimSignatureException_if_parameter_types_for_the_replacement_do_not_match() + { + // Arrange + var shimTests = new Instance(); + + // Act + Action act = () => Shim + .Replace(() => shimTests.GetInt(Is.A())) + .With((Instance instance, int x) => { return int.MinValue; }); // Targets Shim.Replace(Expression>) + + // Assert + act.Should().Throw(because: "the parameter types for the replacement method does not match the original"); + } + [Fact] public void Reports_types_when_throwing_InvalidShimSignatureException() {