From 17ee00209619355d60bfdd0e7bff49b921b260fb Mon Sep 17 00:00:00 2001 From: stakx Date: Sun, 11 Nov 2018 12:40:57 +0100 Subject: [PATCH 1/5] Merge members `Current` and `IsActive` --- src/Moq/FluentMockContext.cs | 12 +++++------- src/Moq/Interception/InterceptionAspects.cs | 8 ++++---- src/Moq/Match.cs | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Moq/FluentMockContext.cs b/src/Moq/FluentMockContext.cs index 82ebe6ae4..975a04671 100644 --- a/src/Moq/FluentMockContext.cs +++ b/src/Moq/FluentMockContext.cs @@ -17,20 +17,18 @@ internal class FluentMockContext : IDisposable private List invocations = new List(); - public static FluentMockContext Current - { - get { return current; } - } - /// /// Having an active fluent mock context means that the invocation /// is being performed in "trial" mode, just to gather the /// target method and arguments that need to be matched later /// when the actual invocation is made. /// - public static bool IsActive + public static bool IsActive(out FluentMockContext context) { - get { return current != null; } + var current = FluentMockContext.current; + + context = current; + return current != null; } public FluentMockContext() diff --git a/src/Moq/Interception/InterceptionAspects.cs b/src/Moq/Interception/InterceptionAspects.cs index d306c6cac..57bf19fbe 100644 --- a/src/Moq/Interception/InterceptionAspects.cs +++ b/src/Moq/Interception/InterceptionAspects.cs @@ -109,7 +109,7 @@ internal sealed class FindAndExecuteMatchingSetup : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - if (FluentMockContext.IsActive) + if (FluentMockContext.IsActive(out _)) { return InterceptionAction.Continue; } @@ -154,9 +154,9 @@ internal sealed class HandleTracking : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { // Track current invocation if we're in "record" mode in a fluent invocation context. - if (FluentMockContext.IsActive) + if (FluentMockContext.IsActive(out var context)) { - FluentMockContext.Current.Add(mock, invocation); + context.Add(mock, invocation); } return InterceptionAction.Continue; } @@ -168,7 +168,7 @@ internal sealed class RecordInvocation : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - if (FluentMockContext.IsActive) + if (FluentMockContext.IsActive(out _)) { // In a fluent invocation context, which is a recorder-like // mode we use to evaluate delegates by actually running them, diff --git a/src/Moq/Match.cs b/src/Moq/Match.cs index 76c0cbe9f..b5e8e531d 100644 --- a/src/Moq/Match.cs +++ b/src/Moq/Match.cs @@ -59,9 +59,9 @@ internal static T Create(Match match) /// private static Match SetLastMatch(Match match) { - if (FluentMockContext.IsActive) + if (FluentMockContext.IsActive(out var context)) { - FluentMockContext.Current.LastMatch = match; + context.LastMatch = match; } return match; From a993170e8e5c6133552b070ccc10048ea4562f97 Mon Sep 17 00:00:00 2001 From: stakx Date: Sun, 11 Nov 2018 14:31:12 +0100 Subject: [PATCH 2/5] `FluentMockContext` can record >1 matcher per invocation Moq currently cannot correctly deal with setter setups using more one matcher, e.g. when setting up an indexer's setter. This is because the `FluentMockContext` only associates at most one matcher with observed invocations. This commit changes how `FluentMockContext` records its observations and how it lets the outside world access them. When asked for the last observed mock invocation, `FluentMockContext` now returns *all* matchers observed immediately before the final invocation. --- src/Moq/ExpressionExtensions.cs | 2 +- src/Moq/Extensions.cs | 5 +- src/Moq/FluentMockContext.cs | 156 +++++++++++++++++--- src/Moq/Interception/InterceptionAspects.cs | 2 +- src/Moq/Match.cs | 6 +- src/Moq/MatcherFactory.cs | 8 +- src/Moq/Mock.cs | 30 ++-- 7 files changed, 158 insertions(+), 51 deletions(-) diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index a0a06d145..f1e63f9c7 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -119,7 +119,7 @@ private static bool PartialMatcherAwareEval_ShouldEvaluate(Expression expression using (var context = new FluentMockContext()) { Expression.Lambda(expression).CompileUsingExpressionCompiler().Invoke(); - return context.LastMatch == null; + return !context.LastObservationWasMatcher(out _); } default: diff --git a/src/Moq/Extensions.cs b/src/Moq/Extensions.cs index 15e7c6d6b..64e395ec8 100644 --- a/src/Moq/Extensions.cs +++ b/src/Moq/Extensions.cs @@ -149,13 +149,12 @@ public static EventWithTarget GetEventWithTarget(this Action event { eventExpression(mock); - if (context.LastInvocation == null) + if (!context.LastObservationWasMockInvocation(out target, out var lastInvocation, out _)) { throw new ArgumentException(Resources.ExpressionIsNotEventAttachOrDetachOrIsNotVirtual); } - addRemove = context.LastInvocation.Invocation.Method; - target = context.LastInvocation.Mock; + addRemove = lastInvocation.Method; } var ev = addRemove.DeclaringType.GetEvent( diff --git a/src/Moq/FluentMockContext.cs b/src/Moq/FluentMockContext.cs index 975a04671..e878ec3f3 100644 --- a/src/Moq/FluentMockContext.cs +++ b/src/Moq/FluentMockContext.cs @@ -15,8 +15,6 @@ internal class FluentMockContext : IDisposable [ThreadStatic] private static FluentMockContext current; - private List invocations = new List(); - /// /// Having an active fluent mock context means that the invocation /// is being performed in "trial" mode, just to gather the @@ -31,58 +29,168 @@ public static bool IsActive(out FluentMockContext context) return current != null; } + private List observations; + public FluentMockContext() { current = this; } - public void Add(Mock mock, Invocation invocation) + public void Dispose() { - invocations.Add(new MockInvocation(mock, invocation, LastMatch)); + if (this.observations != null) + { + for (var i = this.observations.Count - 1; i >= 0; --i) + { + this.observations[i].Dispose(); + } + } + + current = null; } - public MockInvocation LastInvocation + /// + /// Adds the specified mock invocation as an observation. + /// + public void OnInvocation(Mock mock, Invocation invocation) { - get { return invocations.LastOrDefault(); } + if (this.observations == null) + { + this.observations = new List(); + } + + observations.Add(new InvocationObservation(mock, invocation)); } - public Match LastMatch { get; set; } + /// + /// Adds the specified as an observation. + /// + public void OnMatch(Match match) + { + if (this.observations == null) + { + this.observations = new List(); + } + + this.observations.Add(new MatchObservation(match)); + } - public void Dispose() + /// + /// Checks whether the last observed thing was a mock invocation. + /// If , details about that mock invocation are provided via the parameters. + /// + /// The on which an invocation was observed. + /// The observed . + /// The es that were observed just before the invocation. + public bool LastObservationWasMockInvocation(out Mock mock, out Invocation invocation, out Matches matches) { - invocations.Reverse(); - foreach (var invocation in invocations) + if (this.observations != null) { - invocation.Dispose(); + var lastIndex = this.observations.Count - 1; + + if (this.observations[lastIndex] is InvocationObservation invocationRecord) + { + // Determine the first index of all recorded matches that immediately precede + // the last invocation; up to the previous recorded invocation or the beginning + // of the recording (whichever comes first): + int offset = lastIndex; + while (offset > 0 && this.observations[offset - 1] is MatchObservation) + { + --offset; + } + + mock = invocationRecord.Mock; + invocation = invocationRecord.Invocation; + matches = new Matches(this, offset, lastIndex - offset); + return true; + } } - current = null; + mock = default; + invocation = default; + matches = default; + return false; } - internal class MockInvocation : IDisposable + /// + /// Checks whether the last thing observed was a matcher. + /// If , details about that matcher are provided via the parameter. + /// + /// The observed matcher. + public bool LastObservationWasMatcher(out Match match) { + if (this.observations != null && this.observations[this.observations.Count - 1] is MatchObservation matchRecord) + { + match = matchRecord.Match; + return true; + } + + match = default; + return false; + } + + /// + /// Allocation-free pseudo-collection (think `ReadOnlySpan<Match>`) + /// used to access all es associated with a recorded invocation. + /// + public readonly struct Matches + { + private readonly FluentMockContext context; + private readonly int offset; + private readonly int count; + + public Matches(FluentMockContext context, int offset, int count) + { + this.context = context; + this.offset = offset; + this.count = count; + } + + public int Count => this.count; + + public Match this[int index] => ((MatchObservation)this.context.observations[this.offset + index]).Match; + } + + private abstract class Observation : IDisposable + { + protected Observation() + { + } + + public virtual void Dispose() + { + } + } + + private sealed class InvocationObservation : Observation + { + public readonly Mock Mock; + public readonly Invocation Invocation; + private DefaultValueProvider defaultValueProvider; - public MockInvocation(Mock mock, Invocation invocation, Match matcher) + public InvocationObservation(Mock mock, Invocation invocation) { this.Mock = mock; this.Invocation = invocation; - this.Match = matcher; - this.defaultValueProvider = mock.DefaultValueProvider; - // Temporarily set mock default value to Mock so that recursion works. - mock.DefaultValue = DefaultValue.Mock; + this.defaultValueProvider = mock.DefaultValueProvider; + mock.DefaultValueProvider = DefaultValueProvider.Mock; } - public Mock Mock { get; private set; } - - public Invocation Invocation { get; private set; } + public override void Dispose() + { + this.Mock.DefaultValueProvider = this.defaultValueProvider; + } + } - public Match Match { get; private set; } + private sealed class MatchObservation : Observation + { + public readonly Match Match; - public void Dispose() + public MatchObservation(Match match) { - this.Mock.DefaultValueProvider = this.defaultValueProvider; + this.Match = match; } } } diff --git a/src/Moq/Interception/InterceptionAspects.cs b/src/Moq/Interception/InterceptionAspects.cs index 57bf19fbe..2d8c47859 100644 --- a/src/Moq/Interception/InterceptionAspects.cs +++ b/src/Moq/Interception/InterceptionAspects.cs @@ -156,7 +156,7 @@ public override InterceptionAction Handle(Invocation invocation, Mock mock) // Track current invocation if we're in "record" mode in a fluent invocation context. if (FluentMockContext.IsActive(out var context)) { - context.Add(mock, invocation); + context.OnInvocation(mock, invocation); } return InterceptionAction.Continue; } diff --git a/src/Moq/Match.cs b/src/Moq/Match.cs index b5e8e531d..58ba3bd20 100644 --- a/src/Moq/Match.cs +++ b/src/Moq/Match.cs @@ -57,14 +57,12 @@ internal static T Create(Match match) /// methodcall we build the expression using it, rather than the null/default /// value returned from the actual invocation. /// - private static Match SetLastMatch(Match match) + private static void SetLastMatch(Match match) { if (FluentMockContext.IsActive(out var context)) { - context.LastMatch = match; + context.OnMatch(match); } - - return match; } } diff --git a/src/Moq/MatcherFactory.cs b/src/Moq/MatcherFactory.cs index 32a60d1c6..cb613d98b 100644 --- a/src/Moq/MatcherFactory.cs +++ b/src/Moq/MatcherFactory.cs @@ -94,9 +94,9 @@ public static IMatcher CreateMatcher(Expression expression) { Expression.Lambda(call).CompileUsingExpressionCompiler().Invoke(); - if (context.LastMatch != null) + if (context.LastObservationWasMatcher(out var match)) { - return context.LastMatch; + return match; } } @@ -117,9 +117,9 @@ public static IMatcher CreateMatcher(Expression expression) using (var context = new FluentMockContext()) { Expression.Lambda((MemberExpression)expression).CompileUsingExpressionCompiler().Invoke(); - if (context.LastMatch != null) + if (context.LastObservationWasMatcher(out var match)) { - return context.LastMatch; + return match; } } } diff --git a/src/Moq/Mock.cs b/src/Moq/Mock.cs index effd3e156..c2225b8b6 100644 --- a/src/Moq/Mock.cs +++ b/src/Moq/Mock.cs @@ -506,8 +506,7 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress { setterExpression.DynamicInvoke(mock.Object); - var last = context.LastInvocation; - if (last == null) + if (!context.LastObservationWasMockInvocation(out var lastMock, out var lastInvocation, out var lastMatches)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, @@ -515,7 +514,7 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress string.Empty)); } - var setter = last.Invocation.Method; + var setter = lastInvocation.Method; if (!setter.IsPropertySetter()) { throw new ArgumentException(Resources.SetupNotSetter); @@ -527,13 +526,13 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress // because of delegate currying, look at the last parameter for the Action's backing method, not the first var setterExpressionParameters = setterExpression.GetMethodInfo().GetParameters(); var parameterName = setterExpressionParameters[setterExpressionParameters.Length - 1].Name; - var x = Expression.Parameter(last.Invocation.Method.DeclaringType, parameterName); + var x = Expression.Parameter(lastInvocation.Method.DeclaringType, parameterName); - var arguments = last.Invocation.Arguments; + var arguments = lastInvocation.Arguments; var parameters = setter.GetParameters(); var values = new Expression[arguments.Length]; - if (last.Match == null) + if (lastMatches.Count == 0) { // Length == 1 || Length == 2 (Indexer property) for (int i = 0; i < arguments.Length; i++) @@ -543,13 +542,16 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress var lambda = Expression.Lambda( typeof(Action<>).MakeGenericType(x.Type), - Expression.Call(x, last.Invocation.Method, values), + Expression.Call(x, lastInvocation.Method, values), x); - return new SetupSetImplResult(last.Mock, lambda, last.Invocation.Method, values); + return new SetupSetImplResult(lastMock, lambda, lastInvocation.Method, values); } else { + // TODO: Use all observed matchers, not just the last one! + var lastMatch = lastMatches[lastMatches.Count - 1]; + var matchers = new Expression[arguments.Length]; var valueIndex = arguments.Length - 1; var propertyType = setter.GetParameters()[valueIndex].ParameterType; @@ -557,16 +559,16 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress // If the value matcher is not equal to the property // type (i.e. prop is int?, but you use It.IsAny()) // add a cast. - if (last.Match.RenderExpression.Type != propertyType) + if (lastMatch.RenderExpression.Type != propertyType) { - values[valueIndex] = Expression.Convert(last.Match.RenderExpression, propertyType); + values[valueIndex] = Expression.Convert(lastMatch.RenderExpression, propertyType); } else { - values[valueIndex] = last.Match.RenderExpression; + values[valueIndex] = lastMatch.RenderExpression; } - matchers[valueIndex] = new MatchExpression(last.Match); + matchers[valueIndex] = new MatchExpression(lastMatch); for (int i = 0; i < arguments.Length - 1; i++) { @@ -578,10 +580,10 @@ private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpress var lambda = Expression.Lambda( typeof(Action<>).MakeGenericType(x.Type), - Expression.Call(x, last.Invocation.Method, values), + Expression.Call(x, lastInvocation.Method, values), x); - return new SetupSetImplResult(last.Mock, lambda, last.Invocation.Method, matchers); + return new SetupSetImplResult(lastMock, lambda, lastInvocation.Method, matchers); } } } From df4cd2cb86a3b738ebfe1b7bfbc214bb718c19da Mon Sep 17 00:00:00 2001 From: stakx Date: Sun, 11 Nov 2018 15:00:32 +0100 Subject: [PATCH 3/5] Rename `FluentMockContext` to `AmbientObserver` Way too many things in Moq's code base are referred to as "fluent". Let's try to give this component a somewhat more descriptive name. --- ...luentMockContext.cs => AmbientObserver.cs} | 47 ++++++++++++------- src/Moq/ExpressionExtensions.cs | 4 +- src/Moq/Extensions.cs | 4 +- src/Moq/Interception/InterceptionAspects.cs | 13 +++-- src/Moq/Match.cs | 4 +- src/Moq/MatcherFactory.cs | 8 ++-- src/Moq/Mock.cs | 4 +- .../CustomDefaultValueProviderFixture.cs | 2 +- 8 files changed, 50 insertions(+), 36 deletions(-) rename src/Moq/{FluentMockContext.cs => AmbientObserver.cs} (73%) diff --git a/src/Moq/FluentMockContext.cs b/src/Moq/AmbientObserver.cs similarity index 73% rename from src/Moq/FluentMockContext.cs rename to src/Moq/AmbientObserver.cs index e878ec3f3..f1740df39 100644 --- a/src/Moq/FluentMockContext.cs +++ b/src/Moq/AmbientObserver.cs @@ -3,35 +3,46 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Moq { /// - /// Tracks the current mock and interception context. + /// A per-thread observer that records invocations to mocks and matchers for later inspection. /// - internal class FluentMockContext : IDisposable + /// + /// + /// This component requires the active cooperation of the respective subsystems. + /// That is, invoked matchers and mocks call into + /// or if an ambient observer is + /// active on the current thread. + /// + /// + /// This gets used in Moq's API to work around certain limitations of what kind + /// of constructs the Roslyn compilers allow in in-source LINQ expression trees + /// (e.g., assignment and event (un-)subscription are forbidden). Instead of + /// letting user code provide a LINQ expression tree, Moq accepts a normal lambda. + /// While a lambda cannot be directly inspected like a LINQ expression tree, we + /// can instantiate an , execute the lambda, and then + /// check with the observer what invocations happened; and from there, we can + /// "reverse-engineer" a LINQ expression tree (with some loss of accuracy). + /// + /// + internal sealed class AmbientObserver : IDisposable { [ThreadStatic] - private static FluentMockContext current; + private static AmbientObserver current; - /// - /// Having an active fluent mock context means that the invocation - /// is being performed in "trial" mode, just to gather the - /// target method and arguments that need to be matched later - /// when the actual invocation is made. - /// - public static bool IsActive(out FluentMockContext context) + public static bool IsActive(out AmbientObserver observer) { - var current = FluentMockContext.current; + var current = AmbientObserver.current; - context = current; + observer = current; return current != null; } private List observations; - public FluentMockContext() + public AmbientObserver() { current = this; } @@ -135,20 +146,20 @@ public bool LastObservationWasMatcher(out Match match) /// public readonly struct Matches { - private readonly FluentMockContext context; + private readonly AmbientObserver observer; private readonly int offset; private readonly int count; - public Matches(FluentMockContext context, int offset, int count) + public Matches(AmbientObserver observer, int offset, int count) { - this.context = context; + this.observer = observer; this.offset = offset; this.count = count; } public int Count => this.count; - public Match this[int index] => ((MatchObservation)this.context.observations[this.offset + index]).Match; + public Match this[int index] => ((MatchObservation)this.observer.observations[this.offset + index]).Match; } private abstract class Observation : IDisposable diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index f1e63f9c7..d3b9cb8da 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -116,10 +116,10 @@ private static bool PartialMatcherAwareEval_ShouldEvaluate(Expression expression case ExpressionType.Call: case ExpressionType.MemberAccess: // Evaluate everything but matchers: - using (var context = new FluentMockContext()) + using (var observer = new AmbientObserver()) { Expression.Lambda(expression).CompileUsingExpressionCompiler().Invoke(); - return !context.LastObservationWasMatcher(out _); + return !observer.LastObservationWasMatcher(out _); } default: diff --git a/src/Moq/Extensions.cs b/src/Moq/Extensions.cs index 64e395ec8..42f25f205 100644 --- a/src/Moq/Extensions.cs +++ b/src/Moq/Extensions.cs @@ -145,11 +145,11 @@ public static EventWithTarget GetEventWithTarget(this Action event MethodBase addRemove; Mock target; - using (var context = new FluentMockContext()) + using (var observer = new AmbientObserver()) { eventExpression(mock); - if (!context.LastObservationWasMockInvocation(out target, out var lastInvocation, out _)) + if (!observer.LastObservationWasMockInvocation(out target, out var lastInvocation, out _)) { throw new ArgumentException(Resources.ExpressionIsNotEventAttachOrDetachOrIsNotVirtual); } diff --git a/src/Moq/Interception/InterceptionAspects.cs b/src/Moq/Interception/InterceptionAspects.cs index 2d8c47859..f61e89dbb 100644 --- a/src/Moq/Interception/InterceptionAspects.cs +++ b/src/Moq/Interception/InterceptionAspects.cs @@ -109,8 +109,11 @@ internal sealed class FindAndExecuteMatchingSetup : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - if (FluentMockContext.IsActive(out _)) + if (AmbientObserver.IsActive(out _)) { + // Having an active `AmbientObserver` means that the invocation is being performed + // in "trial" mode, just to gather the target method and arguments that need to be + // matched later, when the actual invocation will be made. return InterceptionAction.Continue; } @@ -153,10 +156,10 @@ internal sealed class HandleTracking : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - // Track current invocation if we're in "record" mode in a fluent invocation context. - if (FluentMockContext.IsActive(out var context)) + // If an ambient observer is active, notify it of the current mock invocation. + if (AmbientObserver.IsActive(out var observer)) { - context.OnInvocation(mock, invocation); + observer.OnInvocation(mock, invocation); } return InterceptionAction.Continue; } @@ -168,7 +171,7 @@ internal sealed class RecordInvocation : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - if (FluentMockContext.IsActive(out _)) + if (AmbientObserver.IsActive(out _)) { // In a fluent invocation context, which is a recorder-like // mode we use to evaluate delegates by actually running them, diff --git a/src/Moq/Match.cs b/src/Moq/Match.cs index 58ba3bd20..279667f7b 100644 --- a/src/Moq/Match.cs +++ b/src/Moq/Match.cs @@ -59,9 +59,9 @@ internal static T Create(Match match) /// private static void SetLastMatch(Match match) { - if (FluentMockContext.IsActive(out var context)) + if (AmbientObserver.IsActive(out var observer)) { - context.OnMatch(match); + observer.OnMatch(match); } } } diff --git a/src/Moq/MatcherFactory.cs b/src/Moq/MatcherFactory.cs index cb613d98b..f83174445 100644 --- a/src/Moq/MatcherFactory.cs +++ b/src/Moq/MatcherFactory.cs @@ -90,11 +90,11 @@ public static IMatcher CreateMatcher(Expression expression) var call = (MethodCallExpression)expression; // Try to determine if invocation is to a matcher. - using (var context = new FluentMockContext()) + using (var observer = new AmbientObserver()) { Expression.Lambda(call).CompileUsingExpressionCompiler().Invoke(); - if (context.LastObservationWasMatcher(out var match)) + if (observer.LastObservationWasMatcher(out var match)) { return match; } @@ -114,10 +114,10 @@ public static IMatcher CreateMatcher(Expression expression) else if (expression.NodeType == ExpressionType.MemberAccess) { // Try to determine if invocation is to a matcher. - using (var context = new FluentMockContext()) + using (var observer = new AmbientObserver()) { Expression.Lambda((MemberExpression)expression).CompileUsingExpressionCompiler().Invoke(); - if (context.LastObservationWasMatcher(out var match)) + if (observer.LastObservationWasMatcher(out var match)) { return match; } diff --git a/src/Moq/Mock.cs b/src/Moq/Mock.cs index c2225b8b6..08da67047 100644 --- a/src/Moq/Mock.cs +++ b/src/Moq/Mock.cs @@ -502,11 +502,11 @@ internal static MethodCall SetupSet(Mock mock, LambdaExpression expression, Expr private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpression) { - using (var context = new FluentMockContext()) + using (var observer = new AmbientObserver()) { setterExpression.DynamicInvoke(mock.Object); - if (!context.LastObservationWasMockInvocation(out var lastMock, out var lastInvocation, out var lastMatches)) + if (!observer.LastObservationWasMockInvocation(out var lastMock, out var lastInvocation, out var lastMatches)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, diff --git a/tests/Moq.Tests/CustomDefaultValueProviderFixture.cs b/tests/Moq.Tests/CustomDefaultValueProviderFixture.cs index 0d4738f80..28486558d 100644 --- a/tests/Moq.Tests/CustomDefaultValueProviderFixture.cs +++ b/tests/Moq.Tests/CustomDefaultValueProviderFixture.cs @@ -93,7 +93,7 @@ public void Inner_mocks_inherit_custom_default_value_provider_from_outer_mock() } [Fact] - public void FluentMockContext_properly_restores_custom_default_value_provider() + public void AmbientObserver_properly_restores_custom_default_value_provider() { var customDefaultValueProvider = new ConstantDefaultValueProvider(42); var mock = new Mock() { DefaultValueProvider = customDefaultValueProvider }; From 8a2884ce9aa95fdb1a38093b2ebf880d3cd1a82f Mon Sep 17 00:00:00 2001 From: stakx Date: Sun, 11 Nov 2018 15:32:46 +0100 Subject: [PATCH 4/5] Add some tests for `AmbientObserver` --- tests/Moq.Tests/AmbientObserverFixture.cs | 160 ++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/Moq.Tests/AmbientObserverFixture.cs diff --git a/tests/Moq.Tests/AmbientObserverFixture.cs b/tests/Moq.Tests/AmbientObserverFixture.cs new file mode 100644 index 000000000..ea4966f52 --- /dev/null +++ b/tests/Moq.Tests/AmbientObserverFixture.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System.Reflection; +using System.Runtime.CompilerServices; + +using Xunit; + +namespace Moq.Tests +{ + public class AmbientObserverFixture + { + [Fact] + public void IsActive_returns_false_when_no_AmbientObserver_instantiated() + { + Assert.False(AmbientObserver.IsActive(out _)); + } + + [Fact] + public void IsActive_returns_true_when_AmbientObserver_instantiated() + { + using (var observer = new AmbientObserver()) + { + Assert.True(AmbientObserver.IsActive(out _)); + } + } + + [Fact] + public void IsActive_returns_right_AmbientObserver_when_AmbientObserver_instantiated() + { + using (var observer = new AmbientObserver()) + { + Assert.True(AmbientObserver.IsActive(out var active)); + Assert.Same(observer, active); + } + } + + [Fact] + public void LastObservationWasMatcher_returns_false_after_no_invocations() + { + using (var observer = new AmbientObserver()) + { + Assert.False(observer.LastObservationWasMatcher(out _)); + } + } + + [Fact] + public void LastObservationWasMatcher_returns_false_after_a_mock_invocation() + { + var mock = Mock.Of(); + + using (var observer = new AmbientObserver()) + { + mock.Method(default, default); + + Assert.False(observer.LastObservationWasMatcher(out _)); + } + } + + [Fact] + public void LastObservationWasMatcher_returns_true_after_a_matcher_invocation() + { + using (var observer = new AmbientObserver()) + { + _ = It.IsAny(); + + Assert.True(observer.LastObservationWasMatcher(out _)); + } + } + + [Fact] + public void LastObservationWasMatcher_returns_right_matcher_after_several_matcher_invocations() + { + using (var observer = new AmbientObserver()) + { + _ = It.IsAny(); + _ = It.IsRegex(".*"); + + Assert.True(observer.LastObservationWasMatcher(out var last)); + Assert.True(last.Matches("abc")); + } + } + + [Fact] + public void LastObservationWasMockInvocation_returns_false_after_no_invocations() + { + using (var observer = new AmbientObserver()) + { + Assert.False(observer.LastObservationWasMockInvocation(out _, out _, out _)); + } + } + + [Fact] + public void LastObservationWasMockInvocation_returns_true_after_a_mock_invocation() + { + var mock = Mock.Of(); + + using (var observer = new AmbientObserver()) + { + mock.Method(default, default); + + Assert.True(observer.LastObservationWasMockInvocation(out _, out _, out _)); + } + } + + [Fact] + public void LastObservationWasMockInvocation_returns_last_invoation_after_several_mock_invocations() + { + var mock = Mock.Of(); + + using (var observer = new AmbientObserver()) + { + mock.Method(default, default); + mock.Method(42, "*"); + + Assert.True(observer.LastObservationWasMockInvocation(out _, out var last, out _)); + Assert.Equal(42, last.Arguments[0]); + Assert.Equal("*", last.Arguments[1]); + } + } + + [Fact] + public void LastObservationWasMockInvocation_returns_right_matchers_after_mock_invocation() + { + var mock = Mock.Of(); + + using (var observer = new AmbientObserver()) + { + mock.Method(It.IsAny(), It.IsRegex("abc")); + + Assert.True(observer.LastObservationWasMockInvocation(out _, out var _, out var matches)); + Assert.Equal(2, matches.Count); + Assert.True(matches[0].Matches(42)); + Assert.True(matches[1].Matches("abc")); + } + } + + [Fact] + public void LastObservationWasMockInvocation_does_not_return_matchers_of_previous_mock_invocation() + { + var mock = Mock.Of(); + + using (var observer = new AmbientObserver()) + { + mock.Method(It.IsInRange(1, 10, Range.Inclusive), "*"); + mock.Method(It.IsAny(), It.IsRegex("abc")); + + Assert.True(observer.LastObservationWasMockInvocation(out _, out var _, out var matches)); + Assert.Equal(2, matches.Count); + Assert.True(matches[0].Matches(42)); + Assert.True(matches[1].Matches("abc")); + } + } + + public interface IMockable + { + void Method(int arg1, string arg2); + } + } +} From 00b630a66a8ad394bb4059cdc8debd95f0fa6121 Mon Sep 17 00:00:00 2001 From: stakx Date: Sun, 11 Nov 2018 18:33:19 +0100 Subject: [PATCH 5/5] Clean up new `AmbientObserver` API a bit * Add static `Activate` method that client code uses instead of the ctor; so that there is better symmetry with `IsActive`. * Abbreviate method names to `LastIsInvocation`, `LastIsMatch`, also to mirror `OnInvocation`, `OnMatch` more closely. * Introduce `expression.IsMatch` helper method that abstracts away a `AmbientObserver` usage pattern repeated 3x across the code base. --- src/Moq/AmbientObserver.cs | 15 ++- src/Moq/ExpressionExtensions.cs | 16 ++- src/Moq/Extensions.cs | 6 +- src/Moq/Interception/InterceptionAspects.cs | 8 -- src/Moq/Match.cs | 29 ++--- src/Moq/MatcherFactory.cs | 22 +--- src/Moq/Mock.cs | 122 ++++++++++---------- tests/Moq.Tests/AmbientObserverFixture.cs | 40 +++---- 8 files changed, 124 insertions(+), 134 deletions(-) diff --git a/src/Moq/AmbientObserver.cs b/src/Moq/AmbientObserver.cs index f1740df39..99ce144b9 100644 --- a/src/Moq/AmbientObserver.cs +++ b/src/Moq/AmbientObserver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Moq { @@ -32,6 +33,13 @@ internal sealed class AmbientObserver : IDisposable [ThreadStatic] private static AmbientObserver current; + public static AmbientObserver Activate() + { + Debug.Assert(current == null); + + return current = new AmbientObserver(); + } + public static bool IsActive(out AmbientObserver observer) { var current = AmbientObserver.current; @@ -42,9 +50,8 @@ public static bool IsActive(out AmbientObserver observer) private List observations; - public AmbientObserver() + private AmbientObserver() { - current = this; } public void Dispose() @@ -93,7 +100,7 @@ public void OnMatch(Match match) /// The on which an invocation was observed. /// The observed . /// The es that were observed just before the invocation. - public bool LastObservationWasMockInvocation(out Mock mock, out Invocation invocation, out Matches matches) + public bool LastIsInvocation(out Mock mock, out Invocation invocation, out Matches matches) { if (this.observations != null) { @@ -128,7 +135,7 @@ public bool LastObservationWasMockInvocation(out Mock mock, out Invocation invoc /// If , details about that matcher are provided via the parameter. /// /// The observed matcher. - public bool LastObservationWasMatcher(out Match match) + public bool LastIsMatch(out Match match) { if (this.observations != null && this.observations[this.observations.Count - 1] is MatchObservation matchRecord) { diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index d3b9cb8da..9df9b54d5 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -28,6 +28,15 @@ internal static TDelegate CompileUsingExpressionCompiler(this Express return ExpressionCompiler.Instance.Compile(expression); } + public static bool IsMatch(this Expression expression, out Match match) + { + using (var observer = AmbientObserver.Activate()) + { + Expression.Lambda(expression).CompileUsingExpressionCompiler().Invoke(); + return observer.LastIsMatch(out match); + } + } + /// /// Converts the body of the lambda expression into the referenced by it. /// @@ -115,12 +124,7 @@ private static bool PartialMatcherAwareEval_ShouldEvaluate(Expression expression case ExpressionType.Call: case ExpressionType.MemberAccess: - // Evaluate everything but matchers: - using (var observer = new AmbientObserver()) - { - Expression.Lambda(expression).CompileUsingExpressionCompiler().Invoke(); - return !observer.LastObservationWasMatcher(out _); - } + return !expression.IsMatch(out _); default: return true; diff --git a/src/Moq/Extensions.cs b/src/Moq/Extensions.cs index 42f25f205..275e4ccc3 100644 --- a/src/Moq/Extensions.cs +++ b/src/Moq/Extensions.cs @@ -145,16 +145,16 @@ public static EventWithTarget GetEventWithTarget(this Action event MethodBase addRemove; Mock target; - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { eventExpression(mock); - if (!observer.LastObservationWasMockInvocation(out target, out var lastInvocation, out _)) + if (!observer.LastIsInvocation(out target, out var invocation, out _)) { throw new ArgumentException(Resources.ExpressionIsNotEventAttachOrDetachOrIsNotVirtual); } - addRemove = lastInvocation.Method; + addRemove = invocation.Method; } var ev = addRemove.DeclaringType.GetEvent( diff --git a/src/Moq/Interception/InterceptionAspects.cs b/src/Moq/Interception/InterceptionAspects.cs index f61e89dbb..40c5c780e 100644 --- a/src/Moq/Interception/InterceptionAspects.cs +++ b/src/Moq/Interception/InterceptionAspects.cs @@ -111,9 +111,6 @@ public override InterceptionAction Handle(Invocation invocation, Mock mock) { if (AmbientObserver.IsActive(out _)) { - // Having an active `AmbientObserver` means that the invocation is being performed - // in "trial" mode, just to gather the target method and arguments that need to be - // matched later, when the actual invocation will be made. return InterceptionAction.Continue; } @@ -156,7 +153,6 @@ internal sealed class HandleTracking : InterceptionAspect public override InterceptionAction Handle(Invocation invocation, Mock mock) { - // If an ambient observer is active, notify it of the current mock invocation. if (AmbientObserver.IsActive(out var observer)) { observer.OnInvocation(mock, invocation); @@ -173,10 +169,6 @@ public override InterceptionAction Handle(Invocation invocation, Mock mock) { if (AmbientObserver.IsActive(out _)) { - // In a fluent invocation context, which is a recorder-like - // mode we use to evaluate delegates by actually running them, - // we don't want to count the invocation, or actually run - // previous setups. return InterceptionAction.Continue; } diff --git a/src/Moq/Match.cs b/src/Moq/Match.cs index 279667f7b..5346bf0db 100644 --- a/src/Moq/Match.cs +++ b/src/Moq/Match.cs @@ -41,28 +41,23 @@ public static T Create(Predicate condition, Expression> renderExpr internal static T Create(Match match) { - SetLastMatch(match); - return default(T); - } + // This method is used to set an expression as the last matcher invoked, + // which is used in the SetupSet to allow matchers in the prop = value + // delegate expression. This delegate is executed in "fluent" mode in + // order to capture the value being set, and construct the corresponding + // methodcall. + // This is also used in the MatcherFactory for each argument expression. + // This method ensures that when we execute the delegate, we + // also track the matcher that was invoked, so that when we create the + // methodcall we build the expression using it, rather than the null/default + // value returned from the actual invocation. - /// - /// This method is used to set an expression as the last matcher invoked, - /// which is used in the SetupSet to allow matchers in the prop = value - /// delegate expression. This delegate is executed in "fluent" mode in - /// order to capture the value being set, and construct the corresponding - /// methodcall. - /// This is also used in the MatcherFactory for each argument expression. - /// This method ensures that when we execute the delegate, we - /// also track the matcher that was invoked, so that when we create the - /// methodcall we build the expression using it, rather than the null/default - /// value returned from the actual invocation. - /// - private static void SetLastMatch(Match match) - { if (AmbientObserver.IsActive(out var observer)) { observer.OnMatch(match); } + + return default(T); } } diff --git a/src/Moq/MatcherFactory.cs b/src/Moq/MatcherFactory.cs index f83174445..76bbb518a 100644 --- a/src/Moq/MatcherFactory.cs +++ b/src/Moq/MatcherFactory.cs @@ -87,20 +87,13 @@ public static IMatcher CreateMatcher(Expression expression) if (expression.NodeType == ExpressionType.Call) { - var call = (MethodCallExpression)expression; - - // Try to determine if invocation is to a matcher. - using (var observer = new AmbientObserver()) + if (expression.IsMatch(out var match)) { - Expression.Lambda(call).CompileUsingExpressionCompiler().Invoke(); - - if (observer.LastObservationWasMatcher(out var match)) - { - return match; - } + return match; } #pragma warning disable 618 + var call = (MethodCallExpression)expression; if (call.Method.IsDefined(typeof(MatcherAttribute), true)) { return new MatcherAttributeMatcher(call); @@ -113,14 +106,9 @@ public static IMatcher CreateMatcher(Expression expression) } else if (expression.NodeType == ExpressionType.MemberAccess) { - // Try to determine if invocation is to a matcher. - using (var observer = new AmbientObserver()) + if (expression.IsMatch(out var match)) { - Expression.Lambda((MemberExpression)expression).CompileUsingExpressionCompiler().Invoke(); - if (observer.LastObservationWasMatcher(out var match)) - { - return match; - } + return match; } } diff --git a/src/Moq/Mock.cs b/src/Moq/Mock.cs index 08da67047..a4b249e89 100644 --- a/src/Moq/Mock.cs +++ b/src/Moq/Mock.cs @@ -502,89 +502,93 @@ internal static MethodCall SetupSet(Mock mock, LambdaExpression expression, Expr private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpression) { - using (var observer = new AmbientObserver()) + Mock target; + Invocation invocation; + AmbientObserver.Matches matches; + + using (var observer = AmbientObserver.Activate()) { setterExpression.DynamicInvoke(mock.Object); - if (!observer.LastObservationWasMockInvocation(out var lastMock, out var lastInvocation, out var lastMatches)) + if (!observer.LastIsInvocation(out target, out invocation, out matches)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, Resources.SetupOnNonVirtualMember, string.Empty)); } + } - var setter = lastInvocation.Method; - if (!setter.IsPropertySetter()) - { - throw new ArgumentException(Resources.SetupNotSetter); - } + var setter = invocation.Method; + if (!setter.IsPropertySetter()) + { + throw new ArgumentException(Resources.SetupNotSetter); + } - // No need to call ThrowIfCantOverride as non-overridable would have thrown above already. + // No need to call ThrowIfCantOverride as non-overridable would have thrown above already. - // Get the variable name as used in the actual delegate :) - // because of delegate currying, look at the last parameter for the Action's backing method, not the first - var setterExpressionParameters = setterExpression.GetMethodInfo().GetParameters(); - var parameterName = setterExpressionParameters[setterExpressionParameters.Length - 1].Name; - var x = Expression.Parameter(lastInvocation.Method.DeclaringType, parameterName); + // Get the variable name as used in the actual delegate :) + // because of delegate currying, look at the last parameter for the Action's backing method, not the first + var setterExpressionParameters = setterExpression.GetMethodInfo().GetParameters(); + var parameterName = setterExpressionParameters[setterExpressionParameters.Length - 1].Name; + var x = Expression.Parameter(invocation.Method.DeclaringType, parameterName); - var arguments = lastInvocation.Arguments; - var parameters = setter.GetParameters(); - var values = new Expression[arguments.Length]; + var arguments = invocation.Arguments; + var parameters = setter.GetParameters(); + var values = new Expression[arguments.Length]; - if (lastMatches.Count == 0) + if (matches.Count == 0) + { + // Length == 1 || Length == 2 (Indexer property) + for (int i = 0; i < arguments.Length; i++) { - // Length == 1 || Length == 2 (Indexer property) - for (int i = 0; i < arguments.Length; i++) - { - values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); - } + values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); + } + + var lambda = Expression.Lambda( + typeof(Action<>).MakeGenericType(x.Type), + Expression.Call(x, invocation.Method, values), + x); + + return new SetupSetImplResult(target, lambda, invocation.Method, values); + } + else + { + // TODO: Use all observed matchers, not just the last one! + var lastMatch = matches[matches.Count - 1]; - var lambda = Expression.Lambda( - typeof(Action<>).MakeGenericType(x.Type), - Expression.Call(x, lastInvocation.Method, values), - x); + var matchers = new Expression[arguments.Length]; + var valueIndex = arguments.Length - 1; + var propertyType = setter.GetParameters()[valueIndex].ParameterType; - return new SetupSetImplResult(lastMock, lambda, lastInvocation.Method, values); + // If the value matcher is not equal to the property + // type (i.e. prop is int?, but you use It.IsAny()) + // add a cast. + if (lastMatch.RenderExpression.Type != propertyType) + { + values[valueIndex] = Expression.Convert(lastMatch.RenderExpression, propertyType); } else { - // TODO: Use all observed matchers, not just the last one! - var lastMatch = lastMatches[lastMatches.Count - 1]; - - var matchers = new Expression[arguments.Length]; - var valueIndex = arguments.Length - 1; - var propertyType = setter.GetParameters()[valueIndex].ParameterType; - - // If the value matcher is not equal to the property - // type (i.e. prop is int?, but you use It.IsAny()) - // add a cast. - if (lastMatch.RenderExpression.Type != propertyType) - { - values[valueIndex] = Expression.Convert(lastMatch.RenderExpression, propertyType); - } - else - { - values[valueIndex] = lastMatch.RenderExpression; - } + values[valueIndex] = lastMatch.RenderExpression; + } - matchers[valueIndex] = new MatchExpression(lastMatch); + matchers[valueIndex] = new MatchExpression(lastMatch); - for (int i = 0; i < arguments.Length - 1; i++) - { - // Add the index value for the property indexer - values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); - // TODO: No matcher supported now for the index - matchers[i] = values[i]; - } + for (int i = 0; i < arguments.Length - 1; i++) + { + // Add the index value for the property indexer + values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); + // TODO: No matcher supported now for the index + matchers[i] = values[i]; + } - var lambda = Expression.Lambda( - typeof(Action<>).MakeGenericType(x.Type), - Expression.Call(x, lastInvocation.Method, values), - x); + var lambda = Expression.Lambda( + typeof(Action<>).MakeGenericType(x.Type), + Expression.Call(x, invocation.Method, values), + x); - return new SetupSetImplResult(lastMock, lambda, lastInvocation.Method, matchers); - } + return new SetupSetImplResult(target, lambda, invocation.Method, matchers); } } diff --git a/tests/Moq.Tests/AmbientObserverFixture.cs b/tests/Moq.Tests/AmbientObserverFixture.cs index ea4966f52..25faf7888 100644 --- a/tests/Moq.Tests/AmbientObserverFixture.cs +++ b/tests/Moq.Tests/AmbientObserverFixture.cs @@ -19,7 +19,7 @@ public void IsActive_returns_false_when_no_AmbientObserver_instantiated() [Fact] public void IsActive_returns_true_when_AmbientObserver_instantiated() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { Assert.True(AmbientObserver.IsActive(out _)); } @@ -28,7 +28,7 @@ public void IsActive_returns_true_when_AmbientObserver_instantiated() [Fact] public void IsActive_returns_right_AmbientObserver_when_AmbientObserver_instantiated() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { Assert.True(AmbientObserver.IsActive(out var active)); Assert.Same(observer, active); @@ -38,9 +38,9 @@ public void IsActive_returns_right_AmbientObserver_when_AmbientObserver_instanti [Fact] public void LastObservationWasMatcher_returns_false_after_no_invocations() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { - Assert.False(observer.LastObservationWasMatcher(out _)); + Assert.False(observer.LastIsMatch(out _)); } } @@ -49,34 +49,34 @@ public void LastObservationWasMatcher_returns_false_after_a_mock_invocation() { var mock = Mock.Of(); - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { mock.Method(default, default); - Assert.False(observer.LastObservationWasMatcher(out _)); + Assert.False(observer.LastIsMatch(out _)); } } [Fact] public void LastObservationWasMatcher_returns_true_after_a_matcher_invocation() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { _ = It.IsAny(); - Assert.True(observer.LastObservationWasMatcher(out _)); + Assert.True(observer.LastIsMatch(out _)); } } [Fact] public void LastObservationWasMatcher_returns_right_matcher_after_several_matcher_invocations() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { _ = It.IsAny(); _ = It.IsRegex(".*"); - Assert.True(observer.LastObservationWasMatcher(out var last)); + Assert.True(observer.LastIsMatch(out var last)); Assert.True(last.Matches("abc")); } } @@ -84,9 +84,9 @@ public void LastObservationWasMatcher_returns_right_matcher_after_several_matche [Fact] public void LastObservationWasMockInvocation_returns_false_after_no_invocations() { - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { - Assert.False(observer.LastObservationWasMockInvocation(out _, out _, out _)); + Assert.False(observer.LastIsInvocation(out _, out _, out _)); } } @@ -95,11 +95,11 @@ public void LastObservationWasMockInvocation_returns_true_after_a_mock_invocatio { var mock = Mock.Of(); - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { mock.Method(default, default); - Assert.True(observer.LastObservationWasMockInvocation(out _, out _, out _)); + Assert.True(observer.LastIsInvocation(out _, out _, out _)); } } @@ -108,12 +108,12 @@ public void LastObservationWasMockInvocation_returns_last_invoation_after_severa { var mock = Mock.Of(); - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { mock.Method(default, default); mock.Method(42, "*"); - Assert.True(observer.LastObservationWasMockInvocation(out _, out var last, out _)); + Assert.True(observer.LastIsInvocation(out _, out var last, out _)); Assert.Equal(42, last.Arguments[0]); Assert.Equal("*", last.Arguments[1]); } @@ -124,11 +124,11 @@ public void LastObservationWasMockInvocation_returns_right_matchers_after_mock_i { var mock = Mock.Of(); - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { mock.Method(It.IsAny(), It.IsRegex("abc")); - Assert.True(observer.LastObservationWasMockInvocation(out _, out var _, out var matches)); + Assert.True(observer.LastIsInvocation(out _, out var _, out var matches)); Assert.Equal(2, matches.Count); Assert.True(matches[0].Matches(42)); Assert.True(matches[1].Matches("abc")); @@ -140,12 +140,12 @@ public void LastObservationWasMockInvocation_does_not_return_matchers_of_previou { var mock = Mock.Of(); - using (var observer = new AmbientObserver()) + using (var observer = AmbientObserver.Activate()) { mock.Method(It.IsInRange(1, 10, Range.Inclusive), "*"); mock.Method(It.IsAny(), It.IsRegex("abc")); - Assert.True(observer.LastObservationWasMockInvocation(out _, out var _, out var matches)); + Assert.True(observer.LastIsInvocation(out _, out var _, out var matches)); Assert.Equal(2, matches.Count); Assert.True(matches[0].Matches(42)); Assert.True(matches[1].Matches("abc"));