diff --git a/src/Moq.sln b/src/Moq.sln index bbb47dae..1b4d79db 100644 --- a/src/Moq.sln +++ b/src/Moq.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq", "Moq\Moq\Moq.csproj", "{65F3AB28-9A74-405E-83B2-420D5AD5A459}" EndProject @@ -48,6 +48,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{1DBDFC27 build\Version.targets = build\Version.targets EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.Tests", "Stunts\Stunts.Tests\Stunts.Tests.csproj", "{EDBDA217-CA42-4C82-826F-7D990185EC0F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,6 +96,10 @@ Global {9A09225F-E0BC-4890-BED4-D9F6F5DAC146}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A09225F-E0BC-4890-BED4-D9F6F5DAC146}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A09225F-E0BC-4890-BED4-D9F6F5DAC146}.Release|Any CPU.Build.0 = Release|Any CPU + {EDBDA217-CA42-4C82-826F-7D990185EC0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDBDA217-CA42-4C82-826F-7D990185EC0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDBDA217-CA42-4C82-826F-7D990185EC0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDBDA217-CA42-4C82-826F-7D990185EC0F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +110,7 @@ Global {9ACA3490-778C-4C58-914C-87B9E7C15AC7} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45} {0271D7C1-1A51-4148-944C-1DB4D1946B9C} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45} {1DBDFC27-21EC-4EAC-B51B-84EDC8DBB9D5} = {2093478C-CEA6-4034-BCDE-EDC7A5DD4532} + {EDBDA217-CA42-4C82-826F-7D990185EC0F} = {EEC3EC48-ACB2-4D25-B592-F360F676BE45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DA4CFD03-827D-482B-9304-83456D2A8115} diff --git a/src/Moq/Moq.Sdk.Tests/MockExtensionsTests.cs b/src/Moq/Moq.Sdk.Tests/MockExtensionsTests.cs index 14cb6bc3..68da529d 100644 --- a/src/Moq/Moq.Sdk.Tests/MockExtensionsTests.cs +++ b/src/Moq/Moq.Sdk.Tests/MockExtensionsTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Stunts; using Xunit; @@ -137,6 +138,8 @@ class Mock : IMock public IMockSetup LastSetup { get; set; } + public IEnumerable Setups => Behaviors.OfType(); + public IMockBehavior BehaviorFor(IMockSetup setup) => throw new NotImplementedException(); } } diff --git a/src/Moq/Moq.Sdk/Behavior.cs b/src/Moq/Moq.Sdk/Behavior.cs index dc1584b1..8829aa14 100644 --- a/src/Moq/Moq.Sdk/Behavior.cs +++ b/src/Moq/Moq.Sdk/Behavior.cs @@ -28,7 +28,7 @@ public Behavior(InvokeBehavior invoke, string displayName) /// delegate and friendly display name. /// /// - /// Use this constructor overload whenver constructing the display + /// Use this constructor overload whenever constructing the display /// name is somewhat expensive. /// public Behavior(InvokeBehavior invoke, Lazy displayName) diff --git a/src/Moq/Moq.Sdk/IMockFactory.cs b/src/Moq/Moq.Sdk/IMockFactory.cs index bf46adb4..34d4b7c8 100644 --- a/src/Moq/Moq.Sdk/IMockFactory.cs +++ b/src/Moq/Moq.Sdk/IMockFactory.cs @@ -15,7 +15,7 @@ public interface IMockFactory /// The base type (or main interface) of the mock. /// Additional interfaces implemented by the mock, or an empty array. /// - /// Contructor arguments if the is a class, rather than an interface, or an empty array. + /// Constructor arguments if the is a class, rather than an interface, or an empty array. /// /// A mock that implements in addition to the specified interfaces (if any). object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments); diff --git a/src/Moq/Moq.Sdk/KnownStates.cs b/src/Moq/Moq.Sdk/KnownStates.cs deleted file mode 100644 index d31dc9b8..00000000 --- a/src/Moq/Moq.Sdk/KnownStates.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Moq.Sdk.Properties; - -namespace Moq.Sdk -{ - /// - /// Known states a mock can be in. - /// - public static class KnownStates - { - /// - /// The key in containing a - /// specifying if the mock is being set up or not. - /// - public const string Setup = nameof(KnownStates) + "." + nameof(Setup); - - /// - /// Whether the mock is being set up. - /// - /// The mock to inspect. - /// if the mock is being set up (it has the state key - /// set to ), otherwise. - public static bool InSetup(object mock) - { - var target = mock is IMocked mocked ? - mocked.Mock : throw new ArgumentException(Resources.TargetNotMock, nameof(mock)); - - return target.State.TryGetValue(Setup, out var state) && state.GetValueOrDefault(); - } - } -} diff --git a/src/Moq/Moq.Sdk/MockContext.cs b/src/Moq/Moq.Sdk/MockContext.cs index 469de904..7135eee0 100644 --- a/src/Moq/Moq.Sdk/MockContext.cs +++ b/src/Moq/Moq.Sdk/MockContext.cs @@ -17,7 +17,7 @@ public static class MockContext /// /// The last invocation on the mock, turned into an /// ready for use together with the - /// method to locate the maching to add new behaviors + /// method to locate the matching to add new behaviors /// when a matching invocation is performed. /// /// diff --git a/src/Moq/Moq.Sdk/MockState.cs b/src/Moq/Moq.Sdk/MockState.cs index 085f36c6..22a79c00 100644 --- a/src/Moq/Moq.Sdk/MockState.cs +++ b/src/Moq/Moq.Sdk/MockState.cs @@ -48,14 +48,24 @@ public T GetOrAdd(object key, Func valueFactory) /// regardless of whether there is an existing value assigned. /// public void Set(T value) - => state[typeof(T)] = value; + { + if (value == null) + state.TryRemove(typeof(T), out _); + else + state[typeof(T)] = value; + } /// /// Sets the state of the given type and , /// regardless of whether there is an existing value assigned. /// public void Set(object key, T value) - => state[Key(key)] = value; + { + if (value == null) + state.TryRemove(Key(key), out _); + else + state[Key(key)] = value; + } /// /// Attempts to add the specified value to the mock state. @@ -135,7 +145,7 @@ public bool TryRemove(object key, out T value) value = (T)_value; return result; } - + /// /// Compares the existing value for of the specified with a specified value, /// and if they are equal, updates it with a third value. diff --git a/src/Moq/Moq.Sdk/PropertyBehavior.cs b/src/Moq/Moq.Sdk/PropertyBehavior.cs index fe0d650a..1d61f219 100644 --- a/src/Moq/Moq.Sdk/PropertyBehavior.cs +++ b/src/Moq/Moq.Sdk/PropertyBehavior.cs @@ -40,7 +40,7 @@ public IMethodReturn Invoke(IMethodInvocation invocation, GetNextBehavior getNex return invocation.CreateValueReturn(value); if (invocation.MethodBase.Name.StartsWith("set_", StringComparison.Ordinal) && - (!SetterRequiresSetup || KnownStates.InSetup(invocation.Target))) + (!SetterRequiresSetup || SetupScope.IsActive)) { state.Set("_" + invocation.MethodBase.Name.Substring(4), invocation.Arguments[0]); return invocation.CreateValueReturn(null); diff --git a/src/Moq/Moq.Sdk/SetupScope.cs b/src/Moq/Moq.Sdk/SetupScope.cs new file mode 100644 index 00000000..504c3661 --- /dev/null +++ b/src/Moq/Moq.Sdk/SetupScope.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; + +namespace Moq +{ + /// + /// Marks the mock as being set up, meaning + /// invocation tracking and strict behavior + /// will be suspended until the scope is + /// is disposed. + /// + /// + /// We make an exception with the namespace for this + /// type, since it's intended to be used by users + /// quite regularly, unless they are leveraging the + /// using static Moq.Syntax; which allows the + /// simpler using (Setup()) { ... } syntax. + /// + public class SetupScope : IDisposable + { + static AsyncLocal setup = new AsyncLocal(); + + /// + /// Initializes the setup scope. + /// + public SetupScope() => setup.Value = true; + + /// + /// Whether there is an active setup scope in the running + /// execution context. + /// + public static bool IsActive => setup.Value == true; + + /// + /// Disposes the scope, setting + /// back to . + /// + public void Dispose() => setup.Value = null; + } +} \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/StrictMockBehavior.cs b/src/Moq/Moq.Sdk/StrictMockBehavior.cs index 5b2e19e9..16f509dc 100644 --- a/src/Moq/Moq.Sdk/StrictMockBehavior.cs +++ b/src/Moq/Moq.Sdk/StrictMockBehavior.cs @@ -24,7 +24,7 @@ public IMethodReturn Invoke(IMethodInvocation invocation, GetNextBehavior getNex { if (invocation == null) throw new ArgumentNullException(nameof(invocation)); - if (!KnownStates.InSetup(invocation?.Target)) + if (!SetupScope.IsActive) throw new StrictMockException(); // Otherwise, fallback to returning default values so that diff --git a/src/Moq/Moq.Tests/Mocks/IRecursiveBranchMock.cs b/src/Moq/Moq.Tests/Mocks/IRecursiveBranchMock.cs index 5ea0eb48..b249c6d5 100644 --- a/src/Moq/Moq.Tests/Mocks/IRecursiveBranchMock.cs +++ b/src/Moq/Moq.Tests/Mocks/IRecursiveBranchMock.cs @@ -7,24 +7,27 @@ // //------------------------------------------------------------------------------ +using Moq.Tests.Recursive; +using System.Threading; +using Moq.Sdk; using System; using System.Collections.ObjectModel; using System.Reflection; using Stunts; using System.Runtime.CompilerServices; -using Moq.Tests.Recursive; -using System.Threading; -using Moq.Sdk; namespace Mocks { public partial class IRecursiveBranchMock : IRecursiveBranch, IStunt, IMocked { readonly BehaviorPipeline pipeline = new BehaviorPipeline(); + IMock mock; [CompilerGenerated] ObservableCollection IStunt.Behaviors => pipeline.Behaviors; + IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock(this)); + [CompilerGenerated] public IRecursiveLeaf Leaf => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); @@ -33,13 +36,8 @@ public partial class IRecursiveBranchMock : IRecursiveBranch, IStunt, IMocked [CompilerGenerated] public override int GetHashCode() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); [CompilerGenerated] - public override string ToString() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); - - #region IMocked - IMock mock; - + public IRecursiveLeaf GetLeaf(int index) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), index)); [CompilerGenerated] - IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock(this)); - #endregion + public override string ToString() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); } } \ No newline at end of file diff --git a/src/Moq/Moq.Tests/Mocks/IRecursiveRootMock.cs b/src/Moq/Moq.Tests/Mocks/IRecursiveRootMock.cs index 1837bfd9..573ac9d9 100644 --- a/src/Moq/Moq.Tests/Mocks/IRecursiveRootMock.cs +++ b/src/Moq/Moq.Tests/Mocks/IRecursiveRootMock.cs @@ -7,24 +7,27 @@ // //------------------------------------------------------------------------------ +using Moq.Tests.Recursive; +using System.Threading; +using Moq.Sdk; using System; using System.Collections.ObjectModel; using System.Reflection; using Stunts; using System.Runtime.CompilerServices; -using Moq.Tests.Recursive; -using System.Threading; -using Moq.Sdk; namespace Mocks { public partial class IRecursiveRootMock : IRecursiveRoot, IStunt, IMocked { readonly BehaviorPipeline pipeline = new BehaviorPipeline(); + IMock mock; [CompilerGenerated] ObservableCollection IStunt.Behaviors => pipeline.Behaviors; + IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock(this)); + [CompilerGenerated] public IRecursiveBranch Branch => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); @@ -33,13 +36,8 @@ public partial class IRecursiveRootMock : IRecursiveRoot, IStunt, IMocked [CompilerGenerated] public override int GetHashCode() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); [CompilerGenerated] - public override string ToString() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); - - #region IMocked - IMock mock; - + public IRecursiveLeaf GetLeaf(int index) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), index)); [CompilerGenerated] - IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock(this)); - #endregion + public override string ToString() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); } } \ No newline at end of file diff --git a/src/Moq/Moq.Tests/MoqTests.cs b/src/Moq/Moq.Tests/MoqTests.cs index 9c7f2046..4c63fbd7 100644 --- a/src/Moq/Moq.Tests/MoqTests.cs +++ b/src/Moq/Moq.Tests/MoqTests.cs @@ -213,7 +213,7 @@ public void CanSetupPropertyViaReturnsForStrictMock() var calculator = Mock.Of(MockBehavior.Strict); // NOTE: since the mock is strict, we need to tell we're going to set it up - using (calculator.Setup()) + using (Setup()) { calculator.Mode.Returns(CalculatorMode.Scientific); } diff --git a/src/Moq/Moq.Tests/RecursiveMocks.cs b/src/Moq/Moq.Tests/RecursiveMocks.cs deleted file mode 100644 index 97aeb826..00000000 --- a/src/Moq/Moq.Tests/RecursiveMocks.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Xunit; - -namespace Moq.Tests.Recursive -{ - public class RecursiveMocks - { - - [Fact(Skip = "Pending support for recursive mocks")] - public void CanSetupRecursiveMock() - { - var mock = Mock.Of(MockBehavior.Loose); - - mock.Setup(m => m.Branch.Leaf.Name).Returns("foo"); - - Assert.Equal("foo", mock.Branch.Leaf.Name); - } - } - - public interface IRecursiveRoot - { - IRecursiveBranch Branch { get; } - } - - public interface IRecursiveBranch - { - IRecursiveLeaf Leaf { get; } - } - - public interface IRecursiveLeaf - { - string Name { get; set; } - } -} diff --git a/src/Moq/Moq.Tests/RecursiveMocksTests.cs b/src/Moq/Moq.Tests/RecursiveMocksTests.cs new file mode 100644 index 00000000..b56adc02 --- /dev/null +++ b/src/Moq/Moq.Tests/RecursiveMocksTests.cs @@ -0,0 +1,60 @@ +using Xunit; +using static Moq.Syntax; + +namespace Moq.Tests.Recursive +{ + public class RecursiveMocksTests + { + [Fact] + public void CanSetupRecursiveMockProperty() + { + var mock = Mock.Of(MockBehavior.Loose); + + mock.Setup(m => m.Branch.Leaf.Name).Returns("foo"); + + Assert.Equal("foo", mock.Branch.Leaf.Name); + } + + [Fact] + public void CanSetupRecursiveMockMethod() + { + var mock = Mock.Of(MockBehavior.Loose); + + mock.Setup(m => m.Branch.GetLeaf(1).Name).Returns("foo"); + + Assert.Equal("foo", mock.Branch.GetLeaf(1).Name); + Assert.Equal(null, mock.Branch.GetLeaf(0)); + } + + [Fact] + public void CanSetupRecursiveMockMethodInSetupScope() + { + var mock = Mock.Of(MockBehavior.Loose); + + using (Setup()) + { + mock.Branch.GetLeaf(1).Name.Returns("foo"); + } + + Assert.Equal("foo", mock.Branch.GetLeaf(1).Name); + Assert.Equal(null, mock.Branch.GetLeaf(0)); + } + } + + public interface IRecursiveRoot + { + IRecursiveBranch Branch { get; } + } + + public interface IRecursiveBranch + { + IRecursiveLeaf Leaf { get; } + + IRecursiveLeaf GetLeaf(int index); + } + + public interface IRecursiveLeaf + { + string Name { get; set; } + } +} diff --git a/src/Moq/Moq/CallbackExtension.cs b/src/Moq/Moq/CallbackExtension.cs index 955d92be..7349d88d 100644 --- a/src/Moq/Moq/CallbackExtension.cs +++ b/src/Moq/Moq/CallbackExtension.cs @@ -23,7 +23,7 @@ static TResult Callback(this TResult target, Action 0) { var wrapped = behavior.Behaviors.Pop(); diff --git a/src/Moq/Moq/Extensions.cs b/src/Moq/Moq/Extensions.cs index c19fcc23..b29c9778 100644 --- a/src/Moq/Moq/Extensions.cs +++ b/src/Moq/Moq/Extensions.cs @@ -6,6 +6,8 @@ namespace Moq { static class Extensions { + const string TaskFullName = "System.Threading.Tasks.Task"; + public static void EnsureCompatible(this IMethodInvocation invocation, Delegate @delegate) { var method = @delegate.GetMethodInfo(); @@ -14,5 +16,10 @@ public static void EnsureCompatible(this IMethodInvocation invocation, Delegate // TODO: validate assignability } + + public static bool CanBeIntercepted(this Type type) + => !type.FullName.StartsWith(TaskFullName, StringComparison.Ordinal) && + (type.GetTypeInfo().IsInterface || + (type.GetTypeInfo().IsClass && !type.GetTypeInfo().IsSealed)); } } diff --git a/src/Moq/Moq/ISetup.cs b/src/Moq/Moq/ISetup.cs new file mode 100644 index 00000000..37807284 --- /dev/null +++ b/src/Moq/Moq/ISetup.cs @@ -0,0 +1,10 @@ +namespace Moq +{ + /// + /// Extension point interface for extension methods + /// that act on void methods via . + /// + public interface ISetup : IFluentInterface + { + } +} diff --git a/src/Moq/Moq/RecursiveMockBehavior.cs b/src/Moq/Moq/RecursiveMockBehavior.cs new file mode 100644 index 00000000..9c559cd0 --- /dev/null +++ b/src/Moq/Moq/RecursiveMockBehavior.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Reflection; +using Stunts; + +namespace Moq.Sdk +{ + /// + /// + /// + public class RecursiveMockBehavior : IStuntBehavior + { + public bool AppliesTo(IMethodInvocation invocation) + => SetupScope.IsActive; + + public IMethodReturn Invoke(IMethodInvocation invocation, GetNextBehavior getNext) + { + if (invocation.MethodBase is MethodInfo info && + info.ReturnType != typeof(void) && + info.ReturnType.CanBeIntercepted()) + { + var result = getNext().Invoke(invocation, getNext); + if (result.ReturnValue == null) + { + // Turn the null value into a mock for the current invocation setup + var currentMock = ((IMocked)invocation.Target).Mock; + var recursiveMock = ((IMocked)MockFactory.Default.CreateMock( + // Use the same assembly as the current target + invocation.Target.GetType().GetTypeInfo().Assembly, + info.ReturnType, + new Type[0], + new object[0])).Mock; + + // Clone the current mock's behaviors, except for the setups + foreach (var behavior in currentMock.Behaviors.Where(x => !(x is IMockBehavior))) + recursiveMock.Behaviors.Add(behavior); + + // Set up the current invocation to return the created value + var setup = currentMock.BehaviorFor(MockContext.CurrentSetup); + var returnBehavior = setup.Behaviors.OfType().FirstOrDefault(); + if (returnBehavior != null) + returnBehavior.ValueGetter = () => recursiveMock.Object; + else + setup.Behaviors.Add(new ReturnsBehavior(() => recursiveMock.Object)); + + // Copy over values from the result, so that outputs contain the default values. + var arguments = invocation.Arguments.ToArray(); + var parameters = invocation.MethodBase.GetParameters(); + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + // This covers both out & ref + if (parameter.ParameterType.IsByRef) + arguments[i] = result.Outputs[parameter.Name]; + } + + return invocation.CreateValueReturn(recursiveMock.Object, arguments); + } + + return result; + } + + return getNext().Invoke(invocation, getNext); + } + } +} diff --git a/src/Moq/Moq/ReturnsBehavior.cs b/src/Moq/Moq/ReturnsBehavior.cs index fbe4d02d..a8d68db2 100644 --- a/src/Moq/Moq/ReturnsBehavior.cs +++ b/src/Moq/Moq/ReturnsBehavior.cs @@ -7,7 +7,7 @@ namespace Moq { /// /// A custom behavior for returning values, so that - /// the actual value to return can be replaced on succesive + /// the actual value to return can be replaced on successive /// method calls. /// [DebuggerDisplay("{DebuggerValue}", Name = "Returns", Type = nameof(ReturnsBehavior))] diff --git a/src/Moq/Moq/ReturnsExtension.cs b/src/Moq/Moq/ReturnsExtension.cs index f1361b4a..fd2e788c 100644 --- a/src/Moq/Moq/ReturnsExtension.cs +++ b/src/Moq/Moq/ReturnsExtension.cs @@ -66,8 +66,8 @@ static TResult Returns(Delegate value, InvokeBehavior behavior) var setup = MockContext.CurrentSetup; if (setup != null) { - // TODO: Is this even necessary given that intellisense gives us - // the rigth compiler safety already? + // TODO: Is this even necessary given that IntelliSense gives us + // the right compiler safety already? setup.Invocation.EnsureCompatible(value); var mock = ((IMocked)setup.Invocation.Target).Mock; diff --git a/src/Moq/Moq/Setup.cs b/src/Moq/Moq/Setup.cs deleted file mode 100644 index a9d2dc32..00000000 --- a/src/Moq/Moq/Setup.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.ComponentModel; -using Moq.Properties; -using Moq.Sdk; -using Stunts; - -namespace Moq -{ - /// - /// Extension for marking a mock as being set up, meaning - /// invocation tracking should be suspended. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class SetupExtension - { - /// - /// Marks the mock as being set up, meaning - /// invocation tracking and strict behavior - /// should be suspended until the returned - /// is disposed. - /// - /// - /// - public static IDisposable Setup(this object mock) - { - var target = mock is IMocked mocked ? - mocked.Mock : throw new ArgumentException(Strings.TargetNotMock, nameof(mock)); - - return new SetupDisposable(target); - } - - public static ISetup Setup(this T mock, Action action) - { - using (Setup(mock)) - { - action(mock); - // TODO: what if the extension method - return new SetupAdapter(MockContext.CurrentSetup); - } - } - - public static TResult Setup(this T mock, Func function) - { - using (Setup(mock)) - { - var stunt = mock as IStunt ?? throw new ArgumentException(Strings.TargetNotMock, nameof(mock)); - return function(mock); - } - } - - class SetupDisposable : IDisposable - { - IMock mock; - - public SetupDisposable(IMock mock) - { - this.mock = mock; - mock.State.TryAdd(KnownStates.Setup, true); - } - - public void Dispose() => mock.State.TryRemove(KnownStates.Setup, out var _); - } - } - - class SetupAdapter : ISetup, IFluentInterface - { - public SetupAdapter(IMockSetup setup) => Setup = setup; - - internal IMockSetup Setup { get; } - } - - public interface ISetup - { - } -} - -namespace Moq.Sdk -{ - public static class ISetupExtension - { - public static IMockSetup Sdk(this ISetup setup) => (setup as SetupAdapter)?.Setup; - } -} \ No newline at end of file diff --git a/src/Moq/Moq/SetupExtension.cs b/src/Moq/Moq/SetupExtension.cs new file mode 100644 index 00000000..107d83e8 --- /dev/null +++ b/src/Moq/Moq/SetupExtension.cs @@ -0,0 +1,48 @@ +using System; +using System.ComponentModel; +using Moq.Properties; +using Moq.Sdk; +using Stunts; + +namespace Moq +{ + /// + /// Extension for initiating a setup block, meaning + /// invocation tracking and strict mock behavior should + /// be suspended. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class SetupExtension + { + /// + /// Sets up the mock with the given void method call. + /// + public static ISetup Setup(this T mock, Action action) + { + using (new SetupScope()) + { + action(mock); + return DefaultSetup.Default; + } + } + + /// + /// Sets up the mock with the given function. + /// + public static TResult Setup(this T mock, Func function) + { + using (new SetupScope()) + { + var stunt = mock as IStunt ?? throw new ArgumentException(Strings.TargetNotMock, nameof(mock)); + return function(mock); + } + } + + class DefaultSetup : ISetup + { + public static ISetup Default { get; } = new DefaultSetup(); + + DefaultSetup() { } + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq/Syntax.cs b/src/Moq/Moq/Syntax.cs index 22b7fec2..5809480a 100644 --- a/src/Moq/Moq/Syntax.cs +++ b/src/Moq/Moq/Syntax.cs @@ -1,4 +1,5 @@ using System; +using Moq.Sdk; namespace Moq { @@ -55,5 +56,10 @@ public static class Syntax /// as the event arguments. /// public static TEventHandler Raise(params object[] args) => Moq.Raise.Event(args); + + /// + /// Marks a code block as being setup for mocks. Usage: using (Setup()) { ... }. + /// + public static IDisposable Setup() => new SetupScope(); } } diff --git a/src/Stunts/Stunts.Analyzer/DiagnosticsExtensions.cs b/src/Stunts/Stunts.Analyzer/DiagnosticsExtensions.cs index 30ff0d40..8c7670ce 100644 --- a/src/Stunts/Stunts.Analyzer/DiagnosticsExtensions.cs +++ b/src/Stunts/Stunts.Analyzer/DiagnosticsExtensions.cs @@ -21,7 +21,7 @@ public static class DiagnosticsExtensions }; /// - /// Gets the diangostics that represent build errors that happen when generated + /// Gets the diagnostics that represent build errors that happen when generated /// code is out of date. /// public static Diagnostic[] GetCompilationErrors(this Compilation compilation) diff --git a/src/Stunts/Stunts.Analyzer/StuntCodeAction.cs b/src/Stunts/Stunts.Analyzer/StuntCodeAction.cs index 9aec984c..3c498706 100644 --- a/src/Stunts/Stunts.Analyzer/StuntCodeAction.cs +++ b/src/Stunts/Stunts.Analyzer/StuntCodeAction.cs @@ -50,18 +50,12 @@ protected override async Task GetChangedDocumentAsync(CancellationToke .ToArray(); var generator = SyntaxGenerator.GetGenerator(document.Project); - var stunt = await CreateGenerator(naming).GenerateDocumentAsync(document.Project, symbols, cancellationToken); + var stunts = CreateGenerator(naming); - // This is somewhat expensive, but since we're adding it to the user' solution, we might - // as well make it look great ;) - stunt = await Simplifier.ReduceAsync(stunt).ConfigureAwait(false); - if (document.Project.Language != LanguageNames.VisualBasic) - stunt = await Formatter.FormatAsync(stunt, Formatter.Annotation).ConfigureAwait(false); - - return stunt; + return await CreateStunt(symbols, generator, stunts, cancellationToken); } - async Task CreateStunt(IEnumerable symbols, SyntaxGenerator generator, StuntGenerator stunts, CancellationToken cancellationToken) + async Task CreateStunt(IEnumerable symbols, SyntaxGenerator generator, StuntGenerator stunts, CancellationToken cancellationToken) { var (name, syntax) = stunts.CreateStunt(symbols, generator); @@ -106,7 +100,7 @@ async Task CreateStunt(IEnumerable symbols, SyntaxGe syntax = await stuntDoc.GetSyntaxRootAsync().ConfigureAwait(false); - return stuntDoc.WithSyntaxRoot(syntax).Project.Solution; + return stuntDoc.WithSyntaxRoot(syntax); } } } diff --git a/src/Stunts/Stunts.Analyzer/StuntGeneratorAnalyzer.cs b/src/Stunts/Stunts.Analyzer/StuntGeneratorAnalyzer.cs index 074effa9..ca88f156 100644 --- a/src/Stunts/Stunts.Analyzer/StuntGeneratorAnalyzer.cs +++ b/src/Stunts/Stunts.Analyzer/StuntGeneratorAnalyzer.cs @@ -50,12 +50,12 @@ protected StuntGeneratorAnalyzer(NamingConvention naming, Type generatorAttribut public virtual DiagnosticDescriptor OutdatedDiagnostic => StuntDiagnostics.OutdatedStunt; /// - /// Returns the single this analyer supports. + /// Returns the single this analyzer supports. /// public sealed override ImmutableArray SupportedDiagnostics { // NOTE: this creates the return value at this point because both Missing and Outdated - // descriptors can be overriden as a customization point. + // descriptors can be overridden as a customization point. get { return ImmutableArray.Create(MissingDiagnostic, OutdatedDiagnostic); } } @@ -74,7 +74,7 @@ void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) var generator = context.Compilation.GetTypeByMetadataName(generatorAttribute.FullName); if (generator == null) // This may be an extender authoring error, but another analyzer should ensure proper - // metadata references exist. Typically, the same nuget package that adds this analyzer + // metadata references exist. Typically, the same NuGet package that adds this analyzer // also adds the required assembly references, so this should never happen anyway. return; diff --git a/src/Stunts/Stunts.Analyzer/UnsupportedNestedTypeAnalyzer.cs b/src/Stunts/Stunts.Analyzer/UnsupportedNestedTypeAnalyzer.cs index a5ca4342..f47c11e5 100644 --- a/src/Stunts/Stunts.Analyzer/UnsupportedNestedTypeAnalyzer.cs +++ b/src/Stunts/Stunts.Analyzer/UnsupportedNestedTypeAnalyzer.cs @@ -12,7 +12,7 @@ namespace Stunts /// the and reports unsupported nested types for /// codegen. /// - // TODO: this is a stop-gap meassure until we can figure out why the ImplementInterfaces codefix + // TODO: this is a stop-gap measure until we can figure out why the ImplementInterfaces codefix // is not returning any code actions for the CSharpScaffold (maybe VB too?) when the type to // be implemented is a nested interface :(. In the IDE, the code action is properly available after // generating the (non-working, since the interface isn't implemented) mock class. @@ -58,7 +58,7 @@ protected UnsupportedNestedTypeAnalyzer(NamingConvention naming, Type generatorA } /// - /// Returns the single descriptor this analyer supports. + /// Returns the single descriptor this analyzer supports. /// public sealed override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(descriptor); @@ -77,7 +77,7 @@ void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) var generator = context.Compilation.GetTypeByMetadataName(generatorAttribute.FullName); if (generator == null) // This may be an extender authoring error, but another analyzer should ensure proper - // metadata references exist. Typically, the same nuget package that adds this analyzer + // metadata references exist. Typically, the same NuGet package that adds this analyzer // also adds the required assembly references, so this should never happen anyway. return; diff --git a/src/Stunts/Stunts.Analyzer/ValidateTypesAnalyzer.cs b/src/Stunts/Stunts.Analyzer/ValidateTypesAnalyzer.cs index 0c5ea99c..3d60026e 100644 --- a/src/Stunts/Stunts.Analyzer/ValidateTypesAnalyzer.cs +++ b/src/Stunts/Stunts.Analyzer/ValidateTypesAnalyzer.cs @@ -33,7 +33,7 @@ protected ValidateTypesAnalyzer(Type generatorAttribute) /// /// Returns the single - /// diagnostic this analyer supports. + /// diagnostic this analyzer supports. /// public sealed override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( diff --git a/src/Stunts/Stunts.Sdk/ProcessorPhase.cs b/src/Stunts/Stunts.Sdk/ProcessorPhase.cs index 291d29ec..dbdb270f 100644 --- a/src/Stunts/Stunts.Sdk/ProcessorPhase.cs +++ b/src/Stunts/Stunts.Sdk/ProcessorPhase.cs @@ -33,7 +33,7 @@ public enum ProcessorPhase /// /// Final phase that allows generators to perform additional generation beyond scaffold - /// and inital stunt rewriting. Members generated in this phase are not rewritten at all + /// and initial stunt rewriting. Members generated in this phase are not rewritten at all /// to use the and can consist of language-specific fixups /// or cleanups to make the generated code more idiomatic than the default code fixes may /// provide. diff --git a/src/Stunts/Stunts.Sdk/Processors/DefaultImports.cs b/src/Stunts/Stunts.Sdk/Processors/DefaultImports.cs index 7c567346..02c84dba 100644 --- a/src/Stunts/Stunts.Sdk/Processors/DefaultImports.cs +++ b/src/Stunts/Stunts.Sdk/Processors/DefaultImports.cs @@ -34,7 +34,7 @@ public class DefaultImports : IDocumentProcessor public DefaultImports() : this(DefaultNamespaces) { } /// - /// Initializes the default imports a specific set of namespaces to add. + /// Initializes the default imports with a specific set of namespaces to add. /// public DefaultImports(params string[] namespaces) => this.namespaces = namespaces; diff --git a/src/Stunts/Stunts.Sdk/Processors/SyntaxGeneratorExtensions.cs b/src/Stunts/Stunts.Sdk/Processors/SyntaxGeneratorExtensions.cs index c66fd112..9d7b2258 100644 --- a/src/Stunts/Stunts.Sdk/Processors/SyntaxGeneratorExtensions.cs +++ b/src/Stunts/Stunts.Sdk/Processors/SyntaxGeneratorExtensions.cs @@ -7,7 +7,7 @@ namespace Stunts.Processors { /// - /// Language angnosti helper methods for code generation. + /// Language agnostic helper methods for code generation. /// static class SyntaxGeneratorExtensions { diff --git a/src/Stunts/Stunts.Sdk/StuntGenerator.cs b/src/Stunts/Stunts.Sdk/StuntGenerator.cs index 60f382d6..9f13d6ab 100644 --- a/src/Stunts/Stunts.Sdk/StuntGenerator.cs +++ b/src/Stunts/Stunts.Sdk/StuntGenerator.cs @@ -188,8 +188,8 @@ public StuntGenerator(NamingConvention naming, IEnumerable p public async Task ApplyProcessors(Document document, CancellationToken cancellationToken) { #if DEBUG - // While debugging the geneneration itself, don't let the cancellation timeouts - // from tests to cause this to fail. + // While debugging the generation itself, don't let the cancellation timeouts + // from tests cause this to fail. if (Debugger.IsAttached) cancellationToken = CancellationToken.None; #endif diff --git a/src/Stunts/Stunts.Sdk/SymbolExtensions.cs b/src/Stunts/Stunts.Sdk/SymbolExtensions.cs index 975681cd..e9baa04d 100644 --- a/src/Stunts/Stunts.Sdk/SymbolExtensions.cs +++ b/src/Stunts/Stunts.Sdk/SymbolExtensions.cs @@ -13,7 +13,6 @@ namespace Stunts public static class SymbolExtensions { const string TaskFullName = "System.Threading.Tasks.Task"; - const string ValueTupleFullName = "System.ValueTuple"; /// /// Retrieves the distinct set of symbols that can be intercepted from the @@ -136,7 +135,6 @@ public static bool TryValidateGeneratorTypes(this IEnumerable types public static bool CanBeIntercepted(this ITypeSymbol symbol) => symbol.CanBeReferencedByName && !symbol.ToString().StartsWith(TaskFullName, StringComparison.Ordinal) && - !symbol.ToString().StartsWith(ValueTupleFullName, StringComparison.Ordinal) && (symbol.TypeKind == TypeKind.Interface || (symbol?.TypeKind == TypeKind.Class && symbol?.IsSealed == false)); diff --git a/src/Stunts/Stunts/BehaviorPipeline.cs b/src/Stunts/Stunts/BehaviorPipeline.cs index bac62247..61015ac1 100644 --- a/src/Stunts/Stunts/BehaviorPipeline.cs +++ b/src/Stunts/Stunts/BehaviorPipeline.cs @@ -67,7 +67,7 @@ public BehaviorPipeline() /// Input to the method call. /// The ultimate target of the call. /// Whether to throw the if it has a value after running - /// the beaviors. + /// the behaviors. /// Return value from the pipeline. public IMethodReturn Invoke(IMethodInvocation invocation, InvokeBehavior target, bool throwOnException = false) { diff --git a/src/Stunts/Stunts/IStuntFactory.cs b/src/Stunts/Stunts/IStuntFactory.cs index cb579f8d..956e8bba 100644 --- a/src/Stunts/Stunts/IStuntFactory.cs +++ b/src/Stunts/Stunts/IStuntFactory.cs @@ -15,7 +15,7 @@ public interface IStuntFactory /// The base type (or main interface) of the stunt. /// Additional interfaces implemented by the stunt, or an empty array. /// - /// Contructor arguments if the is a class, rather than an interface, or an empty array. + /// Constructor arguments if the is a class, rather than an interface, or an empty array. /// /// A stunt that implements in addition to the specified interfaces (if any). object CreateStunt(Assembly stuntsAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments); diff --git a/src/Stunts/Stunts/StuntExtensions.cs b/src/Stunts/Stunts/StuntExtensions.cs index 15fb9a64..884769ae 100644 --- a/src/Stunts/Stunts/StuntExtensions.cs +++ b/src/Stunts/Stunts/StuntExtensions.cs @@ -31,7 +31,7 @@ public static IStunt AddBehavior(this IStunt stunt, IStuntBehavior behavior) public static TStunt AddBehavior(this TStunt stunt, InvokeBehavior behavior, AppliesTo appliesTo = null, string name = null) { // We can't just add a constraint to the method signature, because - // proxies are typically geneated and don't expose the IProxy interface directly. + // proxies are typically generated and don't expose the IProxy interface directly. if (stunt is IStunt target) target.Behaviors.Add(StuntBehavior.Create(behavior, appliesTo, name)); else @@ -74,7 +74,7 @@ public static IStunt InsertBehavior(this IStunt stunt, int index, IStuntBehavior } /// - /// Inserts a behavior into the stunt behasvior pipeline at the specified + /// Inserts a behavior into the stunt behavior pipeline at the specified /// index. /// public static TStunt InsertBehavior(this TStunt stunt, int index, InvokeBehavior behavior, AppliesTo appliesTo = null, string name = null) @@ -88,7 +88,7 @@ public static TStunt InsertBehavior(this TStunt stunt, int index, Invoke } /// - /// Inserts a behavior into the stunt behasvior pipeline at the specified + /// Inserts a behavior into the stunt behavior pipeline at the specified /// index. /// public static TStunt InsertBehavior(this TStunt stunt, int index, IStuntBehavior behavior) diff --git a/src/Stunts/Stunts/Stunts.csproj b/src/Stunts/Stunts/Stunts.csproj index 1ceaa077..2a49a5e4 100644 --- a/src/Stunts/Stunts/Stunts.csproj +++ b/src/Stunts/Stunts/Stunts.csproj @@ -16,8 +16,4 @@ - - - - \ No newline at end of file diff --git a/src/build/Version.targets b/src/build/Version.targets index 1a586207..99bbf446 100644 --- a/src/build/Version.targets +++ b/src/build/Version.targets @@ -20,7 +20,7 @@ AssemblyVersion=$(AssemblyVersion)" /> - + $(GitSemVerDashLabel)-pr$(BUILD_SOURCEBRANCH.Substring(10).TrimEnd('/merge')) $(GitSemVerDashLabel)-pr$(APPVEYOR_PULL_REQUEST_NUMBER)