|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Diagnostics.CodeAnalysis; |
| 4 | +using System.Linq; |
| 5 | +using Moq; |
| 6 | +using NUnit.Framework; |
| 7 | +using Rubberduck.Parsing.UIContext; |
| 8 | +using Rubberduck.Parsing.VBA; |
| 9 | +using Rubberduck.UnitTesting; |
| 10 | +using Rubberduck.VBEditor.ComManagement.TypeLibs; |
| 11 | +using Rubberduck.VBEditor.SafeComWrappers; |
| 12 | +using Rubberduck.VBEditor.SafeComWrappers.Abstract; |
| 13 | +using RubberduckTests.Mocks; |
| 14 | + |
| 15 | +namespace RubberduckTests.UnitTesting |
| 16 | +{ |
| 17 | + internal class MockedTestEngine : IDisposable |
| 18 | + { |
| 19 | + public delegate void RunTestMethodCallback(ITypeLibWrapper wrapper, TestMethod method, EventHandler<AssertCompletedEventArgs> assertListener, out long duration); |
| 20 | + public delegate (long Duration, TestResult result) ReturnTestResult(ITypeLibWrapper wrapper, TestMethod method, EventHandler<AssertCompletedEventArgs> assertListener, out long duration); |
| 21 | + |
| 22 | + private const string TestMethodTemplate = |
| 23 | +@"'@TestMethod |
| 24 | +Public Sub TestMethod{0}() |
| 25 | +End Sub"; |
| 26 | + |
| 27 | + private const string IgnoredTestTemplate = |
| 28 | +@"'@TestMethod |
| 29 | +'@IgnoreTest |
| 30 | +Public Sub TestMethod{0}() |
| 31 | +End Sub"; |
| 32 | + |
| 33 | + private const string TestMethodCategoryTemplate = |
| 34 | + @"'@TestMethod(""{1}"") |
| 35 | +Public Sub TestMethod{0}() |
| 36 | +End Sub"; |
| 37 | + |
| 38 | + private const string IgnoredTestCategoryTemplate = |
| 39 | + @"'@TestMethod(""{1}"") |
| 40 | +'@IgnoreTest |
| 41 | +Public Sub TestMethod{0}() |
| 42 | +End Sub"; |
| 43 | + |
| 44 | + private readonly Mock<IFakesFactory> _fakesFactory = new Mock<IFakesFactory>(); |
| 45 | + private readonly Mock<IFakes> _createdFakes = new Mock<IFakes>(); |
| 46 | + private long _durationStub; |
| 47 | + |
| 48 | + private MockedTestEngine() |
| 49 | + { |
| 50 | + Dispatcher.Setup(d => d.InvokeAsync(It.IsAny<Action>())).Callback((Action action) => action.Invoke()).Verifiable(); |
| 51 | + |
| 52 | + TypeLib.Setup(tlm => tlm.Dispose()).Verifiable(); |
| 53 | + WrapperProvider.Setup(p => p.TypeLibWrapperFromProject(It.IsAny<string>())).Returns(TypeLib.Object).Verifiable(); |
| 54 | + |
| 55 | + _fakesFactory.Setup(factory => factory.Create()).Returns(_createdFakes.Object); |
| 56 | + } |
| 57 | + |
| 58 | + public MockedTestEngine(string testModuleCode) : this() |
| 59 | + { |
| 60 | + var builder = new MockVbeBuilder() |
| 61 | + .ProjectBuilder("TestProject1", ProjectProtection.Unprotected) |
| 62 | + .AddComponent("TestModule1", ComponentType.StandardModule, TestModuleHeader + testModuleCode) |
| 63 | + .AddProjectToVbeBuilder(); |
| 64 | + |
| 65 | + Vbe = builder.Build(); |
| 66 | + ParserState = MockParser.Create(Vbe.Object).State; |
| 67 | + TestEngine = new TestEngine(ParserState, _fakesFactory.Object, VbeInteraction.Object, WrapperProvider.Object, Dispatcher.Object, Vbe.Object); |
| 68 | + } |
| 69 | + |
| 70 | + public MockedTestEngine(IReadOnlyList<string> moduleNames, IReadOnlyList<int> methodCounts) : this() |
| 71 | + { |
| 72 | + if (moduleNames.Count != methodCounts.Count) |
| 73 | + { |
| 74 | + Assert.Inconclusive("Test setup error."); |
| 75 | + } |
| 76 | + |
| 77 | + var builder = new MockVbeBuilder(); |
| 78 | + var project = builder.ProjectBuilder("TestProject1", ProjectProtection.Unprotected); |
| 79 | + |
| 80 | + for (var index = 0; index < moduleNames.Count; index++) |
| 81 | + { |
| 82 | + var testModuleCode = string.Join(Environment.NewLine, Enumerable.Range(1, methodCounts[index]).Select(num => GetTestMethod(num))); |
| 83 | + |
| 84 | + project.AddComponent(moduleNames[index], ComponentType.StandardModule, TestModuleHeader + testModuleCode); |
| 85 | + } |
| 86 | + |
| 87 | + project.AddProjectToVbeBuilder(); |
| 88 | + Vbe = builder.Build(); |
| 89 | + ParserState = MockParser.Create(Vbe.Object).State; |
| 90 | + TestEngine = new TestEngine(ParserState, _fakesFactory.Object, VbeInteraction.Object, WrapperProvider.Object, Dispatcher.Object, Vbe.Object); |
| 91 | + } |
| 92 | + |
| 93 | + public MockedTestEngine(int testMethodCount) |
| 94 | + : this(string.Join(Environment.NewLine, Enumerable.Range(1, testMethodCount).Select(num => GetTestMethod(num)))) |
| 95 | + { } |
| 96 | + |
| 97 | + public MockedTestEngine(List<(TestOutcome Outcome, string Output, long duration)> results) |
| 98 | + : this(string.Join(Environment.NewLine, Enumerable.Range(1, results.Count).Select(num => GetTestMethod(num, results[num - 1].Outcome == TestOutcome.Ignored)))) |
| 99 | + { |
| 100 | + ParserState.OnParseRequested(this); |
| 101 | + var testMethodCount = results.Count; |
| 102 | + var testMethods = TestEngine.Tests.ToList(); |
| 103 | + |
| 104 | + if (testMethods.Count != testMethodCount) |
| 105 | + { |
| 106 | + Assert.Inconclusive("Test setup failure."); |
| 107 | + } |
| 108 | + |
| 109 | + for (var test = 0; test < results.Count; test++) |
| 110 | + { |
| 111 | + var (outcome, output, duration) = results[test]; |
| 112 | + SetupAssertCompleted(testMethods[test], new TestResult(outcome, output, duration)); |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + public RubberduckParserState ParserState { get; set; } |
| 117 | + |
| 118 | + public Mock<IVBE> Vbe { get; set; } |
| 119 | + |
| 120 | + public ITestEngine TestEngine { get; set; } |
| 121 | + |
| 122 | + public Mock<IVBEInteraction> VbeInteraction { get; } = new Mock<IVBEInteraction>(); |
| 123 | + |
| 124 | + public Mock<ITypeLibWrapper> TypeLib { get; } = new Mock<ITypeLibWrapper>(); |
| 125 | + |
| 126 | + public Mock<IUiDispatcher> Dispatcher { get; } = new Mock<IUiDispatcher>(); |
| 127 | + |
| 128 | + public Mock<ITypeLibWrapperProvider> WrapperProvider { get; } = new Mock<ITypeLibWrapperProvider>(); |
| 129 | + |
| 130 | + public static string GetTestMethod(int number, bool ignored = false, string category = null) => |
| 131 | + category is null |
| 132 | + ? string.Format(ignored ? IgnoredTestTemplate : TestMethodTemplate, number) |
| 133 | + : string.Format(ignored ? IgnoredTestCategoryTemplate : TestMethodCategoryTemplate, number, category); |
| 134 | + |
| 135 | + public void SetupAssertCompleted(Action action) |
| 136 | + { |
| 137 | + if (action is null) |
| 138 | + { |
| 139 | + VbeInteraction.Setup(ia => ia.RunTestMethod(TypeLib.Object, It.IsAny<TestMethod>(), It.IsAny<EventHandler<AssertCompletedEventArgs>>(), out _durationStub)) |
| 140 | + .Verifiable(); |
| 141 | + return; |
| 142 | + } |
| 143 | + |
| 144 | + var callback = GetAssertCompletedCallback(action); |
| 145 | + |
| 146 | + VbeInteraction |
| 147 | + .Setup(ia => ia.RunTestMethod(TypeLib.Object, It.IsAny<TestMethod>(), It.IsAny<EventHandler<AssertCompletedEventArgs>>(), out _durationStub)) |
| 148 | + .Callback(callback) |
| 149 | + .Verifiable(); |
| 150 | + } |
| 151 | + |
| 152 | + [SuppressMessage("ReSharper", "ImplicitlyCapturedClosure")] //This is fine - the closures' lifespan is limited to the test. |
| 153 | + public void SetupAssertCompleted(TestMethod testMethod, TestResult result) |
| 154 | + { |
| 155 | + Action action; |
| 156 | + switch (result.Outcome) |
| 157 | + { |
| 158 | + // Tests involving this case have concurrency issues when the UIDispatcher is mocked. The callbacks in production |
| 159 | + // are moderated by flushing the message queue. There is no way to mock this behavior because in the testing environment |
| 160 | + // there is no STA boundary being crossed, so the internal implementation of the AssertHandler is not constrained by |
| 161 | + // the rental context that would normally be managed by Interop. These tests *do* pass as of the commit that adds them, |
| 162 | + // but only if the context is force by running them through the debugger. Leaving these in as commented code mainly for |
| 163 | + // the purpose of documenting this. Single asserts are fine. |
| 164 | + |
| 165 | + //case TestOutcome.SpectacularFail: |
| 166 | + // action = () => |
| 167 | + // { |
| 168 | + // for (var failure = 0; failure < 10; failure++) |
| 169 | + // { |
| 170 | + // AssertHandler.OnAssertFailed(result.Output, testMethod.Declaration.IdentifierName); |
| 171 | + // } |
| 172 | + // }; |
| 173 | + // break; |
| 174 | + case TestOutcome.Failed: |
| 175 | + action = () => AssertHandler.OnAssertFailed(result.Output, testMethod.Declaration.IdentifierName); |
| 176 | + break; |
| 177 | + case TestOutcome.Inconclusive: |
| 178 | + action = () => AssertHandler.OnAssertInconclusive(result.Output); |
| 179 | + break; |
| 180 | + case TestOutcome.Succeeded: |
| 181 | + action = AssertHandler.OnAssertSucceeded; |
| 182 | + break; |
| 183 | + default: |
| 184 | + action = () => { }; |
| 185 | + break; |
| 186 | + } |
| 187 | + |
| 188 | + var callback = new RunTestMethodCallback((ITypeLibWrapper _, TestMethod method, EventHandler<AssertCompletedEventArgs> assertHandler, out long duration) => |
| 189 | + { |
| 190 | + duration = 0; |
| 191 | + AssertHandler.OnAssertCompleted += assertHandler; |
| 192 | + action.Invoke(); |
| 193 | + AssertHandler.OnAssertCompleted -= assertHandler; |
| 194 | + }); |
| 195 | + |
| 196 | + VbeInteraction |
| 197 | + .Setup(ia => ia.RunTestMethod(TypeLib.Object, It.Is<TestMethod>(test => testMethod.Equals(test)), It.IsAny<EventHandler<AssertCompletedEventArgs>>(), out _durationStub)) |
| 198 | + .Callback(callback) |
| 199 | + .Verifiable(); |
| 200 | + } |
| 201 | + |
| 202 | + public RunTestMethodCallback GetAssertCompletedCallback(Action action) |
| 203 | + { |
| 204 | + return (ITypeLibWrapper _, TestMethod method, EventHandler<AssertCompletedEventArgs> assertHandler, out long duration) => |
| 205 | + { |
| 206 | + duration = 0; |
| 207 | + AssertHandler.OnAssertCompleted += assertHandler; |
| 208 | + action.Invoke(); |
| 209 | + AssertHandler.OnAssertCompleted -= assertHandler; |
| 210 | + }; |
| 211 | + } |
| 212 | + |
| 213 | + public void Dispose() |
| 214 | + { |
| 215 | + ParserState?.Dispose(); |
| 216 | + } |
| 217 | + |
| 218 | + public const string TestModuleHeader = @"Option Explicit |
| 219 | +Option Private Module |
| 220 | +
|
| 221 | +'@TestModule |
| 222 | +
|
| 223 | +Private Assert As Object |
| 224 | +
|
| 225 | +'@ModuleInitialize |
| 226 | +Public Sub ModuleInitialize() |
| 227 | + 'this method runs once per module. |
| 228 | + Assert = CreateObject(""Rubberduck.AssertClass"") |
| 229 | +End Sub |
| 230 | +
|
| 231 | +'@ModuleCleanup |
| 232 | +Public Sub ModuleCleanup() |
| 233 | + 'this method runs once per module. |
| 234 | +End Sub |
| 235 | +
|
| 236 | +'@TestInitialize |
| 237 | +Public Sub TestInitialize() |
| 238 | + 'this method runs before every test in the module. |
| 239 | +End Sub |
| 240 | +
|
| 241 | +'@TestCleanup |
| 242 | +Public Sub TestCleanup() |
| 243 | + 'this method runs after every test in the module. |
| 244 | +End Sub |
| 245 | +"; |
| 246 | + } |
| 247 | +} |
0 commit comments