diff --git a/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs b/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs index 6f7f2ab8b6..818c26fb5e 100644 --- a/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs +++ b/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using Rubberduck.Parsing.ComReflection; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; using Rubberduck.VBEditor; using Rubberduck.VBEditor.SafeComWrappers; using Rubberduck.VBEditor.SafeComWrappers.Abstract; diff --git a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs b/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs deleted file mode 100644 index ff184a6a1e..0000000000 --- a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices.ComTypes; -using Microsoft.Win32; - -namespace Rubberduck.AddRemoveReferences -{ - public interface IRegisteredLibraryFinderService - { - IEnumerable FindRegisteredLibraries(); - } - - // inspired from https://github.com/rossknudsen/Kavod.ComReflection - public class RegisteredLibraryFinderService : IRegisteredLibraryFinderService - { - private static readonly List IgnoredKeys = new List { "FLAGS", "HELPDIR" }; - - public IEnumerable FindRegisteredLibraries() - { - using (var typelibSubKey = Registry.ClassesRoot.OpenSubKey("TypeLib")) - { - if (typelibSubKey == null) { yield break; } - - foreach (var guidKey in EnumerateSubKeys(typelibSubKey)) - { - var guid = Guid.TryParseExact(guidKey.GetKeyName().ToLowerInvariant(), "B", out var clsid) - ? clsid - : Guid.Empty; - - foreach (var versionKey in EnumerateSubKeys(guidKey)) - { - var name = versionKey.GetValue(string.Empty)?.ToString(); - var version = versionKey.GetKeyName(); - - var flagValue = (LIBFLAGS)0; - using (var flagsKey = versionKey.OpenSubKey("FLAGS")) - { - if (flagsKey != null) - { - var flags = flagsKey.GetValue(string.Empty)?.ToString() ?? "0"; - Enum.TryParse(flags, out flagValue); - } - } - - foreach (var lcid in versionKey.GetSubKeyNames().Where(key => !IgnoredKeys.Contains(key))) - { - if (!int.TryParse(lcid, out var id)) - { - continue; - } - using (var paths = versionKey.OpenSubKey(lcid)) - { - string bit32; - string bit64; - using (var win32 = paths?.OpenSubKey("win32")) - { - bit32 = win32?.GetValue(string.Empty)?.ToString() ?? string.Empty; - } - using (var win64 = paths?.OpenSubKey("win64")) - { - bit64 = win64?.GetValue(string.Empty)?.ToString() ?? string.Empty; - } - - yield return new RegisteredLibraryInfo(guid, name, version, bit32, bit64) - { - Flags = flagValue, - LocaleId = id - }; - } - } - } - } - } - } - - private IEnumerable EnumerateSubKeys(RegistryKey key) - { - foreach (var keyName in key.GetSubKeyNames()) - { - using (var subKey = key.OpenSubKey(keyName)) - { - if (subKey != null) - { - yield return subKey; - } - } - } - } - } - - public static class RegistryKeyExtensions - { - public static string GetKeyName(this RegistryKey key) - { - var name = key?.Name; - return name?.Substring(name.LastIndexOf(@"\", StringComparison.InvariantCultureIgnoreCase) + 1) ?? string.Empty; - } - } -} \ No newline at end of file diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs index 923190a63b..43ebc9c507 100644 --- a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs @@ -5,6 +5,7 @@ using System.Windows.Forms; using NLog; using Rubberduck.AddRemoveReferences; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; using Rubberduck.Parsing.Symbols; using Rubberduck.Parsing.VBA; using Rubberduck.Settings; diff --git a/Rubberduck.Core/UI/Command/ComCommands/ReparseCommand.cs b/Rubberduck.Core/UI/Command/ComCommands/ReparseCommand.cs index 945bf386e2..4b806898bf 100644 --- a/Rubberduck.Core/UI/Command/ComCommands/ReparseCommand.cs +++ b/Rubberduck.Core/UI/Command/ComCommands/ReparseCommand.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Runtime.InteropServices; using Rubberduck.Interaction; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; using Rubberduck.Parsing.VBA; using Rubberduck.Resources; using Rubberduck.Settings; @@ -30,6 +31,7 @@ public class ReparseCommand : ComCommandBase private readonly IMessageBox _messageBox; private readonly RubberduckParserState _state; private readonly GeneralSettings _settings; + private static readonly ICachedTypeService TypeCacheService = CachedTypeService.Instance; public ReparseCommand( IVBE vbe, @@ -93,6 +95,10 @@ protected override void OnExecute(object parameter) } } } + foreach (var project in _state.Projects) + { + TypeCacheService.TryInvalidate(project.Name); + } _state.OnParseRequested(this); } diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMock.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMock.cs new file mode 100644 index 0000000000..255970452a --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMock.cs @@ -0,0 +1,195 @@ +using Moq; +using Rubberduck.Resources.Registration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +// ReSharper disable InconsistentNaming + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.ComMockGuid), + ProgId(RubberduckProgId.ComMockProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(IComMock)) + ] + public class ComMock : IComMock + { + private readonly ComMocked mocked; + private readonly SetupArgumentResolver _resolver; + private readonly SetupExpressionBuilder _setupBuilder; + private readonly IMockProviderInternal _provider; + + internal ComMock(IMockProviderInternal provider, string project, string progId, Mock mock, Type type, IEnumerable supportedInterfaces) + { + Project = project; + ProgId = progId; + Mock = mock; + _provider = provider; + _resolver = new SetupArgumentResolver(); + _setupBuilder = new SetupExpressionBuilder(type, supportedInterfaces, _resolver); + MockedType = type; + + Mock.As().Setup(x => x.Mock).Returns(this); + mocked = new ComMocked(this, supportedInterfaces); + } + + public string Project { get; } + + public string ProgId { get; } + + /// + /// Refer to remarks in for how the + /// parameter is handled. + /// + public void Setup(string Name, object Args = null) + { + var args = _resolver.ResolveArgs(Args); + var setupDatas = _setupBuilder.CreateExpression(Name, args); + + foreach (var setupData in setupDatas) + { + var builder = MockExpressionBuilder.Create(Mock); + builder.As(setupData.DeclaringType) + .Setup(setupData.SetupExpression, setupData.Args) + .Execute(); + } + } + + /// + /// Refer to remarks in for how the + /// parameter is handled. + /// + public void SetupWithReturns(string Name, object Value, object Args = null) + { + var args = _resolver.ResolveArgs(Args); + var setupDatas = _setupBuilder.CreateExpression(Name, args); + + foreach (var setupData in setupDatas) + { + var builder = MockExpressionBuilder.Create(Mock); + builder.As(setupData.DeclaringType) + .Setup(setupData.SetupExpression, setupData.Args, setupData.ReturnType) + .Returns(Value, setupData.ReturnType) + .Execute(); + } + } + + /// + /// Refer to remarks in for how the + /// parameter is handled. + /// + public void SetupWithCallback(string Name, Action Callback, object Args = null) + { + var args = _resolver.ResolveArgs(Args); + var setupDatas = _setupBuilder.CreateExpression(Name, args); + + foreach (var setupData in setupDatas) + { + var builder = MockExpressionBuilder.Create(Mock); + builder.As(setupData.DeclaringType) + .Setup(setupData.SetupExpression, setupData.Args) + .Callback(Callback) + .Execute(); + } + } + + public IComMock SetupChildMock(string Name, object Args) + { + Type type; + var memberInfo = MockedType.GetMember(Name).FirstOrDefault(); + if (memberInfo == null) + { + memberInfo = MockedType.GetInterfaces().SelectMany(face => face.GetMember(Name)).First(); + } + + switch (memberInfo) + { + case FieldInfo fieldInfo: + type = fieldInfo.FieldType; + break; + case PropertyInfo propertyInfo: + type = propertyInfo.PropertyType; + break; + case MethodInfo methodInfo: + type = methodInfo.ReturnType; + break; + default: + throw new InvalidOperationException($"Couldn't resolve member {Name} and acquire a type to mock."); + } + + var childMock = _provider.MockChildObject(this, type); + var target = GetMockedObject(childMock, type); + SetupWithReturns(Name, target, Args); + + return childMock; + } + + private object GetMockedObject(IComMock mock, Type type) + { + var pUnkSource = IntPtr.Zero; + var pUnkTarget = IntPtr.Zero; + + try + { + pUnkSource = Marshal.GetIUnknownForObject(mock.Object); + var iid = type.GUID; + Marshal.QueryInterface(pUnkSource, ref iid, out pUnkTarget); + return Marshal.GetTypedObjectForIUnknown(pUnkTarget, type); + } + finally + { + if (pUnkTarget != IntPtr.Zero) Marshal.Release(pUnkTarget); + if (pUnkSource != IntPtr.Zero) Marshal.Release(pUnkSource); + } + } + + public void Verify(string Name, ITimes Times, [MarshalAs(UnmanagedType.Struct), Optional] object Args) + { + var args = _resolver.ResolveArgs(Args); + var setupDatas = _setupBuilder.CreateExpression(Name, args); + + var throwingExecutions = 0; + MockException lastException = null; + foreach (var setupData in setupDatas) + { + try + { + var builder = MockExpressionBuilder.Create(Mock); + builder.As(setupData.DeclaringType) + .Verify(setupData.SetupExpression, Times, setupData.Args) + .Execute(); + + Rubberduck.UnitTesting.AssertHandler.OnAssertSucceeded(); + } + catch (TargetInvocationException exception) + { + if (exception.InnerException is MockException inner) + { + throwingExecutions++; + lastException = inner; + } + else + { + throw; + } + } + } + if (setupDatas.Count() == throwingExecutions) + { + // if all mocked interfaces failed the .Verify call, then none of them succeeded: + Rubberduck.UnitTesting.AssertHandler.OnAssertFailed(lastException.Message); + } + } + + public object Object => mocked; + + internal Mock Mock { get; } + + internal Type MockedType { get; } + } +} \ No newline at end of file diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMocked.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMocked.cs new file mode 100644 index 0000000000..01bf5b4ba0 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ComMocked.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Moq; +using NLog; +using Rubberduck.Resources.Registration; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.IComMockedGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface IComMocked : IMocked + { + new ComMock Mock { get; } + } + + [ + ComVisible(true), + Guid(RubberduckGuid.ComMockedGuid), + ProgId(RubberduckProgId.ComMockedProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(IComMocked)) + ] + public class ComMocked : IComMocked, ICustomQueryInterface + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + private readonly IEnumerable _supportedInterfaces; + + // Not using auto-property as that leads to ambiguity. For COM compatibility, + // this backs the public field `Mock`, which hides the `Moq.IMocked.Mock` + // property that returns a non-COM-visible `Moq.Mock` object. + private readonly ComMock _comMock; + + internal ComMocked(ComMock mock, IEnumerable supportedInterfaces) + { + _comMock = mock; + _supportedInterfaces = supportedInterfaces; + } + + public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv) + { + try + { + var result = IntPtr.Zero; + var searchIid = iid; // Cannot use ref parameters directly in LINQ + + if (iid == new Guid(RubberduckGuid.ComMockedGuid) || iid == new Guid(RubberduckGuid.IComMockedGuid)) + { + result = Marshal.GetIUnknownForObject(this); + } + else if (iid == new Guid(RubberduckGuid.IID_IDispatch) && !string.IsNullOrWhiteSpace(Mock.Project)) + { + // We cannot return IDispatch directly for VBA types but we can return the IUnknown in its place, + // which is sufficient for COM's needs. + + var pObject = Marshal.GetComInterfaceForObject(_comMock.Mock.Object, _comMock.MockedType); + searchIid = new Guid(RubberduckGuid.IID_IUnknown); + var hr = Marshal.QueryInterface(pObject, ref searchIid, out result); + Marshal.Release(pObject); + if (hr < 0) + { + ppv = IntPtr.Zero; + return CustomQueryInterfaceResult.Failed; + } + } + else + { + // Apparently some COM objects have multiple interface implementations using same GUID + // so first result should suffice to avoid exception when using single. + var type = _supportedInterfaces.FirstOrDefault(x => x.GUID == searchIid); + if (type != null) + { + // Ensure that we return the actual Moq.Mock.Object, not the ComMocked object. + result = Marshal.GetComInterfaceForObject(_comMock.Mock.Object, type); + } + } + + ppv = result; + return result == IntPtr.Zero + ? CustomQueryInterfaceResult.NotHandled + : CustomQueryInterfaceResult.Handled; + } + catch (Exception ex) + { + Logger.Warn(ex, $"Failed to perform IQueryInterface call on {nameof(ComMocked)}. IID requested was {{{iid}}}."); + ppv = IntPtr.Zero; + return CustomQueryInterfaceResult.Failed; + } + } + + // ReSharper disable once ConvertToAutoPropertyWhenPossible -- Leads to ambiguous naming; see comments above + public ComMock Mock => _comMock; + + Mock IMocked.Mock => _comMock.Mock; + } +} \ No newline at end of file diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/IComMock.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/IComMock.cs new file mode 100644 index 0000000000..94ec8a3326 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/IComMock.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Rubberduck.Resources.Registration; + +// ReSharper disable InconsistentNaming + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.IComMockGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface IComMock + { + [DispId(1)] + [Description("Gets the mocked object.")] + object Object { [return: MarshalAs(UnmanagedType.IDispatch)] get; } + + [DispId(2)] + [Description("Gets the name of the loaded project defining the mocked interface.")] + string Project { get; } + + [DispId(3)] + [Description("Gets the programmatic name of the mocked interface.")] + string ProgId { get; } + + [DispId(4)] + [Description("Specifies a setup on the mocked type for a call to a method that does not return a value.")] + void Setup(string Name, [MarshalAs(UnmanagedType.Struct)] object Args = null); + + [DispId(5)] + [Description("Specifies a setup on the mocked type for a call to a value-returning method.")] + void SetupWithReturns(string Name, [MarshalAs(UnmanagedType.Struct)] object Value, [Optional, MarshalAs(UnmanagedType.Struct)] object Args); + + [DispId(6)] + [Description("Specifies a callback (use the AddressOf operator) to invoke when the method is called that receives the original invocation.")] + void SetupWithCallback(string Name, [MarshalAs(UnmanagedType.FunctionPtr)] Action Callback, [Optional, MarshalAs(UnmanagedType.Struct)] object Args); + + [DispId(7)] + [Description("Specifies a setup on the mocked type for a call to an object member of the specified object type.")] + IComMock SetupChildMock(string Name, [Optional, MarshalAs(UnmanagedType.Struct)] object Args); + + [DispId(9)] + [Description("Verifies that a specific invocation matching the given arguments was performed on the mock.")] + void Verify(string Name, ITimes Times, [Optional, MarshalAs(UnmanagedType.Struct)] object Args); + } +} \ No newline at end of file diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ITimes.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ITimes.cs new file mode 100644 index 0000000000..b5e28dbeac --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ITimes.cs @@ -0,0 +1,84 @@ +using Rubberduck.Resources.Registration; +using Rubberduck.VBEditor; +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ComVisible(true)] + [Guid(RubberduckGuid.ITimesGuid)] + [InterfaceType(ComInterfaceType.InterfaceIsDual)] + public interface ITimes + { + [Description("Specifies that a mocked method should be invoked CallCount times as maximum.")] + ITimes AtMost(int CallCount); + + [Description("Specifies that a mocked method should be invoked one time as maximum.")] + ITimes AtMostOnce(); + + [Description("Specifies that a mocked method should be invoked CallCount times as minimum.")] + ITimes AtLeast(int CallCount); + + [Description("Specifies that a mocked method should be invoked one time as minimum.")] + ITimes AtLeastOnce(); + + [Description("Specifies that a mocked method should be invoked between MinCallCount and MaxCallCount times.")] + ITimes Between(int MinCallCount, int MaxCallCount, SetupArgumentRange RangeKind = SetupArgumentRange.Inclusive); + + [Description("Specifies that a mocked method should be invoked exactly CallCount times.")] + ITimes Exactly(int CallCount); + + [Description("Specifies that a mocked method should be invoked exactly one time.")] + ITimes Once(); + + [Description("Specifies that a mocked method should not be invoked.")] + ITimes Never(); + } + + + public static class MoqTimesExt + { + public static ITimes ToRubberduckTimes(this Moq.Times times) => new Times(times); + } + + [ComVisible(true)] + [Guid(RubberduckGuid.TimesGuid)] + [ProgId(RubberduckProgId.TimesProgId)] + [ClassInterface(ClassInterfaceType.None)] + [ComDefaultInterface(typeof(ITimes))] + public class Times : ITimes, IEquatable + { + internal Times() { } + + internal Times(Moq.Times moqTimes) + { + MoqTimes = moqTimes; + } + + public bool Equals(Times other) => MoqTimes.Equals(other?.MoqTimes); + + public override bool Equals(object obj) => Equals(obj as Times); + + public override int GetHashCode() => MoqTimes.GetHashCode(); + + public ITimes AtMost(int CallCount) => new Times(Moq.Times.AtMost(CallCount)); + + public ITimes AtMostOnce() => new Times(Moq.Times.AtMostOnce()); + + public ITimes AtLeast(int CallCount) => new Times(Moq.Times.AtLeast(CallCount)); + + public ITimes AtLeastOnce() => new Times(Moq.Times.AtLeastOnce()); + + public ITimes Between(int MinCallCount, int MaxCallCount, SetupArgumentRange RangeKind = SetupArgumentRange.Inclusive) + => new Times(Moq.Times.Between(MinCallCount, MaxCallCount, RangeKind == SetupArgumentRange.Exclusive ? Moq.Range.Exclusive : Moq.Range.Inclusive )); + + public ITimes Exactly(int CallCount) => new Times(Moq.Times.Exactly(CallCount)); + + public ITimes Once() => new Times(Moq.Times.Once()); + + public ITimes Never() => new Times(Moq.Times.Never()); + + internal Moq.Times MoqTimes { get; } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRef.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRef.cs new file mode 100644 index 0000000000..4aabc15edb --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRef.cs @@ -0,0 +1,28 @@ +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + public class ItByRef + { + // Only a field of a class can be passed by-ref. + public T Value; + + public delegate void ByRefCallback(ref T input); + + public ByRefCallback Callback { get; } + + public static ItByRef Is(T initialValue) + { + return Is(initialValue, null); + } + + public static ItByRef Is(T initialValue, ByRefCallback action) + { + return new ItByRef(initialValue, action); + } + + private ItByRef(T initialValue, ByRefCallback callback) + { + Value = initialValue; + Callback = callback; + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRefReflection.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRefReflection.cs new file mode 100644 index 0000000000..06edbdf995 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/ItByRefReflection.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; +using ExpressiveReflection; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + public class ItByRefMemberInfos + { + public static Type ItByRef(Type type) + { + return typeof(ItByRef<>).MakeGenericType(type); + } + + public static MethodInfo Is(Type type) + { + return Reflection.GetMethodExt(ItByRef(type), nameof(ItByRef.Is), type); + } + + public static FieldInfo Value(Type type) + { + return ItByRef(type).GetField(nameof(ItByRef.Value)); + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockExpressionBuilder.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockExpressionBuilder.cs new file mode 100644 index 0000000000..21bb694fc1 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockExpressionBuilder.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using Moq; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + public interface IRuntimeMock + { + IRuntimeSetup As(Type targetInterface); + } + + public interface IRuntimeSetup : IRuntimeMock, IRuntimeCallback, IRuntimeExecute, IRuntimeVerify + { + IRuntimeCallback Setup(Expression setupExpression, IReadOnlyDictionary forwardedArgs); + IRuntimeReturns Setup(Expression setupExpression, IReadOnlyDictionary forwardedArgs, Type returnType); + } + + public interface IRuntimeVerify : IRuntimeExecute + { + IRuntimeVerify Verify(Expression verifyExpression, ITimes times, IReadOnlyDictionary forwardedArgs); + IRuntimeVerify Verify(Expression verifyExpression, ITimes times, IReadOnlyDictionary forwardedArgs, Type returnType); + } + + public interface IRuntimeCallback : IRuntimeExecute + { + IRuntimeSetup Callback(Action callback); + } + + public interface IRuntimeReturns : IRuntimeExecute + { + IRuntimeSetup Returns(object value, Type type); + } + + public interface IRuntimeExecute + { + object Execute(); + } + + // Some interfaces are already implemented by others but we list them all explicitly for clarity + [SuppressMessage("ReSharper", "RedundantExtendsListEntry")] + public class MockExpressionBuilder : + IRuntimeMock, + IRuntimeSetup, + IRuntimeCallback, + IRuntimeReturns, + IRuntimeVerify, + IRuntimeExecute + { + private readonly Mock _mock; + private readonly Type _mockType; + private readonly ParameterExpression _mockParameterExpression; + private readonly List _args; + private readonly List _lambdaArguments; + + private Expression _expression; + private Type _currentType; + + public static IRuntimeMock Create(Mock runtimeMock) + { + return new MockExpressionBuilder(runtimeMock); + } + + private MockExpressionBuilder(Mock runtimeMock) + { + _mock = runtimeMock; + _mockType = _mock.GetType(); + _mockParameterExpression = Expression.Parameter(_mockType, "mock"); + _args = new List(); + _lambdaArguments = new List + { + _mockParameterExpression + }; + } + + public IRuntimeSetup As(Type targetInterface) + { + var asMethodInfo = MockMemberInfos.As(targetInterface); + _expression = Expression.Call(_mockParameterExpression, asMethodInfo); + _currentType = asMethodInfo.ReturnType; + return this; + } + + public IRuntimeCallback Setup(Expression setupExpression, IReadOnlyDictionary forwardedArgs) + { + switch (setupExpression.Type.GetGenericArguments().Length) + { + case 2: + // It's a returning method so we need to use the Func version of Setup and ignore the return. + Setup(setupExpression, forwardedArgs, setupExpression.Type.GetGenericArguments()[1]); + return this; + case 1: + var setupMethodInfo = MockMemberInfos.Setup(_currentType, null); + // Quoting the setup lambda expression ensures that closures will be applied + _expression = Expression.Call(_expression, setupMethodInfo, Expression.Quote(setupExpression)); + _currentType = setupMethodInfo.ReturnType; + if (forwardedArgs.Any()) + { + _lambdaArguments.AddRange(forwardedArgs.Keys); + _args.AddRange(forwardedArgs.Values); + } + return this; + default: + throw new NotSupportedException("Setup can only handle 1 or 2 arguments as an input"); + } + } + + public IRuntimeReturns Setup(Expression setupExpression, IReadOnlyDictionary forwardedArgs, Type returnType) + { + var setupMethodInfo = MockMemberInfos.Setup(_currentType, returnType); + // Quoting the setup lambda expression ensures that closures will be applied + _expression = Expression.Call(_expression, setupMethodInfo, Expression.Quote(setupExpression)); + _currentType = setupMethodInfo.ReturnType; + if (forwardedArgs.Any()) + { + _lambdaArguments.AddRange(forwardedArgs.Keys); + _args.AddRange(forwardedArgs.Values); + } + return this; + } + + public IRuntimeSetup Callback(Action callback) + { + var callbackMethodInfo = MockMemberInfos.Callback(_currentType); + var callbackType = callbackMethodInfo.DeclaringType; + + var valueParameterExpression = Expression.Parameter(callback.GetType(), "value"); + _lambdaArguments.Add(valueParameterExpression); + + var castCallbackExpression = Expression.Convert(_expression, callbackType); + var callCallbackExpression = + Expression.Call(castCallbackExpression, callbackMethodInfo, valueParameterExpression); + _expression = Expression.Lambda(callCallbackExpression, _lambdaArguments); + _currentType = callbackMethodInfo.ReturnType; + _args.Add(callback); + return this; + } + + public IRuntimeSetup Returns(object value, Type type) + { + var returnsMethodInfo = MockMemberInfos.Returns(_currentType); + var returnsType = returnsMethodInfo.DeclaringType; + + var valueParameterExpression = Expression.Parameter(type, $"value{_args.Count}"); + _lambdaArguments.Add(valueParameterExpression); + + var castReturnExpression = Expression.Convert(_expression, returnsType); + var returnsCallExpression = Expression.Call(castReturnExpression, returnsMethodInfo, valueParameterExpression); + _expression = Expression.Lambda(returnsCallExpression, _lambdaArguments); + _currentType = returnsMethodInfo.ReturnType; + _args.Add(value); + return this; + } + + public object Execute() + { + var args = _args.Count >= 0 ? new List {_mock}.Concat(_args).ToArray() : _args.ToArray(); + + return _expression.NodeType == ExpressionType.Lambda + ? ((LambdaExpression)_expression).Compile().DynamicInvoke(args) + : Expression.Lambda(_expression, _mockParameterExpression).Compile().DynamicInvoke(args); + } + + public IRuntimeVerify Verify(Expression verifyExpression, ITimes times, IReadOnlyDictionary forwardedArgs) + { + switch (verifyExpression.Type.GetGenericArguments().Length) + { + case 2: + // It's a returning method so we need to use the Func overload of Setup and ignore the return. + Verify(verifyExpression, times, forwardedArgs, verifyExpression.Type.GetGenericArguments()[1]); + return this; + case 1: + var verifyMethodInfo = MockMemberInfos.Verify(_currentType); + var rdTimes = (Times)times; + // Quoting the setup lambda expression ensures that closures will be applied + _expression = Expression.Call(_expression, verifyMethodInfo, Expression.Quote(verifyExpression), Expression.Constant(rdTimes.MoqTimes)); + _currentType = verifyMethodInfo.ReturnType; + if (forwardedArgs.Any()) + { + _lambdaArguments.AddRange(forwardedArgs.Keys); + _args.AddRange(forwardedArgs.Values); + } + return this; + default: + throw new NotSupportedException("Verify can only handle 1 or 2 arguments as an input"); + } + } + + public IRuntimeVerify Verify(Expression verifyExpression, ITimes times, IReadOnlyDictionary forwardedArgs, Type returnType) + { + var verifyMethodInfo = MockMemberInfos.Verify(_currentType, returnType); + var rdTimes = (Times)times; + // Quoting the verify lambda expression ensures that closures will be applied + _expression = Expression.Call(_expression, verifyMethodInfo, Expression.Quote(verifyExpression), Expression.Constant(rdTimes.MoqTimes)); + _currentType = verifyMethodInfo.ReturnType; + if (forwardedArgs.Any()) + { + _lambdaArguments.AddRange(forwardedArgs.Keys); + _args.AddRange(forwardedArgs.Values); + } + return this; + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockProvider.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockProvider.cs new file mode 100644 index 0000000000..5a65edec2d --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockProvider.cs @@ -0,0 +1,220 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using Moq; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; +using Rubberduck.Resources.Registration; +using Rubberduck.VBEditor.ComManagement.TypeLibs; +using Rubberduck.VBEditor.SafeComWrappers.Abstract; +using Rubberduck.VBEditor.Utility; +using IMPLTYPEFLAGS = System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS; +using TYPEATTR = System.Runtime.InteropServices.ComTypes.TYPEATTR; +using TYPEKIND = System.Runtime.InteropServices.ComTypes.TYPEKIND; + +// ReSharper disable InconsistentNaming + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.IMockProviderGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface IMockProvider + { + [DispId(1)] + [Description("Creates a new mock for the specified interface.")] + IComMock Mock(string ProgId, [Optional] string Project); + + [DispId(2)] + [Description("Gets an object that creates argument placeholders for an expression.")] + SetupArgumentCreator It { get; } + + [DispId(3)] + [Description("Gets an object that specifies how many times a verifiable invocation should occur.")] + ITimes Times { get; } + } + + [ComVisible(false)] + internal interface IMockProviderInternal : IMockProvider + { + IComMock MockChildObject(IComMock ParentObject, Type childType); + } + + [ + ComVisible(true), + Guid(RubberduckGuid.MockProviderGuid), + ProgId(RubberduckProgId.MockProviderProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(IMockProvider)) + ] + public class MockProvider : IMockProviderInternal + { + private static readonly ICachedTypeService TypeCacheService = CachedTypeService.Instance; + + public MockProvider() + { + It = new SetupArgumentCreator(); + Times = new Times(); + } + + public IComMock Mock(string ProgId, string Project = null) + { + // If already cached, we must re-use the type to work around the + // broken type equivalence. + if (TypeCacheService.TryGetCachedType(Project, ProgId, out var classType)) + { + return CreateComMock(Project, ProgId, classType); + } + + // In order to mock a COM type, we must acquire a Type. However, + // ProgId will only return the coclass, which itself is a collection + // of interfaces, so we must take additional steps to obtain the default + // interface rather than the class itself. + classType = string.IsNullOrWhiteSpace(Project) + ? Type.GetTypeFromProgID(ProgId) + : GetVbaType(ProgId, Project); + + if (classType == null) + { + throw new ArgumentOutOfRangeException(nameof(ProgId), + $"The supplied {ProgId} was not found. The class may not be registered or could not be located with the available metadata."); + } + + return CreateComMock(Project, ProgId, classType); + } + + public IComMock MockChildObject(IComMock ParentObject, Type childType) + { + var project = ParentObject.Project; + var progId = childType.FullName; + childType = TypeCacheService.TryGetCachedTypeFromEquivalentType(project, progId, childType); + return CreateComMock(project, progId, childType); + } + + private ComMock CreateComMock(string project, string progId, Type classType) + { + var targetType = classType.IsInterface ? classType : GetComDefaultInterface(classType); + + var closedMockType = typeof(Mock<>).MakeGenericType(targetType); + var mock = (Mock)Activator.CreateInstance(closedMockType); + + // Ensure that the mock implements all the interfaces to cover the case where + // no setup is performed on the given interface and to ensure that mock can + // be cast successfully. + var asGenericMemberInfo = closedMockType.GetMethod("As"); + System.Diagnostics.Debug.Assert(asGenericMemberInfo != null); + + var supportedTypes = classType.GetInterfaces(); + foreach (var type in supportedTypes) + { + var asMemberInfo = asGenericMemberInfo.MakeGenericMethod(type); + asMemberInfo.Invoke(mock, null); + } + + return new ComMock(this, project, progId, mock, targetType, supportedTypes); + } + + public SetupArgumentCreator It { get; } + public ITimes Times { get; } + + private static Type GetComDefaultInterface(Type classType) + { + Type targetType = null; + + var pTI = Marshal.GetITypeInfoForType(classType); + var ti = (ITypeInfo) Marshal.GetTypedObjectForIUnknown(pTI, typeof(ITypeInfo)); + ti.GetTypeAttr(out var attr); + using (DisposalActionContainer.Create(attr, ptr => ti.ReleaseTypeAttr(ptr))) + { + var typeAttr = Marshal.PtrToStructure(attr); + if (typeAttr.typekind == TYPEKIND.TKIND_COCLASS && typeAttr.cImplTypes > 0) + { + for (var i = 0; i < typeAttr.cImplTypes; i++) + { + ti.GetImplTypeFlags(i, out var implTypeFlags); + + if ((implTypeFlags & IMPLTYPEFLAGS.IMPLTYPEFLAG_FDEFAULT) != + IMPLTYPEFLAGS.IMPLTYPEFLAG_FDEFAULT || + (implTypeFlags & IMPLTYPEFLAGS.IMPLTYPEFLAG_FRESTRICTED) == + IMPLTYPEFLAGS.IMPLTYPEFLAG_FRESTRICTED || + (implTypeFlags & IMPLTYPEFLAGS.IMPLTYPEFLAG_FSOURCE) == + IMPLTYPEFLAGS.IMPLTYPEFLAG_FSOURCE) + { + continue; + } + + ti.GetRefTypeOfImplType(i, out var href); + ti.GetRefTypeInfo(href, out var iTI); + + iTI.GetDocumentation(-1, out var strName, out _, out _, out _); + + targetType = classType.GetInterface(strName, true); + } + } + } + + if (targetType == null) + { + // Could not find the default interface using type infos, so we'll just pick + // whatever's the first listed and hope for the best. + targetType = classType.GetInterfaces().FirstOrDefault() ?? classType; + } + + return targetType; + } + + private static Type GetVbaType(string progId, string projectName) + { + Type classType = null; + + if (!TryGetVbeProject(projectName, out var project)) + { + return null; + } + + var provider = new TypeLibWrapperProviderLite(); + var lib = provider.TypeLibWrapperFromProject(project); + + foreach (var typeInfo in lib.TypeInfos) + { + if (typeInfo.Name != progId) + { + continue; + } + + if (TypeCacheService.TryGetCachedType(typeInfo, projectName, out classType)) + { + break; + } + } + + return classType; + } + + private static bool TryGetVbeProject(string ProjectName, out IVBProject project) + { + var vbe = VbeProvider.Vbe; + project = null; + using (var projects = vbe.VBProjects) + { + foreach (var proj in projects) + { + if (proj.Name != ProjectName) + { + proj.Dispose(); + continue; + } + + project = proj; + break; + } + } + + return project != null; + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockReflection.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockReflection.cs new file mode 100644 index 0000000000..886e911487 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/MockReflection.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using ExpressiveReflection; +using Moq; +using Moq.Language; +using Moq.Language.Flow; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + /// + /// Most methods on the are generic. Because the are + /// different for each closed generic type, we cannot use the open generic . + /// To address this, we need to get the object, and look up the equivalent via + /// handles which are the same regardless of generic parameters used, and return the "closed" version. + /// + public static class MockMemberInfos + { + public static MethodInfo As(Type type) + { + return Reflection.GetMethodExt(typeof(Mock), MockMemberNames.As()).MakeGenericMethod(type); + } + + public static MethodInfo Verify(Type mockType) + { + var typeHandle = mockType.TypeHandle; + var mock = typeof(Mock<>); + + var actionArgExpression = typeof(Expression<>).MakeGenericType(typeof(Action<>)); + var genericMethod = Reflection.GetMethodExt(mock, MockMemberNames.Verify(), actionArgExpression, typeof(Moq.Times)); + var specificMethod = (MethodInfo)MethodBase.GetMethodFromHandle(genericMethod.MethodHandle, typeHandle); + + return specificMethod; + } + + public static MethodInfo Verify(Type mockType, Type returnType) + { + var typeHandle = mockType.TypeHandle; + var mock = typeof(Mock<>); + + var funcArgExpression = typeof(Expression<>).MakeGenericType(returnType != null ? + typeof(Func<,>) : + typeof(Action<>) + ); + var genericMethod = Reflection.GetMethodExt(mock, MockMemberNames.Verify(), funcArgExpression, typeof(Moq.Times)); + var specificMethod = (MethodInfo)MethodBase.GetMethodFromHandle(genericMethod.MethodHandle, typeHandle); + + return returnType != null ? specificMethod.MakeGenericMethod(returnType) : specificMethod; + } + + public static MethodInfo Setup(Type mockType, Type returnType) + { + var typeHandle = mockType.TypeHandle; + var mock = typeof(Mock<>); + var expression = typeof(Expression<>).MakeGenericType(returnType != null ? + typeof(Func<,>) : + typeof(Action<>) + ); + var genericMethod = Reflection.GetMethodExt(mock, MockMemberNames.Setup(), expression); + var specificMethod = (MethodInfo) MethodBase.GetMethodFromHandle(genericMethod.MethodHandle, typeHandle); + return returnType != null ? specificMethod.MakeGenericMethod(returnType) : specificMethod; + } + + public static MethodInfo Returns(Type setupMockType) + { + var typeHandle = setupMockType.GetInterfaces().Single(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IReturns<,>) + ).TypeHandle; + var setup = typeof(IReturns<,>); + var result = typeof(MethodReflection.T); + var genericMethod = Reflection.GetMethodExt(setup, MockMemberNames.Returns(), result); + return (MethodInfo) MethodBase.GetMethodFromHandle(genericMethod.MethodHandle, typeHandle); + } + + public static MethodInfo Callback(Type setupMockType) + { + var typeHandle = setupMockType.GetInterfaces().Single(i => + !i.IsGenericType && + i == typeof(ICallback) + ).TypeHandle; + var setup = typeof(ICallback); + var callback = typeof(Delegate); + var genericMethod = Reflection.GetMethodExt(setup, MockMemberNames.Callback(), callback); + return (MethodInfo) MethodBase.GetMethodFromHandle(genericMethod.MethodHandle, typeHandle); + } + } + + /// + /// Though most members are generic, for the purposes of getting the names + /// they are all the same regardless of the actual closed generic types used + /// so we can just use object as a placeholder for the generic parameters. + /// + public static class MockMemberNames + { + public static string As() + { + return nameof(Mock.As); + } + + public static string Setup() + { + return nameof(Mock.Setup); + } + + public static string Returns() + { + return nameof(ISetup.Returns); + } + + public static string Callback() + { + return nameof(ISetup.Callback); + } + + public static string Verify() + { + return nameof(Mock.Verify); + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinition.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinition.cs new file mode 100644 index 0000000000..d889cb1e47 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinition.cs @@ -0,0 +1,162 @@ +using System.Runtime.InteropServices; +using Moq; +using Rubberduck.Resources.Registration; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.SetupArgumentRangeGuid) + ] + public enum SetupArgumentRange + { + Inclusive = Range.Inclusive, + Exclusive = Range.Exclusive + } + + [ + ComVisible(true), + Guid(RubberduckGuid.SetupArgumentTypeGuid) + ] + public enum SetupArgumentType + { + Is, + IsAny, + IsIn, + IsInRange, + IsNotIn, + IsNotNull + } + + [ + ComVisible(true), + Guid(RubberduckGuid.ISetupArgumentDefinitionGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface ISetupArgumentDefinition + { + [DispId(1)] + SetupArgumentType Type { get; } + + [DispId(2)] + object[] Values { get; } + + [DispId(3)] + SetupArgumentRange? Range + { + [return: MarshalAs(UnmanagedType.Struct)] + get; + } + } + + [ + ComVisible(true), + Guid(RubberduckGuid.SetupArgumentDefinitionGuid), + ProgId(RubberduckProgId.SetupArgumentDefinitionProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(ISetupArgumentDefinition)) + ] + public class SetupArgumentDefinition : ISetupArgumentDefinition + { + internal static SetupArgumentDefinition CreateIs(object value) + { + return new SetupArgumentDefinition(SetupArgumentType.Is, new[] { value }); + } + + internal static SetupArgumentDefinition CreateIsAny() + { + return new SetupArgumentDefinition(SetupArgumentType.IsAny, null); + } + + internal static SetupArgumentDefinition CreateIsIn(object[] values) + { + return new SetupArgumentDefinition(SetupArgumentType.IsIn, values); + } + + internal static SetupArgumentDefinition CreateIsInRange(object start, object end, SetupArgumentRange range) + { + return new SetupArgumentDefinition(SetupArgumentType.IsInRange, new[] { start, end }, range); + } + + internal static SetupArgumentDefinition CreateIsNotIn(object[] values) + { + return new SetupArgumentDefinition(SetupArgumentType.IsNotIn, values); + } + + internal static SetupArgumentDefinition CreateIsNotNull() + { + return new SetupArgumentDefinition(SetupArgumentType.IsNotNull, null); + } + + private SetupArgumentDefinition(SetupArgumentType type, object[] values) + { + Type = type; + Values = values; + } + + private SetupArgumentDefinition(SetupArgumentType type, object[] values, SetupArgumentRange range) : + this(type, values) + { + Range = range; + } + + public SetupArgumentType Type { get; } + public object[] Values { get; } + public SetupArgumentRange? Range { get; } + } + + [ + ComVisible(true), + Guid(RubberduckGuid.ISetupArgumentCreatorGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface ISetupArgumentCreator + { + SetupArgumentDefinition Is(object Value); + SetupArgumentDefinition IsAny(); + SetupArgumentDefinition IsIn(object[] Values); + SetupArgumentDefinition IsInRange(object Start, object End, SetupArgumentRange Range); + SetupArgumentDefinition IsNotIn(object[] Values); + SetupArgumentDefinition IsNotNull(); + } + + [ + ComVisible(true), + Guid(RubberduckGuid.SetupArgumentCreatorGuid), + ProgId(RubberduckProgId.SetupArgumentCreatorProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(ISetupArgumentCreator)) + ] + public class SetupArgumentCreator : ISetupArgumentCreator + { + public SetupArgumentDefinition Is(object Value) + { + return SetupArgumentDefinition.CreateIs(Value); + } + + public SetupArgumentDefinition IsAny() + { + return SetupArgumentDefinition.CreateIsAny(); + } + + public SetupArgumentDefinition IsIn(object[] Values) + { + return SetupArgumentDefinition.CreateIsIn(Values); + } + + public SetupArgumentDefinition IsInRange(object Start, object End, SetupArgumentRange Range) + { + return SetupArgumentDefinition.CreateIsInRange(Start, End, Range); + } + + public SetupArgumentDefinition IsNotIn(object[] Values) + { + return SetupArgumentDefinition.CreateIsNotIn(Values); + } + + public SetupArgumentDefinition IsNotNull() + { + return SetupArgumentDefinition.CreateIsNotNull(); + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinitions.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinitions.cs new file mode 100644 index 0000000000..a151f34e92 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentDefinitions.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Rubberduck.Resources.Registration; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + [ + ComVisible(true), + Guid(RubberduckGuid.ISetupArgumentDefinitionsGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual) + ] + public interface ISetupArgumentDefinitions : IEnumerable + { + [DispId(WellKnownDispIds.Value)] + SetupArgumentDefinition Item(int Index); + + [DispId(1)] + int Count { get; } + + [DispId(WellKnownDispIds.NewEnum)] + IEnumerator _GetEnumerator(); + } + + [ + ComVisible(true), + Guid(RubberduckGuid.SetupArgumentDefinitionsGuid), + ProgId(RubberduckProgId.SetupArgumentDefinitionsProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(ISetupArgumentDefinitions)) + ] + public class SetupArgumentDefinitions : ISetupArgumentDefinitions, IEnumerable + { + private readonly List _definitions; + + public SetupArgumentDefinitions() + { + _definitions = new List(); + } + + public SetupArgumentDefinition Item(int Index) => _definitions.ElementAt(Index); + + public int Count => _definitions.Count; + + public IEnumerator GetEnumerator() => _definitions.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _definitions.GetEnumerator(); + + public IEnumerator _GetEnumerator() => _definitions.GetEnumerator(); + + internal void Add(SetupArgumentDefinition definition) + { + _definitions.Add(definition); + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentResolver.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentResolver.cs new file mode 100644 index 0000000000..91c00cefdd --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupArgumentResolver.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using ExpressiveReflection; +using Moq; +using NLog; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + internal class SetupArgumentResolver + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + /// + /// Converts a variant args into the collection. This supports calls from COM + /// using the Variant data type. + /// + /// + /// The procedure needs to handle the following cases where the variant...: + /// 1) contains a single value + /// 2) contains an Array() of values + /// 3) wraps a single + /// 4) points to a collection. + /// 5) wraps a single object in which case we return a null + /// 6) wraps an array of single object in which case we return a null + /// + /// We must ensure that the arrays are resolved before calling the + /// single object wrapper to ensure we don't end up wrapping the + /// arrays as a single value; do not change the switch order willy-nilly. + /// + /// We also need to handle the special cases with , because + /// the methods and + /// will marshal the Args parameter as a variant, which means we receive it as , + /// not as null if it is omitted. + /// + /// Should be a COM Variant that can be cast into valid values as explained in the remarks + /// A collection or null + public SetupArgumentDefinitions ResolveArgs(object args) + { + switch (args) + { + case Missing missing: + return null; + case SetupArgumentDefinitions definitions: + return definitions; + case SetupArgumentDefinition definition: + return WrapArgumentDefinitions(definition); + case object[] objects: + if (objects.Length == 1 && objects[0] is Missing) + { + return null; + } + return WrapArgumentDefinitions(objects); + case object singleObject: + return WrapArgumentDefinitions(singleObject); + default: + return null; + } + } + + private static SetupArgumentDefinitions WrapArgumentDefinitions(object singleObject) + { + var list = new SetupArgumentDefinitions(); + var isDefinition = SetupArgumentDefinition.CreateIs(singleObject); + list.Add(isDefinition); + return list; + } + + private static SetupArgumentDefinitions WrapArgumentDefinitions(object[] objects) + { + var list = new SetupArgumentDefinitions(); + foreach (var item in objects) + { + switch (item) + { + case SetupArgumentDefinition argumentDefinition: + list.Add(argumentDefinition); + break; + case object[] arrayObjects: + var inDefinition = SetupArgumentDefinition.CreateIsIn(arrayObjects); + list.Add(inDefinition); + break; + case Missing missing: + list.Add(SetupArgumentDefinition.CreateIsAny()); + break; + case object singleObject: + var isDefinition = + SetupArgumentDefinition.CreateIs(singleObject); + list.Add(isDefinition); + break; + case null: + list.Add(SetupArgumentDefinition.CreateIsAny()); + break; + } + } + + return list; + } + + private static SetupArgumentDefinitions WrapArgumentDefinitions(SetupArgumentDefinition setupArgumentDefinition) + { + return new SetupArgumentDefinitions + { + setupArgumentDefinition + }; + } + + /// + /// Transform the collection of into a + /// + /// + /// If a method `Foo` requires one argument, we need to specify the behavior in an expression similar + /// to this: Mock.Setup(x => x.Foo(It.IsAny()). The class is static so we can + /// create call expressions directly on it. + /// + /// Array of returned from the member for which the applies to + /// The collection containing user supplied behavior + /// A read-only list containing the of arguments + public (IReadOnlyList expressions, IReadOnlyDictionary forwardedArgs) ResolveParameters( + IReadOnlyList parameters, + SetupArgumentDefinitions args) + { + var argsCount = args?.Count ?? 0; + if (parameters.Count != argsCount) + { + throw new ArgumentOutOfRangeException(nameof(args), + $"The method expects {parameters.Count} parameters but only {argsCount} argument definitions were supplied. Setting up the mock's behavior requires that all parameters be filled in."); + } + + if (parameters.Count == 0) + { + return (null, null); + } + + var resolvedArguments = new List(); + var forwardedArgs = new Dictionary(); + for (var i = 0; i < parameters.Count; i++) + { + Debug.Assert(args != null, nameof(args) + " != null"); + + var parameter = parameters[i]; + var definition = args.Item(i); + + var (elementType, isRef, isOut) = GetParameterType(parameter); + var parameterType = parameter.ParameterType; + + Expression setupExpression; + if (isRef || isOut) + { + setupExpression = BuildPassByRefArgumentExpression(i, definition, parameterType, elementType, ref forwardedArgs); + } + else + { + setupExpression = BuildPassByValueArgumentExpression(i, definition, parameterType); + } + + resolvedArguments.Add(setupExpression); + } + + return (resolvedArguments, forwardedArgs); + } + + private Expression BuildPassByValueArgumentExpression(int index, SetupArgumentDefinition definition, Type parameterType) + { + var itType = typeof(It); + MethodInfo itMemberInfo; + + var itArgumentExpressions = new List(); + var typeExpression = Expression.Parameter(parameterType, $"p{index:00}"); + + switch (definition.Type) + { + case SetupArgumentType.Is: + itMemberInfo = itType.GetMethods().Single(x => x.Name == nameof(It.Is) && x.IsGenericMethodDefinition && x.GetParameters().All(y => y.ParameterType.GetGenericArguments().All(z => z.GetGenericTypeDefinition() == typeof(Func<,>)))).MakeGenericMethod(parameterType); + var value = definition.Values[0]; + if (value != null && value.GetType() != parameterType) + { + if (TryCast(value, parameterType, out var convertedValue)) + { + value = convertedValue; + } + } + + Expression bodyExpression; + if (parameterType == typeof(object)) + { + // Avoid incorrectly comparing by reference + var equalsInfo = Reflection.GetMethod(() => default(object).Equals(default(object))); + + bodyExpression = Expression.Call(typeExpression, equalsInfo, + Expression.Convert(Expression.Constant(value), parameterType)); + } + else + { + bodyExpression = Expression.Equal(typeExpression, Expression.Convert(Expression.Constant(value), parameterType)); + } + var itLambda = Expression.Lambda(bodyExpression, typeExpression); + itArgumentExpressions.Add(Expression.Quote(itLambda)); + break; + case SetupArgumentType.IsAny: + itMemberInfo = Reflection.GetMethodExt(itType, nameof(It.IsAny)).MakeGenericMethod(parameterType); + break; + case SetupArgumentType.IsIn: + itMemberInfo = Reflection.GetMethodExt(itType, nameof(It.IsIn), typeof(IEnumerable<>)).MakeGenericMethod(parameterType); + var arrayInit = Expression.NewArrayInit(parameterType, + definition.Values.Select(x => Expression.Convert(Expression.Constant(TryCast(x, parameterType, out var c) ? c : x), parameterType))); + itArgumentExpressions.Add(arrayInit); + break; + case SetupArgumentType.IsInRange: + itMemberInfo = Reflection.GetMethodExt(itType, nameof(It.IsInRange), typeof(MethodReflection.T), + typeof(MethodReflection.T), typeof(Range)).MakeGenericMethod(parameterType); + itArgumentExpressions.Add(Expression.Convert(Expression.Constant(TryCast(definition.Values[0], parameterType, out var from) ? from : definition.Values[0]), parameterType)); + itArgumentExpressions.Add(Expression.Convert(Expression.Constant(TryCast(definition.Values[1], parameterType, out var to) ? to : definition.Values[1]), parameterType)); + itArgumentExpressions.Add(definition.Range != null + ? Expression.Constant((Range)definition.Range) + : Expression.Constant(Range.Inclusive)); + break; + case SetupArgumentType.IsNotIn: + itMemberInfo = Reflection.GetMethodExt(itType, nameof(It.IsNotIn), typeof(IEnumerable<>)).MakeGenericMethod(parameterType); + var notArrayInit = Expression.NewArrayInit(parameterType, + definition.Values.Select(x => Expression.Convert(Expression.Constant(TryCast(x, parameterType, out var c) ? c : x), parameterType))); + itArgumentExpressions.Add(notArrayInit); + break; + case SetupArgumentType.IsNotNull: + itMemberInfo = Reflection.GetMethodExt(itType, nameof(It.IsNotNull)).MakeGenericMethod(parameterType); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return Expression.Call(itMemberInfo, itArgumentExpressions); + } + + private Expression BuildPassByRefArgumentExpression(int index, SetupArgumentDefinition definition, Type refType, Type elementType, ref Dictionary forwardedArgs) + { + Expression parameterExpression; + switch (definition.Type) + { + case SetupArgumentType.Is: + /* Example of how to call a mock w/ ref parameter + public void Test() + { + string o = "1"; + var mock = new Mock(); + + mock.Setup(x => x.DoSomething(ref o)) + .Callback(new DoSomethingAction((ref string a) => a = "2")); + + mock.Object.DoSomething(ref o); + + Assert.Equal("2", o) + } + */ + // TODO: Create a collection of variables with constant assignments to put in the setup expression? + // TODO: or better yet, try and pass the definition's value directly as a ref? + // TODO: need to take care that the args passed into DynamicInvoke do not need to be ref'd - it should be + // TODO: passed in as values then made ref within the expression tree. + var name = $"p{index:00}"; + var itByRef = ItByRefMemberInfos.Is(elementType).Invoke(null, new [] {definition.Values[0]}); + var forwardedArgExpression = Expression.Parameter(itByRef.GetType(), name); + forwardedArgs.Add(forwardedArgExpression, itByRef); + parameterExpression = Expression.Field(forwardedArgExpression, ItByRefMemberInfos.Value(elementType)); + return parameterExpression; + case SetupArgumentType.IsAny: + var itRefType = typeof(It.Ref<>).MakeGenericType(elementType); + var itFieldInfo = itRefType.GetField(nameof(It.IsAny)); + parameterExpression = Expression.Parameter(itRefType, "r"); + return Expression.Field(parameterExpression, itFieldInfo); + case SetupArgumentType.IsIn: + throw new NotSupportedException($"The {nameof(SetupArgumentType.IsIn)} type is not implemented for ref argument"); + case SetupArgumentType.IsInRange: + throw new NotSupportedException($"The {nameof(SetupArgumentType.IsInRange)} type is not implemented for ref argument"); + case SetupArgumentType.IsNotIn: + throw new NotSupportedException($"The {nameof(SetupArgumentType.IsNotIn)} type is not implemented for ref argument"); + case SetupArgumentType.IsNotNull: + throw new NotSupportedException($"The {nameof(SetupArgumentType.IsNotIn)} type is not implemented for ref argument"); + default: + throw new ArgumentOutOfRangeException(); + } + } + + private static (Type type, bool isRef, bool isOut) GetParameterType(ParameterInfo parameterInfo) + { + var isRef = false; + var isOut = false; + var parameterType = parameterInfo.ParameterType; + + if (parameterType.IsByRef && parameterInfo.IsOut) + { + isOut = true; + } + + if (!parameterType.IsByRef && !parameterInfo.IsOut) + { + return (parameterType, isRef, isOut); + } + + isRef = true; + parameterType = parameterType.HasElementType ? parameterType.GetElementType() : parameterType; + + return (parameterType, isRef, isOut); + } + + private static bool TryCast(object value, Type type, out object convertedValue) + { + convertedValue = null; + + try + { + convertedValue = VariantConverter.ChangeType(value, type); + } + catch + { + try + { + convertedValue = Convert.ChangeType(value, type); + } + catch + { + Logger.Trace($"Casting failed: the source type was '{value.GetType()}', and the target type wsa '{type.FullName}'"); + } + } + + return convertedValue != null; + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupExpressionBuilder.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupExpressionBuilder.cs new file mode 100644 index 0000000000..2872cc5a6a --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/SetupExpressionBuilder.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Moq; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + internal class SetupExpressionBuilder + { + private readonly Type _type; + private readonly IEnumerable _supportedInterfaces; + private readonly SetupArgumentResolver _resolver; + + public SetupExpressionBuilder(Type type, IEnumerable supportedInterfaces, SetupArgumentResolver resolver) + { + _type = type; + _supportedInterfaces = supportedInterfaces; + _resolver = resolver; + } + + /// + /// Builds the basic expressions using provided inputs. The return can be then expanded upon for + /// specifying behaviors of the given Setup. + /// + /// The member name on the 's interface + /// Arguments required for the member, if any. If none are required, pass in null + /// An list of representing the mock's Setup expression ("mock.Setup(...)") for each implemented interface + internal IEnumerable CreateExpression(string name, SetupArgumentDefinitions args) + { + var setupDatas = new List(); + var membersToSetup = GetMembers(name); + var resolver = new SetupArgumentResolver(); + var (parameterExpressions, forwardedArgs) = resolver.ResolveParameters(membersToSetup.Parameters.ToArray(), args); + var memberType = membersToSetup.ReturnType; + + foreach (var member in membersToSetup.MemberInfos) + { + var typeExpression = Expression.Parameter(member.Key, "x"); + Expression memberAccessExpression; + + switch (member.Value) + { + case PropertyInfo propertyInfo: + memberAccessExpression = parameterExpressions != null + ? (Expression) Expression.Property(typeExpression, propertyInfo, parameterExpressions) + : Expression.MakeMemberAccess(typeExpression, member.Value); + break; + case MethodInfo methodInfo: + memberAccessExpression = parameterExpressions != null + ? Expression.Call(typeExpression, methodInfo, parameterExpressions) + : Expression.Call(typeExpression, methodInfo); + break; + case FieldInfo _: + Debug.Assert(false, "COM and C# interfaces cannot have fields defined. Why are we here?"); + memberAccessExpression = Expression.MakeMemberAccess(typeExpression, member.Value); + break; + default: + throw new InvalidCastException($"Could not convert the member to a property or a method"); + } + + // Finalize the expression within the Setup's lambda. + var expression = Expression.Lambda(memberAccessExpression, typeExpression); + + setupDatas.Add(new SetupData(expression, member.Key, memberType, forwardedArgs)); + } + + return setupDatas; + } + + /// + /// Discover all members from all provided interfaces that are named the same and shares the same + /// signature. + /// + /// The name of member to find on any interfaces + /// + /// A struct that contains the data for each member needed + /// to create an expression. See for more details. + /// + private MemberSetupData GetMembers(string name) + { + var memberInfos = new Dictionary(); + Type returnType = null; + ParameterInfo[] parameters = null; + MemberInfo member = null; + var members = _type.GetMember(name); + + //COM interfaces should not allow for method overloading within same interface + Debug.Assert(members.Length <= 1); + + if (members.Length == 1) + { + member = members.First(); + memberInfos.Add(_type, member); + + (returnType, parameters) = GetMemberInfo(member); + } + + foreach (var subType in _supportedInterfaces) + { + if (subType == _type) + { + continue; + } + + members = subType.GetMember(name); + + //COM interfaces should not allow for method overloading within same interface + Debug.Assert(members.Length <= 1); + + if (members.Length == 0) + { + continue; + } + + if (member == null) + { + member = members.First(); + memberInfos.Add(subType, member); + + (returnType, parameters) = GetMemberInfo(member); + } + else + { + var subMember = members.First(); + var (subReturnType, subParameters) = GetMemberInfo(member); + + if (subMember.Name == member.Name && + subMember.MemberType == member.MemberType && + returnType == subReturnType && + parameters.Length == subParameters.Length && + parameters.All(p => subParameters.Any(sp => + p.Name == sp.Name && + p.Position == sp.Position && + p.ParameterType == sp.ParameterType))) + { + memberInfos.Add(subType, subMember); + } + } + } + + return new MemberSetupData(memberInfos, returnType, parameters); + } + + private static (Type returnType, ParameterInfo[] parameters) GetMemberInfo(MemberInfo member) + { + Type returnType; + ParameterInfo[] parameters; + + switch (member) + { + case PropertyInfo propertyInfo: + returnType = propertyInfo.PropertyType; + parameters = propertyInfo.GetIndexParameters(); + break; + case MethodInfo methodInfo: + returnType = methodInfo.ReturnType; + parameters = methodInfo.GetParameters(); + break; + case FieldInfo fieldInfo: + Debug.Assert(false, "COM and C# interfaces cannot have fields defined. Why are we here?"); + returnType = fieldInfo.FieldType; + parameters = new ParameterInfo[0]; + break; + default: + throw new ArgumentOutOfRangeException(member.Name, $"Found on the interface '{member.ReflectedType?.Name}' but seems to be neither a method nor a property nor a field; the member info type was {member.GetType()}"); + } + + return (returnType, parameters); + } + } + + internal readonly struct MemberSetupData + { + internal IDictionary MemberInfos { get; } + internal Type ReturnType { get; } + internal IEnumerable Parameters { get; } + + internal MemberSetupData(IDictionary memberInfos, Type returnType, + IEnumerable parameters) + { + MemberInfos = memberInfos; + ReturnType = returnType; + Parameters = parameters; + } + } + + /// + /// Provides base for building a lambda, returned by + /// This is used for further developing the lambda expression to invoke other methods that would be provided by the result of + /// the Setup(). + /// + internal readonly struct SetupData + { + /// + /// The base expression representing . Refer to for details. + /// This is usually used as a start for further development of the lambda. + /// + internal Expression SetupExpression { get; } + + /// + /// The containing interface that implements the member being called in the setup expression. + /// + internal Type DeclaringType { get; } + + /// + /// The return type, if any, for the member being called in the setup expression. + /// + internal Type ReturnType { get; } + + /// + /// Any arguments that needs to be passed into the final lambda; usually used for ref parameters + /// + internal IReadOnlyDictionary Args { get; } + + internal SetupData(Expression setupExpression, Type declaringType, Type returnType, IReadOnlyDictionary args = null) + { + SetupExpression = setupExpression; + DeclaringType = declaringType; + ReturnType = returnType; + Args = args ?? new Dictionary(); + } + } +} diff --git a/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/VariantConverter.cs b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/VariantConverter.cs new file mode 100644 index 0000000000..5029162826 --- /dev/null +++ b/Rubberduck.Main/ComClientLibrary/UnitTesting/Mocks/VariantConverter.cs @@ -0,0 +1,220 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Rubberduck.ComClientLibrary.UnitTesting.Mocks +{ + public enum VARENUM + { + VT_EMPTY = 0x0000, + VT_NULL = 0x0001, + VT_I2 = 0x0002, + VT_I4 = 0x0003, + VT_R4 = 0x0004, + VT_R8 = 0x0005, + VT_CY = 0x0006, + VT_DATE = 0x0007, + VT_BSTR = 0x0008, + VT_DISPATCH = 0x0009, + VT_ERROR = 0x000A, + VT_BOOL = 0x000B, + VT_VARIANT = 0x000C, + VT_UNKNOWN = 0x000D, + VT_DECIMAL = 0x000E, + VT_I1 = 0x0010, + VT_UI1 = 0x0011, + VT_UI2 = 0x0012, + VT_UI4 = 0x0013, + VT_I8 = 0x0014, + VT_UI8 = 0x0015, + VT_INT = 0x0016, + VT_UINT = 0x0017, + VT_VOID = 0x0018, + VT_HRESULT = 0x0019, + VT_PTR = 0x001A, + VT_SAFEARRAY = 0x001B, + VT_CARRAY = 0x001C, + VT_USERDEFINED = 0x001D, + VT_LPSTR = 0x001E, + VT_LPWSTR = 0x001F, + VT_RECORD = 0x0024, + VT_INT_PTR = 0x0025, + VT_UINT_PTR = 0x0026, + VT_ARRAY = 0x2000, + VT_BYREF = 0x4000 + } + + [Flags] + public enum VariantConversionFlags : ushort + { + NO_FLAGS = 0x00, + VARIANT_NOVALUEPROP = 0x01, //Prevents the function from attempting to coerce an object to a fundamental type by getting the Value property. Applications should set this flag only if necessary, because it makes their behavior inconsistent with other applications. + VARIANT_ALPHABOOL = 0x02, //Converts a VT_BOOL value to a string containing either "True" or "False". + VARIANT_NOUSEROVERRIDE = 0x04, //For conversions to or from VT_BSTR, passes LOCALE_NOUSEROVERRIDE to the core coercion routines. + VARIANT_LOCALBOOL = 0x08 //For conversions from VT_BOOL to VT_BSTR and back, uses the language specified by the locale in use on the local computer. + } + + /// + /// Handles variant conversions, enabling us to have same implicit conversion behaviors within + /// .NET as we can observe it from VBA/VB6. + /// + /// + /// The function is the same one used internally by VBA/VB6. + /// However, we have to wrap the metadata, which the class helps with. + /// + /// See the link for details on how marshaling are handled with + /// https://docs.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-objects + /// + public static class VariantConverter + { + private const string dllName = "oleaut32.dll"; + + // HRESULT VariantChangeType( + // VARIANTARG *pvargDest, + // const VARIANTARG *pvarSrc, + // USHORT wFlags, + // VARTYPE vt + // ); + [DllImport(dllName, EntryPoint = "VariantChangeType", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)] + private static extern int VariantChangeType(ref object pvargDest, ref object pvarSrc, VariantConversionFlags wFlags, VARENUM vt); + + // HRESULT VariantChangeTypeEx( + // VARIANTARG *pvargDest, + // const VARIANTARG *pvarSrc, + // LCID lcid, + // USHORT wFlags, + // VARTYPE vt + // ); + [DllImport(dllName, EntryPoint = "VariantChangeTypeEx", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)] + private static extern int VariantChangeTypeEx(ref object pvargDest, ref object pvarSrc, int lcid, VariantConversionFlags wFlags, VARENUM vt); + + public static object ChangeType(object value, VARENUM vt) + { + return ChangeType(value, vt, null); + } + + private static bool HRESULT_FAILED(int hr) => hr < 0; + public static object ChangeType(object value, VARENUM vt, CultureInfo cultureInfo) + { + object result = null; + var hr = cultureInfo == null + ? VariantChangeType(ref result, ref value, VariantConversionFlags.NO_FLAGS, vt) + : VariantChangeTypeEx(ref result, ref value, cultureInfo.LCID, VariantConversionFlags.NO_FLAGS, vt); + if (HRESULT_FAILED(hr)) + { + throw Marshal.GetExceptionForHR(hr); + } + + return result; + } + + public static object ChangeType(object value, Type targetType) + { + return ChangeType(value, GetVarEnum(targetType)); + } + + public static object ChangeType(object value, Type targetType, CultureInfo culture) + { + return ChangeType(value, GetVarEnum(targetType), culture); + } + + public static VARENUM GetVarEnum(Type target) + { + switch (target) + { + case null: + return VARENUM.VT_EMPTY; + case Type dbNull when dbNull == typeof(DBNull): + return VARENUM.VT_NULL; + case Type err when err == typeof(ErrorWrapper): + return VARENUM.VT_ERROR; + case Type disp when disp == typeof(DispatchWrapper): + return VARENUM.VT_DISPATCH; + case Type unk when unk == typeof(UnknownWrapper): + return VARENUM.VT_UNKNOWN; + case Type cy when cy == typeof(CurrencyWrapper): + return VARENUM.VT_CY; + case Type b when b == typeof(bool): + return VARENUM.VT_BOOL; + case Type s when s == typeof(sbyte): + return VARENUM.VT_I1; + case Type b when b == typeof(byte): + return VARENUM.VT_UI1; + case Type i16 when i16 == typeof(short): + return VARENUM.VT_I2; + case Type ui16 when ui16 == typeof(ushort): + return VARENUM.VT_UI2; + case Type i32 when i32 == typeof(int): + return VARENUM.VT_I4; + case Type ui32 when ui32 == typeof(uint): + return VARENUM.VT_UI4; + case Type i64 when i64 == typeof(long): + return VARENUM.VT_I8; + case Type ui64 when ui64 == typeof(ulong): + return VARENUM.VT_UI8; + case Type sng when sng == typeof(float): + return VARENUM.VT_R4; + case Type dbl when dbl == typeof(double): + return VARENUM.VT_R8; + case Type dec when dec == typeof(decimal): + return VARENUM.VT_DECIMAL; + case Type dt when dt == typeof(DateTime): + return VARENUM.VT_DATE; + case Type s when s == typeof(string): + return VARENUM.VT_BSTR; + //case Type a when a == typeof(Array): + // return VARENUM.VT_ARRAY; + case Type obj when obj == typeof(object): + case Type var when var == typeof(VariantWrapper): + return VARENUM.VT_VARIANT; + default: + throw new NotSupportedException("Unrecognized system type that cannot be mapped to a VARENUM out of the box."); + } + } + + public static VARENUM GetVarEnum(TypeCode typeCode) + { + switch (typeCode) + { + case TypeCode.Empty: + return VARENUM.VT_EMPTY; + case TypeCode.Object: + return VARENUM.VT_UNKNOWN; + case TypeCode.DBNull: + return VARENUM.VT_NULL; + case TypeCode.Boolean: + return VARENUM.VT_BOOL; + case TypeCode.Char: + return VARENUM.VT_UI2; + case TypeCode.SByte: + return VARENUM.VT_I1; + case TypeCode.Byte: + return VARENUM.VT_UI1; + case TypeCode.Int16: + return VARENUM.VT_I2; + case TypeCode.UInt16: + return VARENUM.VT_UI2; + case TypeCode.Int32: + return VARENUM.VT_I4; + case TypeCode.UInt32: + return VARENUM.VT_UI4; + case TypeCode.Int64: + return VARENUM.VT_I8; + case TypeCode.UInt64: + return VARENUM.VT_UI8; + case TypeCode.Single: + return VARENUM.VT_R4; + case TypeCode.Double: + return VARENUM.VT_R8; + case TypeCode.Decimal: + return VARENUM.VT_DECIMAL; + case TypeCode.DateTime: + return VARENUM.VT_DATE; + case TypeCode.String: + return VARENUM.VT_BSTR; + default: + throw new ArgumentOutOfRangeException(nameof(typeCode), typeCode, null); + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Main/Extension.cs b/Rubberduck.Main/Extension.cs index 9c9d63dd83..9e50e299bc 100644 --- a/Rubberduck.Main/Extension.cs +++ b/Rubberduck.Main/Extension.cs @@ -238,7 +238,7 @@ private void Startup() currentDomain.AssemblyResolve += LoadFromSameFolder; _container = new WindsorContainer().Install(new RubberduckIoCInstaller(_vbe, _addin, _initialSettings, _vbeNativeApi, _beepInterceptor)); - + _container.Resolve(); _app = _container.Resolve(); _app.Startup(); diff --git a/Rubberduck.Main/Properties/AssemblyInfo.cs b/Rubberduck.Main/Properties/AssemblyInfo.cs index dc2ef11c13..7e92143777 100644 --- a/Rubberduck.Main/Properties/AssemblyInfo.cs +++ b/Rubberduck.Main/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using Rubberduck.Resources.Registration; [assembly: InternalsVisibleTo("RubberduckTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid(RubberduckGuid.RubberduckTypeLibGuid)] diff --git a/Rubberduck.Main/Root/InstanceProvider.cs b/Rubberduck.Main/Root/InstanceProvider.cs new file mode 100644 index 0000000000..54732aa6ed --- /dev/null +++ b/Rubberduck.Main/Root/InstanceProvider.cs @@ -0,0 +1,27 @@ +using System; +using Rubberduck.Parsing.VBA; + +namespace Rubberduck.Root +{ + public interface IInstanceProvider + { + RubberduckParserState StateInstance { get; } + } + + public static class InstanceProviderFactory + { + public static IInstanceProvider GetInstanceProvider => new InstanceProvider(); + } + + internal class InstanceProvider : IInstanceProvider + { + private static RubberduckParserState _stateInstance; + public static RubberduckParserState StateInstance + { + get => _stateInstance; + set => _stateInstance = value ?? throw new NullReferenceException(); + } + + RubberduckParserState IInstanceProvider.StateInstance => _stateInstance; + } +} diff --git a/Rubberduck.Main/Root/RubberduckInstanceProviderPropertiesInspector.cs b/Rubberduck.Main/Root/RubberduckInstanceProviderPropertiesInspector.cs new file mode 100644 index 0000000000..4e2b96968d --- /dev/null +++ b/Rubberduck.Main/Root/RubberduckInstanceProviderPropertiesInspector.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Castle.Core; +using Castle.MicroKernel; +using Castle.MicroKernel.ModelBuilder; + +namespace Rubberduck.Root +{ + internal class RubberduckInstanceProviderPropertiesInspector : IContributeComponentModelConstruction + { + public void ProcessModel(IKernel kernel, ComponentModel model) + { + var targetType = model.Implementation; + + if (!(targetType == typeof(InstanceProvider))) + { + return; + } + + var properties = GetProperties(model, targetType); + + foreach (var property in properties) + { + model.AddProperty(BuildDependency(property)); + } + } + + private PropertySet BuildDependency(PropertyInfo property) + { + var dependency = new PropertyDependencyModel(property, isOptional: false); + return new PropertySet(property, dependency); + } + + private IEnumerable GetProperties(ComponentModel model, Type targetType) + { + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly; + return targetType.GetProperties(bindingFlags).ToList() + .Where(property => property.CanWrite + && property.GetSetMethod(true) != null + && !property.PropertyType.IsAbstract + && property.Name.EndsWith("Instance")); + } + } +} diff --git a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs index 50ec64d8dd..c393e10df3 100644 --- a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs +++ b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs @@ -22,6 +22,7 @@ using Rubberduck.Parsing; using Rubberduck.Parsing.Common; using Rubberduck.Parsing.ComReflection; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; using Rubberduck.Parsing.PreProcessing; using Rubberduck.Parsing.Symbols.DeclarationLoaders; using Rubberduck.Parsing.Rewriter; @@ -201,6 +202,8 @@ public void Install(IWindsorContainer container, IConfigurationStore store) RegisterSpecialFactories(container); RegisterFactories(container, assembliesToRegister); + RegisterInstanceProvider(container); + ApplyDefaultInterfaceConvention(container, assembliesToRegister); } @@ -313,7 +316,8 @@ private void ApplyDefaultInterfaceConvention(IWindsorContainer container, Assemb && !type.Name.EndsWith("ConfigProvider") && !type.Name.EndsWith("FakesProvider") && !type.GetInterfaces().Contains(typeof(IInspection)) - && type.NotDisabledOrExperimental(_initialSettings)) + && type.NotDisabledOrExperimental(_initialSettings) + && type != typeof(InstanceProvider)) .WithService.DefaultInterfaces() .LifestyleTransient() ); @@ -337,6 +341,20 @@ private void RegisterFactories(IWindsorContainer container, Assembly[] assemblie } } + private void RegisterInstanceProvider(IWindsorContainer container) + { + /* + container.Register(Types.FromAssemblyContaining() + .IncludeNonPublicTypes() + .Where(type => type == typeof(InstanceProvider)) + .WithServiceSelf() + .LifestyleTransient() + );*/ + container.Register( + Component.For() + .LifestyleSingleton()); + } + private void RegisterSourceCodeHandlers(IWindsorContainer container) { container.Register(Component.For() @@ -947,7 +965,8 @@ private void OverridePropertyInjection(IWindsorContainer container) .Single(); container.Kernel.ComponentModelBuilder.RemoveContributor(propInjector); - container.Kernel.ComponentModelBuilder.AddContributor(new RubberduckPropertiesInspector()); + container.Kernel.ComponentModelBuilder.AddContributor(new RubberduckViewModelPropertiesInspector()); + container.Kernel.ComponentModelBuilder.AddContributor(new RubberduckInstanceProviderPropertiesInspector()); } private void RegisterParsingEngine(IWindsorContainer container) @@ -1136,6 +1155,8 @@ private void RegisterInstances(IWindsorContainer container) container.Register(Component.For().Instance(PersistencePathProvider.Instance).LifestyleSingleton()); container.Register(Component.For().Instance(_vbeNativeApi).LifestyleSingleton()); container.Register(Component.For().Instance(_beepInterceptor).LifestyleSingleton()); + container.Register(Component.For().Instance(CachedTypeService.Instance).LifestyleSingleton()); + container.Register(Component.For().Instance(TypeLibQueryService.Instance).LifestyleSingleton()); } } } diff --git a/Rubberduck.Main/Root/RubberduckPropertiesInspector.cs b/Rubberduck.Main/Root/RubberduckViewModelPropertiesInspector.cs similarity index 64% rename from Rubberduck.Main/Root/RubberduckPropertiesInspector.cs rename to Rubberduck.Main/Root/RubberduckViewModelPropertiesInspector.cs index 1ce0ba4d45..4a6cf53a3e 100644 --- a/Rubberduck.Main/Root/RubberduckPropertiesInspector.cs +++ b/Rubberduck.Main/Root/RubberduckViewModelPropertiesInspector.cs @@ -1,21 +1,17 @@ -using Castle.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Castle.Core; using Castle.MicroKernel; using Castle.MicroKernel.ModelBuilder; -using Castle.MicroKernel.ModelBuilder.Inspectors; -using Castle.MicroKernel.SubSystems.Conversion; using Rubberduck.UI; using Rubberduck.UI.Command; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Rubberduck.Root { // Loosely based on https://github.com/castleproject/Windsor/blob/36fbebd9a471f88b43044f39704dc5f19e669e6f/src/Castle.Windsor/MicroKernel/ModelBuilder/Inspectors/PropertiesDependenciesModelInspector.cs - class RubberduckPropertiesInspector : IContributeComponentModelConstruction + internal class RubberduckViewModelPropertiesInspector : IContributeComponentModelConstruction { public void ProcessModel(IKernel kernel, ComponentModel model) { @@ -27,11 +23,7 @@ public void ProcessModel(IKernel kernel, ComponentModel model) return; } - var properties = GetProperties(model, targetType) - .Where(property => property.CanWrite - && property.GetSetMethod() != null - && property.PropertyType.IsBasedOn(typeof(CommandBase)) - && !property.PropertyType.IsAbstract); + var properties = GetProperties(model, targetType); foreach (var property in properties) { @@ -45,10 +37,14 @@ private PropertySet BuildDependency(PropertyInfo property) return new PropertySet(property, dependency); } - private List GetProperties(ComponentModel model, Type targetType) + private IEnumerable GetProperties(ComponentModel model, Type targetType) { - var bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - return targetType.GetProperties(bindingFlags).ToList(); + const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + return targetType.GetProperties(bindingFlags).ToList() + .Where(property => property.CanWrite + && property.GetSetMethod() != null + && property.PropertyType.IsBasedOn(typeof(CommandBase)) + && !property.PropertyType.IsAbstract); } } } diff --git a/Rubberduck.Main/Rubberduck.Main.csproj b/Rubberduck.Main/Rubberduck.Main.csproj index 4b32aa584d..9420c27a9d 100644 --- a/Rubberduck.Main/Rubberduck.Main.csproj +++ b/Rubberduck.Main/Rubberduck.Main.csproj @@ -35,6 +35,7 @@ + $(MSBuildProgramFiles32)\Common Files\microsoft shared\MSEnv\PublicAssemblies\extensibility.dll True @@ -66,7 +67,7 @@ 4.6.4 - 4.2.1 + 4.4.0 4.1.0 @@ -74,11 +75,15 @@ 2.7.6684 + 4.5.10 4.5.10 + + 4.13.0 + \ No newline at end of file diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/CachedTypeService.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/CachedTypeService.cs new file mode 100644 index 0000000000..c9051a13fb --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/CachedTypeService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + /// + /// Provide caching service for types that should be considered equivalent. + /// + /// + /// The provider aims to work around a deficiency in the , particularly for + /// COM interop types. The issue is that when we create a derived from methods such as + /// or , new types are + /// returned for each invocation, even for the same ProgID or ITypeInfo. That will cause problems later such + /// as being unable to cast an instance from one type to another, even though they are based on exactly the + /// same ProgID/ITypeInfo/etc.. In those cases, the incorrectly returns + /// false. Thus, those methods should be wrapped in the TryCachedType methods to ensure that + /// the repeated invocation will continue to return exactly same . + /// + /// For details on the issue with the , refer to: + /// https://developercommunity.visualstudio.com/content/problem/422208/typeisequivalent-does-not-behave-according-to-the.html + /// + public interface ICachedTypeService + { + bool TryInvalidate(string project, string progId = null); + bool TryGetCachedType(string progId, out Type type); + bool TryGetCachedType(string project, string progId, out Type type); + bool TryGetCachedType(ITypeInfo typeInfo, out Type type); + bool TryGetCachedType(ITypeInfo typeInfo, string project, out Type type); + Type TryGetCachedTypeFromEquivalentType(string project, string progId, Type type); + } + + public class CachedTypeService : ICachedTypeService + { + private static readonly ConcurrentDictionary TypeCaches; + private static readonly Lazy LazyInstance; + private static readonly ITypeLibQueryService QueryService; + + static CachedTypeService() + { + TypeCaches = new ConcurrentDictionary(); + TypeCaches.TryAdd(string.Empty, new LibraryTypeCache(string.Empty)); + + LazyInstance = new Lazy(() => new CachedTypeService()); + QueryService = TypeLibQueryService.Instance; + } + + /// + /// Provided primarily for uses outside the CW's DI, mainly within Rubberduck.Main. + /// + public static ICachedTypeService Instance => LazyInstance.Value; + + public bool TryGetCachedType(string progId, out Type type) + { + return TryGetCachedType(string.Empty, progId, out type); + } + + public bool TryGetCachedType(string project, string progId, out Type type) + { + if (TryGetValue(project, progId, out type)) + { + return type != null; + } + + type = Type.GetTypeFromProgID(progId); + if (type == null) + { + return type != null; + } + + if (!TryAddTypeInternal(project, progId, ref type)) + { + type = null; + } + + return type != null; + } + + public bool TryGetCachedType(ITypeInfo typeInfo, out Type type) + { + return TryGetCachedType(typeInfo, string.Empty, out type); + } + + public bool TryGetCachedType(ITypeInfo typeInfo, string project, out Type type) + { + var progId = QueryService.GetOrCreateProgIdFromITypeInfo(typeInfo); + return TryGetCachedType(typeInfo, project, progId, out type); + } + + private bool TryGetCachedType(ITypeInfo typeInfo, string project, string progId, out Type type) + { + if (TryGetValue(project, progId, out type)) + { + return type != null; + } + + if (!QueryService.TryGetTypeFromITypeInfo(typeInfo, out type)) + { + return type != null; + } + + if (!TryAddTypeInternal(project, progId, ref type)) + { + return false; + } + + return type != null; + } + + public Type TryGetCachedTypeFromEquivalentType(string project, string progId, Type type) + { + var cache = TypeCaches.GetOrAdd(project?.ToLowerInvariant() ?? string.Empty, s => new LibraryTypeCache(s)); + return cache.GetOrAdd(progId, type); + } + + /// + /// Because a can have several interfaces and those may be further used in + /// downstream operations, it's important to also cache those interfaces to ensure we do not + /// return a different type for a given interface that's implemented by the cached type. + /// + /// Additionally, we ensure that we do not cache any types + /// as those are not useful in production. In that case, we must discover the type library + /// using the and call . + /// + /// True if the type and all its interface were added. False otherwise + private bool TryAddTypeInternal(string project, string progId, ref Type type) + { + // Using local function because we don't want to accidentally add types without + // having went through the logic of checking & obtaining the types. + bool TryAdd(string progIdToAdd, Type typeToAdd) + { + var cache = TypeCaches.GetOrAdd(project?.ToLowerInvariant() ?? string.Empty, s => new LibraryTypeCache(s)); + return cache.AddType(progIdToAdd, typeToAdd); + } + + // Ensure we do not cache the generic System.__ComObject, which is useless. + if (type.Name == "__ComObject") + { + return QueryService.TryGetTypeInfoFromProgId(progId, out var typeInfo) + && TryGetCachedType(typeInfo, project?.ToLowerInvariant() ?? string.Empty, progId, out type); + } + + if (!TryAdd(progId, type)) + { + return false; + } + + return type.GetInterfaces() + .Where(face => face.FullName != null) + .All(face => TryAdd(face.FullName, face)); + } + + private static bool TryGetValue(string project, string progId, out Type type) + { + if (TypeCaches.TryGetValue(project?.ToLowerInvariant() ?? string.Empty, out var cache)) + { + return cache.TryGetType(progId, out type); + } + + type = null; + return false; + } + + public bool TryInvalidate(string project, string progId = null) + { + if (TypeCaches.TryGetValue(project?.ToLowerInvariant() ?? string.Empty, out var cache)) + { + if (!string.IsNullOrWhiteSpace(progId)) + { + return cache.Remove(progId); + } + else + { + return TypeCaches.TryRemove(cache.Key, out _); + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/IRegisteredLibraryFinderService.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/IRegisteredLibraryFinderService.cs new file mode 100644 index 0000000000..3445748494 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/IRegisteredLibraryFinderService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + public interface IRegisteredLibraryFinderService + { + IEnumerable FindRegisteredLibraries(); + bool TryGetRegisteredLibraryInfo(Guid typeLibGuid, out RegisteredLibraryInfo info); + } +} \ No newline at end of file diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/LibraryTypeCache.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/LibraryTypeCache.cs new file mode 100644 index 0000000000..baa51b6057 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/LibraryTypeCache.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + internal interface ILibraryTypeCache + { + string Key { get; } + bool TryGetType(string progId, out Type type); + bool AddType(string progId, Type type); + Type GetOrAdd(string progId, Type type); + bool Remove(string progId); + } + + internal sealed class LibraryTypeCache : ILibraryTypeCache + { + private readonly ConcurrentDictionary _cache; + + public LibraryTypeCache(string key) + { + Key = key; + _cache = new ConcurrentDictionary(); + } + + public string Key { get; } + + public bool TryGetType(string progId, out Type type) + { + return _cache.TryGetValue(progId.ToLowerInvariant(), out type); + } + + public bool AddType(string progId, Type type) + { + if (_cache.ContainsKey(progId.ToLowerInvariant())) + { + return false; + } + + _cache.AddOrUpdate(progId.ToLowerInvariant(), p => type, (p, t) => type); + return true; + } + + public Type GetOrAdd(string progId, Type type) + { + return _cache.GetOrAdd(progId.ToLowerInvariant(), s => type); + } + + public bool Remove(string progId) + { + return _cache.TryRemove(progId, out _); + } + } +} diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryFinderService.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryFinderService.cs new file mode 100644 index 0000000000..4d5cc32851 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryFinderService.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Win32; +using LIBFLAGS = System.Runtime.InteropServices.ComTypes.LIBFLAGS; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + public class RegisteredLibraryFinderService : IRegisteredLibraryFinderService + { + private static readonly List IgnoredKeys = new List { "FLAGS", "HELPDIR" }; + + public IEnumerable FindRegisteredLibraries() + { + using (var typelibSubKey = Registry.ClassesRoot.OpenSubKey("TypeLib")) + { + if (typelibSubKey == null) { yield break; } + + foreach (var guidKey in EnumerateSubKeys(typelibSubKey)) + { + foreach (var registeredLibraryInfo in ParseTypeLibRegistryData(guidKey)) + { + yield return registeredLibraryInfo; + } + } + } + } + + public bool TryGetRegisteredLibraryInfo(Guid typeLibGuid, out RegisteredLibraryInfo info) + { + info = null; + using (var typelibSubKey = Registry.ClassesRoot.OpenSubKey($"TypeLib\\{typeLibGuid:B}")) + { + if (typelibSubKey == null) + { + return false; + } + + var infos = ParseTypeLibRegistryData(typelibSubKey).ToList(); + switch(infos.Count) + { + case 0: + return false; + case 1: + info = infos.FirstOrDefault(); + break; + default: + info = infos.OrderByDescending(x => x.Major).ThenByDescending(x => x.Minor).FirstOrDefault(); + break; + } + + return info != null; + } + } + + private IEnumerable ParseTypeLibRegistryData(RegistryKey guidKey) + { + var guid = Guid.TryParseExact(guidKey.GetKeyName().ToLowerInvariant(), "B", out var clsid) + ? clsid + : Guid.Empty; + + foreach (var versionKey in EnumerateSubKeys(guidKey)) + { + var name = versionKey.GetValue(string.Empty)?.ToString(); + var version = versionKey.GetKeyName(); + + var flagValue = (LIBFLAGS) 0; + using (var flagsKey = versionKey.OpenSubKey("FLAGS")) + { + if (flagsKey != null) + { + var flags = flagsKey.GetValue(string.Empty)?.ToString() ?? "0"; + Enum.TryParse(flags, out flagValue); + } + } + + foreach (var lcid in versionKey.GetSubKeyNames().Where(key => !IgnoredKeys.Contains(key))) + { + if (!int.TryParse(lcid, out var id)) + { + continue; + } + + using (var paths = versionKey.OpenSubKey(lcid)) + { + string bit32; + string bit64; + using (var win32 = paths?.OpenSubKey("win32")) + { + bit32 = win32?.GetValue(string.Empty)?.ToString() ?? string.Empty; + } + + using (var win64 = paths?.OpenSubKey("win64")) + { + bit64 = win64?.GetValue(string.Empty)?.ToString() ?? string.Empty; + } + + yield return new RegisteredLibraryInfo(guid, name, version, bit32, bit64) + { + Flags = flagValue, + LocaleId = id + }; + } + } + } + } + + private IEnumerable EnumerateSubKeys(RegistryKey key) + { + foreach (var keyName in key.GetSubKeyNames()) + { + using (var subKey = key.OpenSubKey(keyName)) + { + if (subKey != null) + { + yield return subKey; + } + } + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryInfo.cs similarity index 88% rename from Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs rename to Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryInfo.cs index 27b06df61a..45d031c09f 100644 --- a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryInfo.cs @@ -4,22 +4,8 @@ using Path = System.IO.Path; using System.Runtime.InteropServices.ComTypes; -namespace Rubberduck.AddRemoveReferences +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection { - public struct RegisteredLibraryKey - { - public Guid Guid { get; } - public int Major { get; } - public int Minor { get; } - - public RegisteredLibraryKey(Guid guid, int major, int minor) - { - Guid = guid; - Major = major; - Minor = minor; - } - } - public class RegisteredLibraryInfo { private static readonly Dictionary NativeLocaleNames = new Dictionary diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryKey.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryKey.cs new file mode 100644 index 0000000000..8e5e607e71 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegisteredLibraryKey.cs @@ -0,0 +1,18 @@ +using System; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + public struct RegisteredLibraryKey + { + public Guid Guid { get; } + public int Major { get; } + public int Minor { get; } + + public RegisteredLibraryKey(Guid guid, int major, int minor) + { + Guid = guid; + Major = major; + Minor = minor; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegistryKeyExtensions.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegistryKeyExtensions.cs new file mode 100644 index 0000000000..3c5d735fa0 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/RegistryKeyExtensions.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Win32; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + // inspired from https://github.com/rossknudsen/Kavod.ComReflection + + public static class RegistryKeyExtensions + { + public static string GetKeyName(this RegistryKey key) + { + var name = key?.Name; + return name?.Substring(name.LastIndexOf(@"\", StringComparison.InvariantCultureIgnoreCase) + 1) ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Parsing/ComReflection/TypeLibReflection/TypeLibQueryService.cs b/Rubberduck.Parsing/ComReflection/TypeLibReflection/TypeLibQueryService.cs new file mode 100644 index 0000000000..5bea61d7a8 --- /dev/null +++ b/Rubberduck.Parsing/ComReflection/TypeLibReflection/TypeLibQueryService.cs @@ -0,0 +1,200 @@ +using System; +using System.IO.Abstractions; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using Microsoft.Win32; +using Rubberduck.InternalApi.Common; +using Rubberduck.VBEditor.Utility; +using TYPEATTR = System.Runtime.InteropServices.ComTypes.TYPEATTR; + +namespace Rubberduck.Parsing.ComReflection.TypeLibReflection +{ + public interface ITypeLibQueryService + { + bool TryGetTypeFromITypeInfo(ITypeInfo typeInfo, out Type type); + bool TryGetProgIdFromClsid(Guid clsid, out string progId); + bool TryGetTypeInfoFromProgId(string progId, out ITypeInfo typeInfo); + string GetOrCreateProgIdFromITypeInfo(ITypeInfo typeInfo); + } + + public class TypeLibQueryService : ITypeLibQueryService + { + [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true)] + private static extern int CLSIDFromProgID(string lpszProgID, out Guid lpclsid); + + [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true)] + private static extern int ProgIDFromCLSID([In]ref Guid clsid, [MarshalAs(UnmanagedType.LPWStr)]out string lplpszProgID); + + [DllImport("oleaut32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true)] + private static extern int LoadTypeLib(string fileName, out ITypeLib typeLib); + + private static readonly Lazy LazyInstance = new Lazy(); + private static readonly RegisteredLibraryFinderService Finder = new RegisteredLibraryFinderService(); + + /// + /// Provided primarily for uses outside the CW's DI, mainly within Rubberduck.Main. + /// + public static ITypeLibQueryService Instance => LazyInstance.Value; + + public bool TryGetTypeFromITypeInfo(ITypeInfo typeInfo, out Type type) + { + type = null; + var ptr = Marshal.GetComInterfaceForObject(typeInfo, typeof(ITypeInfo)); + if (ptr == IntPtr.Zero) + { + return false; + } + + using (DisposalActionContainer.Create(ptr, x => Marshal.Release(x))) + { + type = Marshal.GetTypeForITypeInfo(ptr); + } + + return type != null; + } + + public string GetOrCreateProgIdFromITypeInfo(ITypeInfo typeInfo) + { + typeInfo.GetTypeAttr(out var pAttr); + if (pAttr != IntPtr.Zero) + { + using (DisposalActionContainer.Create(pAttr, typeInfo.ReleaseTypeAttr)) + { + var attr = Marshal.PtrToStructure(pAttr); + var clsid = attr.guid; + if (TryGetProgIdFromClsid(clsid, out var progId)) + { + return progId; + } + } + } + + var typeName = Marshal.GetTypeInfoName(typeInfo); + typeInfo.GetContainingTypeLib(out var typeLib, out _); + var libName = Marshal.GetTypeLibName(typeLib); + + return string.Concat(libName, ".", typeName); + } + + public bool TryGetProgIdFromClsid(Guid clsid, out string progId) + { + return ProgIDFromCLSID(ref clsid, out progId) == 0; + } + + public bool TryGetTypeInfoFromProgId(string progId, out ITypeInfo typeInfo) + { + typeInfo = null; + if (CLSIDFromProgID(progId, out var clsid) != 0) + { + return false; + } + + if (!TryGetTypeLibFromClsid(clsid, out var lib)) + { + return false; + } + + lib.GetTypeInfoOfGuid(ref clsid, out typeInfo); + return typeInfo != null; + } + + private static bool TryGetTypeLibFromClsid(Guid clsid, out ITypeLib lib) + { + lib = null; + + using (var clsidKey = Registry.ClassesRoot.OpenSubKey($"CLSID\\{clsid:B}")) + { + if (clsidKey == null) + { + return false; + } + + if (!TryLoadTypeLibFromPath(TryGetTypeLibPath, clsidKey, out lib)) + { + return true; + } + + if (TryLoadTypeLibFromPath(TryGetInProcServerPath, clsidKey, out lib)) + { + return true; + } + + if (TryLoadTypeLibFromPath(TryGetLocalServerPath, clsidKey, out lib)) + { + return true; + } + + return false; + } + } + + private delegate bool GetPathFunction(RegistryKey clsidKey, out string path); + private static bool TryLoadTypeLibFromPath(GetPathFunction getPathFunction, RegistryKey clsidKey, out ITypeLib lib) + { + lib = null; + if (!getPathFunction(clsidKey, out var path)) + { + return false; + } + + if (LoadTypeLib(path, out lib) == 0) + { + return true; + } + + var file = FileSystemProvider.FileSystem.Path.GetFileName(path); + return LoadTypeLib(file, out lib) == 0; + } + + private static bool TryGetTypeLibPath(RegistryKey clsidKey, out string path) + { + path = null; + + using (var clsidTypeLibKey = clsidKey.OpenSubKey("TypeLib")) + { + if (clsidTypeLibKey == null) + { + return false; + } + + if (Guid.TryParseExact(((string) clsidTypeLibKey.GetValue(null)).ToLowerInvariant(), "B", out var libGuid) + && Finder.TryGetRegisteredLibraryInfo(libGuid, out var info)) + { + path = info.FullPath; + } + } + + return !string.IsNullOrWhiteSpace(path); + } + + private static bool TryGetInProcServerPath(RegistryKey clsidKey, out string path) + { + using (var procServerKey = clsidKey.OpenSubKey("InprocServer32")) + { + if (procServerKey != null) + { + path = procServerKey.GetValue(null) as string; + return true; + } + + path = null; + return false; + } + } + + private static bool TryGetLocalServerPath(RegistryKey clsidKey, out string path) + { + using (var localServerKey = clsidKey.OpenSubKey("LocalServer32")) + { + if (localServerKey != null) + { + path = localServerKey.GetValue(null) as string; + return true; + } + + path = null; + return false; + } + } + } +} diff --git a/Rubberduck.Resources/Registration/RubberduckGuid.cs b/Rubberduck.Resources/Registration/RubberduckGuid.cs index af8ceeb3d6..98a07ce2e7 100644 --- a/Rubberduck.Resources/Registration/RubberduckGuid.cs +++ b/Rubberduck.Resources/Registration/RubberduckGuid.cs @@ -2,6 +2,9 @@ { public static class RubberduckGuid { + public const string IID_IUnknown = "00000000-0000-0000-C000-000000000046"; + public const string IID_IDispatch = "00020400-0000-0000-C000-000000000046"; + // Guid Suffix private const string GuidSuffix = "-43F0-3B33-B105-9B8188A6F040"; @@ -26,6 +29,20 @@ public static class RubberduckGuid public const string IFakeGuid = UnitTestingGuidspace + "DF" + GuidSuffix; public const string IVerifyGuid = UnitTestingGuidspace + "E0" + GuidSuffix; public const string IStubGuid = UnitTestingGuidspace + "E1" + GuidSuffix; + public const string IMockProviderGuid = UnitTestingGuidspace + "E2" + GuidSuffix; + public const string MockProviderGuid = UnitTestingGuidspace + "E3" + GuidSuffix; + public const string IComMockGuid = UnitTestingGuidspace + "E4" + GuidSuffix; + public const string ComMockGuid = UnitTestingGuidspace + "E5" + GuidSuffix; + public const string ISetupArgumentDefinitionGuid = UnitTestingGuidspace + "E6" + GuidSuffix; + public const string SetupArgumentDefinitionGuid = UnitTestingGuidspace + "E7" + GuidSuffix; + public const string ISetupArgumentDefinitionsGuid = UnitTestingGuidspace + "E8" + GuidSuffix; + public const string SetupArgumentDefinitionsGuid = UnitTestingGuidspace + "E9" + GuidSuffix; + public const string ISetupArgumentCreatorGuid = UnitTestingGuidspace + "EA" + GuidSuffix; + public const string SetupArgumentCreatorGuid = UnitTestingGuidspace + "EB" + GuidSuffix; + public const string IComMockedGuid = UnitTestingGuidspace + "EC" + GuidSuffix; + public const string ComMockedGuid = UnitTestingGuidspace + "ED" + GuidSuffix; + public const string ITimesGuid = UnitTestingGuidspace + "EE" + GuidSuffix; + public const string TimesGuid = UnitTestingGuidspace + "EF" + GuidSuffix; // Rubberduck API Guids: private const string ApiGuidspace = "69E0F7"; @@ -48,6 +65,8 @@ public static class RubberduckGuid public const string DeclarationTypeGuid = RecordGuidspace + "23" + GuidSuffix; public const string AccessibilityGuid = RecordGuidspace + "24" + GuidSuffix; public const string ParserStateGuid = RecordGuidspace + "25" + GuidSuffix; + public const string SetupArgumentRangeGuid = RecordGuidspace + "26" + GuidSuffix; + public const string SetupArgumentTypeGuid = RecordGuidspace + "27" + GuidSuffix; // Debug Guids: private const string DebugGuidspace = "69E101"; diff --git a/Rubberduck.Resources/Registration/RubberduckProgId.cs b/Rubberduck.Resources/Registration/RubberduckProgId.cs index 7dc9f157e3..d09ea4270e 100644 --- a/Rubberduck.Resources/Registration/RubberduckProgId.cs +++ b/Rubberduck.Resources/Registration/RubberduckProgId.cs @@ -13,10 +13,16 @@ public static class RubberduckProgId public const string IdentifierReferencesProgId = BaseNamespace + "IdentifierReferences"; public const string ParserStateProgId = BaseNamespace + "ParserState"; public const string ApiProviderProgId = BaseNamespace + "ApiProvider"; - + public const string MockProviderProgId = BaseNamespace + "MockProvider"; + public const string ComMockProgId = BaseNamespace + "ComMock"; + public const string ComMockedProgId = BaseNamespace + "ComMocked"; + public const string SetupArgumentDefinitionProgId = BaseNamespace + "MockArgumentDefinition"; + public const string SetupArgumentDefinitionsProgId = BaseNamespace + "MockArgumentDefinitions"; + public const string SetupArgumentCreatorProgId = BaseNamespace + "MockArgumentCreator"; public const string AssertClassProgId = BaseNamespace + "AssertClass"; public const string PermissiveAssertClassProgId = BaseNamespace + "PermissiveAssertClass"; public const string FakesProviderProgId = BaseNamespace + "FakesProvider"; + public const string TimesProgId = BaseNamespace + "Times"; public const string DebugAddinObject = BaseNamespace + "VBETypeLibsAPI"; } diff --git a/Rubberduck.UnitTesting/CodeGeneration/TestCodeGeneratorStatics.cs b/Rubberduck.UnitTesting/CodeGeneration/TestCodeGeneratorStatics.cs index ad3ec0eeb6..f31f1481ab 100644 --- a/Rubberduck.UnitTesting/CodeGeneration/TestCodeGeneratorStatics.cs +++ b/Rubberduck.UnitTesting/CodeGeneration/TestCodeGeneratorStatics.cs @@ -81,11 +81,13 @@ End If private static string LateBindingDeclarations => @" Private Assert As Object - Private Fakes As Object"; + Private Fakes As Object + Private Mocks As Object"; private static string EarlyBindingDeclarations => @" Private Assert As Rubberduck.{0} - Private Fakes As Rubberduck.FakesProvider"; + Private Fakes As Rubberduck.FakesProvider + Private Mocks As Rubberduck.MockProvider"; private static string DualBindingDeclarations => $@"#Const {LateBindConstName} = {LateBindDirectiveName} @@ -98,11 +100,13 @@ End If private static string LateBindingInitialization => @" Set Assert = CreateObject(""Rubberduck.{0}"") - Set Fakes = CreateObject(""Rubberduck.FakesProvider"")"; + Set Fakes = CreateObject(""Rubberduck.FakesProvider"") + Set Mocks = CreateObject(""Rubberduck.MockProvider"")"; private static string EarlyBindingInitialization => @" Set Assert = New Rubberduck.{0} - Set Fakes = New Rubberduck.FakesProvider"; + Set Fakes = New Rubberduck.FakesProvider + Set Mocks = New Rubberduck.MockProvider"; private static string DualBindingInitialization => $@"#If {LateBindConstName} Then @@ -131,6 +135,7 @@ End Sub {ModuleCleanupComment} Set Assert = Nothing Set Fakes = Nothing + Set Mocks = Nothing End Sub '@TestInitialize diff --git a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Abstract/ITypeLibWrapperProvider.cs b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Abstract/ITypeLibWrapperProvider.cs index 2c090e8781..f6b9d1efb5 100644 --- a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Abstract/ITypeLibWrapperProvider.cs +++ b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Abstract/ITypeLibWrapperProvider.cs @@ -2,9 +2,13 @@ namespace Rubberduck.VBEditor.ComManagement.TypeLibs.Abstract { - public interface ITypeLibWrapperProvider + public interface ITypeLibWrapperProvider : ITypeLibWrapperProviderLite { ITypeLibWrapper TypeLibWrapperFromProject(string projectId); + } + + public interface ITypeLibWrapperProviderLite + { ITypeLibWrapper TypeLibWrapperFromProject(IVBProject project); } } diff --git a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibWrapperProvider.cs b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibWrapperProvider.cs index 044d4f1f0e..416c4542ca 100644 --- a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibWrapperProvider.cs +++ b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibWrapperProvider.cs @@ -5,7 +5,7 @@ // ReSharper disable once CheckNamespace namespace Rubberduck.VBEditor.ComManagement.TypeLibs { - public class TypeLibWrapperProvider : ITypeLibWrapperProvider + public class TypeLibWrapperProvider : TypeLibWrapperProviderLite, ITypeLibWrapperProvider { private readonly IProjectsProvider _projectsProvider; @@ -27,7 +27,10 @@ public ITypeLibWrapper TypeLibWrapperFromProject(string projectId) var project = _projectsProvider.Project(projectId); return TypeLibWrapperFromProject(project); } + } + public class TypeLibWrapperProviderLite : ITypeLibWrapperProviderLite + { public ITypeLibWrapper TypeLibWrapperFromProject(IVBProject project) { return project != null ? TypeLibWrapper.FromVBProject(project) : null; diff --git a/RubberduckTests/ComMock/ItByRefTests.cs b/RubberduckTests/ComMock/ItByRefTests.cs new file mode 100644 index 0000000000..8f1dac9664 --- /dev/null +++ b/RubberduckTests/ComMock/ItByRefTests.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; +using Times = Moq.Times; + +namespace RubberduckTests.ComMock +{ + [TestFixture] + [Category("ComMock.ItByRef")] + public class ItByRefTests + { + [Test] + public void Basic_Ref_Setup() + { + var mock = new Mock(); + var byRef = ItByRef.Is(1, (ref int x) => x = 2); + mock.Setup(x => x.DoInt(ref byRef.Value)).Callback(byRef.Callback); + var obj = mock.Object; + obj.DoInt(ref byRef.Value); + + Assert.AreEqual(2, byRef.Value); + mock.Verify(x => x.DoInt(ref byRef.Value), Times.Once); + } + + [Test] + public void Multiple_Ref_Setup() + { + var mock = new Mock(); + var byRef1 = ItByRef.Is(1, (ref int x) => x = 2); + var byRef2 = ItByRef.Is(3, (ref int x) => x = 5); + mock.Setup(x => x.DoInt(ref byRef1.Value)).Callback(byRef1.Callback); + mock.Setup(x => x.DoInt(ref byRef2.Value)).Callback(byRef2.Callback); + + var obj = mock.Object; + obj.DoInt(ref byRef1.Value); + obj.DoInt(ref byRef2.Value); + + Assert.AreEqual(2, byRef1.Value); + Assert.AreEqual(5, byRef2.Value); + mock.Verify(x => x.DoInt(ref byRef1.Value), Times.Once); + mock.Verify(x => x.DoInt(ref byRef2.Value), Times.Once); + } + + [Test] + public void Null_Ref_Setup() + { + var mock = new Mock(); + var byRef = ItByRef.Is(null, (ref string x) => x = string.Empty); + mock.Setup(x => x.DoString(ref byRef.Value)).Callback(byRef.Callback); + var obj = mock.Object; + obj.DoString(ref byRef.Value); + + var testString = "abc"; + obj.DoString(ref testString); + + Assert.AreEqual(string.Empty, byRef.Value); + Assert.AreEqual("abc", testString); + + mock.Verify(x => x.DoString(ref byRef.Value), Times.Once); + } + + [Test] + public void Basic_Ref_Setup_Returns() + { + var mock = new Mock(); + var byRef = ItByRef.Is(1); + mock.Setup(x => x.ReturnInt(ref byRef.Value)).Returns(2); + var obj = mock.Object; + var actual = obj.ReturnInt(ref byRef.Value); + + var negativeRef = 0; + var negativeActual = obj.ReturnInt(ref negativeRef); + + Assert.AreEqual(2, actual); + Assert.AreEqual(0, negativeActual); + } + + [Test] + public void ItByRefMemberInfos_Is() + { + Assert.IsNotNull(ItByRefMemberInfos.Is(typeof(int))); + } + + [Test] + [TestCase(nameof(ITestRef.DoInt), 1)] + [TestCase(nameof(ITestRef.DoString), "abc")] + public void Test_ByRef_Setup(string memberName, object value) + { + var definitions = new SetupArgumentDefinitions + { + SetupArgumentDefinition.CreateIs(value) + }; + var resolver = new SetupArgumentResolver(); + var builder = new SetupExpressionBuilder(typeof(ITestRef), new List(), resolver); + + var mock = new Mock(); + var setupDatas = builder.CreateExpression(memberName, definitions); + var setupData = setupDatas.First(); + + var called = false; + void Action() + { + called = true; + } + + MockExpressionBuilder.Create(mock) + .As(typeof(ITestRef)) + .Setup(setupData.SetupExpression, setupData.Args) + .Callback(Action) + .Execute(); + + object refParam = null; + switch (memberName) + { + case nameof(ITestRef.DoInt): + var refInt = (int) value; + mock.Object.DoInt(ref refInt); + refParam = refInt; + break; + case nameof(ITestRef.DoString): + var refString = (string) value; + mock.Object.DoString(ref refString); + refParam = refString; + break; + default: + Assert.Fail("Missing case for a member call"); + return; + } + + Assert.AreEqual(true, called); + Assert.AreEqual(value, refParam); + } + } +} diff --git a/RubberduckTests/ComMock/MockArgumentResolverTests.cs b/RubberduckTests/ComMock/MockArgumentResolverTests.cs new file mode 100644 index 0000000000..a05c6dddd3 --- /dev/null +++ b/RubberduckTests/ComMock/MockArgumentResolverTests.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Moq; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; + +namespace RubberduckTests.ComMock +{ + [TestFixture] + [Category("ComMocks.MockArgumentResolverTests")] + public class MockArgumentResolverTests + { + [Test] + public void Resolve_Args_Null_Returns_Null() + { + var resolver = ArrangeMockArgumentResolver(); + var results = resolver.ResolveArgs(null); + + Assert.IsNull(results); + } + + [Test] + public void Resolve_Args_Missing_Returns_Null() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = Missing.Value; + var results = resolver.ResolveArgs(arg); + + Assert.IsNull(results); + } + + [Test] + public void Resolve_Args_Missing_In_Array_Returns_Null() + { + var resolver = ArrangeMockArgumentResolver(); + object[] arg = {Missing.Value}; + var results = resolver.ResolveArgs(arg); + + Assert.IsNull(results); + } + + [Test] + public void Resolve_Args_Two_Missing_Returns_Two_IsAny() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = new[] {Missing.Value, Missing.Value}; + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(2, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.IsAny, definition.Type); + } + } + + [Test] + public void Resolve_Args_Two_Nulls_Returns_Two_IsAny() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = new object[] {null, null}; + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(2, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.IsAny, definition.Type); + } + } + + [Test] + public void Resolve_Args_Definition_Returns_Definitions() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = SetupArgumentDefinition.CreateIs(1); + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(1, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + Assert.AreEqual(1, definition.Values.Single()); + } + } + + [Test] + public void Resolve_Args_Definition_In_Array_Returns_Definitions() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = new[] { SetupArgumentDefinition.CreateIs(1) }; + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(1, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + Assert.AreEqual(1, definition.Values.Single()); + } + } + + [Test] + public void Resolve_Args_Two_Definition_Returns_Definitions() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = new[] { SetupArgumentDefinition.CreateIs(1), SetupArgumentDefinition.CreateIs(2) }; + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(2, results.Count); + + var i = 1; + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + Assert.AreEqual(i++, definition.Values.Single()); + } + } + + [Test] + public void Resolve_Args_Definitions_Returns_Itself() + { + var definitions = new SetupArgumentDefinitions + { + SetupArgumentDefinition.CreateIs(1), + SetupArgumentDefinition.CreateIs(2) + }; + + var resolver = ArrangeMockArgumentResolver(); + var results = resolver.ResolveArgs(definitions); + + Assert.AreSame(definitions, results); + } + + [Test] + public void Resolve_Args_Objects_Returns_Definitions() + { + var resolver = ArrangeMockArgumentResolver(); + var arg = new object[] {1, 2}; // must be boxed since we take them as variants from COM + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(2, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + } + } + + [Test] + [TestCase(1)] + [TestCase("1")] + [TestCase("")] + [TestCase(1.0)] + public void Resolve_Args_Single_Argument_Returns_Definitions(object arg) + { + var resolver = ArrangeMockArgumentResolver(); + var results = resolver.ResolveArgs(arg); + + Assert.AreEqual(1, results.Count); + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + } + } + + [Test] + public void Resolve_Args_Single_Array_Returns_In_Definition() + { + var array = new object[] {1, 3, 5}; // must be boxed because we get it as variant from COM + var resolver = ArrangeMockArgumentResolver(); + var args = new object[] {array}; // arrays must be double-wrapped + var results = resolver.ResolveArgs(args); + + Assert.AreEqual(1, results.Count); + var result = results.Single(); + Assert.AreEqual(SetupArgumentType.IsIn, result.Type); + Assert.AreEqual(array, result.Values); + } + + [Test] + public void Resolve_Args_Two_Array_Returns_In_Definition() + { + var array1 = new object[] {1, 3, 5}; // must be boxed because we get it as variant from COM + var array2 = new object[] {2, 4, 6}; + var resolver = ArrangeMockArgumentResolver(); + var args = new object[] { array1, array2 }; // arrays must be double-wrapped + var results = resolver.ResolveArgs(args); + + Assert.AreEqual(2, results.Count); + var i = 0; + foreach (var definition in results) + { + Assert.AreEqual(SetupArgumentType.IsIn, definition.Type); + Assert.AreEqual(args[i++], definition.Values); + } + } + + [Test] + public void Resolve_Args_Mixed_Array_And_Single_Returns_In_Definition() + { + var array = new object[] { 1, 3, 5 }; // must be boxed because we get it as variant from COM + object singleObject = 2; + var resolver = ArrangeMockArgumentResolver(); + var args = new object[] {array, singleObject}; // arrays must be double-wrapped + var results = resolver.ResolveArgs(args); + + Assert.AreEqual(2, results.Count); + Assert.AreEqual(SetupArgumentType.IsIn, results.First().Type); + Assert.AreEqual(array, results.First().Values); + Assert.AreEqual(SetupArgumentType.Is, results.Last().Type); + Assert.AreEqual(singleObject, results.Last().Values.Single()); + } + + [Test] + public void Resolve_Args_Mixed_Single_And_Array_Returns_In_Definition() + { + var array = new object[] { 1, 3, 5 }; // must be boxed because we get it as variant from COM + object singleObject = 2; + var resolver = ArrangeMockArgumentResolver(); + var args = new object[] {singleObject, array}; // arrays must be double-wrapped + var results = resolver.ResolveArgs(args); + + Assert.AreEqual(2, results.Count); + Assert.AreEqual(SetupArgumentType.Is, results.First().Type); + Assert.AreEqual(singleObject, results.First().Values.Single()); + Assert.AreEqual(SetupArgumentType.IsIn, results.Last().Type); + Assert.AreEqual(array, results.Last().Values); + } + + [Test] + [TestCase(1, 1)] + [TestCase(1, "1")] + [TestCase(1, "")] + [TestCase(1, 1.0)] + [TestCase("1", 1)] + [TestCase("1", "1")] + [TestCase("1", "")] + [TestCase("1", 1.0)] + [TestCase("", 1)] + [TestCase("", "1")] + [TestCase("", "")] + [TestCase("", 1.0)] + [TestCase(1.0, 1)] + [TestCase(1.0, "1")] + [TestCase(1.0, "")] + [TestCase(1.0, 1.0)] + public void Resolve_Args_Two_Argument_Returns_Definitions(object arg1, object arg2) + { + var resolver = ArrangeMockArgumentResolver(); + var args = new[] {arg1, arg2}; + var results = resolver.ResolveArgs(args); + + Assert.AreEqual(2, results.Count); + + var i = 0; + foreach (var definition in results) + { + var arg = args.ElementAt(i++); + Assert.AreEqual(SetupArgumentType.Is, definition.Type); + Assert.AreEqual(arg,definition.Values.Single()); + } + } + + [Test] + + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsAny, typeof(int), 1)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsAny, typeof(int), 2.2)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsAny, typeof(int), "1")] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsAny, typeof(int), null)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsAny, typeof(string), 1)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsAny, typeof(string), 2.2)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsAny, typeof(string), "1")] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsAny, typeof(string), null)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsAny, typeof(object), 1)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsAny, typeof(object), 2.2)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsAny, typeof(object), "1")] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsAny, typeof(object), null)] + + [TestCase(MethodSelection.DoInt, SetupArgumentType.Is, typeof(int), 1)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.Is, typeof(int), 2.2)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.Is, typeof(int), "1")] + [TestCase(MethodSelection.DoInt, SetupArgumentType.Is, typeof(int), null)] + [TestCase(MethodSelection.DoString, SetupArgumentType.Is, typeof(string), 1)] + [TestCase(MethodSelection.DoString, SetupArgumentType.Is, typeof(string), 2.2)] + [TestCase(MethodSelection.DoString, SetupArgumentType.Is, typeof(string), "1")] + [TestCase(MethodSelection.DoString, SetupArgumentType.Is, typeof(string), null)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.Is, typeof(object), 1)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.Is, typeof(object), 2.2)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.Is, typeof(object), "1")] + [TestCase(MethodSelection.DoObject, SetupArgumentType.Is, typeof(object), null)] + + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotNull, typeof(int), 1)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotNull, typeof(int), 2.2)] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotNull, typeof(int), "1")] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotNull, typeof(int), null)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotNull, typeof(string), 1)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotNull, typeof(string), 2.2)] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotNull, typeof(string), "1")] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotNull, typeof(string), null)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotNull, typeof(object), 1)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotNull, typeof(object), 2.2)] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotNull, typeof(object), "1")] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotNull, typeof(object), null)] + + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsIn, typeof(int), new[] {1, 3, 5})] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsIn, typeof(int), new[] {2.2, 4.4, 6.6})] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsIn, typeof(int), new[] {"1", "3", "5"})] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsIn, typeof(string), new[] { 1, 3, 5 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsIn, typeof(string), new[] { 2.2, 4.4, 6.6 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsIn, typeof(string), new[] { "1", "3", "5" })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsIn, typeof(object), new[] { 1, 3, 5 })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsIn, typeof(object), new[] { 2.2, 4.4, 6.6 })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsIn, typeof(object), new[] { "1", "3", "5" })] + + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotIn, typeof(int), new[] { 1, 3, 5 })] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotIn, typeof(int), new[] { 2.2, 4.4, 6.6 })] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsNotIn, typeof(int), new[] { "1", "3", "5" })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotIn, typeof(string), new[] { 1, 3, 5 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotIn, typeof(string), new[] { 2.2, 4.4, 6.6 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsNotIn, typeof(string), new[] { "1", "3", "5" })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotIn, typeof(object), new[] { 1, 3, 5 })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotIn, typeof(object), new[] { 2.2, 4.4, 6.6 })] + [TestCase(MethodSelection.DoObject, SetupArgumentType.IsNotIn, typeof(object), new[] { "1", "3", "5" })] + + // Cannot use objects for IsInRange because it does not have IComparable + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsInRange, typeof(int), new[] { 1, 5 })] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsInRange, typeof(int), new[] { 2.2, 6.6 })] + [TestCase(MethodSelection.DoInt, SetupArgumentType.IsInRange, typeof(int), new[] { "1", "5" })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsInRange, typeof(string), new[] { 1, 5 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsInRange, typeof(string), new[] { 2.2, 6.6 })] + [TestCase(MethodSelection.DoString, SetupArgumentType.IsInRange, typeof(string), new[] { "1", "5" })] + + public void It_SingleParameter_Tests(MethodSelection methodSelection, SetupArgumentType argumentType, Type returnType, object value) + { + ArgumentSetup[] argumentSetups; + if (value != null && value.GetType().IsArray) + { + var values = ((IEnumerable) value).Cast().ToArray(); + argumentSetups = ArrangeArgumentSetup(argumentType, returnType, values); + } + else + { + argumentSetups = ArrangeArgumentSetup(argumentType, returnType, value); + } + + var assertData = ArrangeAssertData(methodSelection, argumentSetups); + + AssertMockArgumentResolver(assertData); + } + + public static string MockArgumentMapper(SetupArgumentType argumentType) + { + switch (argumentType) + { + case SetupArgumentType.Is: + return nameof(It.Is); + case SetupArgumentType.IsAny: + return nameof(It.IsAny); + case SetupArgumentType.IsIn: + return nameof(It.IsIn); + case SetupArgumentType.IsInRange: + return nameof(It.IsInRange); + case SetupArgumentType.IsNotIn: + return nameof(It.IsNotIn); + case SetupArgumentType.IsNotNull: + return nameof(It.IsNotNull); + default: + throw new ArgumentOutOfRangeException(nameof(argumentType), argumentType, null); + } + } + + public enum MethodSelection + { + DoInt, + DoString, + DoObject + } + + public static (Type type, string name) MethodSelector(MethodSelection selection) + { + switch (selection) + { + case MethodSelection.DoInt: + return (typeof(ITest3), nameof(ITest3.DoInt)); + case MethodSelection.DoString: + return (typeof(ITest3), nameof(ITest3.DoString)); + case MethodSelection.DoObject: + return (typeof(ITest3), nameof(ITest3.DoObject)); + default: + throw new ArgumentException($"Invalid enumeration for {nameof(MethodSelection)}"); + } + } + + internal static ArgumentSetup[] ArrangeArgumentSetup(SetupArgumentType argumentType, Type returnType, object[] value) + { + return new[] + { + new ArgumentSetup(argumentType, MockArgumentMapper(argumentType), returnType, value) + }; + } + + internal static ArgumentSetup[] ArrangeArgumentSetup(SetupArgumentType argumentType, Type returnType, object value) + { + return new[] + { + new ArgumentSetup(argumentType, MockArgumentMapper(argumentType), returnType, value) + }; + } + + internal static AssertData ArrangeAssertData(MethodSelection methodSelection, ArgumentSetup[] argumentSetups) + { + var (returnType, methodName) = MethodSelector(methodSelection); + + return new AssertData( + returnType, + methodName, + argumentSetups + ); + } + + internal static SetupArgumentResolver ArrangeMockArgumentResolver() + { + return new SetupArgumentResolver(); + } + + internal void AssertMockArgumentResolver(AssertData data) + { + var resolver = ArrangeMockArgumentResolver(); + var parameterInfos = data.TargetType.GetMethod(data.MethodName)?.GetParameters(); + + Assert.IsNotNull(parameterInfos, "Reflection on method failed"); + + var mockDefinitions = new SetupArgumentDefinitions(); + foreach (var setup in data.ArgumentSetups) + { + SetupArgumentDefinition definition; + switch (setup.ArgumentType) + { + case SetupArgumentType.Is: + definition = SetupArgumentDefinition.CreateIs(setup.Value.Single()); + break; + case SetupArgumentType.IsAny: + definition = SetupArgumentDefinition.CreateIsAny(); + break; + case SetupArgumentType.IsIn: + definition = SetupArgumentDefinition.CreateIsIn(setup.Value); + break; + case SetupArgumentType.IsInRange: + Assert.AreEqual(2, setup.Value.Length); + definition=SetupArgumentDefinition.CreateIsInRange(setup.Value[0], setup.Value[1], SetupArgumentRange.Inclusive); + break; + case SetupArgumentType.IsNotIn: + definition = SetupArgumentDefinition.CreateIsNotIn(setup.Value); + break; + case SetupArgumentType.IsNotNull: + definition = SetupArgumentDefinition.CreateIsNotNull(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + mockDefinitions.Add(definition); + } + + var (expressions, args) = resolver.ResolveParameters(parameterInfos, mockDefinitions); + + Assert.AreEqual(parameterInfos.Length, expressions.Count); + + var i = 0; + foreach (var expression in expressions) + { + var assertData = data.ArgumentSetups.ElementAt(i++); + + Assert.AreEqual(assertData.ReturnType, expression.Type); + Assert.IsTrue(expression.ToString().StartsWith(string.Concat(assertData.ItType, "("))); + } + } + } + + internal readonly struct ArgumentSetup + { + public SetupArgumentType ArgumentType { get; } + public string ItType { get; } + public Type ReturnType { get; } + public object[] Value { get; } + + public ArgumentSetup(SetupArgumentType argumentType, string itType, Type returnType, object[] value) + { + ArgumentType = argumentType; + ItType = itType; + ReturnType = returnType; + Value = value; + } + + public ArgumentSetup(SetupArgumentType argumentType, string itType, Type returnType, object value) + { + ArgumentType = argumentType; + ItType = itType; + ReturnType = returnType; + { + Value = new[] { value }; + } + } + } + + internal readonly struct AssertData + { + public Type TargetType { get; } + public string MethodName { get; } + public IEnumerable ArgumentSetups { get; } + public AssertData(Type targetType, string methodName, IEnumerable argumentSetups) + { + TargetType = targetType; + MethodName = methodName; + ArgumentSetups = argumentSetups; + } + } +} diff --git a/RubberduckTests/ComMock/MockExpressionBuilderTests.cs b/RubberduckTests/ComMock/MockExpressionBuilderTests.cs new file mode 100644 index 0000000000..816f87671f --- /dev/null +++ b/RubberduckTests/ComMock/MockExpressionBuilderTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Moq; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; + +namespace RubberduckTests.ComMock +{ + [TestFixture] + [Category("ComMocks.MockExpressionBuilderTests")] + public class MockExpressionBuilderTests + { + [Test] + public void As_Compiles() + { + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + + builder.As(typeof(ITest1)) + .Execute(); + } + + [Test] + public void Setup_Void_Method_Compiles() + { + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoExpression(); + + builder.As(typeof(ITest1)) + .Setup(expression, ArrangeForwardedArgs()) + .Execute(); + } + + [Test] + public void Setup_ReturningMethod_WithReturnIgnored_Compiles() + { + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoThisExpression(); + + builder.As(typeof(ITest1)) + .Setup(expression, ArrangeForwardedArgs()) + .Execute(); + } + + [Test] + public void Setup_ReturningMethod_Compiles() + { + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoThisExpression(); + + builder.As(typeof(ITest1)) + .Setup(expression, ArrangeForwardedArgs(), typeof(int)) + .Execute(); + } + + [Test] + public void SetupWithReturns_Compiles() + { + const int expected = 42; + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoThisExpression(); + + builder.As(typeof(ITest1)) + .Setup(expression, ArrangeForwardedArgs(), typeof(int)) + .Returns(expected, typeof(int)) + .Execute(); + + Assert.AreEqual(expected, mock.Object.DoThis()); + } + + [Test] + public void SetupWithCallback_Compiles() + { + const int expected = 42; + var actual = 0; + var action = new Action(() => { actual = expected; }); + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoExpression(); + + builder.As(typeof(ITest1)) + .Setup(expression, ArrangeForwardedArgs()) + .Callback(action) + .Execute(); + + mock.Object.Do(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void Verify_Compiles() + { + // Moq.Mock.Verify throws if it's invoked and the verification fails. + + var mock = new Mock(); + var builder = MockExpressionBuilder.Create(mock); + var expression = ArrangeSetupDoExpression(); + + var badTimes = Moq.Times.Once().ToRubberduckTimes(); // test would be inconclusive with exactly 1 invoke. + + // inner exception would be the MockException. + var exception = Assert.Catch(() => + builder.As(typeof(ITest1)) + .Verify(expression, badTimes, ArrangeForwardedArgs()) + .Execute()); + Assert.IsTrue(exception.InnerException is MockException); + } + + private static IReadOnlyDictionary ArrangeForwardedArgs() + { + return new Dictionary(); + } + + private static Expression ArrangeSetupDoExpression() + { + // x => x.Do() + + var typeParameterExpression = Expression.Parameter(typeof(ITest1), "x"); + var methodInfo = typeof(ITest1).GetMethod(nameof(ITest1.Do)); + var callExpression = Expression.Call(typeParameterExpression, methodInfo); + return Expression.Lambda(callExpression, typeParameterExpression); + } + + private static Expression ArrangeSetupDoThisExpression() + { + // x => x.DoThis() + + var typeParameterExpression = Expression.Parameter(typeof(ITest1), "x"); + var methodInfo = typeof(ITest1).GetMethod(nameof(ITest1.DoThis)); + var callExpression = Expression.Call(typeParameterExpression, methodInfo); + return Expression.Lambda(callExpression, typeParameterExpression); + } + } +} diff --git a/RubberduckTests/ComMock/MockProviderTests.cs b/RubberduckTests/ComMock/MockProviderTests.cs new file mode 100644 index 0000000000..2cb2a04248 --- /dev/null +++ b/RubberduckTests/ComMock/MockProviderTests.cs @@ -0,0 +1,647 @@ +using System; +using System.Runtime.InteropServices; +using Moq; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; +using Rubberduck.Parsing.ComReflection.TypeLibReflection; +using Rubberduck.Resources.Registration; + +namespace RubberduckTests.ComMock +{ + [TestFixture] + [Category("ComMocks")] + public class MockProviderTests + { + // NOTE: this class includes unit tests that deals with COM internals. To ensure + // the tests work, it's best to stick to only COM objects that are a part of + // Windows such as Scripting library (scrrun.dll) + + private const string CLSID_FileSystemObject_String = "0D43FE01-F093-11CF-8940-00A0C9054228"; + private const string IID_FileSystem3_String = "2A0B9D10-4B87-11D3-A97A-00104B365C9F"; + private const string IID_FileSystem_String = "0AB5A3D0-E5B6-11D0-ABF5-00A0C90FFFC0"; + + private static Guid CLSID_FileSystemObject = new Guid(CLSID_FileSystemObject_String); + private static Guid IID_IFileSystem3 = new Guid(IID_FileSystem3_String); + private static Guid IID_IFileSystem = new Guid(IID_FileSystem_String); + + [Test] + public void MockProvider_Returns_ComMocked() + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + var obj = mock.Object; + + Assert.IsInstanceOf(obj); + } + + [Test] + public void MockProvider_Create_Correct_Mock() + { + var pUnk = IntPtr.Zero; + var pCom = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pCom); + Assert.AreNotEqual(IntPtr.Zero, pCom); + } + finally + { + if (pCom != IntPtr.Zero) Marshal.Release(pCom); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + public void Mocked_Implements_IDispatch() + { + var pUnk = IntPtr.Zero; + var pDis = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + var iid = new Guid("{00020400-0000-0000-C000-000000000046}"); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pDis); + + Assert.AreEqual(0, hr); + Assert.AreNotEqual(IntPtr.Zero, pDis); + } + finally + { + if (pDis != IntPtr.Zero) Marshal.Release(pDis); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + public void Mocked_Implements_IComMocked() + { + var pUnk = IntPtr.Zero; + var pDis = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + var iid = new Guid(RubberduckGuid.IComMockedGuid); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pDis); + + Assert.AreEqual(0, hr); + Assert.AreNotEqual(IntPtr.Zero, pDis); + } + finally + { + if (pDis != IntPtr.Zero) Marshal.Release(pDis); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("Scripting.FileSystemObject", "Scripting.FileSystemObject")] + [TestCase("Scripting.FileSystemObject", "scripting.filesystemobject")] + [TestCase("Scripting.FileSystemObject", "Scripting.Filesystemobject")] + [TestCase("Scripting.FileSystemObject", "SCRIPTING.FILESYSTEMOBJECT")] + [TestCase("Scripting.FileSystemObject", "sCrIpTiNg.FiLeSyStEmObJeCt")] + public void Mock_Returns_Same_Type(string input1, string input2) + { + var provider1 = new MockProvider(); + var provider2 = new MockProvider(); + + var mock1 = provider1.Mock(input1); + var mock2 = provider2.Mock(input2); + + Assert.AreEqual(mock1.Object.GetType(), mock2.Object.GetType()); + } + + [Test] + public void Mock_NoSetup_Returns_Null() + { + var pUnk = IntPtr.Zero; + var pMocked = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pMocked); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the mocked type"); + } + + dynamic proxy = Marshal.GetObjectForIUnknown(pMocked); + + Assert.IsNull(proxy.GetTempName()); + Assert.IsNull(proxy.BuildPath("abc", "def")); + } + finally + { + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + if (pMocked != IntPtr.Zero) Marshal.Release(pMocked); + } + } + + [Test] + public void Mock_Setup_No_Args_Returns_Specified_Value() + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var expected = "foo"; + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("GetTempName", expected); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.GetTempName()); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("abc", "def")] + [TestCase("", "")] + [TestCase(null, null)] + [TestCase("abc", null)] + [TestCase(null, "def")] + [TestCase("abc", "")] + [TestCase("", "def")] + public void Mock_Setup_Args_Returns_Specified_Value(string input1, string input2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var expected = "foobar"; + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", expected, new object[] {provider.It.IsAny(), provider.It.IsAny()}); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("foobar", "abc", "def")] + [TestCase("foobar", "", "")] + [TestCase(null, null, null)] + [TestCase(null, "abc", null)] + [TestCase(null, null, "def")] + [TestCase("foobar", "abc", "")] + [TestCase("foobar", "", "def")] + public void Mock_Setup_NonEmpty_Args_Returns_Specified_Value(string expected, string input1, string input2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", expected, new object[] { provider.It.IsNotNull(), provider.It.IsNotNull() }); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("foobar" ,"abc", "def")] + [TestCase(null, "def", "abc")] + [TestCase(null, "", "")] + [TestCase(null, null, null)] + [TestCase(null, "abc", null)] + public void Mock_Setup_Specified_Args_Returns_Specified_Value(string expected, string input1, string input2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", "foobar", new object[] { provider.It.Is("abc"), provider.It.Is("def") }); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("foobar", "foo", "bar", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + [TestCase("foobar", "baz", "baz", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + [TestCase("foobar", "foo", "bar", new[] { "foo" }, new[] { "bar" })] + [TestCase(null, "bar", "foo", new[] { "foo" }, new[] { "bar" })] + [TestCase(null, "derp", "duh", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + public void Mock_Setup_Args_List_Returns_Specified_Value(string expected, string input1, string input2, string[] list1, string[] list2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", "foobar", new object[] { provider.It.IsIn(list1), provider.It.IsIn(list2) }); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase(null, "foo", "bar", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + [TestCase(null, "baz", "baz", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + [TestCase(null, "foo", "bar", new[] { "foo" }, new[] { "bar" })] + [TestCase("foobar", "bar", "foo", new[] { "foo" }, new[] { "bar" })] + [TestCase("foobar", "derp", "duh", new[] { "foo", "baz" }, new[] { "bar", "baz" })] + public void Mock_Setup_Args_NotInList_Returns_Specified_Value(string expected, string input1, string input2, string[] list1, string[] list2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", "foobar", new object[] { provider.It.IsNotIn(list1), provider.It.IsNotIn(list2) }); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase("foobar", "a", "d", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase("foobar", "b", "e", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase("foobar", "c", "f", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase("foobar", "c", "d", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase("foobar", "a", "f", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase(null, "d", "d", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase(null, "a", "a", SetupArgumentRange.Inclusive, "a", "c", "d", "f")] + [TestCase("foobar", "b", "e", SetupArgumentRange.Exclusive, "a", "c", "d", "f")] + [TestCase(null, "a", "e", SetupArgumentRange.Exclusive, "a", "c", "d", "f")] + [TestCase(null, "c", "e", SetupArgumentRange.Exclusive, "a", "c", "d", "f")] + [TestCase(null, "b", "d", SetupArgumentRange.Exclusive, "a", "c", "d", "f")] + [TestCase(null, "b", "f", SetupArgumentRange.Exclusive, "a", "c", "d", "f")] + public void Mock_Setup_Args_Range_Returns_Specified_Value(string expected, string input1, string input2, SetupArgumentRange type, string start1, string end1, string start2, string end2) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mock = provider.Mock("Scripting.FileSystemObject"); + mock.SetupWithReturns("BuildPath", "foobar", new object[] { provider.It.IsInRange(start1, end1, type), provider.It.IsInRange(start2, end2, type)}); + var obj = mock.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.BuildPath(input1, input2)); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + public void Mock_FileSystemObject_Via_ComMocked() + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + if (!CachedTypeService.Instance.TryGetCachedType("Scripting.FileSystemObject", out var targetType)) + { + throw new InvalidOperationException("Unable to locate the ProgId `Scripting.FileSystemObject`"); + } + var closedMockType = typeof(Mock<>).MakeGenericType(targetType); + var mock = (Mock)Activator.CreateInstance(closedMockType); + var mockedProvider = new Mock(); + var comMock = new Rubberduck.ComClientLibrary.UnitTesting.Mocks.ComMock(mockedProvider.Object, string.Empty, "Scripting.FileSystemObject", mock, targetType, targetType.GetInterfaces()); + var comMocked = new ComMocked(comMock, targetType.GetInterfaces()); + var obj = comMocked; + + pUnk = Marshal.GetIUnknownForObject(obj); + var hr = Marshal.QueryInterface(pUnk, ref IID_IFileSystem3, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed"); + } + + dynamic proxy = Marshal.GetObjectForIUnknown(pProxy); + + Assert.IsInstanceOf(targetType, proxy); + foreach (var face in targetType.GetInterfaces()) + { + Assert.IsInstanceOf(face, proxy); + } + Assert.AreNotEqual(pUnk, pProxy); + Assert.AreNotSame(obj, proxy); + Assert.IsInstanceOf(obj); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase(IID_FileSystem3_String, true, "abc")] + [TestCase(IID_FileSystem_String, true, "abc")] + [TestCase(IID_FileSystem3_String, false, "def")] + [TestCase(IID_FileSystem_String, false, "def")] + [TestCase(IID_FileSystem3_String, false, "")] + [TestCase(IID_FileSystem_String, false, "")] + public void Mock_Setup_Property_Specified_Args_Returns_Specified_Object(string IID, bool expected, string input) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mockFso = provider.Mock("Scripting.FileSystemObject"); + var mockDrives = mockFso.SetupChildMock("Drives"); + var mockDrive = mockDrives.SetupChildMock("Item", provider.It.Is("abc")); + mockDrive.SetupWithReturns("Path", "foobar"); + var obj = mockFso.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var iid = new Guid(IID); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.Drives[input] != null); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase(IID_FileSystem3_String, "foobar", "abc")] + [TestCase(IID_FileSystem_String, "foobar", "abc")] + public void Mock_Setup_Property_Specified_Args_Returns_Specified_Value(string IID, string expected, string input) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + try + { + var provider = new MockProvider(); + var mockFso = provider.Mock("Scripting.FileSystemObject"); + var mockDrives = mockFso.SetupChildMock("Drives"); + var mockDrive = mockDrives.SetupChildMock("Item", provider.It.Is("abc")); + mockDrive.SetupWithReturns("Path", "foobar"); + var obj = mockFso.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var iid = new Guid(IID); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.AreEqual(expected, mocked.Drives[input].Path); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + } + } + + [Test] + [TestCase(IID_FileSystem3_String)] + [TestCase(IID_FileSystem_String)] + public void Mock_Verify_NoSetup_NoOp(string IID) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + const int expectedAssertsCompleted = 1; + const Rubberduck.UnitTesting.TestOutcome expectedOutcome = Rubberduck.UnitTesting.TestOutcome.Succeeded; + var assertsCompleted = 0; + Rubberduck.UnitTesting.TestOutcome outcome = Rubberduck.UnitTesting.TestOutcome.Unknown; + var handleAssertCompleted = new EventHandler((o, e) => { outcome = e.Outcome; assertsCompleted++; }); + + try + { + Rubberduck.UnitTesting.AssertHandler.OnAssertCompleted += handleAssertCompleted; + + var provider = new MockProvider(); + var mockFso = provider.Mock("Scripting.FileSystemObject"); + var obj = mockFso.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var iid = new Guid(IID); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.IsNull(mocked.Drives, "Expected null reference for not-setup object property."); + + mockFso.Verify("Drives", provider.Times.Never()); + Assert.AreEqual(expectedAssertsCompleted, assertsCompleted, "Expected asserts mismatched."); + Assert.AreEqual(expectedOutcome, outcome, "Expected test outcome mismatched."); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + Rubberduck.UnitTesting.AssertHandler.OnAssertCompleted -= handleAssertCompleted; + } + } + + [Test] + [TestCase(IID_FileSystem3_String)] + [TestCase(IID_FileSystem_String)] + public void Mock_Verify_WithSetup_WithExpectedInvocationCount_Passes(string IID) + { + var pUnk = IntPtr.Zero; + var pProxy = IntPtr.Zero; + + const int expectedAssertsCompleted = 1; + const Rubberduck.UnitTesting.TestOutcome expectedOutcome = Rubberduck.UnitTesting.TestOutcome.Succeeded; + var assertsCompleted = 0; + Rubberduck.UnitTesting.TestOutcome outcome = Rubberduck.UnitTesting.TestOutcome.Unknown; + var handleAssertCompleted = new EventHandler((o, e) => { outcome = e.Outcome; assertsCompleted++; }); + + try + { + Rubberduck.UnitTesting.AssertHandler.OnAssertCompleted += handleAssertCompleted; + + var provider = new MockProvider(); + var mockFso = provider.Mock("Scripting.FileSystemObject"); + var mockDrives = mockFso.SetupChildMock("Drives"); + var obj = mockFso.Object; + + pUnk = Marshal.GetIUnknownForObject(obj); + + var iid = new Guid(IID); + var hr = Marshal.QueryInterface(pUnk, ref iid, out pProxy); + if (hr != 0) + { + throw new InvalidCastException("QueryInterface failed on the proxy type"); + } + + dynamic mocked = Marshal.GetObjectForIUnknown(pProxy); + Assert.IsNotNull(mocked.Drives, "Expected non-null reference for set-up object property."); + + mockFso.Verify("Drives", provider.Times.Once()); + Assert.AreEqual(expectedAssertsCompleted, assertsCompleted, "Expected asserts mismatched."); + + Assert.AreEqual(expectedOutcome, outcome, "Expected test outcome mismatched."); + } + finally + { + if (pProxy != IntPtr.Zero) Marshal.Release(pProxy); + if (pUnk != IntPtr.Zero) Marshal.Release(pUnk); + Rubberduck.UnitTesting.AssertHandler.OnAssertCompleted -= handleAssertCompleted; + } + } + + /* Commented to remove the PIA reference to Scripting library, but keeping code in one day they fix type equivalence? + [Test] + public void Type_From_ITypeInfo_Are_Equivalent() + { + var other = typeof(FileSystemObject); + var service = new TypeLibQueryService(); + if (service.TryGetTypeInfoFromProgId("Scripting.FileSystemObject", out var type)) + { + Assert.IsTrue(type.IsEquivalentTo(typeof(FileSystemObject))); + } + } + */ + } +} diff --git a/RubberduckTests/ComMock/MoqReflectionAssert.cs b/RubberduckTests/ComMock/MoqReflectionAssert.cs new file mode 100644 index 0000000000..6035521bbb --- /dev/null +++ b/RubberduckTests/ComMock/MoqReflectionAssert.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Moq; +using Moq.Language; +using Moq.Language.Flow; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; + +namespace RubberduckTests.ComMock +{ + internal delegate void Callback(); + + [TestFixture] + [Category("ComMocks.MoqReflection")] + public class MoqReflectionAssert + { + [Test] + public void As_Method_Exists() + { + var asMethod = MockMemberInfos.As(typeof(object)); + var foundMethod = typeof(Mock<>).GetMethods().Single(x => + x.Name == nameof(Mock.As) && + x.IsGenericMethod && + x.GetGenericArguments().Length == 1 && + x.GetParameters().Length == 0); + + Assert.AreEqual(asMethod.GetType(), foundMethod.GetType()); + } + + [Test] + public void Setup_Method_Exists() + { + var setupMethod = MockMemberInfos.Setup(typeof(Mock), null); + var foundMethod = typeof(Mock<>).GetMethods().Single(x => + x.Name == nameof(Mock.Setup) && + x.IsGenericMethod && + x.GetGenericArguments().Length == 1 && + x.GetParameters().Length == 1); + + Assert.AreEqual(setupMethod.GetType(), foundMethod.GetType()); + } + + [Test] + public void Setup_Without_Returns_Is_Executed_On_ITest1() + { + var mocked = new Mock(); + Expression> expression = x => x.DoThis(); + var setupMethod = MockMemberInfos.Setup(mocked.GetType(), null); + + // We need to verify this succeeds + setupMethod.Invoke(mocked, new object[] { expression }); + } + + [Test] + public void Setup_Without_Returns_Is_Executed_On_ITest2() + { + var mocked = new Mock(); + Expression> expression = x => x.DoThat(); + var setupMethod = MockMemberInfos.Setup(mocked.GetType(), null); + + // We need to verify this succeeds + setupMethod.Invoke(mocked, new object[] { expression }); + } + + [Test] + public void Setup_With_Returns_Is_Executed_On_ITest1() + { + var mocked = new Mock(); + Expression> expression = x => x.DoThis(); + var setupMethod = MockMemberInfos.Setup(mocked.GetType(), typeof(int)); + + // We need to verify this succeeds + setupMethod.Invoke(mocked, new object[] {expression}); + } + + [Test] + public void Setup_With_Returns_Is_Executed_On_ITest2() + { + var mocked = new Mock(); + Expression> expression = x => x.DoThat(); + var setupMethod = MockMemberInfos.Setup(mocked.GetType(), typeof(string)); + + // We need to verify this succeeds + setupMethod.Invoke(mocked, new object[] { expression }); + } + + [Test] + public void Returns_Method_Exists() + { + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThis()); + var returnMethod = MockMemberInfos.Returns(setup.GetType()); + var foundMethod = typeof(IReturns<,>).GetMethods().Single(x => + x.Name == nameof(IReturns.Returns) && + x.IsGenericMethod && + x.GetGenericArguments().Length == 1 && + x.GetParameters().Length == 1); + + Assert.AreEqual(returnMethod.GetType(), foundMethod.GetType()); + } + + [Test] + public void Returns_Is_Executed_On_ITest1() + { + const int expected = 42; + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThis()); + var returnMethod = MockMemberInfos.Returns(setup.GetType()); + + returnMethod.Invoke(setup, new object[] { expected }); + + var test = mocked.Object; + Assert.AreEqual(expected, test.DoThis()); + } + + [Test] + public void Returns_Is_Executed_On_ITest2() + { + const string expected = "abc"; + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThat()); + var returnMethod = MockMemberInfos.Returns(setup.GetType()); + + returnMethod.Invoke(setup, new object[] { expected }); + + var test = mocked.Object; + Assert.AreEqual(expected, test.DoThat()); + } + + [Test] + public void Callback_Method_Exists() + { + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThis()); + var returnMethod = MockMemberInfos.Callback(setup.GetType()); + var foundMethod = typeof(ICallback).GetMethods().Single(x => + x.Name == nameof(ISetup.Callback) && + !x.IsGenericMethod && + x.GetGenericArguments().Length == 0 && + x.GetParameters().Length == 1 && + x.GetParameters()[0].ParameterType == typeof(Delegate)); + + Assert.AreEqual(returnMethod.GetType(), foundMethod.GetType()); + } + + [Test] + public void Callback_Is_Executed_On_ITest1() + { + const bool expected = true; + var actual = false; + void Expression() => actual = true; + + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThis()); + var callbackMethod = MockMemberInfos.Callback(setup.GetType()); + + callbackMethod.Invoke(setup, new object[] { (Callback)Expression }); + + var test = mocked.Object; + test.DoThis(); + Assert.AreEqual(expected, actual); + } + + [Test] + public void Callback_Is_Executed_On_ITest2() + { + const bool expected = true; + var actual = false; + void Expression() => actual = true; + + var mocked = new Mock(); + var setup = mocked.Setup(x => x.DoThat()); + var callbackMethod = MockMemberInfos.Callback(setup.GetType()); + + callbackMethod.Invoke(setup, new object[] { (Callback) Expression }); + + var test = mocked.Object; + test.DoThat(); + Assert.AreEqual(expected, actual); + } + } +} diff --git a/RubberduckTests/ComMock/TypesForTesting.cs b/RubberduckTests/ComMock/TypesForTesting.cs new file mode 100644 index 0000000000..95cb5dc1b5 --- /dev/null +++ b/RubberduckTests/ComMock/TypesForTesting.cs @@ -0,0 +1,127 @@ +using System; + +namespace RubberduckTests.ComMock +{ + + public interface ITest1 + { + void Do(); + int DoThis(); + } + + public interface ITest2 + { + string DoThat(); + } + + public interface ITest3 + { + void DoInt(int something); + void DoString(string something); + void DoObject(object something); + } + + public interface ITestRef + { + void DoInt(ref int something); + void DoString(ref string something); + void DoObject(ref object something); + + int ReturnInt(ref int something); + } + + public class ConvertibleTest : IConvertible + { + private readonly TypeCode _code; + + public ConvertibleTest(TypeCode code) + { + _code = code; + } + + public TypeCode GetTypeCode() + { + return _code; + } + + public bool ToBoolean(IFormatProvider provider) + { + return true; + } + + public char ToChar(IFormatProvider provider) + { + return 't'; + } + + public sbyte ToSByte(IFormatProvider provider) + { + return 1; + } + + public byte ToByte(IFormatProvider provider) + { + return 1; + } + + public short ToInt16(IFormatProvider provider) + { + return 1; + } + + public ushort ToUInt16(IFormatProvider provider) + { + return 1; + } + + public int ToInt32(IFormatProvider provider) + { + return 1; + } + + public uint ToUInt32(IFormatProvider provider) + { + return 1; + } + + public long ToInt64(IFormatProvider provider) + { + return 1; + } + + public ulong ToUInt64(IFormatProvider provider) + { + return 1; + } + + public float ToSingle(IFormatProvider provider) + { + return 1; + } + + public double ToDouble(IFormatProvider provider) + { + return 1; + } + + public decimal ToDecimal(IFormatProvider provider) + { + return 1; + } + + public DateTime ToDateTime(IFormatProvider provider) + { + return DateTime.MinValue; + } + + public string ToString(IFormatProvider provider) + { + return "true"; + } + + public object ToType(Type conversionType, IFormatProvider provider) + { + return this; + } + } +} diff --git a/RubberduckTests/ComMock/VariantConverterTests.cs b/RubberduckTests/ComMock/VariantConverterTests.cs new file mode 100644 index 0000000000..cf9c6ad382 --- /dev/null +++ b/RubberduckTests/ComMock/VariantConverterTests.cs @@ -0,0 +1,458 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using NUnit.Framework; +using Rubberduck.ComClientLibrary.UnitTesting.Mocks; + +namespace RubberduckTests.ComMock +{ + [TestFixture] + public class VariantConverterTests + { + private const string TheOneTrueDateFormat = "yyyy-MM-dd HH:mm:ss"; + + [Test] + [TestCase(true, typeof(bool), ExpectedResult = true)] + [TestCase(false, typeof(bool), ExpectedResult = false)] + [TestCase("1", typeof(bool), ExpectedResult = true)] + [TestCase("0", typeof(bool), ExpectedResult = false)] + [TestCase("-1", typeof(bool), ExpectedResult = true)] + [TestCase(1.0, typeof(bool), ExpectedResult = true)] + [TestCase(0.0, typeof(bool), ExpectedResult = false)] + [TestCase(-0.0, typeof(bool), ExpectedResult = false)] + [TestCase(-1.0, typeof(bool), ExpectedResult = true)] + [TestCase(1, typeof(bool), ExpectedResult = true)] + [TestCase(0, typeof(bool), ExpectedResult = false)] + [TestCase(-0, typeof(bool), ExpectedResult = false)] + [TestCase(-1, typeof(bool), ExpectedResult = true)] + + [TestCase(true, typeof(byte), ExpectedResult = 255)] + [TestCase(false, typeof(byte), ExpectedResult = 0)] + [TestCase("1", typeof(byte), ExpectedResult = 1)] + [TestCase("0", typeof(byte), ExpectedResult = 0)] + [TestCase("255", typeof(byte), ExpectedResult = 255)] + [TestCase(1.0, typeof(byte), ExpectedResult = 1)] + [TestCase(0.0, typeof(byte), ExpectedResult = 0)] + [TestCase(-0.0, typeof(byte), ExpectedResult = 0)] + [TestCase(255.0, typeof(byte), ExpectedResult = 255)] + [TestCase(1, typeof(byte), ExpectedResult = 1)] + [TestCase(0, typeof(byte), ExpectedResult = 0)] + [TestCase(-0, typeof(byte), ExpectedResult = 0)] + [TestCase(255, typeof(byte), ExpectedResult = 255)] + + [TestCase(true, typeof(sbyte), ExpectedResult = -1)] + [TestCase(false, typeof(sbyte), ExpectedResult = 0)] + [TestCase("1", typeof(sbyte), ExpectedResult = 1)] + [TestCase("0", typeof(sbyte), ExpectedResult = 0)] + [TestCase("-1", typeof(sbyte), ExpectedResult = -1)] + [TestCase(1.0, typeof(sbyte), ExpectedResult = 1)] + [TestCase(0.0, typeof(sbyte), ExpectedResult = 0)] + [TestCase(-0.0, typeof(sbyte), ExpectedResult = 0)] + [TestCase(-1.0, typeof(sbyte), ExpectedResult = -1)] + [TestCase(1, typeof(sbyte), ExpectedResult = 1)] + [TestCase(0, typeof(sbyte), ExpectedResult = 0)] + [TestCase(-0, typeof(sbyte), ExpectedResult = 0)] + [TestCase(-1, typeof(sbyte), ExpectedResult = -1)] + + [TestCase(true, typeof(short), ExpectedResult = -1)] + [TestCase(false, typeof(short), ExpectedResult = 0)] + [TestCase("1", typeof(short), ExpectedResult = 1)] + [TestCase("0", typeof(short), ExpectedResult = 0)] + [TestCase("-1", typeof(short), ExpectedResult = -1)] + [TestCase(1.0, typeof(short), ExpectedResult = 1)] + [TestCase(0.0, typeof(short), ExpectedResult = 0)] + [TestCase(-0.0, typeof(short), ExpectedResult = 0)] + [TestCase(-1.0, typeof(short), ExpectedResult = -1)] + [TestCase(1, typeof(short), ExpectedResult = 1)] + [TestCase(0, typeof(short), ExpectedResult = 0)] + [TestCase(-0, typeof(short), ExpectedResult = 0)] + [TestCase(-1, typeof(short), ExpectedResult = -1)] + + [TestCase(true, typeof(ushort), ExpectedResult = 65535)] + [TestCase(false, typeof(ushort), ExpectedResult = 0)] + [TestCase("1", typeof(ushort), ExpectedResult = 1)] + [TestCase("0", typeof(ushort), ExpectedResult = 0)] + [TestCase("255", typeof(ushort), ExpectedResult = 255)] + [TestCase(1.0, typeof(ushort), ExpectedResult = 1)] + [TestCase(0.0, typeof(ushort), ExpectedResult = 0)] + [TestCase(-0.0, typeof(ushort), ExpectedResult = 0)] + [TestCase(255.0, typeof(ushort), ExpectedResult = 255)] + [TestCase(1, typeof(ushort), ExpectedResult = 1)] + [TestCase(0, typeof(ushort), ExpectedResult = 0)] + [TestCase(-0, typeof(ushort), ExpectedResult = 0)] + [TestCase(255, typeof(ushort), ExpectedResult = 255)] + + [TestCase(true, typeof(int), ExpectedResult = -1)] + [TestCase(false, typeof(int), ExpectedResult = 0)] + [TestCase("1", typeof(int), ExpectedResult = 1)] + [TestCase("0", typeof(int), ExpectedResult = 0)] + [TestCase("-1", typeof(int), ExpectedResult = -1)] + [TestCase(1.0, typeof(int), ExpectedResult = 1)] + [TestCase(0.0, typeof(int), ExpectedResult = 0)] + [TestCase(-0.0, typeof(int), ExpectedResult = 0)] + [TestCase(-1.0, typeof(int), ExpectedResult = -1)] + [TestCase(1, typeof(int), ExpectedResult = 1)] + [TestCase(0, typeof(int), ExpectedResult = 0)] + [TestCase(-0, typeof(int), ExpectedResult = 0)] + [TestCase(-1, typeof(int), ExpectedResult = -1)] + + [TestCase(true, typeof(uint), ExpectedResult = 4294967295)] + [TestCase(false, typeof(uint), ExpectedResult = 0)] + [TestCase("1", typeof(uint), ExpectedResult = 1)] + [TestCase("0", typeof(uint), ExpectedResult = 0)] + [TestCase("255", typeof(uint), ExpectedResult = 255)] + [TestCase(1.0, typeof(uint), ExpectedResult = 1)] + [TestCase(0.0, typeof(uint), ExpectedResult = 0)] + [TestCase(-0.0, typeof(uint), ExpectedResult = 0)] + [TestCase(255.0, typeof(uint), ExpectedResult = 255)] + [TestCase(1, typeof(uint), ExpectedResult = 1)] + [TestCase(0, typeof(uint), ExpectedResult = 0)] + [TestCase(-0, typeof(uint), ExpectedResult = 0)] + [TestCase(255, typeof(uint), ExpectedResult = 255)] + + [TestCase(true, typeof(long), ExpectedResult = -1)] + [TestCase(false, typeof(long), ExpectedResult = 0)] + [TestCase("1", typeof(long), ExpectedResult = 1)] + [TestCase("0", typeof(long), ExpectedResult = 0)] + [TestCase("-1", typeof(long), ExpectedResult = -1)] + [TestCase(1.0, typeof(long), ExpectedResult = 1)] + [TestCase(0.0, typeof(long), ExpectedResult = 0)] + [TestCase(-0.0, typeof(long), ExpectedResult = 0)] + [TestCase(-1.0, typeof(long), ExpectedResult = -1)] + [TestCase(1, typeof(long), ExpectedResult = 1)] + [TestCase(0, typeof(long), ExpectedResult = 0)] + [TestCase(-0, typeof(long), ExpectedResult = 0)] + [TestCase(-1, typeof(long), ExpectedResult = -1)] + + [TestCase(true, typeof(ulong), ExpectedResult = 18446744073709551615)] + [TestCase(false, typeof(ulong), ExpectedResult = 0)] + [TestCase("1", typeof(ulong), ExpectedResult = 1)] + [TestCase("0", typeof(ulong), ExpectedResult = 0)] + [TestCase("255", typeof(ulong), ExpectedResult = 255)] + [TestCase(1.0, typeof(ulong), ExpectedResult = 1)] + [TestCase(0.0, typeof(ulong), ExpectedResult = 0)] + [TestCase(-0.0, typeof(ulong), ExpectedResult = 0)] + [TestCase(255.0, typeof(ulong), ExpectedResult = 255)] + [TestCase(1, typeof(ulong), ExpectedResult = 1)] + [TestCase(0, typeof(ulong), ExpectedResult = 0)] + [TestCase(-0, typeof(ulong), ExpectedResult = 0)] + [TestCase(255, typeof(ulong), ExpectedResult = 255)] + + [TestCase(true, typeof(float), ExpectedResult = -1f)] + [TestCase(false, typeof(float), ExpectedResult = 0f)] + [TestCase("1", typeof(float), ExpectedResult = 1f)] + [TestCase("0", typeof(float), ExpectedResult = 0f)] + [TestCase("-1", typeof(float), ExpectedResult = -1f)] + [TestCase(1.0, typeof(float), ExpectedResult = 1.0f)] + [TestCase(0.0, typeof(float), ExpectedResult = 0.0f)] + [TestCase(-0.0, typeof(float), ExpectedResult = -0.0f)] + [TestCase(-1.0, typeof(float), ExpectedResult = -1.0f)] + [TestCase(1, typeof(float), ExpectedResult = 1f)] + [TestCase(0, typeof(float), ExpectedResult = 0f)] + [TestCase(-0, typeof(float), ExpectedResult = 0f)] + [TestCase(-1, typeof(float), ExpectedResult = -1f)] + + [TestCase(true, typeof(double), ExpectedResult = -1d)] + [TestCase(false, typeof(double), ExpectedResult = 0d)] + [TestCase("1", typeof(double), ExpectedResult = 1d)] + [TestCase("0", typeof(double), ExpectedResult = 0d)] + [TestCase("-1", typeof(double), ExpectedResult = -1d)] + [TestCase(1.0, typeof(double), ExpectedResult = 1.0d)] + [TestCase(0.0, typeof(double), ExpectedResult = 0.0d)] + [TestCase(-0.0, typeof(double), ExpectedResult = -0.0d)] + [TestCase(-1.0, typeof(double), ExpectedResult = -1.0d)] + [TestCase(1, typeof(double), ExpectedResult = 1d)] + [TestCase(0, typeof(double), ExpectedResult = 0d)] + [TestCase(-0, typeof(double), ExpectedResult = 0d)] + [TestCase(-1, typeof(double), ExpectedResult = -1d)] + + [TestCase(true, typeof(decimal), ExpectedResult = -1)] + [TestCase(false, typeof(decimal), ExpectedResult = 0)] + [TestCase("1", typeof(decimal), ExpectedResult = 1)] + [TestCase("0", typeof(decimal), ExpectedResult = 0)] + [TestCase("-1", typeof(decimal), ExpectedResult = -1)] + [TestCase(1.0, typeof(decimal), ExpectedResult = 1.0)] + [TestCase(0.0, typeof(decimal), ExpectedResult = 0.0)] + [TestCase(-0.0, typeof(decimal), ExpectedResult = -0.0)] + [TestCase(-1.0, typeof(decimal), ExpectedResult = -1.0)] + [TestCase(1, typeof(decimal), ExpectedResult = 1)] + [TestCase(0, typeof(decimal), ExpectedResult = 0)] + [TestCase(-0, typeof(decimal), ExpectedResult = 0)] + [TestCase(-1, typeof(decimal), ExpectedResult = -1)] + [TestCase(1.0d, typeof(string), ExpectedResult = "1")] + [TestCase(0.0d, typeof(string), ExpectedResult = "0")] + // Unstable test case - it will pass in VS runnner but fail in Resharper runnger + // [TestCase(-0.0d, typeof(string), ExpectedResult = "0")] + [TestCase(-1.0d, typeof(string), ExpectedResult = "-1")] + [TestCase(true, typeof(string), ExpectedResult = "-1")] + [TestCase(false, typeof(string), ExpectedResult = "0")] + [TestCase("1", typeof(string), ExpectedResult = "1")] + [TestCase("0", typeof(string), ExpectedResult = "0")] + [TestCase("-1", typeof(string), ExpectedResult = "-1")] + [TestCase(1, typeof(string), ExpectedResult = "1")] + [TestCase(0, typeof(string), ExpectedResult = "0")] + [TestCase(-0, typeof(string), ExpectedResult = "0")] + [TestCase(-1, typeof(string), ExpectedResult = "-1")] + + public object Test_ObjectConversion_SimpleValues(object value, Type targetType) + { + var result = VariantConverter.ChangeType(value, targetType); + + if (result is DateTime dt) + { + return dt.ToString(TheOneTrueDateFormat); + } + + return result; + } + + [Test] + [TestCase(true, typeof(DateTime), ExpectedResult = "1899-12-29 00:00:00")] + [TestCase(false, typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase("1899/12/31", typeof(DateTime), ExpectedResult = "1899-12-31 00:00:00")] + [TestCase("1899/12/30", typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase("1899/12/29", typeof(DateTime), ExpectedResult = "1899-12-29 00:00:00")] + [TestCase("1899-12-30", typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase("04/12/2000", typeof(DateTime), ExpectedResult = "2000-04-12 00:00:00")] + [TestCase("12/04/2000", typeof(DateTime), ExpectedResult = "2000-12-04 00:00:00")] + [TestCase("13/04/2000", typeof(DateTime), ExpectedResult = "2000-04-13 00:00:00")] + [TestCase(1.0, typeof(DateTime), ExpectedResult = "1899-12-31 00:00:00")] + [TestCase(0.0, typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase(-0.0, typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase(-1.0, typeof(DateTime), ExpectedResult = "1899-12-29 00:00:00")] + [TestCase(1, typeof(DateTime), ExpectedResult = "1899-12-31 00:00:00")] + [TestCase(0, typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase(-0, typeof(DateTime), ExpectedResult = "1899-12-30 00:00:00")] + [TestCase(-1, typeof(DateTime), ExpectedResult = "1899-12-29 00:00:00")] + [TestCase(-657434.00001157413d, typeof(DateTime), ExpectedResult = "0100-01-01 00:00:01")] + [TestCase(-657434.000011574d, typeof(DateTime), ExpectedResult = "0100-01-01 00:00:01")] + [TestCase(2958465.999988426d, typeof(DateTime), ExpectedResult = "9999-12-31 23:59:59")] + [TestCase(2958465.99998843d, typeof(DateTime), ExpectedResult = "9999-12-31 23:59:59")] + [TestCase(0.5d, typeof(DateTime), ExpectedResult = "1899-12-30 12:00:00")] + [TestCase(-0.5d, typeof(DateTime), ExpectedResult = "1899-12-30 12:00:00")] + public string Test_ObjectConversion_SimpleValues_To_Date(object value, Type targetType) + { + var culture = new CultureInfo("en-US"); + var result = VariantConverter.ChangeType(value, targetType, culture); + + if (result is DateTime dt) + { + return dt.ToString(TheOneTrueDateFormat); + } + + // Invalid result + return string.Empty; + } + + /// + /// Do NOT write localization-sensitive tests in this test. + /// Use instead + /// + [Test] + [TestCase(1.2, typeof(int), ExpectedResult = 1)] + [TestCase(1.2, typeof(float), ExpectedResult = 1.2f)] + [TestCase(1.2, typeof(double), ExpectedResult = 1.2d)] + [TestCase(1.2, typeof(decimal), ExpectedResult = 1.2)] + [TestCase(1.2, typeof(DateTime), ExpectedResult = "1899-12-31 04:48:00")] + public object Test_ObjectConversion_Currency(decimal value, Type targetType) + { + var cy = new CurrencyWrapper(value); + var result = VariantConverter.ChangeType(cy, targetType); + + if(result is DateTime dt) + { + return dt.ToString(TheOneTrueDateFormat); + } + + return result; + } + + [Test] + [TestCase(1.0, typeof(string), "en-US", ExpectedResult = "1")] + [TestCase(0.0, typeof(string), "en-US", ExpectedResult = "0")] + [TestCase(-1.0, typeof(string), "en-US", ExpectedResult = "-1")] + [TestCase(0.1, typeof(string), "en-US", ExpectedResult = "0.1")] + [TestCase(-0.1, typeof(string), "en-US", ExpectedResult = "-0.1")] + [TestCase(1.0, typeof(string), "de-DE", ExpectedResult = "1")] + [TestCase(0.0, typeof(string), "de-DE", ExpectedResult = "0")] + [TestCase(-1.0, typeof(string), "de-DE", ExpectedResult = "-1")] + [TestCase(0.1, typeof(string), "de-DE", ExpectedResult = "0,1")] + [TestCase(-0.1, typeof(string), "de-DE", ExpectedResult = "-0,1")] + public object Test_ObjectConversion_Currency_Localized(decimal value, Type targetType, string locale) + { + var cy = new CurrencyWrapper(value); + return VariantConverter.ChangeType(cy, targetType, new CultureInfo(locale)); + } + + /// + /// Do NOT write localization-sensitive tests in this test. + /// Use instead + /// + /// Also, some double values can be ambiguous. See + /// for the coverages of those ambiguous results. + /// + [Test] + [TestCase("1899-12-30 00:00:00", typeof(int), ExpectedResult = 0)] + [TestCase("1899-12-31 00:00:00", typeof(int), ExpectedResult = 1)] + [TestCase("1899-12-29 00:00:00", typeof(int), ExpectedResult = -1)] + [TestCase("1899-12-30 00:00:00", typeof(double), ExpectedResult = 0d)] + [TestCase("1899-12-31 00:00:00", typeof(double), ExpectedResult = 1d)] + [TestCase("1899-12-29 00:00:00", typeof(double), ExpectedResult = -1d)] + [TestCase("1899-12-30 12:00:00", typeof(double), ExpectedResult = 0.5d)] + [TestCase("1899-12-29 12:00:00", typeof(double), ExpectedResult = -1.5d)] + [TestCase("0100-01-01 00:00:00", typeof(double), ExpectedResult = -657434d)] + [TestCase("0100-01-01 00:00:01", typeof(double), ExpectedResult = -657434.00001157413d)] // Note: VBA returns -657434.000011574d but accepts the other values as equal + [TestCase("9999-12-31 23:59:59", typeof(double), ExpectedResult = 2958465.999988426d)] // Note: VBA returns 2958465.99998843d but accepts the other values as equal + public object Test_ObjectConversion_Date(string value, Type targetType) + { + var date = DateTime.ParseExact(value, TheOneTrueDateFormat, CultureInfo.InvariantCulture); + return VariantConverter.ChangeType(date, targetType); + } + + [Test] + [TestCase("1899-12-30 00:00:00", typeof(string), "en-US", ExpectedResult = "12:00:00 AM")] + [TestCase("1899-12-31 00:00:00", typeof(string), "en-US", ExpectedResult = "12/31/1899")] + [TestCase("1899-12-29 00:00:00", typeof(string), "en-US", ExpectedResult = "12/29/1899")] + [TestCase("1899-12-30 01:00:00", typeof(string), "en-US", ExpectedResult = "1:00:00 AM")] + [TestCase("1899-12-31 11:00:00", typeof(string), "en-US", ExpectedResult = "12/31/1899 11:00:00 AM")] + [TestCase("1899-12-29 13:00:00", typeof(string), "de-DE", ExpectedResult = "29.12.1899 13:00:00")] + [TestCase("1899-12-30 00:00:00", typeof(string), "de-DE", ExpectedResult = "00:00:00")] + [TestCase("1899-12-31 00:00:00", typeof(string), "de-DE", ExpectedResult = "31.12.1899")] + [TestCase("1899-12-29 00:00:00", typeof(string), "de-DE", ExpectedResult = "29.12.1899")] + [TestCase("1899-12-30 01:00:00", typeof(string), "de-DE", ExpectedResult = "01:00:00")] + [TestCase("1899-12-31 11:00:00", typeof(string), "de-DE", ExpectedResult = "31.12.1899 11:00:00")] + [TestCase("1899-12-29 13:00:00", typeof(string), "de-DE", ExpectedResult = "29.12.1899 13:00:00")] + public object Test_ObjectConversion_Date_Localized(string value, Type targetType, string locale) + { + var culture = new CultureInfo(locale); + var date = DateTime.ParseExact(value, TheOneTrueDateFormat, culture); + return VariantConverter.ChangeType(date, targetType, culture); + } + + [Test] + [TestCase(typeof(string))] + [TestCase(typeof(byte))] + [TestCase(typeof(short))] + [TestCase(typeof(ushort))] + [TestCase(typeof(int))] + [TestCase(typeof(uint))] // the doc says default marshal will use it but this doesn't seem to be the case when converting + [TestCase(typeof(long))] + [TestCase(typeof(ulong))] + public void Test_Error_Is_Not_Convertible(Type targetType) + { + Assert.Throws(() => + { + var err = new ErrorWrapper(1); + VariantConverter.ChangeType(err, targetType); + }); + } + + [Test] + public void Test_ObjectConversion_Object() + { + var obj = new object(); + var unk = new UnknownWrapper(obj); + var result = VariantConverter.ChangeType(unk, typeof(DispatchWrapper)); + + Assert.AreSame(obj, result); + + var result2 = VariantConverter.ChangeType(result, typeof(UnknownWrapper)); + + Assert.AreSame(unk.WrappedObject, result2); + } + + [Test] + [TestCase(1)] + [TestCase("1")] + [TestCase(1.0)] + [TestCase("")] + [TestCase(null)] + public void Test_ObjectConversion_DbNull(object value) + { + var result = VariantConverter.ChangeType(value, VARENUM.VT_NULL); + Assert.IsInstanceOf(result); + } + + [Test] + [TestCase(1)] + [TestCase("1")] + [TestCase(1.0)] + [TestCase("")] + [TestCase(null)] + public void Test_ObjectConversion_Null(object value) + { + var result = VariantConverter.ChangeType(value, VARENUM.VT_EMPTY); + Assert.IsNull(result); + } + + [Test] + [TestCase(VARENUM.VT_NULL, TypeCode.DBNull, ExpectedResult = typeof(DBNull))] + [TestCase(VARENUM.VT_BSTR, TypeCode.String, ExpectedResult = typeof(string))] + [TestCase(VARENUM.VT_CY, TypeCode.Decimal, ExpectedResult = typeof(decimal))] + [TestCase(VARENUM.VT_DATE, TypeCode.DateTime, ExpectedResult = typeof(DateTime))] + [TestCase(VARENUM.VT_DECIMAL, TypeCode.Decimal, ExpectedResult = typeof(decimal))] + [TestCase(VARENUM.VT_I2, TypeCode.Int16, ExpectedResult = typeof(short))] + [TestCase(VARENUM.VT_I4, TypeCode.Int32, ExpectedResult = typeof(int))] + [TestCase(VARENUM.VT_I8, TypeCode.Int64, ExpectedResult = typeof(long))] + [TestCase(VARENUM.VT_R4, TypeCode.Single, ExpectedResult = typeof(float))] + [TestCase(VARENUM.VT_R8, TypeCode.Double, ExpectedResult = typeof(double))] + [TestCase(VARENUM.VT_UI2, TypeCode.UInt16, ExpectedResult = typeof(ushort))] + [TestCase(VARENUM.VT_UI4, TypeCode.UInt32, ExpectedResult = typeof(uint))] + [TestCase(VARENUM.VT_UI8, TypeCode.UInt64, ExpectedResult = typeof(ulong))] + [TestCase(VARENUM.VT_UNKNOWN, TypeCode.Object, ExpectedResult = typeof(ConvertibleTest))] + public Type Test_ObjectConversion_IConvertible(VARENUM vt, TypeCode code) + { + var convertible = new ConvertibleTest(code); + var result = VariantConverter.ChangeType(convertible, vt); + return result.GetType(); + } + + [Test] + public void Test_ObjectConversion_IConvertible_Null() + { + var convertible = new ConvertibleTest(TypeCode.Empty); + var result = VariantConverter.ChangeType(convertible, VARENUM.VT_EMPTY); + Assert.IsNull(result); + } + + /* + [Test] + public void Test_Array_Conversion() + { + var array = new List + { + 0x48, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x2C, + 0x00, + 0x20, + 0x00, + 0x77, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x6C, + 0x00, + 0x64, + 0x00, + 0x21, + 0x00 + }; + + var result = VariantConversion.VariantChangeType(array, VARENUM.VT_BSTR); + + Assert.AreEqual("Hello, world!", result); + } + */ + } +} diff --git a/RubberduckTests/RubberduckTests.csproj b/RubberduckTests/RubberduckTests.csproj index 7cc948b69e..90d8bc378d 100644 --- a/RubberduckTests/RubberduckTests.csproj +++ b/RubberduckTests/RubberduckTests.csproj @@ -77,7 +77,7 @@ 4.1.0 - 4.8.2 + 4.13.0 4.5.10 @@ -91,9 +91,6 @@ 3.10.0 - - 4.4.0 - diff --git a/docs/Attributions.md b/docs/Attributions.md index 2fc27bee60..6af47b4fc3 100644 --- a/docs/Attributions.md +++ b/docs/Attributions.md @@ -24,6 +24,10 @@ Without the EasyHook library, many of our more advanced Unit Testing features wo EasyHook is released under the [MIT license](https://github.com/EasyHook/EasyHook#license). +### [Moq](https://github.com/moq/moq4) + +Moq is used within Ruberduck's unit tests for its code and also as mocking engine for VBA mocking framework. Moq makes it easy to create mocks of any types, and rather than investing hours in writing our own mocking framework for VBA, we simply wrap the methods for ease of use within VBA and delegate the work to Moq. + ### [WPF Localization Using RESX Files](http://www.codeproject.com/Articles/35159/WPF-Localization-Using-RESX-Files) This library makes localizing WPF applications at runtime using resx files a breeze. Thank you [Grant Frisken](http://www.codeproject.com/script/Membership/View.aspx?mid=1079060)!