Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
74b8604
#26 Change `thisType.IsValueType` to `constructor.IsValueType`
Miista May 2, 2024
ebd7e64
#31 Add support for shimming conversion operators
Miista May 2, 2024
6e2f3bc
#31 Add addition, subtraction, and multiplication
Miista May 2, 2024
96fe226
#31 Add support for more operators
Miista May 2, 2024
1d6553e
#31 Add support for more operators
Miista May 2, 2024
438f662
#26 Add regression test for Miista/pose#26
Miista May 2, 2024
072e337
#31 Add tests for implicit and explicit casting operators
Miista May 2, 2024
2b54e32
Update Poser.nuspec
Miista May 2, 2024
6c16d52
Merge pull request #40 from Miista/26-enumisdefined-cannot-be-called-…
Miista May 2, 2024
6a51a1f
Improve error message when parameter types do not match
Miista May 2, 2024
1127fed
#36 Update badges in README
Miista May 2, 2024
ae06d78
Merge pull request #41 from Miista/release/release-2.0.1
Miista May 2, 2024
ca37f36
Add more tests for shimming operators
Miista May 2, 2024
872d3ac
Merge pull request #42 from Miista/36-update-badges-in-readme
Miista May 2, 2024
10c1ddd
Expand tests for operator shimming
Miista May 2, 2024
e4fe06c
#34: Increase test coverage
Miista May 2, 2024
fb7516f
#34: Add test cases for switch opcode
Miista May 2, 2024
fc3afe4
#34: Disable exception filter tests for .NET Framework 4.7 + 4.8
Miista May 2, 2024
a02ccb2
#34: Add tests for shimming method with parameters
Miista May 2, 2024
6e7189e
Swap usages of DEBUG for TRACE
Miista May 2, 2024
71b5b9f
Merge remote-tracking branch 'refs/remotes/origin/master' into 31-add…
Miista May 2, 2024
5d1800e
Add more target frameworks for tests
Miista May 2, 2024
42b0921
#31: Update README
Miista May 2, 2024
cf4c879
#31: Update README
Miista May 2, 2024
c95bdd1
Document why `++` and `--` operators are not supported
Miista May 2, 2024
a3d46cf
Provide better exception message when we cannot shim an operator
Miista May 2, 2024
6551051
Add override for ExcludeFromCodeCoverage for targets below .NET 5
Miista May 2, 2024
6c45878
Add coverage for throwing when parameter types do not match
Miista May 2, 2024
8757971
Provide helpful clue when instance types do not match
Miista May 2, 2024
cf4b10b
WIP: Being able to shim constrained virtual call
Miista May 2, 2024
df97836
Fix being able to shim constrained virtual call
Miista May 2, 2024
55f4068
#34 Exclude more types from code coverage
Miista May 2, 2024
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
54 changes: 50 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -115,6 +117,12 @@ Shim structShim = Shim.Replace(() => Is.A<MyStruct>().DoSomething()).With(

_Note: You cannot shim methods on specific instances of Value Types_

### Shim operators

```csharp
var operatorShim = Shim.Replace(() => Is.A<TimeSpan>() + Is.A<TimeSpan>()).With(
delegate(TimeSpan l, TimeSpan r) { return TimeSpan.Zero; });
```
### Isolating your code

```csharp
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions nuget/Poser.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Poser</id>
<version>2.0.0</version>
<version>2.0.1</version>
<title>Pose</title>
<authors>Søren Guldmund</authors>
<owners>Søren Guldmund</owners>
Expand All @@ -16,7 +16,7 @@
<copyright>Copyright 2024</copyright>
<readme>docs\README.md</readme>
<releaseNotes>
Provide better exception message when we cannot create instance.
Fix bug where `Enum.IsDefined` could not be called from within `PoseContext.Isolate`.
</releaseNotes>

<dependencies>
Expand Down
10 changes: 1 addition & 9 deletions src/Pose/Exceptions/InvalidShimSignatureException.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 0 additions & 10 deletions src/Pose/Exceptions/MethodRewriteException.cs
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
9 changes: 9 additions & 0 deletions src/Pose/Exceptions/UnsupportedExpressionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Pose.Exceptions
{
using System;

public class UnsupportedExpressionException : Exception
{
public UnsupportedExpressionException(string message) : base(message) { }
}
}
36 changes: 36 additions & 0 deletions src/Pose/Extensions/ExpressionTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
2 changes: 2 additions & 0 deletions src/Pose/Extensions/ILGeneratorExtensions.cs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
55 changes: 48 additions & 7 deletions src/Pose/Helpers/ShimHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;

using Pose.Exceptions;
using Pose.Extensions;

namespace Pose.Helpers
{
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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}'");
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/Pose/IL/MethodRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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

Expand Down Expand Up @@ -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);
Expand Down
Loading