From 9381ea39c9c612e8ee039653d435bac5462cb957 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Wed, 30 Sep 2026 19:02:11 -0300 Subject: [PATCH 1/2] Add StuntReference, Stunt.For/Get and StuntBuilder --- AGENTS.md | 110 ++++ readme.md | 83 ++- src/Stunts.Package/Stunt.cs | 353 +++++++++++- src/Stunts.Package/Stunt.vb | 508 +++++++++++++++++- .../Scenarios/StuntBuilder.cs | 123 +++++ 5 files changed, 1160 insertions(+), 17 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/Stunts.UnitTests/Scenarios/StuntBuilder.cs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ac82faf0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,110 @@ +# Stunts design notes + +## Behavior configuration API + +`AddBehavior`/`InsertBehavior` (in `src/Stunts/StuntExtensions.cs`) are generic extension +methods constrained to `IStunt`: + +```csharp +public static TStunt AddBehavior(this TStunt stunt, ...) where TStunt : IStunt +``` + +The constraint makes the API discoverable only on actual stunts (instead of any object), and +the generic type parameter preserves the concrete receiver type so calls can be chained. + +## `Stunt.Of` vs `Stunt.For` vs `Stunt.Get` + +`src/Stunts.Package/Stunt.cs` ships as content into consuming projects and is the +`[StuntGenerator]`-annotated factory API: + +| API | Returns | Use | +|-----|---------|-----| +| `Stunt.Of(...)` | `T` | Just the stunt, no behavior configuration needed | +| `Stunt.For(...)` | `StuntReference` | Create and configure behaviors | +| `Stunt.Get(T stunt)` | `StuntReference` | Configure an already created stunt | +| `Stunt.Builder()` | `StuntBuilder` | Configure behaviors once, build many stunts | + +`StuntReference` implements `IStunt`, so the behavior extension methods apply to it +directly, and exposes the stunt via `ToObject()`. + +`Stunt.For` passes a `Lazy`. The stunt is constructed on the first `ToObject()` call +(the implicit conversion calls `ToObject()` too). That call installs the behaviors configured +so far with `BehaviorPipelineFactory.UseAmbient`, so virtual members invoked from a base +constructor are intercepted. If none were added, the factory already current at that call is +used. After construction, `Behaviors` forwards to the stunt's own pipeline. `Stunt.Get` +still wraps an already constructed instance and forwards `Behaviors` immediately. + +An implicit conversion to `T` is also declared, but **C# does not allow user-defined +conversions to interface types**, so it only kicks in for class and delegate stunts. Since most +stunts are interfaces, `ToObject()` is the usage pattern tests and docs should show. This +limitation is why `Stunt.Of` keeps returning `T` instead of the reference type. + +Delegate stunts are unwrapped when the instance is materialized: for a delegate `T`, the +generated stunt is the delegate's `Target`. + +The VB content file (`src/Stunts.Package/Stunt.vb`) mirrors the C# one: `StuntReference(Of T)`, +`StuntBuilder`, `Get`, and the `Of`/`For`/`Build` overloads (including the delegate ones), with two +VB-specific notes: + +- VB cannot constrain a type parameter to `System.Delegate` (BC32061), so the delegate `Of`/`For` + overloads are declared as `(Of T)(implementation As T)` without a constraint. Behavior is + identical (the argument is passed as the single constructor argument), but a call passing a + lone `Nothing` (e.g. `Stunt.Of(Of IFoo)(Nothing)`) is ambiguous: use + `Stunt.Of(Of IFoo)(New Object() {Nothing})`. The overload is kept because it's what gives + lambdas their parameter type inference in `Stunt.Of(Of MyDelegate)(Function(x, y) x + y)`. +- The `Widening` conversion to `T` has the same interface limitation as C# (BC30512 under + `Option Strict On`), so `ToObject()` is the pattern to show. + +There is no VB project in the solution, so changes to this file are validated by compiling it +in a scratch VB project referencing `src/Stunts/bin/Debug/netstandard2.0/Stunts.dll`. + +Every factory overload (including the `For` and `Build` ones) must carry `[StuntGenerator]`: the source +generator keys off that attribute on the invoked method (instance or static) and uses the call site's +generic type arguments to decide which stunt types to generate. + +## `StuntBuilder` + +`StuntBuilder` (also in `Stunt.cs`/`Stunt.vb`) implements `IStunt`, so the `AddBehavior`/ +`InsertBehavior` extension methods configure the list of behaviors *being built*, returning the +builder itself for chaining. Its `Build` overloads mirror `Stunt.Of` one to one (including +the delegate one and the `T1`..`T8` extra interfaces), but wrap the stunt creation in +`BehaviorPipelineFactory.UseAmbient` with a factory that seeds every new pipeline from the +builder's behaviors. + +That's the key difference with `Stunt.Of`/`Stunt.For`: the behaviors are already in the pipeline +when the stunt constructor runs, so they can intercept virtual members invoked from base class +constructors (`ClassProxyTests.VirtualCallDuringConstructionUsesThePipelineFactory` shows the +raw ambient-factory version of the same thing). + +`BehaviorPipeline`'s `IEnumerable` constructor copies the list, so each built +stunt gets a snapshot of the behaviors at build time, while sharing the behavior *instances* +(a single `RecordingBehavior` records all stunts from the builder). + +The scenario at `src/Stunts.UnitTests/Scenarios/StuntBuilder.cs` covers all of the above through +the real source generator (it uses the namespace `Stunts.Scenarios.Builders` because a +`StuntBuilder` namespace segment would shadow the type). + +## Test usage pattern + +Tests create the reference, configure behaviors on it, and assign the stunt to an explicitly +typed local so the invoked type is obvious. Behaviors added before `ToObject()` are in place +during construction; behaviors added after still modify the live pipeline: + +```csharp +var stunt = Stunt.For(); +ICalculator calculator = stunt.ToObject(); + +stunt.AddBehavior(new DefaultValueBehavior()); + +Assert.Equal(0, calculator.Add(1, 2)); +``` + +Note that calling members on the reference itself would target `StuntReference` (for +`ToString`, `GetHashCode` and `Equals`), not the stunt. + +## Building locally + +The `Stunts` and `Stunts.CodeAnalysis` assemblies are consumed as analyzers by other projects +in the solution, so a full `dotnet build` may fail with file locks (`CS2012`) when an IDE has +the solution open. Close the IDE, and if needed build projects one at a time with +`/p:UseSharedCompilation=false`. Tests run with `dnx --yes retest`. diff --git a/readme.md b/readme.md index e6312bd6..f2dee629 100644 --- a/readme.md +++ b/readme.md @@ -36,13 +36,24 @@ Stunts essentially implements the [proxy pattern](https://en.wikipedia.org/wiki/ ## Usage +```csharp +var stunt = Stunt.For(); +ICalculator calc = stunt.ToObject(); + +stunt.AddBehavior((invocation, next) => ...); +``` + +`Stunt.Of` returns the stunt directly, and `Stunt.Get(stunt)` gets a `StuntReference` for an existing one, so behaviors can be added after the fact: + ```csharp ICalculator calc = Stunt.Of(); -calc.AddBehavior((invocation, next) => ...); +Stunt.Get(calc).AddBehavior((invocation, next) => ...); ``` -`AddBehavior`/`InsertBehavior` overloads allow granular control of the stunt's behavior pipeline, which is basically a [chain of responsibility](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern) that invokes all configured behaviors that apply to the current invocation. Individual behaviors can determine whether to short-circuit the call or call the next behavior in the chain. +> NOTE: `StuntReference` converts implicitly to `T` for classes and delegates. C# does not allow user-defined conversions to interfaces, so `ToObject()` is always available. + +`AddBehavior`/`InsertBehavior` are extension methods on `IStunt` (which `StuntReference` implements) and allow granular control of the stunt's behavior pipeline, which is basically a [chain of responsibility](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern) that invokes all configured behaviors that apply to the current invocation. Individual behaviors can determine whether to short-circuit the call or call the next behavior in the chain. Behaviors can also dynamically determine whether they apply to a given invocation by providing the optional `appliesTo` argument. In addition to the delegate-based overloads (called *anonymous behaviors*), you can also create behaviors by implementing the `IStuntBehavior` interface: @@ -64,6 +75,46 @@ Some commonly used behaviors that are generally useful are provided in the libra * `RecordingBehavior`: simple behavior that keeps track of all invocations, for troubleshooting or reporting. +## Building Stunts + +When you need the same behaviors on multiple stunts, `Stunt.Builder()` returns a `StuntBuilder` +that collects behaviors (with the very same `AddBehavior`/`InsertBehavior` extension methods) and +applies them to every stunt it builds, with the same `Build` overloads as `Stunt.Of`: + +```csharp +var builder = Stunt.Builder() + .AddBehavior(new RecordingBehavior()) + .AddBehavior(new DefaultValueBehavior()); + +ICalculator calculator = builder.Build(); +IStore store = builder.Build(); +``` + +Since the behaviors are in place *before* the stunt is instantiated (via an ambient +`BehaviorPipelineFactory`), they also intercept virtual members invoked from base class +constructors, which isn't possible when behaviors are added to an already created stunt: + +```csharp +public class Greeter +{ + public Greeter() => Seen = Name(); + public string Seen { get; } + public virtual string Name() => "base"; +} + +Greeter greeter = Stunt.Builder() + .AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Greeter.Name) + ? invocation.CreateValueReturn("proxy") + : next(invocation, next)) + .Build(); + +// greeter.Seen == "proxy" +``` + +Each `Build` call takes a snapshot of the behaviors configured at that point, so behaviors added +to the builder afterwards don't affect the stunts already built. Behavior instances themselves are +shared, so a single `RecordingBehavior` records the invocations of all stunts from that builder. + ## Customizing Stunt Creation If you want to centrally configure all your stunts, the easiest way is to simply provide your own factory method (i.e. `Stub.Of`), which in turn calls the `Stunt.Of` provided. For example: @@ -72,10 +123,11 @@ If you want to centrally configure all your stunts, the easiest way is to simply public static class Stub { [StuntGenerator] - public static T Of() => Stunt.Of() + public static T Of() => Stunt.For() .AddBehavior(new RecordingBehavior()) .AddBehavior(new DefaultEqualityBehavior()) - .AddBehavior(new DefaultValueBehavior()); + .AddBehavior(new DefaultValueBehavior()) + .ToObject(); } ``` @@ -117,9 +169,10 @@ The examples below use `ICalculator` from [the samples](samples/Samples/Core/ICa An anonymous behavior can short-circuit a call. The `appliesTo` predicate limits it to the two-argument `Add` overload: ```csharp -var calc = Stunt.Of().AddBehavior( +var calc = Stunt.For().AddBehavior( (call, _) => call.CreateValueReturn(42), - call => call.MethodBase.Name == nameof(ICalculator.Add) && call.Arguments.Count == 2); + call => call.MethodBase.Name == nameof(ICalculator.Add) && call.Arguments.Count == 2) + .ToObject(); calc.Add(2, 3); // 42 ``` @@ -129,9 +182,10 @@ calc.Add(2, 3); // 42 Arguments are available by name (or index), so a behavior can use the values passed by the caller: ```csharp -var calc = Stunt.Of().AddBehavior( +var calc = Stunt.For().AddBehavior( (call, _) => call.CreateValueReturn(call.Arguments.Get("x") + call.Arguments.Get("y")), - call => call.MethodBase.Name == nameof(ICalculator.Add) && call.Arguments.Count == 2); + call => call.MethodBase.Name == nameof(ICalculator.Add) && call.Arguments.Count == 2) + .ToObject(); calc.Add(2, 3); // 5 ``` @@ -142,11 +196,12 @@ Behaviors run in order. Put recording first to capture calls and results, and a ```csharp var recorder = new RecordingBehavior(); -var calc = Stunt.Of() +var calc = Stunt.For() .AddBehavior(recorder) .AddBehavior((call, _) => call.CreateValueReturn(5), call => call.MethodBase.Name == nameof(ICalculator.Add) && call.Arguments.Count == 2) - .AddBehavior(new DefaultValueBehavior()); + .AddBehavior(new DefaultValueBehavior()) + .ToObject(); calc.Add(2, 3); // 5 var calls = recorder.Invocations.Count; // 1 @@ -159,21 +214,21 @@ Register a factory when the built-in defaults are not suitable. Here, each call ```csharp var defaults = new DefaultValueProvider(); defaults.Register(() => "Hello!"); -var greet = Stunt.Of>().AddBehavior(new DefaultValueBehavior(defaults)); +var greet = Stunt.For>().AddBehavior(new DefaultValueBehavior(defaults)).ToObject(); greet(); // "Hello!" ``` ### Intercept a real implementation -Pass a delegate implementation to `Stunt.Of` and call `next` to forward to it. Behaviors can change arguments before forwarding: +Pass a delegate implementation to `Stunt.For` and call `next` to forward to it. Behaviors can change arguments before forwarding: ```csharp -var add = Stunt.Of>((x, y) => x + y).AddBehavior((call, next) => +var add = Stunt.For>((x, y) => x + y).AddBehavior((call, next) => { call.Arguments.Set(0, 10); return next(call, next); -}); +}).ToObject(); add(1, 2); // 12 ``` diff --git a/src/Stunts.Package/Stunt.cs b/src/Stunts.Package/Stunt.cs index 63658aaa..3d56b883 100644 --- a/src/Stunts.Package/Stunt.cs +++ b/src/Stunts.Package/Stunt.cs @@ -5,13 +5,251 @@ // the code is regenerated. // -#nullable disable // So we can keep a single file regardless of the nullability of the calling project. +#nullable enable // So we can keep a single file regardless of the nullability of the calling project. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Stunts { + /// + /// Holds a reference to a stunt of type , exposing its + /// for configuration, and allowing conversion to + /// via . + /// + /// + /// + /// When constructed from a , the stunt is created on the first + /// call. Behaviors added before that call initialize the pipeline + /// used during construction, including virtual members invoked from a base constructor, + /// via . If none were added, the factory + /// already current at that call is used. + /// + /// + /// The implicit conversion to only applies when + /// is a class or a delegate, since C# does not allow + /// user-defined conversions to interface types. Use otherwise. + /// The conversion calls . + /// + /// + [CompilerGenerated] + [ExcludeFromCodeCoverage] + partial class StuntReference : IStunt + { + readonly IList behaviors = new List(); + readonly Lazy lazy; + IStunt? stunt = default; + + /// + /// Initializes a new instance of that constructs the stunt + /// on the first call. + /// + /// The deferred stunt of type . + /// Thrown when is . + public StuntReference(Lazy value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + lazy = value; + } + + /// + /// Initializes a new instance of for an already constructed stunt. + /// + /// The stunt of type to reference. + /// Thrown when the value does not implement . + public StuntReference(T value) + : this(new Lazy(() => value)) + { + // Construction already happened, so behaviors apply to the live instance immediately. + stunt = Unwrap(value); + } + + /// + /// Gets the behaviors associated with the referenced stunt. + /// + /// + /// Before the stunt is constructed, this is the list that seeds its pipeline. + /// Afterwards it is the stunt's own behavior list. + /// + public IList Behaviors => stunt?.Behaviors ?? behaviors; + + /// + /// Gets the referenced stunt of type , constructing it on the first call. + /// + public T ToObject() + { + if (stunt == null) + { + // Capture the current factory first: the ambient scope below hides it, + // and an unconfigured reference should still construct with that factory. + var fallback = BehaviorPipelineFactory.Default; + using var _ = BehaviorPipelineFactory.UseAmbient(new ReferencePipelineFactory(behaviors, fallback)); + var created = lazy.Value; + var resolved = Unwrap(created); + // Dynamic proxies create their pipeline on the first intercept, which includes this getter. + EnsurePipeline(resolved); + stunt = resolved; + return created; + } + + return lazy.Value; + } + + /// + /// Allows implicit conversion from to + /// whenever is not an interface. + /// + public static implicit operator T(StuntReference stunt) => stunt.ToObject(); + + static IStunt Unwrap(T value) => + // A delegate stunt is the bound delegate; its target is the generated instance. + value as IStunt ?? + (value as Delegate)?.Target as IStunt ?? + throw new ArgumentException($"Value of type {typeof(T)} does not implement IStunt.", nameof(value)); + + static void EnsurePipeline(IStunt instance) => _ = instance.Behaviors; + + class ReferencePipelineFactory : IBehaviorPipelineFactory + { + readonly IList behaviors; + readonly IBehaviorPipelineFactory fallback; + + public ReferencePipelineFactory(IList behaviors, IBehaviorPipelineFactory fallback) + { + this.behaviors = behaviors; + this.fallback = fallback; + } + + public BehaviorPipeline CreatePipeline() + => behaviors.Count == 0 ? fallback.CreatePipeline() : new BehaviorPipeline(behaviors); + } + } + + /// + /// Builds stunts that all share the same set of , which are + /// applied from the very beginning of the stunt instantiation, including any virtual + /// members invoked from a base class constructor. + /// + /// + /// Behaviors are configured with the same AddBehavior/InsertBehavior + /// extension methods available for any , and each Build + /// call creates the stunt with a pipeline initialized with a snapshot of the + /// behaviors configured at that point, via . + /// + [CompilerGenerated] + [ExcludeFromCodeCoverage] + partial class StuntBuilder : IStunt + { + /// + /// Behaviors applied to every stunt created by this builder. + /// + public IList Behaviors { get; } = new List(); + + T Create(object[] constructorArgs, params Type[] interfaces) + { + using (BehaviorPipelineFactory.UseAmbient(new BuilderPipelineFactory(Behaviors))) + return (T)StuntFactory.Default.CreateStunt(typeof(Stunt).Assembly, typeof(T), interfaces, constructorArgs); + } + + /// + /// Creates a stunt that inherits or implements the type . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs); + + /// + /// Creates a stunt for the delegate that forwards to + /// . The builder behaviors run first; + /// next invokes . + /// + [StuntGenerator] + public T Build(T implementation) where T : Delegate + { + if (implementation == null) + throw new ArgumentNullException(nameof(implementation)); + + return Create(new object[] { implementation }); + } + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , and + /// . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7)); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// , and . + /// + [StuntGenerator] + public T Build(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); + + class BuilderPipelineFactory : IBehaviorPipelineFactory + { + readonly IEnumerable behaviors; + + public BuilderPipelineFactory(IEnumerable behaviors) => this.behaviors = behaviors; + + // The pipeline copies the behaviors, so later builder changes don't affect created stunts. + public BehaviorPipeline CreatePipeline() => new BehaviorPipeline(behaviors); + } + } + /// /// Instantiates stunts for the specified types. /// @@ -22,6 +260,22 @@ partial class Stunt static T Create(object[] constructorArgs, params Type[] interfaces) => (T)StuntFactory.Default.CreateStunt(typeof(Stunt).Assembly, typeof(T), interfaces, constructorArgs); + static StuntReference Defer(Func factory) => + new StuntReference(new Lazy(factory)); + + /// + /// Creates a that applies the same behaviors to every + /// stunt it builds, from the moment of their instantiation. + /// + public static StuntBuilder Builder() => new StuntBuilder(); + + /// + /// Gets a reference to an existing stunt, so behaviors can be added to it after the fact. + /// + /// An existing stunt, typically created via one of the Of overloads. + /// Thrown when the value does not implement . + public static StuntReference Get(T stunt) => new StuntReference(stunt); + /// /// Creates a stunt that inherits or implements the type . /// @@ -38,6 +292,7 @@ public static T Of(T implementation) where T : Delegate { if (implementation == null) throw new ArgumentNullException(nameof(implementation)); + return Create(new object[] { implementation }); } @@ -105,5 +360,99 @@ public static T Of(T implementation) where T : Delegate /// [StuntGenerator] public static T Of(params object[] constructorArgs) => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); + + /// + /// Creates a stunt that inherits or implements the type , + /// returning a reference to it that allows configuring its behaviors. + /// The instance is constructed on the first call. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs)); + + /// + /// Creates a stunt for the delegate that forwards to + /// , returning a reference to it that allows + /// configuring its behaviors. Behaviors added afterwards run first; + /// next invokes . + /// + [StuntGenerator] + public static StuntReference For(T implementation) where T : Delegate + { + if (implementation == null) + throw new ArgumentNullException(nameof(implementation)); + + return Defer(() => Create(new object[] { implementation })); + } + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , returning a reference + /// to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements and , + /// returning a reference to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , and + /// , returning a reference to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// and , returning a reference + /// to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , and , + /// returning a reference to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// and , returning a reference to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// and , returning a reference + /// to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7))); + + /// + /// Creates a stunt that inherits or implements and + /// additionally implements , , + /// , , + /// , and , + /// returning a reference to it that allows configuring its behaviors. + /// + [StuntGenerator] + public static StuntReference For(params object[] constructorArgs) => Defer(() => Create(constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8))); } -} \ No newline at end of file +} diff --git a/src/Stunts.Package/Stunt.vb b/src/Stunts.Package/Stunt.vb index f6beadb6..ee2d00e3 100644 --- a/src/Stunts.Package/Stunt.vb +++ b/src/Stunts.Package/Stunt.vb @@ -1,9 +1,313 @@ +' +' This code was included from Stunts. +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' + +Imports System +Imports System.Collections.Generic Imports System.Diagnostics.CodeAnalysis Imports System.Reflection Imports System.Runtime.CompilerServices Namespace Global.Stunts + ''' + ''' Holds a reference to a stunt of type , exposing its + ''' for configuration, and allowing conversion to + ''' via . + ''' + ''' + ''' When constructed from a , the stunt is created on the first + ''' call. Behaviors added before that call initialize the pipeline + ''' used during construction, including virtual members invoked from a base constructor, + ''' via . If none were added, the factory + ''' already current at that call is used. + ''' The widening conversion calls . + ''' + + + Partial Friend Class StuntReference(Of T) + Implements IStunt + + Private ReadOnly lazy As Lazy(Of T) + ' Behaviors configured before the stunt exists. Construction copies them into the pipeline. + Private ReadOnly behaviorList As IList(Of IStuntBehavior) = New List(Of IStuntBehavior)() + Private stunt As IStunt + + ''' + ''' Initializes a new instance of that constructs the stunt + ''' on the first call. + ''' + ''' The deferred stunt of type . + ''' Thrown when is . + Public Sub New(ByVal value As Lazy(Of T)) + If value Is Nothing Then + Throw New ArgumentNullException(NameOf(value)) + End If + + lazy = value + End Sub + + ''' + ''' Initializes a new instance of for an already constructed stunt. + ''' + ''' The stunt of type to reference. + ''' Thrown when the value does not implement . + Public Sub New(ByVal value As T) + Me.New(New Lazy(Of T)(Function() value)) + ' Construction already happened, so behaviors apply to the live instance immediately. + stunt = Unwrap(value) + End Sub + + ''' + ''' Gets the behaviors associated with the referenced stunt. + ''' + ''' + ''' Before the stunt is constructed, this is the list that seeds its pipeline. + ''' Afterwards it is the stunt's own behavior list. + ''' + Public ReadOnly Property Behaviors As IList(Of IStuntBehavior) Implements IStunt.Behaviors + Get + If stunt Is Nothing Then + Return behaviorList + End If + + Return stunt.Behaviors + End Get + End Property + + ''' + ''' Gets the referenced stunt of type , constructing it on the first call. + ''' + Public Function ToObject() As T + If stunt Is Nothing Then + ' Capture the current factory first: the ambient scope below hides it, + ' and an unconfigured reference should still construct with that factory. + Dim fallback = BehaviorPipelineFactory.Default + Using BehaviorPipelineFactory.UseAmbient(New ReferencePipelineFactory(behaviorList, fallback)) + Dim created = lazy.Value + Dim resolved = Unwrap(created) + ' Dynamic proxies create their pipeline on the first intercept, which includes this getter. + EnsurePipeline(resolved) + stunt = resolved + Return created + End Using + End If + + Return lazy.Value + End Function + + ''' + ''' Allows implicit conversion from to . + ''' + Public Shared Widening Operator CType(ByVal stunt As StuntReference(Of T)) As T + Return stunt.ToObject() + End Operator + + Private Shared Function Unwrap(ByVal value As T) As IStunt + ' A delegate stunt is the bound delegate; its target is the generated instance. + Dim instance = TryCast(DirectCast(value, Object), IStunt) + If instance Is Nothing Then + Dim target = TryCast(DirectCast(value, Object), [Delegate]) + If target IsNot Nothing Then + instance = TryCast(target.Target, IStunt) + End If + End If + + If instance Is Nothing Then + Throw New ArgumentException($"Value of type {GetType(T)} does not implement IStunt.", NameOf(value)) + End If + + Return instance + End Function + + Private Shared Sub EnsurePipeline(ByVal instance As IStunt) + Dim unused = instance.Behaviors + End Sub + + Private Class ReferencePipelineFactory + Implements IBehaviorPipelineFactory + + Private ReadOnly behaviors As IList(Of IStuntBehavior) + Private ReadOnly fallback As IBehaviorPipelineFactory + + Public Sub New(ByVal behaviors As IList(Of IStuntBehavior), ByVal fallback As IBehaviorPipelineFactory) + Me.behaviors = behaviors + Me.fallback = fallback + End Sub + + Public Function CreatePipeline(Of TStunt)() As BehaviorPipeline Implements IBehaviorPipelineFactory.CreatePipeline + If behaviors.Count = 0 Then + Return fallback.CreatePipeline(Of TStunt)() + End If + + Return New BehaviorPipeline(behaviors) + End Function + End Class + End Class + + ''' + ''' Builds stunts that all share the same set of , which are + ''' applied from the very beginning of the stunt instantiation, including any virtual + ''' members invoked from a base class constructor. + ''' + ''' + ''' Behaviors are configured with the same AddBehavior/InsertBehavior + ''' extension methods available for any , and each Build + ''' call creates the stunt with a pipeline initialized with a snapshot of the + ''' behaviors configured at that point, via . + ''' + + + Partial Friend Class StuntBuilder + Implements IStunt + + ''' + ''' Behaviors applied to every stunt created by this builder. + ''' + Public ReadOnly Property Behaviors As IList(Of IStuntBehavior) Implements IStunt.Behaviors + Get + Return behaviorList + End Get + End Property + + Private ReadOnly behaviorList As IList(Of IStuntBehavior) = New List(Of IStuntBehavior) + + Private Function Create(Of T)(ByVal constructorArgs As Object(), ParamArray interfaces As Type()) As T + Using BehaviorPipelineFactory.UseAmbient(New BuilderPipelineFactory(behaviorList)) + Return DirectCast(StuntFactory.[Default].CreateStunt(GetType(Stunt).Assembly, GetType(T), interfaces, constructorArgs), T) + End Using + End Function + + ''' + ''' Creates a stunt that inherits or implements the type . + ''' + + Public Function Build(Of T)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs) + End Function + + ''' + ''' Creates a stunt for the delegate that forwards to + ''' . The builder behaviors run first; + ''' next invokes . + ''' + ''' + ''' Unlike C#, Visual Basic cannot constrain to be a delegate, + ''' so this overload is also a candidate for a single constructor argument of type + ''' , which behaves identically. + ''' + + Public Function Build(Of T)(ByVal implementation As T) As T + If implementation Is Nothing Then + Throw New ArgumentNullException(NameOf(implementation)) + End If + + Return Create(Of T)(New Object() {implementation}) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements . + ''' + + Public Function Build(Of T, T1)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements and . + ''' + + Public Function Build(Of T, T1, T2)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , and + ''' . + ''' + + Public Function Build(Of T, T1, T2, T3)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' and . + ''' + + Public Function Build(Of T, T1, T2, T3, T4)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , and . + ''' + + Public Function Build(Of T, T1, T2, T3, T4, T5)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and . + ''' + + Public Function Build(Of T, T1, T2, T3, T4, T5, T6)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and . + ''' + + Public Function Build(Of T, T1, T2, T3, T4, T5, T6, T7)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7)) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' , and . + ''' + + Public Function Build(Of T, T1, T2, T3, T4, T5, T6, T7, T8)(ParamArray constructorArgs As Object()) As T + Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7), GetType(T8)) + End Function + + Private Class BuilderPipelineFactory + Implements IBehaviorPipelineFactory + + Private ReadOnly behaviors As IEnumerable(Of IStuntBehavior) + + Public Sub New(ByVal behaviors As IEnumerable(Of IStuntBehavior)) + Me.behaviors = behaviors + End Sub + + ' The pipeline copies the behaviors, so later builder changes don't affect created stunts. + Public Function CreatePipeline(Of TStunt)() As BehaviorPipeline Implements IBehaviorPipelineFactory.CreatePipeline + Return New BehaviorPipeline(behaviors) + End Function + End Class + End Class + + ''' + ''' Instantiates stunts for the specified types. + ''' Partial Friend Class Stunt @@ -11,49 +315,251 @@ Namespace Global.Stunts Return DirectCast(StuntFactory.[Default].CreateStunt(GetType(Stunt).Assembly, GetType(T), interfaces, constructorArgs), T) End Function + Private Shared Function Defer(Of TResult)(ByVal factory As Func(Of TResult)) As StuntReference(Of TResult) + Return New StuntReference(Of TResult)(New Lazy(Of TResult)(factory)) + End Function + + ''' + ''' Creates a that applies the same behaviors to every + ''' stunt it builds, from the moment of their instantiation. + ''' + Public Shared Function Builder() As StuntBuilder + Return New StuntBuilder() + End Function + + ''' + ''' Gets a reference to an existing stunt, so behaviors can be added to it after the fact. + ''' + ''' An existing stunt, typically created via one of the Of overloads. + ''' Thrown when the value does not implement . + Public Shared Function [Get](Of T)(ByVal stunt As T) As StuntReference(Of T) + Return New StuntReference(Of T)(stunt) + End Function + + ''' + ''' Creates a stunt that inherits or implements the type . + ''' Public Shared Function [Of](Of T)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs) End Function + ''' + ''' Creates a stunt for the delegate that forwards to + ''' . Behaviors added afterwards run first; + ''' next invokes . + ''' + ''' + ''' Unlike C#, Visual Basic cannot constrain to be a delegate, + ''' so this overload is also a candidate for a single constructor argument of type + ''' , which behaves identically. + ''' + + Public Shared Function [Of](Of T)(ByVal implementation As T) As T + If implementation Is Nothing Then + Throw New ArgumentNullException(NameOf(implementation)) + End If + + Return Create(Of T)(New Object() {implementation}) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements . + ''' Public Shared Function [Of](Of T, T1)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements and . + ''' Public Shared Function [Of](Of T, T1, T2)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , and + ''' . + ''' Public Shared Function [Of](Of T, T1, T2, T3)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' and . + ''' Public Shared Function [Of](Of T, T1, T2, T3, T4)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , and . + ''' Public Shared Function [Of](Of T, T1, T2, T3, T4, T5)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and . + ''' Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and . + ''' Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7)) End Function + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' , and . + ''' Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7, T8)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7), GetType(T8)) End Function + + ''' + ''' Creates a stunt that inherits or implements the type , + ''' returning a reference to it that allows configuring its behaviors. + ''' The instance is constructed on the first call. + ''' + + Public Shared Function [For](Of T)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs)) + End Function + + ''' + ''' Creates a stunt for the delegate that forwards to + ''' , returning a reference to it that allows + ''' configuring its behaviors. Behaviors added afterwards run first; + ''' next invokes . + ''' + ''' + ''' Unlike C#, Visual Basic cannot constrain to be a delegate, + ''' so this overload is also a candidate for a single constructor argument of type + ''' , which behaves identically. + ''' + + Public Shared Function [For](Of T)(ByVal implementation As T) As StuntReference(Of T) + If implementation Is Nothing Then + Throw New ArgumentNullException(NameOf(implementation)) + End If + + Return Defer(Function() Create(Of T)(New Object() {implementation})) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , returning a reference + ''' to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements and , + ''' returning a reference to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , and + ''' , returning a reference to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' and , returning a reference + ''' to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3, T4)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , and , + ''' returning a reference to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3, T4, T5)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and , returning a reference to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3, T4, T5, T6)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' and , returning a reference + ''' to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3, T4, T5, T6, T7)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7))) + End Function + + ''' + ''' Creates a stunt that inherits or implements and + ''' additionally implements , , + ''' , , + ''' , and , + ''' returning a reference to it that allows configuring its behaviors. + ''' + + Public Shared Function [For](Of T, T1, T2, T3, T4, T5, T6, T7, T8)(ParamArray constructorArgs As Object()) As StuntReference(Of T) + Return Defer(Function() Create(Of T)(constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7), GetType(T8))) + End Function End Class -End Namespace \ No newline at end of file +End Namespace + diff --git a/src/Stunts.UnitTests/Scenarios/StuntBuilder.cs b/src/Stunts.UnitTests/Scenarios/StuntBuilder.cs new file mode 100644 index 00000000..1a917ca2 --- /dev/null +++ b/src/Stunts.UnitTests/Scenarios/StuntBuilder.cs @@ -0,0 +1,123 @@ +#pragma warning disable IDE0079 +#pragma warning disable CS0436 +using System; +using Xunit; + +namespace Stunts.Scenarios.Builders +{ + public interface ICalculator + { + int Add(int x, int y); + } + + public class Greeter + { + public Greeter() => Seen = Name(); + + public string Seen { get; } + + public virtual string Name() => "base"; + } + + public delegate int Adder(int x, int y); + + /// + /// A builder applies the same behaviors to every stunt it builds, from + /// the very moment of their instantiation. + /// + public class Test : IRunnable + { + public void Run() + { + BehaviorsApplyToBuiltStunts(); + BehaviorsApplyDuringConstruction(); + BehaviorsAreSharedAcrossBuilds(); + BehaviorsAreSnapshotAtBuildTime(); + BuildsAdditionalInterfaces(); + BuildsDelegates(); + } + + public void BehaviorsApplyToBuiltStunts() + { + var builder = Stunt.Builder(); + + // The behavior extension methods preserve the builder type, so they can be chained. + StuntBuilder chained = builder.AddBehavior(new DefaultValueBehavior()); + + Assert.Same(builder, chained); + + ICalculator calculator = builder.Build(); + + // No post-creation configuration was needed for the behavior to apply. + Assert.Equal(0, calculator.Add(1, 2)); + Assert.Single(Stunt.Get(calculator).Behaviors); + } + + public void BehaviorsApplyDuringConstruction() + { + var builder = Stunt.Builder() + .AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Greeter.Name) + ? invocation.CreateValueReturn("proxy") + : next(invocation, next)); + + Greeter greeter = builder.Build(); + + // Something Stunt.Of cannot do, since the behavior would be added after the fact. + Assert.Equal("proxy", greeter.Seen); + } + + public void BehaviorsAreSharedAcrossBuilds() + { + var recorder = new RecordingBehavior(); + var builder = Stunt.Builder() + .AddBehavior(recorder) + .AddBehavior(new DefaultValueBehavior()); + + ICalculator first = builder.Build(); + ICalculator second = builder.Build(); + + first.Add(1, 2); + second.Add(3, 4); + + Assert.NotSame(first, second); + // Two constructor invocations plus the two Add calls. + Assert.Equal(4, recorder.Invocations.Count); + } + + public void BehaviorsAreSnapshotAtBuildTime() + { + var builder = Stunt.Builder().AddBehavior(new DefaultValueBehavior()); + + ICalculator first = builder.Build(); + + builder.InsertBehavior(0, (invocation, next) => invocation.CreateValueReturn(42)); + + ICalculator second = builder.Build(); + + Assert.Equal(2, builder.Behaviors.Count); + Assert.Equal(0, first.Add(1, 2)); + Assert.Equal(42, second.Add(1, 2)); + } + + public void BuildsAdditionalInterfaces() + { + ICalculator calculator = Stunt.Builder() + .AddBehavior(new DefaultValueBehavior()) + .Build(); + + Assert.IsAssignableFrom(calculator); + Assert.Equal(0, calculator.Add(1, 2)); + } + + public void BuildsDelegates() + { + var recorder = new RecordingBehavior(); + Adder adder = Stunt.Builder() + .AddBehavior(recorder) + .Build((x, y) => x + y); + + Assert.Equal(3, adder(1, 2)); + Assert.Single(recorder.Invocations); + } + } +} From f0732c0ea1d04b323632180926329a4eb5252f04 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Wed, 30 Sep 2026 19:04:29 -0300 Subject: [PATCH 2/2] Update tests so method behaviors skip the constructor invocation. --- src/Stunts.UnitTests/Castle/AsyncTests.cs | 54 +++++ src/Stunts.UnitTests/Castle/AttributeTests.cs | 19 +- .../Castle/ClassProxyTests.cs | 91 +++++--- .../Castle/CompositionTests.cs | 132 +++++++++++ src/Stunts.UnitTests/Castle/GenericsTests.cs | 28 +-- .../Castle/InterfaceProxyTests.cs | 214 ++++++++++++++++++ src/Stunts.UnitTests/Castle/MembersTests.cs | 32 +-- .../Castle/ParametersTests.cs | 136 +++++++++++ src/Stunts.UnitTests/Castle/PipelineTests.cs | 149 ++++++++++++ .../DynamicStuntFactoryTests.cs | 19 +- .../Scenarios/OverrideObject.cs | 9 +- .../Scenarios/SignatureRefs.cs | 39 ++-- .../Scenarios/ValueTypeNull.cs | 27 ++- 13 files changed, 835 insertions(+), 114 deletions(-) create mode 100644 src/Stunts.UnitTests/Castle/AsyncTests.cs create mode 100644 src/Stunts.UnitTests/Castle/CompositionTests.cs create mode 100644 src/Stunts.UnitTests/Castle/InterfaceProxyTests.cs create mode 100644 src/Stunts.UnitTests/Castle/ParametersTests.cs create mode 100644 src/Stunts.UnitTests/Castle/PipelineTests.cs diff --git a/src/Stunts.UnitTests/Castle/AsyncTests.cs b/src/Stunts.UnitTests/Castle/AsyncTests.cs new file mode 100644 index 00000000..825a5cce --- /dev/null +++ b/src/Stunts.UnitTests/Castle/AsyncTests.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Stunts.UnitTests.Castle +{ + /// + /// Castle async interception: the behavior returns a task, and can compose + /// the task produced by proceed. + /// + public class AsyncTests : IRunnable + { + public void Run() + { + BehaviorCanReturnATask().GetAwaiter().GetResult(); + BehaviorCanComposeTheProceededTask().GetAwaiter().GetResult(); + } + + public async Task BehaviorCanReturnATask() + { + IService stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.MethodBase.Name == nameof(IService.GetAsync) + ? invocation.CreateValueReturn(Task.FromResult(7)) + : invocation.CreateValueReturn(Task.CompletedTask)).ToObject(); + + await stunt.DoAsync(); + + Assert.Equal(7, await stunt.GetAsync()); + } + + public async Task BehaviorCanComposeTheProceededTask() + { + Service stunt = Stunt.For().AddBehavior((invocation, next) => + { + var proceeded = next(invocation, next); + var task = (Task)proceeded.ReturnValue; + return invocation.CreateValueReturn(task.ContinueWith(completed => completed.Result + 1)); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal(4, await stunt.GetAsync()); + } + + public interface IService + { + Task DoAsync(); + + Task GetAsync(); + } + + public class Service + { + public virtual Task GetAsync() => Task.FromResult(3); + } + } +} diff --git a/src/Stunts.UnitTests/Castle/AttributeTests.cs b/src/Stunts.UnitTests/Castle/AttributeTests.cs index 860db1e0..c957a35e 100644 --- a/src/Stunts.UnitTests/Castle/AttributeTests.cs +++ b/src/Stunts.UnitTests/Castle/AttributeTests.cs @@ -22,7 +22,7 @@ public void Run() public void ClassAttributesAndMembers() { - var stunt = Stunt.Of(4); + Marked stunt = Stunt.Of(4); var type = stunt.GetType(); Assert.NotNull(type.GetCustomAttribute()); @@ -82,7 +82,7 @@ public void OmittedArgumentsArriveAsTheDefault() int number = 0; decimal amount = 0; string label = null; - var marked = Stunt.Of(0).AddBehavior((invocation, next) => + Marked marked = Stunt.For(0).AddBehavior((invocation, next) => { switch (invocation.MethodBase.Name) { @@ -98,7 +98,7 @@ public void OmittedArgumentsArriveAsTheDefault() } return next(invocation, next); - }); + }).ToObject(); Assert.Equal(5, marked.Defaulted()); Assert.Equal(5, number); @@ -112,12 +112,12 @@ public void OmittedArgumentsArriveAsTheDefault() int echo = 0; string text = null; - var optional = Stunt.Of().AddBehavior((invocation, next) => + IOptional optional = Stunt.For().AddBehavior((invocation, next) => { echo = invocation.Arguments.Get("value"); text = invocation.Arguments.Get("text"); return invocation.CreateValueReturn(echo + text); - }); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); Assert.Equal("7hi", optional.Echo()); Assert.Equal(7, echo); @@ -129,15 +129,16 @@ public void OmittedArgumentsArriveAsTheDefault() public void ReflectionInvokeUsesTheCopiedDefault() { - var marked = Stunt.Of(0); + Marked marked = Stunt.Of(0); var markedType = marked.GetType(); Assert.Equal(5, InvokeOptional(markedType.GetMethod(nameof(Marked.Defaulted)), marked, Type.Missing)); Assert.Equal(1.5m, InvokeOptional(markedType.GetMethod(nameof(Marked.Price)), marked, Type.Missing)); Assert.Equal("none", InvokeOptional(markedType.GetMethod(nameof(Marked.OptionalName)), marked, Type.Missing)); - var optional = Stunt.Of().AddBehavior((invocation, next) => - invocation.CreateValueReturn(invocation.Arguments.Get("value") + invocation.Arguments.Get("text"))); + IOptional optional = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn(invocation.Arguments.Get("value") + invocation.Arguments.Get("text")), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); var echo = optional.GetType().GetMethod(nameof(IOptional.Echo)); Assert.Equal("7hi", InvokeOptional(echo, optional, Type.Missing, Type.Missing)); @@ -154,7 +155,7 @@ static object InvokeOptional(MethodInfo method, object instance, params object[] public void InterfaceGuidAndParameterAttribute() { - var stunt = Stunt.Of(); + IMarked stunt = Stunt.Of(); var type = stunt.GetType(); Assert.Equal(new Guid("11111111-1111-1111-1111-111111111111"), type.GUID); diff --git a/src/Stunts.UnitTests/Castle/ClassProxyTests.cs b/src/Stunts.UnitTests/Castle/ClassProxyTests.cs index 04e156d0..7374e8c0 100644 --- a/src/Stunts.UnitTests/Castle/ClassProxyTests.cs +++ b/src/Stunts.UnitTests/Castle/ClassProxyTests.cs @@ -24,6 +24,7 @@ public void Run() InternalConstructorInTheSameAssemblyIsReachable(); ParamsConstructorAcceptsAnArray(); VirtualCallDuringConstructionUsesThePipelineFactory(); + BehaviorsConfiguredBeforeToObjectApplyDuringConstruction(); NestedClassAndCharReturn(); InternalClassInTheSameAssembly(); ParameterNamesAreReplicated(); @@ -33,21 +34,22 @@ public void Run() public void VirtualCallProceedsToBase() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + Counter counter = stunt.ToObject(); - Assert.Equal(1, stunt.Next()); + Assert.Equal(1, counter.Next()); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Counter.Next) ? invocation.CreateValueReturn(40) : next(invocation, next)); - Assert.Equal(40, stunt.Next()); + Assert.Equal(40, counter.Next()); } public void MostDerivedOverrideIsTheImplementation() { - var stunt = Stunt.Of(); + DerivedCounter stunt = Stunt.Of(); Assert.Equal(2, stunt.Next()); } @@ -55,11 +57,11 @@ public void MostDerivedOverrideIsTheImplementation() public void NonVirtualMembersAreNotIntercepted() { var calls = 0; - var stunt = Stunt.Of().AddBehavior((invocation, next) => + Counter stunt = Stunt.For().AddBehavior((invocation, next) => { calls++; return next(invocation, next); - }); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); Assert.Equal(7, stunt.Fixed()); Assert.Equal(0, calls); @@ -67,74 +69,75 @@ public void NonVirtualMembersAreNotIntercepted() public void SelfCallHitsTheOverride() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + Counter stunt = Stunt.For().AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Counter.Next) ? invocation.CreateValueReturn(9) - : next(invocation, next)); + : next(invocation, next)).ToObject(); Assert.Equal(9, stunt.ThroughSelf()); } public void AbstractMemberThrowsUntilABehaviorReturns() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + AbstractCounter counter = stunt.ToObject(); - Assert.Throws(() => stunt.Next()); + Assert.Throws(() => counter.Next()); stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn(4)); - Assert.Equal(4, stunt.Next()); + Assert.Equal(4, counter.Next()); } public void ProceedOnAbstractMemberThrows() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => next(invocation, next)); + AbstractCounter stunt = Stunt.For().AddBehavior((invocation, next) => next(invocation, next)).ToObject(); Assert.Throws(() => stunt.Next()); } public void ProtectedVirtualIsIntercepted() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + Counter stunt = Stunt.For().AddBehavior((invocation, next) => invocation.MethodBase.Name == "Secret" ? invocation.CreateValueReturn(11) - : next(invocation, next)); + : next(invocation, next)).ToObject(); Assert.Equal(11, stunt.CallSecret()); } public void ConstructorArgumentsReachTheBase() { - var named = Stunt.Of("Ada"); + Named named = Stunt.Of("Ada"); Assert.Equal("Ada", named.Name); - var disabled = Stunt.Of("Ada", false); + Named disabled = Stunt.Of("Ada", false); Assert.Equal("Ada", disabled.Name); Assert.False(disabled.Enabled); - var missing = Stunt.Of(new object[] { null }); + Named missing = Stunt.Of(new object[] { null }); Assert.Null(missing.Name); } public void ProtectedConstructorIsReachable() { - var stunt = Stunt.Of(5); + ProtectedCtor stunt = Stunt.Of(5); Assert.Equal(5, stunt.Value); } public void InternalConstructorInTheSameAssemblyIsReachable() { - var stunt = Stunt.Of(); + InternalCtor stunt = Stunt.Of(); Assert.True(stunt.Created); } public void ParamsConstructorAcceptsAnArray() { - var empty = Stunt.Of(); - var filled = Stunt.Of(new int[] { 1, 2, 3 }); - var head = Stunt.Of(5); + ParamsCtor empty = Stunt.Of(); + ParamsCtor filled = Stunt.Of(new int[] { 1, 2, 3 }); + IntAndParams head = Stunt.Of(5); Assert.Equal(0, empty.Count); Assert.Equal(3, filled.Count); @@ -145,40 +148,54 @@ public void ParamsConstructorAcceptsAnArray() public void VirtualCallDuringConstructionUsesThePipelineFactory() { using var ambient = BehaviorPipelineFactory.UseAmbient(new CtorBehaviorFactory()); - var stunt = Stunt.Of(); + CtorCaller stunt = Stunt.Of(); Assert.Equal("proxy", stunt.Seen); } + public void BehaviorsConfiguredBeforeToObjectApplyDuringConstruction() + { + var stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.MethodBase.Name == nameof(CtorCaller.Name) + ? invocation.CreateValueReturn("proxy") + : next(invocation, next)); + + CtorCaller created = stunt.ToObject(); + + Assert.Equal("proxy", created.Seen); + Assert.Equal("proxy", created.Name()); + } + public void NestedClassAndCharReturn() { - var stunt = Stunt.Of(); + Outer.Inner stunt = Stunt.Of(); Assert.Equal('a', stunt.Letter()); } public void InternalClassInTheSameAssembly() { - var stunt = Stunt.Of(); + InternalCounter stunt = Stunt.Of(); Assert.Equal(1, stunt.Next()); } public void InternalAndPrivateProtectedMembersAreIntercepted() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + Gate gate = stunt.ToObject(); - Assert.True(stunt.Opened); - Assert.Equal(1, stunt.Next()); - Assert.Equal(2, stunt.CallSecret()); + Assert.True(gate.Opened); + Assert.Equal(1, gate.Next()); + Assert.Equal(2, gate.CallSecret()); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == "Secret" ? invocation.CreateValueReturn(9) : next(invocation, next)); - Assert.Equal(9, stunt.CallSecret()); - Assert.Equal(1, stunt.Next()); + Assert.Equal(9, gate.CallSecret()); + Assert.Equal(1, gate.Next()); } public void PrivateNestedTypeIsProxiedFromInsideItsContainer() @@ -188,7 +205,8 @@ public void PrivateNestedTypeIsProxiedFromInsideItsContainer() public void ParameterNamesAreReplicated() { - var method = Stunt.Of().GetType().GetMethod(nameof(Counter.Add)); + Counter stunt = Stunt.Of(); + var method = stunt.GetType().GetMethod(nameof(Counter.Add)); var parameters = method.GetParameters(); Assert.Equal("left", parameters[0].Name); @@ -316,17 +334,18 @@ public partial class Shelter { public static void Run() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + Hidden hidden = stunt.ToObject(); - Assert.Equal(1, stunt.Next()); - Assert.Equal(3, stunt.CallSecret()); + Assert.Equal(1, hidden.Next()); + Assert.Equal(3, hidden.CallSecret()); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == "Secret" ? invocation.CreateValueReturn(9) : next(invocation, next)); - Assert.Equal(9, stunt.CallSecret()); + Assert.Equal(9, hidden.CallSecret()); } class Hidden diff --git a/src/Stunts.UnitTests/Castle/CompositionTests.cs b/src/Stunts.UnitTests/Castle/CompositionTests.cs new file mode 100644 index 00000000..41d19fcf --- /dev/null +++ b/src/Stunts.UnitTests/Castle/CompositionTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Stunts.UnitTests.Castle +{ + /// + /// Castle composition proxies (interface with target, swappable target, + /// class proxy with target, mixins) expressed as behaviors. + /// + public class CompositionTests : IRunnable + { + public void Run() + { + InterfaceProxyForwardsToATarget(); + TargetCanBeSwappedForTheSameProxy(); + ClassProxyForwardsVirtualCallsAndKeepsItsOwnNonVirtualState(); + MixinForwardsEachInterfaceToItsOwnInstance(); + } + + public void InterfaceProxyForwardsToATarget() + { + var target = new Adder(); + ICalculator stunt = Stunt.For().AddBehavior( + Forward(target), invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal(5, stunt.Add(2, 3)); + Assert.NotSame(target, stunt); + } + + public void TargetCanBeSwappedForTheSameProxy() + { + ICalculator current = new Adder(); + ICalculator stunt = Stunt.For().AddBehavior( + (invocation, next) => Forward(current)(invocation, next), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal(5, stunt.Add(2, 3)); + + current = new Multiplier(); + + Assert.Equal(6, stunt.Add(2, 3)); + } + + public void ClassProxyForwardsVirtualCallsAndKeepsItsOwnNonVirtualState() + { + var target = new Store { Stored = 7 }; + Store stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.MethodBase.Name == nameof(Store.Read) + ? invocation.CreateValueReturn(target.Read()) + : next(invocation, next)).ToObject(); + + Assert.Equal(7, stunt.Read()); + Assert.Equal(0, stunt.ReadFixed()); + } + + public void MixinForwardsEachInterfaceToItsOwnInstance() + { + var name = new NameMixin(); + var age = new AgeMixin(); + IName stunt = Stunt.For().AddBehavior((invocation, next) => + { + var target = invocation.MethodBase.Name == "get_" + nameof(IName.Name) ? (object)name : age; + return invocation.CreateValueReturn(Invoke(target, invocation)); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal("Ada", stunt.Name); + Assert.Equal(36, ((IAge)stunt).Age); + } + + static ExecuteHandler Forward(object target) + => (invocation, next) => invocation.CreateValueReturn(Invoke(target, invocation)); + + static object Invoke(object target, IMethodInvocation invocation) + { + var parameters = invocation.MethodBase.GetParameters(); + var args = parameters.Select((parameter, index) => invocation.Arguments.GetValue(index)).ToArray(); + var method = target.GetType().GetMethod( + invocation.MethodBase.Name, + BindingFlags.Instance | BindingFlags.Public, + null, + parameters.Select(parameter => parameter.ParameterType).ToArray(), + null); + return method.Invoke(target, args); + } + + public interface ICalculator + { + int Add(int x, int y); + } + + public class Adder : ICalculator + { + public int Add(int x, int y) => x + y; + } + + public class Multiplier : ICalculator + { + public int Add(int x, int y) => x * y; + } + + public class Store + { + public int Stored { get; set; } + + public virtual int Read() => Stored; + + public int ReadFixed() => Stored; + } + + public interface IName + { + string Name { get; } + } + + public interface IAge + { + int Age { get; } + } + + public class NameMixin : IName + { + public string Name => "Ada"; + } + + public class AgeMixin : IAge + { + public int Age => 36; + } + } +} diff --git a/src/Stunts.UnitTests/Castle/GenericsTests.cs b/src/Stunts.UnitTests/Castle/GenericsTests.cs index 41d74fe0..7d115b97 100644 --- a/src/Stunts.UnitTests/Castle/GenericsTests.cs +++ b/src/Stunts.UnitTests/Castle/GenericsTests.cs @@ -23,7 +23,7 @@ public void Run() public void GenericInterfaceMethodSeesTheConstructedMethod() { - var stunt = Stunt.Of>().AddBehavior((invocation, next) => + IBox stunt = Stunt.For>().AddBehavior((invocation, next) => { var method = (MethodInfo)invocation.MethodBase; @@ -32,32 +32,34 @@ public void GenericInterfaceMethodSeesTheConstructedMethod() Assert.Equal(typeof(int), method.GetParameters()[0].ParameterType); return invocation.CreateValueReturn(invocation.Arguments.Get("value") * 2); - }); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); Assert.Equal(42, stunt.Echo(21)); } public void GenericClassMethodProceedsToBase() { - var stunt = Stunt.Of>("Ada"); + var stunt = Stunt.For>("Ada"); + Box box = stunt.ToObject(); - Assert.Equal("Bea", stunt.Echo("Bea")); - Assert.Null(stunt.Echo(null!)); + Assert.Equal("Bea", box.Echo("Bea")); + Assert.Null(box.Echo(null!)); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Box.Echo) ? invocation.CreateValueReturn("proxy") : next(invocation, next)); - Assert.Equal("proxy", stunt.Echo("Bea")); + Assert.Equal("proxy", box.Echo("Bea")); } public void GenericMethodWithInAndOutParameters() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + IGenericParameters stunt = Stunt.For().AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(IGenericParameters.Read) ? invocation.CreateValueReturn(invocation.Arguments.Get("value").X) - : invocation.CreateValueReturn(true, "Ada")); + : invocation.CreateValueReturn(true, "Ada"), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); var point = new Point(6); @@ -68,17 +70,17 @@ public void GenericMethodWithInAndOutParameters() public void ClosedGenericInterface() { - var stunt = Stunt.Of>().AddBehavior((invocation, next) => + IBox stunt = Stunt.For>().AddBehavior((invocation, next) => invocation.MethodBase.Name.StartsWith("set_", StringComparison.Ordinal) ? invocation.CreateReturn() - : invocation.CreateValueReturn("Ada")); + : invocation.CreateValueReturn("Ada")).ToObject(); Assert.Equal("Ada", stunt.Value); } public void ClosedGenericClassUsesTheSuppliedConstructor() { - var stunt = Stunt.Of>("Ada"); + Box stunt = Stunt.Of>("Ada"); Assert.Equal("Ada", stunt.Value); Assert.Equal("Ada!", stunt.Decorate("!")); @@ -86,14 +88,14 @@ public void ClosedGenericClassUsesTheSuppliedConstructor() public void ClassInheritingAnOpenGenericCanBeProxiedClosed() { - var stunt = Stunt.Of(); + StringBox stunt = Stunt.Of(); Assert.Equal("base", stunt.Value); } public void GenericConstraintsAreGenerated() { - var stunt = Stunt.Of().AddBehavior(new DefaultValueBehavior()); + IConstraints stunt = Stunt.For().AddBehavior(new DefaultValueBehavior()).ToObject(); stunt.ClassAndNew(); stunt.Struct(); diff --git a/src/Stunts.UnitTests/Castle/InterfaceProxyTests.cs b/src/Stunts.UnitTests/Castle/InterfaceProxyTests.cs new file mode 100644 index 00000000..d08e38a1 --- /dev/null +++ b/src/Stunts.UnitTests/Castle/InterfaceProxyTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using Xunit; + +namespace Stunts.UnitTests.Castle +{ + /// + /// Castle interface proxies without a target, plus additional interfaces + /// and a class used as the base type of an interface proxy. + /// + public class InterfaceProxyTests : IRunnable + { + public void Run() + { + InterfaceWithoutTargetThrowsUntilABehaviorReturns(); + InheritedInterfaceMembersAreImplemented(); + AdditionalInterfacesAreImplemented(); + ClassCanBeTheBaseOfAdditionalInterfaces(); + IdenticalSignaturesShareOneImplementation(); + SameGenericInterfaceTwiceUsesAnExplicitImplementation(); + CovariantInterfaceMembersAreBothCallable(); + EmptyGenericMarkerInterface(); + NonVirtualInterfaceImplementationIsNotReimplemented(); + VirtualInterfaceImplementationIsIntercepted(); + } + + public void InterfaceWithoutTargetThrowsUntilABehaviorReturns() + { + var stunt = Stunt.For(); + ICalculator calculator = stunt.ToObject(); + + Assert.IsAssignableFrom(calculator); + Assert.Throws(() => calculator.Add(1, 2)); + + stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn(3)); + + Assert.Equal(3, calculator.Add(1, 2)); + } + + public void InheritedInterfaceMembersAreImplemented() + { + var seen = new List(); + IDerived stunt = Stunt.For().AddBehavior((invocation, next) => + { + seen.Add(invocation.MethodBase.Name); + return invocation.CreateReturn(); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + stunt.Base(); + stunt.Derived(); + + Assert.Equal(new[] { nameof(IBase.Base), nameof(IDerived.Derived) }, seen); + } + + public void AdditionalInterfacesAreImplemented() + { + ICalculator stunt = Stunt.Of(); + + Assert.IsAssignableFrom(stunt); + Assert.IsAssignableFrom(stunt); + } + + public void ClassCanBeTheBaseOfAdditionalInterfaces() + { + var stunt = Stunt.For(); + Calculator calculator = stunt.ToObject(); + + Assert.IsAssignableFrom(calculator); + Assert.IsAssignableFrom(calculator); + Assert.Equal(3, calculator.Add(1, 2)); + + stunt.AddBehavior(new DefaultValueBehavior()); + ((IDisposable)calculator).Dispose(); + } + + public void IdenticalSignaturesShareOneImplementation() + { + var calls = 0; + ILeft stunt = Stunt.For().AddBehavior((invocation, next) => + { + calls++; + return invocation.CreateReturn(); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + ((ILeft)stunt).Run(); + ((IRight)stunt).Run(); + + Assert.Equal(2, calls); + } + + public void SameGenericInterfaceTwiceUsesAnExplicitImplementation() + { + var values = new Dictionary(); + ISlot stunt = Stunt.For, ISlot>().AddBehavior((invocation, next) => + { + var method = (MethodInfo)invocation.MethodBase; + var slot = Slot(method); + if (invocation.MethodBase.Name.Contains("set_")) + { + values[slot] = invocation.Arguments.GetValue(0); + return invocation.CreateReturn(); + } + + values.TryGetValue(slot, out var value); + return invocation.CreateValueReturn(value); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + ((ISlot)stunt).Value = 4; + ((ISlot)stunt).Value = "four"; + + Assert.Equal(4, ((ISlot)stunt).Value); + Assert.Equal("four", ((ISlot)stunt).Value); + } + + static string Slot(MethodBase method) + { + var name = method.Name; + var dot = name.LastIndexOf('.'); + if (dot >= 0) + return name.Substring(0, dot); + + var info = (MethodInfo)method; + var type = info.ReturnType == typeof(void) + ? info.GetParameters()[0].ParameterType + : info.ReturnType; + return type.FullName; + } + + public void CovariantInterfaceMembersAreBothCallable() + { + IEnumerable stunt = Stunt.For>().AddBehavior(new DefaultValueBehavior()).ToObject(); + + Assert.Null(stunt.GetEnumerator()); + Assert.Null(((IEnumerable)stunt).GetEnumerator()); + } + + public void EmptyGenericMarkerInterface() + { + IMarker stunt = Stunt.Of>(); + + Assert.IsAssignableFrom>(stunt); + } + + public void NonVirtualInterfaceImplementationIsNotReimplemented() + { + var calls = 0; + SealedImpl stunt = Stunt.For().AddBehavior((invocation, next) => + { + calls++; + return next(invocation, next); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal(5, ((ICalculator)stunt).Add(2, 3)); + Assert.Equal(0, calls); + } + + public void VirtualInterfaceImplementationIsIntercepted() + { + VirtualImpl stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn(8)).ToObject(); + + Assert.Equal(8, ((ICalculator)stunt).Add(2, 3)); + } + + public interface ICalculator + { + int Add(int x, int y); + } + + public class Calculator : ICalculator + { + public virtual int Add(int x, int y) => x + y; + } + + public interface IBase + { + void Base(); + } + + public interface IDerived : IBase + { + void Derived(); + } + + public interface ILeft + { + void Run(); + } + + public interface IRight + { + void Run(); + } + + public interface ISlot + { + T Value { get; set; } + } + + public interface IMarker { } + + public class SealedImpl : ICalculator + { + public int Add(int x, int y) => x + y; + } + + public class VirtualImpl : ICalculator + { + public virtual int Add(int x, int y) => x + y; + } + } +} diff --git a/src/Stunts.UnitTests/Castle/MembersTests.cs b/src/Stunts.UnitTests/Castle/MembersTests.cs index e8fb2011..757abeef 100644 --- a/src/Stunts.UnitTests/Castle/MembersTests.cs +++ b/src/Stunts.UnitTests/Castle/MembersTests.cs @@ -23,7 +23,7 @@ public void Run() public void PropertyRoundTripOnAClass() { - var stunt = Stunt.Of(); + Switch stunt = Stunt.Of(); stunt.Name = "on"; @@ -33,7 +33,7 @@ public void PropertyRoundTripOnAClass() public void PropertyCanBeSuppliedByABehavior() { string stored = null; - var stunt = Stunt.Of().AddBehavior((invocation, next) => + INamed stunt = Stunt.For().AddBehavior((invocation, next) => { if (invocation.MethodBase.Name.StartsWith("set_", StringComparison.Ordinal)) { @@ -42,7 +42,7 @@ public void PropertyCanBeSuppliedByABehavior() } return invocation.CreateValueReturn(stored); - }); + }).ToObject(); stunt.Name = "Ada"; @@ -52,11 +52,11 @@ public void PropertyCanBeSuppliedByABehavior() public void SetterOnlyProperty() { var seen = 0; - var stunt = Stunt.Of().AddBehavior((invocation, next) => + IWriteOnly stunt = Stunt.For().AddBehavior((invocation, next) => { seen = (int)invocation.Arguments.GetValue(0); return invocation.CreateReturn(); - }); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); stunt.Value = 12; @@ -65,7 +65,7 @@ public void SetterOnlyProperty() public void IndexerRoundTrip() { - var stunt = Stunt.Of(); + Bag stunt = Stunt.Of(); stunt[2] = "two"; @@ -75,7 +75,7 @@ public void IndexerRoundTrip() public void VirtualEventReachesTheBaseField() { - var stunt = Stunt.Of(); + Switch stunt = Stunt.Of(); var raised = false; stunt.Toggled += (_, _) => raised = true; @@ -88,14 +88,14 @@ public void InterfaceEventAddAndRemoveAreIntercepted() { var added = 0; var removed = 0; - var stunt = Stunt.Of().AddBehavior((invocation, next) => + INotify stunt = Stunt.For().AddBehavior((invocation, next) => { if (invocation.MethodBase.Name.StartsWith("add_", StringComparison.Ordinal)) added++; if (invocation.MethodBase.Name.StartsWith("remove_", StringComparison.Ordinal)) removed++; return invocation.CreateReturn(); - }); + }).ToObject(); void Handler(object sender, EventArgs args) { } @@ -109,11 +109,11 @@ void Handler(object sender, EventArgs args) { } public void DifferentlyCasedMembersStayDistinct() { var seen = new List(); - var stunt = Stunt.Of().AddBehavior((invocation, next) => + ICased stunt = Stunt.For().AddBehavior((invocation, next) => { seen.Add(invocation.MethodBase.Name); return invocation.CreateValueReturn(seen.Count); - }); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); Assert.Equal(1, stunt.foo()); Assert.Equal(2, stunt.Foo()); @@ -122,26 +122,26 @@ public void DifferentlyCasedMembersStayDistinct() public void NarrowedAccessorsStayNarrowAndAreIntercepted() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + Levels stunt = Stunt.For().AddBehavior((invocation, next) => invocation.MethodBase.Name == "set_Name" ? invocation.CreateReturn() - : next(invocation, next)); + : next(invocation, next)).ToObject(); stunt.Rename("Ada"); Assert.Equal("base", stunt.Name); - var hidden = Stunt.Of(); + Levels hidden = Stunt.Of(); hidden.Hidden = 4; Assert.Equal(4, hidden.Read()); - var coded = Stunt.Of(); + Levels coded = Stunt.Of(); coded.SetCode("A1"); Assert.Equal("A1", coded.Code); - var slots = Stunt.Of(); + Levels slots = Stunt.Of(); slots.Put(1, "one"); Assert.Equal("one", slots[1]); diff --git a/src/Stunts.UnitTests/Castle/ParametersTests.cs b/src/Stunts.UnitTests/Castle/ParametersTests.cs new file mode 100644 index 00000000..79fa6b15 --- /dev/null +++ b/src/Stunts.UnitTests/Castle/ParametersTests.cs @@ -0,0 +1,136 @@ +using System; +using Xunit; + +namespace Stunts.UnitTests.Castle +{ + /// + /// ref, out, in, params, and array parameters from the Castle suites. + /// + public class ParametersTests : IRunnable + { + public void Run() + { + OutAndRefArgumentsCanBeReplaced(); + RefAndOutReachTheBaseImplementation(); + InParameterIsVisibleToTheBehavior(); + ParamsArrayIsPassedThrough(); + MultidimensionalArrayParameter(); + NullableValueAndOutDecimal(); + } + + public void OutAndRefArgumentsCanBeReplaced() + { + IMath stunt = Stunt.For().AddBehavior((invocation, next) => + { + var left = invocation.Arguments.Get("left"); + var right = invocation.Arguments.Get("right"); + return invocation.CreateValueReturn(true, left + 1, right, (left + right) * 2); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + var left = 2; + var right = 3; + var ok = stunt.Combine(ref left, ref right, out var doubled); + + Assert.True(ok); + Assert.Equal(3, left); + Assert.Equal(3, right); + Assert.Equal(10, doubled); + } + + public void RefAndOutReachTheBaseImplementation() + { + MathClass stunt = Stunt.Of(); + var value = 2; + + Assert.True(stunt.TryDouble(ref value, out var doubled)); + Assert.Equal(4, value); + Assert.Equal(4, doubled); + } + + public void InParameterIsVisibleToTheBehavior() + { + IPoints stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn(invocation.Arguments.Get("point").X), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + var point = new Point(6); + + Assert.Equal(6, stunt.X(in point)); + } + + public void ParamsArrayIsPassedThrough() + { + IMath stunt = Stunt.For().AddBehavior((invocation, next) => + { + var values = invocation.Arguments.Get("values"); + var sum = 0; + foreach (var value in values) + sum += value; + return invocation.CreateValueReturn(sum); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + Assert.Equal(6, stunt.Sum(1, 2, 3)); + } + + public void MultidimensionalArrayParameter() + { + IMath stunt = Stunt.For().AddBehavior((invocation, next) => + { + var cells = invocation.Arguments.Get("cells"); + return invocation.CreateValueReturn(cells[0, 1] + cells[1, 0]); + }, invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + var cells = new int[2, 2]; + cells[0, 1] = 4; + cells[1, 0] = 5; + + Assert.Equal(9, stunt.Sum(cells)); + } + + public void NullableValueAndOutDecimal() + { + IMath stunt = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn(true, (int?)null, 1.5m), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); + + var ok = stunt.TryPrice(out int? units, out var price); + + Assert.True(ok); + Assert.Null(units); + Assert.Equal(1.5m, price); + } + + public interface IMath + { + bool Combine(ref int left, ref int right, out int doubled); + + int Sum(params int[] values); + + int Sum(int[,] cells); + + bool TryPrice(out int? units, out decimal price); + } + + public class MathClass + { + public virtual bool TryDouble(ref int value, out int doubled) + { + value *= 2; + doubled = value; + return true; + } + } + + public readonly struct Point + { + public Point(int x) => X = x; + + public int X { get; } + } + + public interface IPoints + { + int X(in Point point); + } + } +} diff --git a/src/Stunts.UnitTests/Castle/PipelineTests.cs b/src/Stunts.UnitTests/Castle/PipelineTests.cs new file mode 100644 index 00000000..bba39c3d --- /dev/null +++ b/src/Stunts.UnitTests/Castle/PipelineTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Stunts.UnitTests.Castle +{ + /// + /// Castle interceptor pipeline: order, short-circuit, proceed, retry, + /// and per-method selection. + /// + public class PipelineTests : IRunnable + { + public void Run() + { + BehaviorsRunInOrderAroundTheTarget(); + BehaviorCanShortCircuit(); + BehaviorCanProceedTwice(); + AppliesToSelectsBehaviorsPerMethod(); + ExceptionFromTheTargetCanBeCaughtAndReplaced(); + InvocationExposesMethodArgumentsAndTarget(); + } + + public void BehaviorsRunInOrderAroundTheTarget() + { + var log = new List(); + var stunt = Stunt.For(); + stunt.AddBehavior((invocation, next) => + { + log.Add("outer"); + var result = next(invocation, next); + log.Add("outer-done"); + return result; + }, invocation => !invocation.MethodBase.IsConstructor); + stunt.AddBehavior((invocation, next) => + { + log.Add("inner"); + var result = next(invocation, next); + log.Add("inner-done"); + return result; + }, invocation => !invocation.MethodBase.IsConstructor); + + Counter counter = stunt.ToObject(); + + Assert.Equal(1, counter.Next()); + Assert.Equal(new[] { "outer", "inner", "inner-done", "outer-done" }, log); + } + + public void BehaviorCanShortCircuit() + { + var reached = false; + var stunt = Stunt.For().AddBehavior((invocation, next) => invocation.CreateValueReturn(5)); + stunt.AddBehavior((invocation, next) => + { + reached = true; + return next(invocation, next); + }); + + Counter counter = stunt.ToObject(); + + Assert.Equal(5, counter.Next()); + Assert.False(reached); + } + + public void BehaviorCanProceedTwice() + { + var stunt = Stunt.For().AddBehavior((invocation, next) => + { + next(invocation, next); + return next(invocation, next); + }).ToObject(); + + Counter counter = stunt; + + Assert.Equal(2, counter.Next()); + Assert.Equal(2, counter.Count); + } + + public void AppliesToSelectsBehaviorsPerMethod() + { + var stunt = Stunt.For(); + stunt.AddBehavior( + (invocation, next) => invocation.CreateValueReturn(10), + invocation => invocation.MethodBase.Name == nameof(Counter.Next)); + stunt.AddBehavior( + (invocation, next) => invocation.CreateValueReturn(20), + invocation => invocation.MethodBase.Name == nameof(Counter.Other)); + + Counter counter = stunt.ToObject(); + + Assert.Equal(10, counter.Next()); + Assert.Equal(20, counter.Other()); + } + + public void ExceptionFromTheTargetCanBeCaughtAndReplaced() + { + var stunt = Stunt.For().AddBehavior((invocation, next) => + { + try + { + return next(invocation, next); + } + catch (InvalidOperationException exception) + { + return invocation.CreateExceptionReturn(new ApplicationException("wrapped", exception)); + } + }).ToObject(); + + var exception = Assert.Throws(() => ((Counter)stunt).Boom()); + + Assert.IsType(exception.InnerException); + } + + public void InvocationExposesMethodArgumentsAndTarget() + { + IMethodInvocation seen = null; + var stunt = Stunt.For().AddBehavior((invocation, next) => + { + seen = invocation; + return invocation.CreateValueReturn(0); + }).ToObject(); + + Counter counter = stunt; + + counter.Add(2, 3); + + Assert.Same(counter, seen.Target); + Assert.Equal(nameof(Counter.Add), seen.MethodBase.Name); + Assert.Equal(2, seen.Arguments.Get("left")); + Assert.Equal(3, seen.Arguments.Get("right")); + } + + public class Counter + { + public int Count { get; private set; } + + public virtual int Next() + { + Count++; + return Count; + } + + public virtual int Other() => 0; + + public virtual int Add(int left, int right) => left + right; + + public virtual void Boom() => throw new InvalidOperationException("nope"); + } + } +} diff --git a/src/Stunts.UnitTests/DynamicStuntFactoryTests.cs b/src/Stunts.UnitTests/DynamicStuntFactoryTests.cs index 69b0226c..67debe19 100644 --- a/src/Stunts.UnitTests/DynamicStuntFactoryTests.cs +++ b/src/Stunts.UnitTests/DynamicStuntFactoryTests.cs @@ -15,23 +15,25 @@ public void TestFactory() var factory = new DynamicStuntFactory(); - var calculator = (ICalculator)factory.CreateStunt(Assembly.GetExecutingAssembly(), - typeof(ICalculator), Array.Empty(), Array.Empty()); + var stunt = new StuntReference((ICalculator)factory.CreateStunt(Assembly.GetExecutingAssembly(), + typeof(ICalculator), Array.Empty(), Array.Empty())); + + ICalculator calculator = stunt.ToObject(); var recorder = new RecordingBehavior(); - calculator.AddBehavior(recorder); + stunt.AddBehavior(recorder); - calculator.AddBehavior( + stunt.AddBehavior( (m, n) => new MethodReturn(m, "foo", m.Arguments), m => m.MethodBase.Name == "ToString", "ToString"); - calculator.AddBehavior( + stunt.AddBehavior( (m, n) => new MethodReturn(m, 42, m.Arguments), m => m.MethodBase.Name == "GetHashCode", "GetHashCode"); - calculator.AddBehavior( + stunt.AddBehavior( (m, n) => new MethodReturn(m, true, m.Arguments), m => m.MethodBase.Name == "Equals", "Equals"); @@ -81,10 +83,9 @@ public void ConstructorInterceptionNotSupported() using var ambient = BehaviorPipelineFactory.UseAmbient(new RecordingBehaviorPipelineFactory()); StuntFactory.LocalDefault = new DynamicStuntFactory(); - var calculator = Stunt.Of(); - var stunt = calculator as IStunt; + var stunt = Stunt.For(); + _ = stunt.ToObject(); - Assert.NotNull(stunt); Assert.Single(stunt.Behaviors); // Cannot record ctor call Assert.Empty(((RecordingBehavior)stunt.Behaviors[0]).Invocations); diff --git a/src/Stunts.UnitTests/Scenarios/OverrideObject.cs b/src/Stunts.UnitTests/Scenarios/OverrideObject.cs index 448e1db8..ac81cd9d 100644 --- a/src/Stunts.UnitTests/Scenarios/OverrideObject.cs +++ b/src/Stunts.UnitTests/Scenarios/OverrideObject.cs @@ -14,19 +14,20 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + BaseType value = stunt.ToObject(); var recorder = new RecordingBehavior(); stunt.AddBehavior(recorder); - stunt.GetHashCode(); + value.GetHashCode(); Assert.Single(recorder.Invocations); Assert.Equal(nameof(GetHashCode), recorder.Invocations[0].Invocation.MethodBase.Name); - stunt.ToString(); + value.ToString(); Assert.Equal(2, recorder.Invocations.Count); Assert.Equal(nameof(ToString), recorder.Invocations[1].Invocation.MethodBase.Name); - stunt.Equals(new object()); + value.Equals(new object()); Assert.Equal(3, recorder.Invocations.Count); Assert.Equal(nameof(Equals), recorder.Invocations[2].Invocation.MethodBase.Name); } diff --git a/src/Stunts.UnitTests/Scenarios/SignatureRefs.cs b/src/Stunts.UnitTests/Scenarios/SignatureRefs.cs index 33f65903..cced771e 100644 --- a/src/Stunts.UnitTests/Scenarios/SignatureRefs.cs +++ b/src/Stunts.UnitTests/Scenarios/SignatureRefs.cs @@ -57,14 +57,16 @@ public void Run() static void RefReadonlyKeepsTheModifierAndTheValue() { var stunt = Stunt.Of(); - ref readonly var current = ref stunt.Current(); + Counter counter = stunt; + + ref readonly var current = ref counter.Current(); Assert.Equal(7, current); var amount = 3; - stunt.Add(in amount); - Assert.Equal(10, stunt.Current()); + counter.Add(in amount); + Assert.Equal(10, counter.Current()); - var type = stunt.GetType(); + var type = counter.GetType(); Assert.Equal( typeof(Counter).GetMethod(nameof(Counter.Current))!.ReturnParameter.GetRequiredCustomModifiers(), type.GetMethod(nameof(Counter.Current))!.ReturnParameter.GetRequiredCustomModifiers()); @@ -75,43 +77,49 @@ static void RefReadonlyKeepsTheModifierAndTheValue() static void SpanIsVisibleToTheBehavior() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior((invocation, next) => { var span = (SpanRef)invocation.Arguments.GetValue("data")!; span.Value[0] = 10; return next(invocation, next); - }); + }, invocation => !invocation.MethodBase.IsConstructor); + + Buffer buffer = stunt.ToObject(); - Assert.Equal(15, stunt.Sum(new[] { 1, 2, 3 })); + Assert.Equal(15, buffer.Sum(new[] { 1, 2, 3 })); } static void SpanReturnProceedsThroughStructRef() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior((invocation, next) => next(invocation, next)); + Buffer buffer = stunt.ToObject(); + var data = new[] { 4, 5 }; - Assert.Equal(4, stunt.First(data)[0]); + Assert.Equal(4, buffer.First(data)[0]); } static void CustomRefStructRoundTrips() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior((invocation, next) => { var token = (StructRef)invocation.Arguments.GetValue("token")!; Assert.Equal(1, token.Value.Value); token.Value = new Token(4); return next(invocation, next); - }); + }, invocation => !invocation.MethodBase.IsConstructor); + + Parser parser = stunt.ToObject(); - Assert.Equal(4, stunt.Read(new Token(1))); + Assert.Equal(4, parser.Read(new Token(1))); } static void PointerIsVisibleToTheBehavior() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior((invocation, next) => { var pointer = (PointerRef)invocation.Arguments.GetValue("value")!; @@ -121,12 +129,13 @@ static void PointerIsVisibleToTheBehavior() } return next(invocation, next); - }); + }, invocation => !invocation.MethodBase.IsConstructor); var value = 4; + Reader reader = stunt.ToObject(); unsafe { - Assert.Equal(9, stunt.Read(&value)); + Assert.Equal(9, reader.Read(&value)); } Assert.Equal(9, value); diff --git a/src/Stunts.UnitTests/Scenarios/ValueTypeNull.cs b/src/Stunts.UnitTests/Scenarios/ValueTypeNull.cs index 6103c2e5..00034e1d 100644 --- a/src/Stunts.UnitTests/Scenarios/ValueTypeNull.cs +++ b/src/Stunts.UnitTests/Scenarios/ValueTypeNull.cs @@ -28,12 +28,12 @@ public void Run() static void NullReturnThrowsDescriptiveArgumentNullException() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => - invocation.CreateValueReturn((object)null)); + IValues values = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn((object)null)).ToObject(); var exception = Assert.Throws(() => { - stunt.GetValue(); + values.GetValue(); }); Assert.Equal(nameof(IMethodReturn.ReturnValue), exception.ParamName); @@ -43,11 +43,12 @@ static void NullReturnThrowsDescriptiveArgumentNullException() static void NullReturnWithRefArgumentThrowsDescriptiveArgumentNullException() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => - invocation.CreateValueReturn((object)null, invocation.Arguments.Get("value"))); + IValues values = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn((object)null, invocation.Arguments.Get("value")), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); var value = 1; - var exception = Assert.Throws(() => stunt.GetValue(ref value)); + var exception = Assert.Throws(() => values.GetValue(ref value)); Assert.Equal(nameof(IMethodReturn.ReturnValue), exception.ParamName); Assert.Contains(nameof(IMethodReturn.ReturnValue), exception.Message); @@ -56,11 +57,12 @@ static void NullReturnWithRefArgumentThrowsDescriptiveArgumentNullException() static void NullDelegateReturnWithRefArgumentThrowsDescriptiveArgumentNullException() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => - invocation.CreateValueReturn((object)null, invocation.Arguments.Get("value"))); + ValueDelegate values = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn((object)null, invocation.Arguments.Get("value")), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); var value = 1; - var exception = Assert.Throws(() => stunt(ref value)); + var exception = Assert.Throws(() => values(ref value)); Assert.Equal(nameof(IMethodReturn.ReturnValue), exception.ParamName); Assert.Contains(nameof(IMethodReturn.ReturnValue), exception.Message); @@ -69,12 +71,13 @@ static void NullDelegateReturnWithRefArgumentThrowsDescriptiveArgumentNullExcept static void NullOutputThrowsDescriptiveArgumentNullException() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => - invocation.CreateValueReturn(true, (object)null)); + IValues values = Stunt.For().AddBehavior((invocation, next) => + invocation.CreateValueReturn(true, (object)null), + invocation => !invocation.MethodBase.IsConstructor).ToObject(); var exception = Assert.Throws(() => { - stunt.TryGetValue(out _); + values.TryGetValue(out _); }); Assert.Equal("value", exception.ParamName);