diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..5c23afc0c4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +# Build Rubberduck in GitHub Actions using windows-2019 +# For now we don't run the tests, but might add it in the future. +# This is a simple build script that builds the Rubberduck project using MSBuild. +# It uses the latest version of Visual Studio 2019 and the .NET Framework 4.6.2 + +name: Build Rubberduck + +on: + workflow_dispatch: + +jobs: + build: + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.1 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v1.1.1 + + - name: Restore NuGet packages + run: | + nuget RubberduckMeta.sln + nuget restore Rubberduck.sln + + - name: Build Solution (Release) + run: msbuild Rubberduck.sln /p:Configuration=Release /p:Platform="Any CPU" /p:TargetFrameworkVersion=v4.6.2 /verbosity:minimal + + - name: Upload Build Artifacts + uses: actions/upload-artifact@v4 + with: + name: rubberduck-Release + path: | + Rubberduck.Main/bin/Release/net462/ + if-no-files-found: error + diff --git a/Rubberduck.InternalApi/Common/StringLineBuilder.cs b/Rubberduck.InternalApi/Common/StringLineBuilder.cs new file mode 100644 index 0000000000..c1d576ff44 --- /dev/null +++ b/Rubberduck.InternalApi/Common/StringLineBuilder.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rubberduck.InternalApi.Common +{ + /// + /// Extension to StringBuilder to allow adding text line by line. + /// + public class StringLineBuilder + { + private readonly StringBuilder _document = new StringBuilder(); + + public override string ToString() => _document.ToString(); + + public void AppendLine(string value = "") + => _document.Append(value + "\r\n"); + + public void AppendLineNoNullChars(string value) + => AppendLine(value.Replace("\0", string.Empty)); + } +} diff --git a/Rubberduck.Main/Extension.cs b/Rubberduck.Main/Extension.cs index 3e9880c29c..2f2d981ae5 100644 --- a/Rubberduck.Main/Extension.cs +++ b/Rubberduck.Main/Extension.cs @@ -2,6 +2,7 @@ using Extensibility; using NLog; using Rubberduck.Common.WinAPI; +using Rubberduck.ExternalApi; using Rubberduck.Resources; using Rubberduck.Resources.Registration; using Rubberduck.Root; @@ -9,6 +10,7 @@ using Rubberduck.Settings; using Rubberduck.SettingsProvider; using Rubberduck.UI; +using Rubberduck.UnitTesting; using Rubberduck.VBEditor.ComManagement; using Rubberduck.VBEditor.ComManagement.TypeLibs; using Rubberduck.VBEditor.Events; @@ -50,12 +52,16 @@ public class _Extension : IDTExtensibility2 private bool _isInitialized; private bool _isBeginShutdownExecuted; + private IExternalAPI _externalAPI; + private GeneralSettings _initialSettings; private IWindsorContainer _container; private App _app; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public void OnAddInsUpdate(ref Array custom) { } [SuppressMessage("ReSharper", "InconsistentNaming")] @@ -91,12 +97,10 @@ public void OnConnection(object Application, ext_ConnectMode ConnectMode, object Console.WriteLine(e); } } - - [Conditional("DEBUG")] private void SetAddInObject() { - // FOR DEBUGGING/DEVELOPMENT PURPOSES, ALLOW ACCESS TO SOME VBETypeLibsAPI FEATURES FROM VBA - _addin.Object = new VBETypeLibsAPI_Object(_vbe); + _externalAPI = new ExternalAPI(new VBETypeLibsAPI_Object(_vbe)); + _addin.Object = _externalAPI; } private Assembly LoadFromSameFolder(object sender, ResolveEventArgs args) @@ -239,6 +243,8 @@ private void Startup() _app = _container.Resolve(); _app.Startup(); + InitializeExternalAPI(); + _isInitialized = true; } catch (Exception e) @@ -248,6 +254,11 @@ private void Startup() } } + private void InitializeExternalAPI() + { + _externalAPI.InitializeAPIs(_container.Resolve()); + } + private void HandleAppDomainException(object sender, UnhandledExceptionEventArgs e) { var message = e.IsTerminating diff --git a/Rubberduck.Main/ExternalAPIs/ExternalAPI.cs b/Rubberduck.Main/ExternalAPIs/ExternalAPI.cs new file mode 100644 index 0000000000..e90900303c --- /dev/null +++ b/Rubberduck.Main/ExternalAPIs/ExternalAPI.cs @@ -0,0 +1,66 @@ +using Rubberduck.Resources.Registration; +using Rubberduck.UnitTesting; +using Rubberduck.VBEditor.ComManagement.TypeLibs; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Rubberduck.ExternalApi +{ + [ + ComVisible(true), + Guid(RubberduckGuid.IExternalAPIInterfaceGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual), + EditorBrowsable(EditorBrowsableState.Always) + ] + public interface IExternalAPI + { + [DispId(1)] + void InitializeAPIs(ITestEngine testEngine); + [DispId(2)] + ITestEngineAPI TestEngineAPI { get; } + [DispId(3)] + IVBETypeLibsAPI_Object VBETypeLibsAPI { get; } + } + + [ + ComVisible(true), + Guid(RubberduckGuid.ExternalAPIObjectGuid), + ProgId(RubberduckProgId.ExternalAPIObject), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(IExternalAPI)), + EditorBrowsable(EditorBrowsableState.Always) + ] + public class ExternalAPI : IExternalAPI + { + private readonly IVBETypeLibsAPI_Object _vbeTypeLibsAPI_Object; + private ITestEngineAPI _testEngineAPI; + + public ExternalAPI(IVBETypeLibsAPI_Object vbeTypeLibsAPI_Object) + { + _vbeTypeLibsAPI_Object = vbeTypeLibsAPI_Object; + } + + public void InitializeAPIs(ITestEngine testEngine) + { + _testEngineAPI = new TestEngineAPI(testEngine); + } + + public IVBETypeLibsAPI_Object VBETypeLibsAPI { get => _vbeTypeLibsAPI_Object; } + public ITestEngineAPI TestEngineAPI + { + get + { + if (_testEngineAPI == null) + { + throw new InvalidOperationException("TestEngineAPI is not initialized."); + } + return _testEngineAPI; + } + } + } +} diff --git a/Rubberduck.Main/ExternalAPIs/TestEngineAPI.cs b/Rubberduck.Main/ExternalAPIs/TestEngineAPI.cs new file mode 100644 index 0000000000..b90355d8a6 --- /dev/null +++ b/Rubberduck.Main/ExternalAPIs/TestEngineAPI.cs @@ -0,0 +1,66 @@ +using Rubberduck.InternalApi.Common; +using Rubberduck.Resources.Registration; +using Rubberduck.UnitTesting; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Rubberduck.ExternalApi +{ + [ + ComVisible(true), + Guid(RubberduckGuid.ITestEngineAPIInterfaceGuid), + InterfaceType(ComInterfaceType.InterfaceIsDual), + EditorBrowsable(EditorBrowsableState.Always) + ] + public interface ITestEngineAPI + { + [DispId(1)] + string RunAllTestsAndGetResults(string filePath); + } + + [ + ComVisible(true), + Guid(RubberduckGuid.TestEngineAPIObjectGuid), + ProgId(RubberduckProgId.TestEngineAPIProgId), + ClassInterface(ClassInterfaceType.None), + ComDefaultInterface(typeof(ITestEngineAPI)), + EditorBrowsable(EditorBrowsableState.Always) + ] + public class TestEngineAPI : ITestEngineAPI + { + private readonly ITestEngine _testEngine; + + public TestEngineAPI(ITestEngine testEngine) + { + _testEngine = testEngine; + } + + /// + /// Runs all unit tests and returns the results as a formatted string. + /// + /// A string containing the test results. + public string RunAllTestsAndGetResults(string logPath) + { + + // Note that we can't use CanRun in case we are triggering the test via VBA since DesignMode is always set to false when you run a macro. + // and CanRun interprets this as "not ready to run tests". + + var task = Task.Run(() => { + var output = _testEngine.RunWithResults(_testEngine.Tests); + if (!string.IsNullOrEmpty(logPath)) + { + FileSystemProvider.FileSystem.File.WriteAllText(logPath, output.ToString()); + } + }); + + return "Task started to run tests asynchronously. Check the log file for results."; + + } + } + +} diff --git a/Rubberduck.Resources/Registration/RubberduckGuid.cs b/Rubberduck.Resources/Registration/RubberduckGuid.cs index 1429bc8307..638c7c04ef 100644 --- a/Rubberduck.Resources/Registration/RubberduckGuid.cs +++ b/Rubberduck.Resources/Registration/RubberduckGuid.cs @@ -71,7 +71,7 @@ public static class RubberduckGuid public const string ITimesGuid = UnitTestingGuidspace + "EE" + GuidSuffix; public const string TimesGuid = UnitTestingGuidspace + "EF" + GuidSuffix; - // Rubberduck API Guids: + // Rubberduck Internal API Guids: private const string ApiGuidspace = "69E0F7"; public const string IDeclarationGuid = ApiGuidspace + "81" + GuidSuffix; public const string DeclarationClassGuid = ApiGuidspace + "82" + GuidSuffix; @@ -87,6 +87,12 @@ public static class RubberduckGuid public const string IIdentifierReferencesGuid = ApiGuidspace + "8C" + GuidSuffix; public const string IdentifierReferencesClassGuid = ApiGuidspace + "8D" + GuidSuffix; + // Rubberduck External API Guids: + public const string ExternalAPIObjectGuid = ApiGuidspace + "A0" + GuidSuffix; + public const string IExternalAPIInterfaceGuid = ApiGuidspace + "A1" + GuidSuffix; + public const string TestEngineAPIObjectGuid = ApiGuidspace + "A2" + GuidSuffix; + public const string ITestEngineAPIInterfaceGuid = ApiGuidspace + "A3" + GuidSuffix; + // Enum Guids: private const string RecordGuidspace = "69E100"; public const string DeclarationTypeGuid = RecordGuidspace + "23" + GuidSuffix; diff --git a/Rubberduck.Resources/Registration/RubberduckProgId.cs b/Rubberduck.Resources/Registration/RubberduckProgId.cs index fdb3b18419..d79e83564a 100644 --- a/Rubberduck.Resources/Registration/RubberduckProgId.cs +++ b/Rubberduck.Resources/Registration/RubberduckProgId.cs @@ -50,5 +50,8 @@ public static class RubberduckProgId public const string TimesProgId = BaseNamespace + "Times"; public const string DebugAddinObject = BaseNamespace + "VBETypeLibsAPI"; + + public const string ExternalAPIObject = BaseNamespace + "ExternalAPI"; + public const string TestEngineAPIProgId = BaseNamespace + "TestEngineAPI"; } } diff --git a/Rubberduck.UnitTesting/UnitTesting/ITestEngine.cs b/Rubberduck.UnitTesting/UnitTesting/ITestEngine.cs index c47a639cb3..7a134aa700 100644 --- a/Rubberduck.UnitTesting/UnitTesting/ITestEngine.cs +++ b/Rubberduck.UnitTesting/UnitTesting/ITestEngine.cs @@ -17,6 +17,7 @@ public interface ITestEngine bool CanRun { get; } bool CanRepeatLastRun { get; } void Run(IEnumerable tests); + string RunWithResults(IEnumerable tests); void RunByOutcome(TestOutcome outcome); void RepeatLastRun(); void RequestCancellation(); diff --git a/Rubberduck.UnitTesting/UnitTesting/TestEngine.cs b/Rubberduck.UnitTesting/UnitTesting/TestEngine.cs index 6a79d7b644..40bd124678 100644 --- a/Rubberduck.UnitTesting/UnitTesting/TestEngine.cs +++ b/Rubberduck.UnitTesting/UnitTesting/TestEngine.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; using NLog; using Rubberduck.InternalApi.Extensions; @@ -15,6 +16,7 @@ using Rubberduck.VBEditor.ComManagement; using Rubberduck.VBEditor.ComManagement.TypeLibs.Abstract; using Rubberduck.VBEditor.SafeComWrappers.Abstract; +using Rubberduck.InternalApi.Common; namespace Rubberduck.UnitTesting { @@ -158,6 +160,103 @@ public void Run(IEnumerable tests) RunInternal(queued); }); } + public string RunWithResults(IEnumerable tests) + { + if (tests == null) + { + // Trigger the ParseRequest programmatically to make the Tests property available. + var parseCompletion = new TaskCompletionSource(); + EventHandler parseCompletedHandler = null; + + parseCompletedHandler = (sender, args) => + { + if (args.State == ParserState.Ready) + { + _state.StateChanged -= parseCompletedHandler; // Unsubscribe from the event + parseCompletion.SetResult(true); // Signal that parsing is complete + } + }; + + _state.StateChanged += parseCompletedHandler; + _state.OnParseRequested(this); + + parseCompletion.Task.Wait(); + + tests = Tests; + } + + var queued = tests.ToList(); + var results = new List(); + + foreach (var test in queued.Where(item => _knownOutcomes.ContainsKey(item))) + { + _knownOutcomes.Remove(test); + } + + Task.Run(() => + { + var suspensionResult = _state.OnSuspendParser(this, AllowedRunStates, () => + { + results.AddRange(RunWhileSuspendedWithResults(tests)); + }); + + switch (suspensionResult.Outcome) + { + case SuspensionOutcome.Completed: + break; + case SuspensionOutcome.Canceled: + Logger.Debug("Test execution canceled."); + break; + default: + Logger.Warn($"Test execution failed with suspension outcome {suspensionResult.Outcome}."); + if (suspensionResult.EncounteredException != null) + { + Logger.Error(suspensionResult.EncounteredException); + } + break; + } + }).GetAwaiter().GetResult(); // Ensure the task completes before returning results. + + var resultBuilder = new StringLineBuilder(); + foreach (var result in results) + { + // Get the TestName, but stop at the first \r\n to get only the signature + int index = result.TestName.IndexOf("\r\n"); + var signature = index >= 0 ? result.TestName.Substring(0, index) : result.TestName; + resultBuilder.AppendLine($"{result.Result.Outcome}: {signature}"); + } + + return resultBuilder.ToString(); + } + + private IEnumerable RunWhileSuspendedWithResults(IEnumerable tests) + { + var results = new List(); + + var testTask = _uiDispatcher.StartTask(() => + { + results.AddRange(RunWhileSuspendedOnUiThread(tests)); + }); + testTask.Wait(); + + return results; + } + + private T TestResultOrTestInfo(TestMethod test, TestResult testResult) + { + if (typeof(T) == typeof(TestResult)) + { + return (T)(object)testResult; + } + else if (typeof(T) == typeof(TestInfo)) + { + return (T)(object)new TestInfo(test.TestCode, testResult); + } + else + { + throw new InvalidOperationException("Unsupported type for test result."); + } + } public void RunByOutcome(TestOutcome outcome) { @@ -235,10 +334,16 @@ protected void RunWhileSuspended(IEnumerable tests) private void RunWhileSuspendedOnUiThread(IEnumerable tests) { + RunWhileSuspendedOnUiThread(tests); + } + + private IEnumerable RunWhileSuspendedOnUiThread(IEnumerable tests) + { + var results = new List(); var testMethods = tests as IList ?? tests.ToList(); if (!testMethods.Any()) { - return; + return results; } _lastRun.Clear(); @@ -252,9 +357,12 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) Logger.Warn(e); foreach (var test in testMethods) { - OnTestCompleted(test, new TestResult(TestOutcome.Failed, AssertMessages.Prerequisite_EarlyBindingReferenceMissing)); + var testResult = new TestResult(TestOutcome.Failed, AssertMessages.Prerequisite_EarlyBindingReferenceMissing); + var result = TestResultOrTestInfo(test, testResult); + OnTestCompleted(test, testResult); + results.Add(result); } - return; + return results; } var overallTime = new Stopwatch(); @@ -283,7 +391,9 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) Logger.Error(ex, "Unexpected COM exception while initializing tests for module {0}. The module will be skipped.", moduleName.Name); foreach (var method in moduleTestMethods) { - OnTestCompleted(method, new TestResult(TestOutcome.Unknown, AssertMessages.TestRunner_ModuleInitializeFailure)); + var result = new TestResult(TestOutcome.Unknown, AssertMessages.TestRunner_ModuleInitializeFailure); + OnTestCompleted(method, result); + results.Add(TestResultOrTestInfo(method, result)); } continue; } @@ -294,7 +404,9 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) // no need to run setup/teardown for ignored tests if (test.Declaration.Annotations.Any(a => a.Annotation is IgnoreTestAnnotation)) { - OnTestCompleted(test, new TestResult(TestOutcome.Ignored)); + var result = new TestResult(TestOutcome.Ignored); + OnTestCompleted(test, result); + results.Add(TestResultOrTestInfo(test, result)); continue; } @@ -307,7 +419,9 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) } catch (COMException trace) { - OnTestCompleted(test, new TestResult(TestOutcome.Inconclusive, AssertMessages.TestRunner_TestInitializeFailure)); + var newResult = new TestResult(TestOutcome.Inconclusive, AssertMessages.TestRunner_TestInitializeFailure); + OnTestCompleted(test, newResult); + results.Add(TestResultOrTestInfo(test, newResult)); Logger.Trace(trace, "Unexpected COMException when running TestInitialize"); continue; } @@ -328,6 +442,7 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) // we can trigger this event, because cleanup can fail without affecting the result OnTestCompleted(test, result); + results.Add(TestResultOrTestInfo(test, result)); RunTestCleanup(typeLibWrapper, testCleanup); } @@ -354,13 +469,15 @@ private void RunWhileSuspendedOnUiThread(IEnumerable tests) catch (Exception ex) { // FIXME somehow notify the user of this mess - Logger.Error(ex, "Unexpected expection while running unit tests; unit tests will be aborted"); + Logger.Error(ex, "Unexpected exception while running unit tests; unit tests will be aborted"); } CancellationRequested = false; overallTime.Stop(); TestRunCompleted?.Invoke(this, new TestRunCompletedEventArgs(overallTime.ElapsedMilliseconds)); + + return results; } private void RunTestCleanup(ITypeLibWrapper wrapper, List cleanupMethods) diff --git a/Rubberduck.UnitTesting/UnitTesting/TestResult.cs b/Rubberduck.UnitTesting/UnitTesting/TestResult.cs index d3f0ed5aa0..6ba20ac2ac 100644 --- a/Rubberduck.UnitTesting/UnitTesting/TestResult.cs +++ b/Rubberduck.UnitTesting/UnitTesting/TestResult.cs @@ -24,4 +24,23 @@ public override bool Equals(object obj) && Output == other.Output; } } + + public readonly struct TestInfo + { + public TestInfo(string testName, TestResult result) + { + TestName = testName; + Result = result; + } + public string TestName { get; } + public TestResult Result { get; } + public override int GetHashCode() => HashCode.Compute(TestName, Result); + public override bool Equals(object obj) + { + return obj is TestInfo other + && TestName == other.TestName + && Result.Equals(other.Result); + } + + } } diff --git a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibsAPI.cs b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibsAPI.cs index 0e3f527445..a524735115 100644 --- a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibsAPI.cs +++ b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Public/TypeLibsAPI.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Rubberduck.InternalApi.Common; using Rubberduck.Resources.Registration; using Rubberduck.VBEditor.ComManagement.TypeLibs.Abstract; @@ -68,6 +69,7 @@ public class VBETypeLibsAPI_Object : IVBETypeLibsAPI_Object { private IVBE _ide; private readonly VBETypeLibsAPI _api; + private object _testEngineProvider; public VBETypeLibsAPI_Object(IVBE ide) { diff --git a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/DocClassHelper.cs b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/DocClassHelper.cs index 1a7c4bda77..566e7c74f0 100644 --- a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/DocClassHelper.cs +++ b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/DocClassHelper.cs @@ -3,22 +3,6 @@ namespace Rubberduck.VBEditor.ComManagement.TypeLibs.Utility { - /// - /// Extension to StringBuilder to allow adding text line by line. - /// - internal class StringLineBuilder - { - private readonly StringBuilder _document = new StringBuilder(); - - public override string ToString() => _document.ToString(); - - public void AppendLine(string value = "") - => _document.Append(value + "\r\n"); - - public void AppendLineNoNullChars(string value) - => AppendLine(value.Replace("\0", string.Empty)); - } - /// /// An enumeration used for identifying the type of a VBA document class /// diff --git a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/TypeInfoDocumentationExtensions.cs b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/TypeInfoDocumentationExtensions.cs index aea0c3c014..ae671168a7 100644 --- a/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/TypeInfoDocumentationExtensions.cs +++ b/Rubberduck.VBEEditor/ComManagement/TypeLibs/Utility/TypeInfoDocumentationExtensions.cs @@ -1,4 +1,5 @@ using Rubberduck.VBEditor.ComManagement.TypeLibs.Abstract; +using Rubberduck.InternalApi.Common; namespace Rubberduck.VBEditor.ComManagement.TypeLibs.Utility {