diff --git a/.gitignore b/.gitignore index b57d6c37..9aa07c02 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ mono_crash.*.json .fake .ionide +gdUnit4Net.sln.DotSettings.user diff --git a/PackageVersions.props b/PackageVersions.props index f71c1845..782376a9 100644 --- a/PackageVersions.props +++ b/PackageVersions.props @@ -5,7 +5,7 @@ 17.9.0 4.9.2 4.18.4 - 4.2.5 - 1.1.2 + 4.3.0 + 2.0.0 diff --git a/api/src/api/TestAdapterReporter.cs b/api/src/api/TestAdapterReporter.cs index dcd7f1d8..f481103e 100644 --- a/api/src/api/TestAdapterReporter.cs +++ b/api/src/api/TestAdapterReporter.cs @@ -1,10 +1,43 @@ namespace GdUnit4.Api; + using System; +using System.IO; +using System.IO.Pipes; +using System.Security.Principal; using Newtonsoft.Json; internal class TestAdapterReporter : ITestEventListener { + public const string PipeName = "gdunit4-event-pipe"; + private readonly NamedPipeClientStream client; + private readonly StreamWriter? writer; + + public TestAdapterReporter() + { + client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous, TokenImpersonationLevel.Impersonation); + if (!client.IsConnected) + try + { + Console.WriteLine("Try to connect to GdUnit4 test report server!"); + client.Connect(TimeSpan.FromSeconds(5)); + writer = new StreamWriter(client) { AutoFlush = true }; + writer.WriteLine("TestAdapterReporter: Successfully connected to GdUnit4 test report server!"); + } + catch (TimeoutException e) + { + Console.Error.WriteLine(e); + throw; + } + } + + public void Dispose() + { + writer?.WriteLine("TestAdapterReporter: Disconnecting from GdUnit4 test report server."); + writer?.Dispose(); + client.Dispose(); + } + public bool IsFailed { get; set; } public void PublishEvent(TestEvent e) @@ -12,6 +45,6 @@ public void PublishEvent(TestEvent e) if (e.IsFailed || e.IsError) IsFailed = true; var json = JsonConvert.SerializeObject(e); - Console.WriteLine($"GdUnitTestEvent:{json}"); + writer!.WriteLine($"GdUnitTestEvent:{json}"); } } diff --git a/api/src/api/TestReporter.cs b/api/src/api/TestReporter.cs index 11c6639f..52b2ebce 100644 --- a/api/src/api/TestReporter.cs +++ b/api/src/api/TestReporter.cs @@ -1,19 +1,21 @@ namespace GdUnit4.Api; + using System; -using GdUnit4.Core; +using Core; internal class TestReporter : ITestEventListener { - public bool IsFailed { get; set; } - private static readonly GdUnitConsole Console = new(); - public TestReporter() - { } + public bool IsFailed { get; set; } public void PublishEvent(TestEvent testEvent) => PrintStatus(testEvent); + public void Dispose() + { + } + private void PrintStatus(TestEvent testEvent) { switch (testEvent.Type) @@ -74,9 +76,6 @@ void WriteStatus(TestEvent testEvent) private static void WriteFailureReport(TestEvent testEvent) { - foreach (var report in testEvent.Reports) - { - Console.Println(report.ToString().RichTextNormalize().Indentation(2), ConsoleColor.DarkCyan); - } + foreach (var report in testEvent.Reports) Console.Println(report.ToString().RichTextNormalize().Indentation(2), ConsoleColor.DarkCyan); } } diff --git a/api/src/api/TestRunner.cs b/api/src/api/TestRunner.cs index e758216a..834808ef 100644 --- a/api/src/api/TestRunner.cs +++ b/api/src/api/TestRunner.cs @@ -1,74 +1,68 @@ namespace GdUnit4.Api; + using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using CommandLine; - -using GdUnit4.Executions; -using GdUnit4.Core; -using Newtonsoft.Json; - -public partial class TestRunner : Godot.Node -{ +using CommandLine; - public class Options - { - [Option(Required = false, HelpText = "If FailFast=true the test run will abort on first test failure.")] - public bool FailFast { get; set; } = false; +using Core; - [Option(Required = false, HelpText = "Runs the Runner in test adapter mode.")] - public bool TestAdapter { get; set; } +using Executions; - [Option(Required = false, HelpText = "The test runner config.")] - public string ConfigFile { get; set; } = ""; +using Godot; - [Option(Required = false, HelpText = "Adds the given test suite or directory to the execution pipeline.")] - public string Add { get; set; } = ""; - } +using Newtonsoft.Json; +public partial class TestRunner : Node +{ private bool FailFast { get; set; } = true; public async Task RunTests() { - var cmdArgs = Godot.OS.GetCmdlineArgs(); + var cmdArgs = OS.GetCmdlineArgs(); await new Parser(with => - { - with.EnableDashDash = true; - with.IgnoreUnknownArguments = true; - }) - .ParseArguments(cmdArgs) - .WithParsedAsync(async o => - { - FailFast = o.FailFast; - var exitCode = await (o.TestAdapter - ? RunTests(LoadTestSuites(o.ConfigFile), new TestAdapterReporter()) - : RunTests(LoadTestSuites(new DirectoryInfo(o.Add)), new TestReporter())); - Console.WriteLine($"Testrun ends with exit code: {exitCode}, FailFast:{FailFast}"); - GetTree().Quit(exitCode); - }); + { + with.EnableDashDash = true; + with.IgnoreUnknownArguments = true; + }) + .ParseArguments(cmdArgs) + .WithParsedAsync(async o => + { + FailFast = o.FailFast; + var exitCode = await (o.TestAdapter + ? RunTests(LoadTestSuites(o.ConfigFile), new TestAdapterReporter()) + : RunTests(LoadTestSuites(new DirectoryInfo(o.Add)), new TestReporter())); + Console.WriteLine($"Testrun ends with exit code: {exitCode}, FailFast:{FailFast}"); + GetTree().Quit(exitCode); + }); } private async Task RunTests(IEnumerable testSuites, ITestEventListener listener) { - if (!testSuites.Any()) + using (listener) { - Console.Error.WriteLine("No testsuite's specified!, Abort!"); - return -1; - } - using Executor executor = new(); - executor.AddTestEventListener(listener); + if (!testSuites.Any()) + { + Console.Error.WriteLine("No testsuite's specified!, Abort!"); + return -1; + } - foreach (var testSuite in testSuites) - { - await executor.ExecuteInternally(testSuite!); - if (listener.IsFailed && FailFast) - break; + using Executor executor = new(); + executor.AddTestEventListener(listener); + + foreach (var testSuite in testSuites) + { + await executor.ExecuteInternally(testSuite!); + if (listener.IsFailed && FailFast) + break; + } + + return listener.IsFailed ? 100 : 0; } - return listener.IsFailed ? 100 : 0; } private static TestSuite? TryCreateTestSuite(KeyValuePair> entry) @@ -109,13 +103,26 @@ private static IEnumerable LoadTestSuites(DirectoryInfo rootDir, stri Console.WriteLine($"Scanning for test suites in: {currentDir.FullName}"); foreach (var filePath in Directory.EnumerateFiles(currentDir.FullName, searchPattern)) - { if (GdUnitTestSuiteBuilder.ParseType(filePath, true) != null) yield return new TestSuite(filePath); - } foreach (var directory in currentDir.GetDirectories()) stack.Push(directory); } } + + public class Options + { + [Option(Required = false, HelpText = "If FailFast=true the test run will abort on first test failure.")] + public bool FailFast { get; set; } = false; + + [Option(Required = false, HelpText = "Runs the Runner in test adapter mode.")] + public bool TestAdapter { get; set; } + + [Option(Required = false, HelpText = "The test runner config.")] + public string ConfigFile { get; set; } = ""; + + [Option(Required = false, HelpText = "Adds the given test suite or directory to the execution pipeline.")] + public string Add { get; set; } = ""; + } } diff --git a/api/src/core/event/TestEventListener.cs b/api/src/core/event/TestEventListener.cs index 355c7c7e..84800685 100644 --- a/api/src/core/event/TestEventListener.cs +++ b/api/src/core/event/TestEventListener.cs @@ -1,7 +1,8 @@ - namespace GdUnit4; -internal interface ITestEventListener +using System; + +internal interface ITestEventListener : IDisposable { bool IsFailed { get; protected set; } diff --git a/api/src/core/execution/Executor.cs b/api/src/core/execution/Executor.cs index d2fb14e8..8d868323 100644 --- a/api/src/core/execution/Executor.cs +++ b/api/src/core/execution/Executor.cs @@ -2,101 +2,36 @@ namespace GdUnit4.Executions; using System; using System.Collections.Generic; -using System.Threading.Tasks; -using System.Linq; using System.IO; -using Newtonsoft.Json; +using System.Linq; +using System.Threading.Tasks; + +using Core; -using GdUnit4.Core; +using Godot; + +using Newtonsoft.Json; -public sealed partial class Executor : Godot.RefCounted, IExecutor +public sealed partial class Executor : RefCounted, IExecutor { - [Godot.Signal] + [Signal] public delegate void ExecutionCompletedEventHandler(); private readonly List eventListeners = new(); - private class GdTestEventListenerDelegator : ITestEventListener - { - private readonly Godot.GodotObject listener; - - public bool IsFailed { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public GdTestEventListenerDelegator(Godot.GodotObject listener) - => this.listener = listener; - - public void PublishEvent(TestEvent testEvent) - { - Godot.Collections.Dictionary data = new() - { - { "type", testEvent.Type.ToVariant() }, - { "resource_path", testEvent.ResourcePath.ToVariant() }, - { "suite_name", testEvent.SuiteName.ToVariant() }, - { "test_name", testEvent.TestName.ToVariant() }, - { "total_count", testEvent.TotalCount.ToVariant() }, - { "statistics", ToGdUnitEventStatistics(testEvent.Statistics) } - }; - - if (testEvent.Reports.Any()) - { - var serializedReports = testEvent.Reports.Select(report => report.Serialize()).ToGodotArray(); - data.Add("reports", serializedReports); - } - listener.Call("PublishEvent", data); - } - - private Godot.Collections.Dictionary ToGdUnitEventStatistics(IDictionary statistics) - { - var converted = new Godot.Collections.Dictionary(); - foreach (var (key, value) in statistics) - converted[key.ToString().ToLower().ToVariant()] = value.ToVariant(); - return converted; - } - } + private bool ReportOrphanNodesEnabled => GdUnit4Settings.IsVerboseOrphans(); - public IExecutor AddGdTestEventListener(Godot.GodotObject listener) + public IExecutor AddGdTestEventListener(GodotObject listener) { // I want to using anonyms implementation to remove the extra delegator class eventListeners.Add(new GdTestEventListenerDelegator(listener)); return this; } - internal void AddTestEventListener(ITestEventListener listener) - => eventListeners.Add(listener); - - private bool ReportOrphanNodesEnabled => GdUnit4Settings.IsVerboseOrphans(); - - public bool IsExecutable(Godot.Node node) => node is CsNode; - - private class GdUnitRunnerConfig - { - public Dictionary> Included { get; set; } = new(); - } - - /// - /// Loads the list of included tests from GdUnitRunner.cfg if exists - /// - /// - /// - private IEnumerable? LoadTestFilter(CsNode testSuite) - { - // try to load runner config written by gdunit4 plugin - var configPath = Path.Combine(Directory.GetCurrentDirectory(), "addons/gdUnit4/GdUnitRunner.cfg"); - if (!File.Exists(configPath)) - return null; - - var testSuitePath = testSuite.ResourcePath(); - var json = File.ReadAllText(configPath); - var runnerConfig = JsonConvert.DeserializeObject(json); - // Filter by testSuitePath and add values from runnerConfig.Included to the list - var filteredTests = runnerConfig?.Included - .Where(entry => entry.Key.EndsWith(testSuitePath)) - .SelectMany(entry => entry.Value); - return filteredTests?.Any() == true ? filteredTests : null; - } + public bool IsExecutable(Node node) => node is CsNode; /// - /// Execute a testsuite, is called externally from Godot test suite runner + /// Execute a testsuite, is called externally from Godot test suite runner /// /// public void Execute(CsNode testSuite) @@ -121,6 +56,31 @@ public void Execute(CsNode testSuite) } } + internal void AddTestEventListener(ITestEventListener listener) + => eventListeners.Add(listener); + + /// + /// Loads the list of included tests from GdUnitRunner.cfg if exists + /// + /// + /// + private IEnumerable? LoadTestFilter(CsNode testSuite) + { + // try to load runner config written by gdunit4 plugin + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "addons/gdUnit4/GdUnitRunner.cfg"); + if (!File.Exists(configPath)) + return null; + + var testSuitePath = testSuite.ResourcePath(); + var json = File.ReadAllText(configPath); + var runnerConfig = JsonConvert.DeserializeObject(json); + // Filter by testSuitePath and add values from runnerConfig.Included to the list + var filteredTests = runnerConfig?.Included + .Where(entry => entry.Key.EndsWith(testSuitePath)) + .SelectMany(entry => entry.Value); + return filteredTests?.Any() == true ? filteredTests : null; + } + internal async Task ExecuteInternally(TestSuite testSuite) { try @@ -141,4 +101,50 @@ internal async Task ExecuteInternally(TestSuite testSuite) testSuite.Dispose(); } } + + private class GdTestEventListenerDelegator : ITestEventListener + { + private readonly GodotObject listener; + + public GdTestEventListenerDelegator(GodotObject listener) + => this.listener = listener; + + public bool IsFailed { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public void PublishEvent(TestEvent testEvent) + { + Godot.Collections.Dictionary data = new() + { + { "type", testEvent.Type.ToVariant() }, + { "resource_path", testEvent.ResourcePath.ToVariant() }, + { "suite_name", testEvent.SuiteName.ToVariant() }, + { "test_name", testEvent.TestName.ToVariant() }, + { "total_count", testEvent.TotalCount.ToVariant() }, + { "statistics", ToGdUnitEventStatistics(testEvent.Statistics) } + }; + + if (testEvent.Reports.Any()) + { + var serializedReports = testEvent.Reports.Select(report => report.Serialize()).ToGodotArray(); + data.Add("reports", serializedReports); + } + + listener.Call("PublishEvent", data); + } + + public void Dispose() => listener.Dispose(); + + private Godot.Collections.Dictionary ToGdUnitEventStatistics(IDictionary statistics) + { + var converted = new Godot.Collections.Dictionary(); + foreach (var (key, value) in statistics) + converted[key.ToString().ToLower().ToVariant()] = value.ToVariant(); + return converted; + } + } + + private class GdUnitRunnerConfig + { + public Dictionary> Included { get; } = new(); + } } diff --git a/gdUnit4.sln b/gdUnit4Net.sln similarity index 100% rename from gdUnit4.sln rename to gdUnit4Net.sln diff --git a/gdUnit4Net.sln.DotSettings.user b/gdUnit4Net.sln.DotSettings.user new file mode 100644 index 00000000..837f21cb --- /dev/null +++ b/gdUnit4Net.sln.DotSettings.user @@ -0,0 +1,12 @@ + + True + <SessionState ContinuousTestingMode="0" IsActive="True" Name="BoolAssertTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>VsTest::B97C5043-B4BE-4156-BE0F-FDCBDECA61F6::net8.0::executor://gdunit4.testadapter/#GdUnit4.Tests.Asserts.BoolAssertTest</TestId> + </TestAncestor> +</SessionState> + + + + TRACE + D:\development\workspace\gdUnit4Net\test\.runsettings \ No newline at end of file diff --git a/icon.svg b/icon.svg deleted file mode 100644 index adc26df6..00000000 --- a/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/project.godot b/project.godot deleted file mode 100644 index b1f4958b..00000000 --- a/project.godot +++ /dev/null @@ -1,19 +0,0 @@ -; Engine configuration file. -; It's best edited using the editor UI and not directly, -; since the parameters that go here are not all obvious. -; -; Format: -; [section] ; section goes between [] -; param=value ; assign values to parameters - -config_version=5 - -[application] - -config/name="gdUnit4" -config/features=PackedStringArray("4.2.2", "C#", "Forward Plus") -config/icon="res://icon.svg" - -[dotnet] - -project/assembly_name="gdUnit4" diff --git a/test/src/GdUnit4NetAPITest.cs b/test/src/GdUnit4NetAPITest.cs index adf4d0a0..c0c734e8 100644 --- a/test/src/GdUnit4NetAPITest.cs +++ b/test/src/GdUnit4NetAPITest.cs @@ -16,5 +16,5 @@ public void IsTestSuite() [TestCase] public void Version() - => AssertThat(GdUnit4NetAPI.Version()).StartsWith("4.2"); + => AssertThat(GdUnit4NetAPI.Version()).StartsWith("4.3"); } diff --git a/test/src/core/ExecutorTest.cs b/test/src/core/ExecutorTest.cs index f237db34..03670b68 100644 --- a/test/src/core/ExecutorTest.cs +++ b/test/src/core/ExecutorTest.cs @@ -4,12 +4,17 @@ namespace GdUnit4.Tests.Core; using System.Collections.Generic; using System.Threading.Tasks; +using Executions; + using GdUnit4.Asserts; using GdUnit4.Core; -using GdUnit4.Executions; + +using Godot; using static Assertions; + using static TestEvent.TYPE; + using static TestReport.ReportType; [TestSuite] @@ -17,16 +22,38 @@ namespace GdUnit4.Tests.Core; public class ExecutorTest : ITestEventListener #pragma warning restore CA1001 // Types that own disposable fields should be disposable { - private Executor executor = null!; private readonly List events = new(); #pragma warning disable CS0649 // enable to verbose debug event private readonly bool verbose; #pragma warning restore CS0649 + private Executor executor = null!; public bool IsFailed { get; set; } + + void IDisposable.Dispose() + { + executor?.Dispose(); + GC.SuppressFinalize(this); + } + + void ITestEventListener.PublishEvent(TestEvent e) + { + if (verbose) + { + Console.WriteLine("-------------------------------"); + Console.WriteLine($"Event Type: {e.Type}, SuiteName: {e.SuiteName}, TestName: {e.TestName}, Statistics: {e.Statistics}"); + Console.WriteLine($"ErrorCount: {e.ErrorCount}, FailedCount: {e.FailedCount}, OrphanCount: {e.OrphanCount}"); + var reports = new List(e.Reports).ConvertAll(r => new TestReport(r.Type, r.LineNumber, r.Message.RichTextNormalize())); + if (verbose) + reports.ForEach(r => Console.WriteLine($"Reports -> {r}")); + } + + events.Add(e); + } + [Before] public void Before() { @@ -36,12 +63,11 @@ public void Before() [BeforeTest] public void InitTest() - => Godot.ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, true); + => ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, true); [AfterTest] public void TeardownTest() - => Godot.ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, true); - + => ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, true); private static TestSuite LoadTestSuite(string clazzPath) { @@ -52,19 +78,6 @@ private static TestSuite LoadTestSuite(string clazzPath) }; return testSuite; } - void ITestEventListener.PublishEvent(TestEvent e) - { - if (verbose) - { - Console.WriteLine("-------------------------------"); - Console.WriteLine($"Event Type: {e.Type}, SuiteName: {e.SuiteName}, TestName: {e.TestName}, Statistics: {e.Statistics}"); - Console.WriteLine($"ErrorCount: {e.ErrorCount}, FailedCount: {e.FailedCount}, OrphanCount: {e.OrphanCount}"); - var reports = new List(e.Reports).ConvertAll(r => new TestReport(r.Type, r.LineNumber, r.Message.RichTextNormalize())); - if (verbose) - reports.ForEach(r => Console.WriteLine($"Reports -> {r}")); - } - events.Add(e); - } private async Task> ExecuteTestSuite(TestSuite testSuite) { @@ -81,15 +94,13 @@ private async Task> ExecuteTestSuite(TestSuite testSuite) private List ExpectedEvents(string suiteName, params string[] testCaseNames) { - var expectedEvents = new List - { - Tuple(TESTSUITE_BEFORE, suiteName, "Before", testCaseNames.Length) - }; + var expectedEvents = new List { Tuple(TESTSUITE_BEFORE, suiteName, "Before", testCaseNames.Length) }; foreach (var testCase in testCaseNames) { expectedEvents.Add(Tuple(TESTCASE_BEFORE, suiteName, testCase, 0)); expectedEvents.Add(Tuple(TESTCASE_AFTER, suiteName, testCase, 0)); } + expectedEvents.Add(Tuple(TESTSUITE_AFTER, suiteName, "After", 0)); return expectedEvents; } @@ -101,30 +112,33 @@ private List ExpectedEvents(string suiteName, params string[] testCaseNa AssertArray(events).ExtractV(Extr("Type"), Extr("TestName"), Extr("ErrorCount"), Extr("FailedCount"), Extr("OrphanCount")); private IEnumerableAssert AssertEventStates(List events) => - AssertArray(events).ExtractV(Extr("Type"), Extr("TestName"), Extr("IsSuccess"), Extr("IsWarning"), Extr("IsFailed"), Extr("IsError")); + AssertArray(events).ExtractV(Extr("Type"), Extr("TestName"), Extr("IsSuccess"), Extr("IsWarning"), Extr("IsFailed"), Extr("IsError")); private IEnumerableAssert AssertReports(List events) { var extractedEvents = events.ConvertAll(e => { var reports = new List(e.Reports).ConvertAll(r => new TestReport(r.Type, r.LineNumber, r.Message.RichTextNormalize())); - return new { e.TestName, EventType = e.Type, Reports = reports }; + return new + { + e.TestName, + EventType = e.Type, + Reports = reports + }; }); return AssertArray(extractedEvents).ExtractV(Extr("EventType"), Extr("TestName"), Extr("Reports")); } private static List ExpectedTestCase(string suiteName, string testName, List testCaseParams) { - var expectedEvents = new List - { - Tuple(TESTCASE_BEFORE, suiteName, testName, 0) - }; + var expectedEvents = new List { Tuple(TESTCASE_BEFORE, suiteName, testName, 0) }; foreach (var testCaseParam in testCaseParams) { var testCaseName = TestCase.BuildDisplayName(testName, testCaseParam); expectedEvents.Add(Tuple(TESTCASE_BEFORE, suiteName, testCaseName, 0)); expectedEvents.Add(Tuple(TESTCASE_AFTER, suiteName, testCaseName, 0)); } + expectedEvents.Add(Tuple(TESTCASE_AFTER, suiteName, testName, 0)); return expectedEvents; } @@ -133,7 +147,7 @@ private static List ExpectedTestCase(string suiteName, string testName, public async Task ExecuteSuccess() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteAllStagesSuccess.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -171,7 +185,7 @@ public async Task ExecuteSuccess() public async Task ExecuteFailureOnStageBefore() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnStageBefore.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -205,14 +219,14 @@ public async Task ExecuteFailureOnStageBefore() Tuple(TESTCASE_AFTER, "TestCase1", new List()), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), Tuple(TESTCASE_AFTER, "TestCase2", new List()), - Tuple(TESTSUITE_AFTER, "After", new List() { new(FAILURE, 12, "failed on Before()") })); + Tuple(TESTSUITE_AFTER, "After", new List { new(FAILURE, 12, "failed on Before()") })); } [TestCase(Description = "Verifies report a failure on stage 'After'.")] public async Task ExecuteFailureOnStageAfter() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnStageAfter.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -246,14 +260,14 @@ public async Task ExecuteFailureOnStageAfter() Tuple(TESTCASE_AFTER, "TestCase1", new List()), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), Tuple(TESTCASE_AFTER, "TestCase2", new List()), - Tuple(TESTSUITE_AFTER, "After", new List() { new(FAILURE, 16, "failed on After()") })); + Tuple(TESTSUITE_AFTER, "After", new List { new(FAILURE, 16, "failed on After()") })); } [TestCase(Description = "Verifies report a failure on stage 'BeforeTest'.")] public async Task ExecuteFailureOnStageBeforeTest() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnStageBeforeTest.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -283,9 +297,9 @@ public async Task ExecuteFailureOnStageBeforeTest() AssertReports(events).ContainsExactly( Tuple(TESTSUITE_BEFORE, "Before", new List()), Tuple(TESTCASE_BEFORE, "TestCase1", new List()), - Tuple(TESTCASE_AFTER, "TestCase1", new List() { new(FAILURE, 20, "failed on BeforeTest()") }), + Tuple(TESTCASE_AFTER, "TestCase1", new List { new(FAILURE, 20, "failed on BeforeTest()") }), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), - Tuple(TESTCASE_AFTER, "TestCase2", new List() { new(FAILURE, 20, "failed on BeforeTest()") }), + Tuple(TESTCASE_AFTER, "TestCase2", new List { new(FAILURE, 20, "failed on BeforeTest()") }), Tuple(TESTSUITE_AFTER, "After", new List())); } @@ -293,7 +307,7 @@ public async Task ExecuteFailureOnStageBeforeTest() public async Task ExecuteFailureOnStageAfterTest() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnStageAfterTest.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -323,9 +337,9 @@ public async Task ExecuteFailureOnStageAfterTest() AssertReports(events).ContainsExactly( Tuple(TESTSUITE_BEFORE, "Before", new List()), Tuple(TESTCASE_BEFORE, "TestCase1", new List()), - Tuple(TESTCASE_AFTER, "TestCase1", new List() { new(FAILURE, 24, "failed on AfterTest()") }), + Tuple(TESTCASE_AFTER, "TestCase1", new List { new(FAILURE, 24, "failed on AfterTest()") }), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), - Tuple(TESTCASE_AFTER, "TestCase2", new List() { new(FAILURE, 24, "failed on AfterTest()") }), + Tuple(TESTCASE_AFTER, "TestCase2", new List { new(FAILURE, 24, "failed on AfterTest()") }), Tuple(TESTSUITE_AFTER, "After", new List())); } @@ -333,7 +347,7 @@ public async Task ExecuteFailureOnStageAfterTest() public async Task ExecuteFailureOnTestCase1() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnTestCase1.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -362,12 +376,15 @@ public async Task ExecuteFailureOnTestCase1() AssertReports(events).ContainsExactly( Tuple(TESTSUITE_BEFORE, "Before", new List()), Tuple(TESTCASE_BEFORE, "TestCase1", new List()), - Tuple(TESTCASE_AFTER, "TestCase1", new List() { new(FAILURE, 27, """ - Expecting be equal: - "TestCase1" - but is - "invalid" - """) }), + Tuple(TESTCASE_AFTER, "TestCase1", new List + { + new(FAILURE, 27, """ + Expecting be equal: + "TestCase1" + but is + "invalid" + """) + }), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), Tuple(TESTCASE_AFTER, "TestCase2", new List()), Tuple(TESTSUITE_AFTER, "After", new List())); @@ -377,7 +394,7 @@ but is public async Task ExecuteFailureOnMultiStages() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailOnMultiStages.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); @@ -409,23 +426,25 @@ public async Task ExecuteFailureOnMultiStages() AssertReports(events).ContainsExactly( Tuple(TESTSUITE_BEFORE, "Before", new List()), Tuple(TESTCASE_BEFORE, "TestCase1", new List()), - Tuple(TESTCASE_AFTER, "TestCase1", new List() { - new(FAILURE, 20, "failed on BeforeTest()"), - new(FAILURE, 28, """ - Expecting be empty: - but is - "TestCase1" - """)}), + Tuple(TESTCASE_AFTER, "TestCase1", new List + { + new(FAILURE, 20, "failed on BeforeTest()"), + new(FAILURE, 28, """ + Expecting be empty: + but is + "TestCase1" + """) + }), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), - Tuple(TESTCASE_AFTER, "TestCase2", new List() { new(FAILURE, 20, "failed on BeforeTest()") }), - Tuple(TESTSUITE_AFTER, "After", new List() { new(FAILURE, 16, "failed on After()") })); + Tuple(TESTCASE_AFTER, "TestCase2", new List { new(FAILURE, 20, "failed on BeforeTest()") }), + Tuple(TESTSUITE_AFTER, "After", new List { new(FAILURE, 16, "failed on After()") })); } [TestCase(Description = "GD-63: Execution must detect orphan nodes in the different test stages.")] public async Task ExecuteFailureOrphanNodesDetected() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailAndOrphansDetected.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); var events = await ExecuteTestSuite(testSuite); AssertTestCaseNames(events) @@ -461,56 +480,59 @@ public async Task ExecuteFailureOrphanNodesDetected() Tuple(TESTSUITE_BEFORE, "Before", new List()), Tuple(TESTCASE_BEFORE, "TestCase1", new List()), // ends with warnings - Tuple(TESTCASE_AFTER, "TestCase1", new List() { - new(WARN, 0, """ - WARNING: - Detected <2> orphan nodes during test setup stage! - Check SetupTest:26 and TearDownTest:34 for unfreed instances! - """), - new(WARN, 39, """ - WARNING: - Detected <3> orphan nodes during test execution! - """) + Tuple(TESTCASE_AFTER, "TestCase1", new List + { + new(WARN, 0, """ + WARNING: + Detected <2> orphan nodes during test setup stage! + Check SetupTest:26 and TearDownTest:34 for unfreed instances! + """), + new(WARN, 39, """ + WARNING: + Detected <3> orphan nodes during test execution! + """) } ), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), // ends with failure and warnings - Tuple(TESTCASE_AFTER, "TestCase2", new List() { - new(WARN, 0, """ - WARNING: - Detected <2> orphan nodes during test setup stage! - Check SetupTest:26 and TearDownTest:34 for unfreed instances! - """), - new(WARN, 48, """ - WARNING: - Detected <4> orphan nodes during test execution! - """), - new(FAILURE, 54, """ - Expecting be empty: - but is - "TestCase2" - """) + Tuple(TESTCASE_AFTER, "TestCase2", new List + { + new(WARN, 0, """ + WARNING: + Detected <2> orphan nodes during test setup stage! + Check SetupTest:26 and TearDownTest:34 for unfreed instances! + """), + new(WARN, 48, """ + WARNING: + Detected <4> orphan nodes during test execution! + """), + new(FAILURE, 54, """ + Expecting be empty: + but is + "TestCase2" + """) } ), // and one orphan detected at stage 'After' - Tuple(TESTSUITE_AFTER, "After", new List() { + Tuple(TESTSUITE_AFTER, "After", new List + { new(WARN, 0, """ - WARNING: - Detected <1> orphan nodes during test suite setup stage! - Check SetupSuite:15 and TearDownSuite:22 for unfreed instances! - """) - }) - ); + WARNING: + Detected <1> orphan nodes during test suite setup stage! + Check SetupSuite:15 and TearDownSuite:22 for unfreed instances! + """) + }) + ); } [TestCase(Description = "GD-62: Execution must ignore detect orphan nodes if is disabled.")] public async Task ExecuteFailureOrphanNodesDetectionDisabled() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteFailAndOrphansDetected.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2"); // simulate test suite execution with disabled orphan detection - Godot.ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, false); + ProjectSettings.SetSetting(GdUnit4Settings.REPORT_ORPHANS, false); var events = await ExecuteTestSuite(testSuite); AssertTestCaseNames(events) @@ -541,12 +563,14 @@ public async Task ExecuteFailureOrphanNodesDetectionDisabled() Tuple(TESTCASE_AFTER, "TestCase1", new List()), Tuple(TESTCASE_BEFORE, "TestCase2", new List()), // ends with failure - Tuple(TESTCASE_AFTER, "TestCase2", new List() { + Tuple(TESTCASE_AFTER, "TestCase2", new List + { new(FAILURE, 54, """ - Expecting be empty: - but is - "TestCase2" - """) }), + Expecting be empty: + but is + "TestCase2" + """) + }), Tuple(TESTSUITE_AFTER, "After", new List())); } @@ -554,7 +578,7 @@ but is public async Task ExecuteAbortOnTimeOut() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteAbortOnTestTimeout.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "TestCase1", "TestCase2", "TestCase3", "TestCase4", "TestCase5" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("TestCase1", "TestCase2", "TestCase3", "TestCase4", "TestCase5"); var events = await ExecuteTestSuite(testSuite); @@ -584,7 +608,6 @@ public async Task ExecuteAbortOnTimeOut() // expect test succeeded Tuple(TESTCASE_BEFORE, "TestCase5", 0, 0, 0), Tuple(TESTCASE_AFTER, "TestCase5", 0, 0, 0), - Tuple(TESTSUITE_AFTER, "After", 0, 0, 0) ); @@ -618,17 +641,18 @@ public async Task ExecuteAbortOnTimeOut() Tuple(TESTSUITE_BEFORE, "Before", new List()), // reports a test interruption due to a timeout Tuple(TESTCASE_BEFORE, "TestCase1", new List()), - Tuple(TESTCASE_AFTER, "TestCase1", new List(){ - new(INTERRUPTED, 31, "The execution has timed out after 1s.") } + Tuple(TESTCASE_AFTER, "TestCase1", new List { new(INTERRUPTED, 31, "The execution has timed out after 1s.") } ), // reports a test failure Tuple(TESTCASE_BEFORE, "TestCase2", new List()), - Tuple(TESTCASE_AFTER, "TestCase2", new List(){ - new(FAILURE, 43, """ - Expecting be equal: - 'False' but is 'True' - """) } + Tuple(TESTCASE_AFTER, "TestCase2", new List + { + new(FAILURE, 43, """ + Expecting be equal: + 'False' but is 'True' + """) + } ), // succeeds with no reports @@ -637,17 +661,18 @@ public async Task ExecuteAbortOnTimeOut() // reports a method signature failure Tuple(TESTCASE_BEFORE, "TestCase4", new List()), - Tuple(TESTCASE_AFTER, "TestCase4", new List(){ - new(FAILURE, 55, """ - Invalid method signature found at: TestCase4. - You must return a for an asynchronously specified method. - """) } + Tuple(TESTCASE_AFTER, "TestCase4", new List + { + new(FAILURE, 55, """ + Invalid method signature found at: TestCase4. + You must return a for an asynchronously specified method. + """) + } ), // succeeds with no reports Tuple(TESTCASE_BEFORE, "TestCase5", new List()), Tuple(TESTCASE_AFTER, "TestCase5", new List()), - Tuple(TESTSUITE_AFTER, "After", new List())); } @@ -655,46 +680,50 @@ public async Task ExecuteAbortOnTimeOut() public async Task ExecuteParameterizedTest() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteParameterizedTests.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { - "ParameterizedBoolValue", - "ParameterizedIntValues", - "ParameterizedIntValuesFail", - "ParameterizedSingleTest" }); + AssertArray(testSuite.TestCases).Extract("Name") + .ContainsExactly("ParameterizedBoolValue", "ParameterizedIntValues", "ParameterizedIntValuesFail", "ParameterizedSingleTest"); var events = await ExecuteTestSuite(testSuite); var suiteName = "TestSuiteParameterizedTests"; - var expectedEvents = new List + var expectedEvents = new List { Tuple(TESTSUITE_BEFORE, suiteName, "Before", 4) }; + expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedBoolValue", new List { - Tuple(TESTSUITE_BEFORE, suiteName, "Before", 4) - }; - expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedBoolValue", new List { - new object[] { 0, false }, new object[] { 1, true } })); - expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedIntValues", new List { - new object[] { 1, 2, 3, 6 }, new object[] { 3, 4, 5, 12 }, new object[] { 6, 7, 8, 21 } })); - expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedIntValuesFail", new List { - new object[] { 1, 2, 3, 6 }, new object[] { 3, 4, 5, 11 }, new object[] { 6, 7, 8, 22 } })); - expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedSingleTest", new List { - new object[] { true } })); + new object[] { 0, false }, + new object[] { 1, true } + })); + expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedIntValues", new List + { + new object[] { 1, 2, 3, 6 }, + new object[] { 3, 4, 5, 12 }, + new object[] { 6, 7, 8, 21 } + })); + expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedIntValuesFail", new List + { + new object[] { 1, 2, 3, 6 }, + new object[] { 3, 4, 5, 11 }, + new object[] { 6, 7, 8, 22 } + })); + expectedEvents.AddRange(ExpectedTestCase(suiteName, "ParameterizedSingleTest", new List { new object[] { true } })); expectedEvents.Add(Tuple(TESTSUITE_AFTER, suiteName, "After", 0)); AssertTestCaseNames(events).ContainsExactly(expectedEvents); AssertEventStates(events).Contains( Tuple(TESTSUITE_BEFORE, "Before", true, false, false, false), Tuple(TESTCASE_BEFORE, "ParameterizedBoolValue", true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", new object[] { 0, false }), true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", new object[] { 1, true }), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", 0, false), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", 1, true), true, false, false, false), Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue"), true, false, false, false), Tuple(TESTCASE_BEFORE, TestCase.BuildDisplayName("ParameterizedIntValues"), true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 1, 2, 3, 6 }), true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 3, 4, 5, 12 }), true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 6, 7, 8, 21 }), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 1, 2, 3, 6), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 3, 4, 5, 12), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 6, 7, 8, 21), true, false, false, false), Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues"), true, false, false, false), // a test with failing test cases Tuple(TESTCASE_BEFORE, "ParameterizedIntValuesFail", true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 1, 2, 3, 6 }), true, false, false, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 3, 4, 5, 11 }), false, false, true, false), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 6, 7, 8, 22 }), false, false, true, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 1, 2, 3, 6), true, false, false, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 3, 4, 5, 11), false, false, true, false), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 6, 7, 8, 22), false, false, true, false), Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail"), false, false, true, false), // the single parameterized test Tuple(TESTCASE_BEFORE, "ParameterizedSingleTest", true, false, false, false), @@ -707,23 +736,25 @@ public async Task ExecuteParameterizedTest() AssertReports(events).Contains( Tuple(TESTSUITE_BEFORE, "Before", new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", new object[] { 0, false }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", new object[] { 1, true }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 1, 2, 3, 6 }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 3, 4, 5, 12 }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", new object[] { 6, 7, 8, 21 }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 1, 2, 3, 6 }), new List()), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 3, 4, 5, 11 }), new List(){ + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", 0, false), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedBoolValue", 1, true), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 1, 2, 3, 6), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 3, 4, 5, 12), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValues", 6, 7, 8, 21), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 1, 2, 3, 6), new List()), + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 3, 4, 5, 11), new List + { new(FAILURE, 25, """ - Expecting be equal: - '11' but is '12' - """) + Expecting be equal: + '11' but is '12' + """) }), - Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", new object[] { 6, 7, 8, 22 }), new List(){ + Tuple(TESTCASE_AFTER, TestCase.BuildDisplayName("ParameterizedIntValuesFail", 6, 7, 8, 22), new List + { new(FAILURE, 25, """ - Expecting be equal: - '22' but is '21' - """) + Expecting be equal: + '22' but is '21' + """) }), Tuple(TESTSUITE_AFTER, "After", new List()) ); @@ -733,7 +764,7 @@ public async Task ExecuteParameterizedTest() public async Task ExecuteTestWithExceptions() { var testSuite = LoadTestSuite("src/core/resources/testsuites/mono/TestSuiteAllTestsFailWithExceptions.cs"); - AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly(new string[] { "ExceptionIsThrownOnSceneInvoke", "ExceptionAtAsyncMethod", "ExceptionAtSyncMethod" }); + AssertArray(testSuite.TestCases).Extract("Name").ContainsExactly("ExceptionIsThrownOnSceneInvoke", "ExceptionAtAsyncMethod", "ExceptionAtSyncMethod"); var events = await ExecuteTestSuite(testSuite); @@ -765,16 +796,24 @@ public async Task ExecuteTestWithExceptions() // check for failure reports AssertReports(events).Contains( Tuple(TESTSUITE_BEFORE, "Before", new List()), - Tuple(TESTCASE_AFTER, "ExceptionIsThrownOnSceneInvoke", new List() { new(FAILURE, 12, """ - Test Exception - """) }), - Tuple(TESTCASE_AFTER, "ExceptionAtAsyncMethod", new List() { new(FAILURE, 24, """ - outer exception - """) }), - Tuple(TESTCASE_AFTER, "ExceptionAtSyncMethod", new List() { new(FAILURE, 28, """ - outer exception - """) }), + Tuple(TESTCASE_AFTER, "ExceptionIsThrownOnSceneInvoke", new List + { + new(FAILURE, 12, """ + Test Exception + """) + }), + Tuple(TESTCASE_AFTER, "ExceptionAtAsyncMethod", new List + { + new(FAILURE, 24, """ + outer exception + """) + }), + Tuple(TESTCASE_AFTER, "ExceptionAtSyncMethod", new List + { + new(FAILURE, 28, """ + outer exception + """) + }), Tuple(TESTSUITE_AFTER, "After", new List())); } - } diff --git a/testadapter/src/GdUnit4TestDiscoverer.cs b/testadapter/src/GdUnit4TestDiscoverer.cs index 8bfa4c9a..b7fcaa42 100644 --- a/testadapter/src/GdUnit4TestDiscoverer.cs +++ b/testadapter/src/GdUnit4TestDiscoverer.cs @@ -8,6 +8,8 @@ namespace GdUnit4.TestAdapter; using System.Threading; using System.Threading.Tasks; +using Discovery; + using Microsoft.TestPlatform.AdapterUtilities; using Microsoft.TestPlatform.AdapterUtilities.ManagedNameUtilities; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -15,13 +17,15 @@ namespace GdUnit4.TestAdapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -using GdUnit4.TestAdapter.Discovery; -using GdUnit4.TestAdapter.Settings; +using Settings; + +using static Discovery.CodeNavigationDataProvider; + +using static Settings.GdUnit4Settings; -using static GdUnit4.TestAdapter.Discovery.CodeNavigationDataProvider; -using static GdUnit4.TestAdapter.Settings.GdUnit4Settings; -using static GdUnit4.TestAdapter.Extensions.TestCaseExtensions; -using static GdUnit4.TestAdapter.Utilities.Utils; +using static Extensions.TestCaseExtensions; + +using static Utilities.Utils; [DefaultExecutorUri(GdUnit4TestExecutor.ExecutorUri)] [ExtensionUri(GdUnit4TestExecutor.ExecutorUri)] @@ -29,23 +33,18 @@ namespace GdUnit4.TestAdapter; [FileExtension(".exe")] public sealed class GdUnit4TestDiscoverer : ITestDiscoverer { - - internal static bool MatchReturnType(MethodInfo method, Type returnType) - => method == null - ? throw new ArgumentNullException(nameof(method)) - : returnType == null ? throw new ArgumentNullException(nameof(returnType)) : method.ReturnType.Equals(returnType); - public void DiscoverTests( IEnumerable sources, IDiscoveryContext discoveryContext, IMessageLogger logger, ITestCaseDiscoverySink discoverySink) { - if (!CheckGdUnit4ApiVersion(logger, new Version("4.2.2"))) + if (!CheckGdUnit4ApiMinimumRequiredVersion(logger, new Version("4.3.0"))) { - logger.SendMessage(TestMessageLevel.Error, $"Abort the test discovery."); + logger.SendMessage(TestMessageLevel.Error, "Abort the test discovery."); return; } + var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(discoveryContext.RunSettings?.SettingsXml); var gdUnitSettingsProvider = discoveryContext.RunSettings?.GetSettings(RunSettingsXmlNode) as GdUnit4SettingsProvider; var gdUnitSettings = gdUnitSettingsProvider?.Settings ?? new GdUnit4Settings(); @@ -76,7 +75,8 @@ public void DiscoverTests( { var navData = codeNavigationProvider.GetNavigationData(className, mi); if (!navData.IsValid) - logger.SendMessage(TestMessageLevel.Informational, $"Can't collect code navigation data for {className}:{mi.Name} GetNavigationData -> {navData.Source}:{navData.Line}"); + logger.SendMessage(TestMessageLevel.Informational, + $"Can't collect code navigation data for {className}:{mi.Name} GetNavigationData -> {navData.Source}:{navData.Line}"); ManagedNameHelper.GetManagedName(mi, out var managedType, out var managedMethod, out var hierarchyValues); ManagedNameParser.ParseManagedMethodName(managedMethod, out var methodName, out var parameterCount, out var parameterTypes); @@ -104,24 +104,31 @@ public void DiscoverTests( Interlocked.Increment(ref testCasesDiscovered); discoverySink.SendTestCase(t); }); - }); logger.SendMessage(TestMessageLevel.Informational, $"Discover: TestSuite {className} with {testCasesDiscovered} TestCases found."); - }; + } + logger.SendMessage(TestMessageLevel.Informational, $"Discover tests done, {testSuiteDiscovered} TestSuites and total {testsTotalDiscovered} Tests found."); } } + internal static bool MatchReturnType(MethodInfo method, Type returnType) + => method == null + ? throw new ArgumentNullException(nameof(method)) + : returnType == null + ? throw new ArgumentNullException(nameof(returnType)) + : method.ReturnType.Equals(returnType); + private List TestCasePropertiesAsTraits(MethodInfo mi) => mi.GetCustomAttributes(typeof(TestCaseAttribute)) - .Cast() - .Where(attr => attr.Arguments?.Length != 0) - .Select(attr => attr.Name == null - ? new Trait(string.Empty, attr.Arguments.Formatted()) - : new Trait(attr.Name, attr.Arguments.Formatted()) - ) - .ToList(); + .Cast() + .Where(attr => attr.Arguments?.Length != 0) + .Select(attr => attr.Name == null + ? new Trait(string.Empty, attr.Arguments.Formatted()) + : new Trait(attr.Name, attr.Arguments.Formatted()) + ) + .ToList(); private TestCase BuildTestCase(TestCaseDescriptor descriptor, string assemblyPath, CodeNavigation navData) { @@ -137,9 +144,7 @@ private TestCase BuildTestCase(TestCaseDescriptor descriptor, string assemblyPat testCase.SetPropertyValue(ManagedTypeProperty, descriptor.ManagedType); testCase.SetPropertyValue(ManagedMethodProperty, descriptor.ManagedMethod); if (descriptor.HierarchyValues?.Count > 0) - { testCase.SetPropertyValue(HierarchyProperty, descriptor.HierarchyValues?.ToArray()); - } return testCase; } @@ -173,6 +178,5 @@ private static IEnumerable FilterWithoutTestAdapter(IEnumerable assemblyPaths.Where(assembly => !assembly.Contains(".TestAdapter.")); private static bool IsTestSuite(Type type) => - type.IsClass && !type.IsAbstract && Attribute.IsDefined(type, typeof(TestSuiteAttribute)); - + type is { IsClass: true, IsAbstract: false } && Attribute.IsDefined(type, typeof(TestSuiteAttribute)); } diff --git a/testadapter/src/GdUnit4TestExecutor.cs b/testadapter/src/GdUnit4TestExecutor.cs index c5eec861..aab400cc 100644 --- a/testadapter/src/GdUnit4TestExecutor.cs +++ b/testadapter/src/GdUnit4TestExecutor.cs @@ -2,102 +2,112 @@ namespace GdUnit4.TestAdapter; using System; using System.Collections.Generic; +using System.Linq; + +using Discovery; + +using Execution; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -using GdUnit4.TestAdapter.Discovery; -using GdUnit4.TestAdapter.Settings; -using static GdUnit4.TestAdapter.Utilities.Utils; +using Settings; + +using static Utilities.Utils; + +using ITestExecutor = Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter.ITestExecutor; [ExtensionUri(ExecutorUri)] public class GdUnit4TestExecutor : ITestExecutor, IDisposable { - /// - /// The Uri used to identify the GdUnit4 Executor - /// + /// + /// The Uri used to identify the GdUnit4 Executor + /// public const string ExecutorUri = "executor://GdUnit4.TestAdapter"; - private Execution.TestExecutor? executor; - - private IFrameworkHandle? frameworkHandle; - // Test properties supported for filtering private readonly Dictionary supportedProperties = new(StringComparer.OrdinalIgnoreCase) { ["FullyQualifiedName"] = TestCaseProperties.FullyQualifiedName, - ["Name"] = TestCaseProperties.DisplayName, + ["Name"] = TestCaseProperties.DisplayName }; + private TestExecutor? executor; + + private IFrameworkHandle? fh; + + public void Dispose() + { + executor?.Dispose(); + GC.SuppressFinalize(this); + } + /// - /// Runs only the tests specified by parameter 'tests'. + /// Runs only the tests specified by parameter 'tests'. /// /// Tests to be run. /// Context to use when executing the tests. - /// Handle to the framework to record results and to do framework operations. + /// Handle to the framework to record results and to do framework operations. public void RunTests(IEnumerable? tests, IRunContext? runContext, IFrameworkHandle? frameworkHandle) { _ = tests ?? throw new ArgumentNullException(nameof(tests), "Argument 'tests' is null, abort!"); _ = runContext ?? throw new ArgumentNullException(nameof(runContext), "Argument 'runContext' is null, abort!"); - _ = frameworkHandle ?? throw new ArgumentNullException(nameof(frameworkHandle), "Argument 'frameworkHandle' is null, abort!"); + fh = frameworkHandle ?? throw new ArgumentNullException(nameof(frameworkHandle), "Argument 'frameworkHandle' is null, abort!"); + + if (!CheckGdUnit4ApiMinimumRequiredVersion(fh, new Version("4.3.0"))) + { + fh.SendMessage(TestMessageLevel.Error, "Abort the test discovery."); + return; + } var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(runContext.RunSettings?.SettingsXml); var runSettings = XmlRunSettingsUtilities.GetTestRunParameters(runContext.RunSettings?.SettingsXml); var gdUnitSettings = runContext.RunSettings?.GetSettings(GdUnit4Settings.RunSettingsXmlNode) as GdUnit4SettingsProvider; - var filterExpression = runContext.GetTestCaseFilter(supportedProperties.Keys, (propertyName) => + var filterExpression = runContext.GetTestCaseFilter(supportedProperties.Keys, propertyName => { supportedProperties.TryGetValue(propertyName, out var testProperty); return testProperty; }); - this.frameworkHandle = frameworkHandle; - - executor = new Execution.TestExecutor(runConfiguration, gdUnitSettings?.Settings ?? new GdUnit4Settings()); - executor.Run(frameworkHandle, runContext, tests); + executor = new TestExecutor(runConfiguration, gdUnitSettings?.Settings ?? new GdUnit4Settings()); + executor.Run(fh, runContext, tests.ToList()); } /// - /// Runs 'all' the tests present in the specified 'containers'. + /// Runs 'all' the tests present in the specified 'containers'. /// /// Path to test container files to look for tests in. /// Context to use when executing the tests. - /// Handle to the framework to record results and to do framework operations. + /// Handle to the framework to record results and to do framework operations. public void RunTests(IEnumerable? tests, IRunContext? runContext, IFrameworkHandle? frameworkHandle) { _ = tests ?? throw new ArgumentNullException(nameof(tests), "Argument 'containers' is null, abort!"); _ = runContext ?? throw new ArgumentNullException(nameof(runContext), "Argument 'runContext' is null, abort!"); - _ = frameworkHandle ?? throw new ArgumentNullException(nameof(frameworkHandle), "Argument 'frameworkHandle' is null, abort!"); + fh = frameworkHandle ?? throw new ArgumentNullException(nameof(frameworkHandle), "Argument 'frameworkHandle' is null, abort!"); - if (!CheckGdUnit4ApiVersion(frameworkHandle, new Version("4.2.2"))) + if (!CheckGdUnit4ApiMinimumRequiredVersion(fh, new Version("4.3.0"))) { - frameworkHandle.SendMessage(TestMessageLevel.Error, $"Abort the test discovery."); + fh.SendMessage(TestMessageLevel.Error, "Abort the test discovery."); return; } TestCaseDiscoverySink discoverySink = new(); - new GdUnit4TestDiscoverer().DiscoverTests(tests, runContext, frameworkHandle, discoverySink); + new GdUnit4TestDiscoverer().DiscoverTests(tests, runContext, fh, discoverySink); var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(runContext.RunSettings?.SettingsXml); var gdUnitSettings = runContext.RunSettings?.GetSettings(GdUnit4Settings.RunSettingsXmlNode) as GdUnit4SettingsProvider; - this.frameworkHandle = frameworkHandle; - executor = new Execution.TestExecutor(runConfiguration, gdUnitSettings?.Settings ?? new GdUnit4Settings()); - executor.Run(frameworkHandle, runContext, discoverySink.TestCases); + executor = new TestExecutor(runConfiguration, gdUnitSettings?.Settings ?? new GdUnit4Settings()); + executor.Run(fh, runContext, discoverySink.TestCases); } /// - /// Cancel the execution of the tests. + /// Cancel the execution of the tests. /// public void Cancel() { - frameworkHandle?.SendMessage(TestMessageLevel.Informational, "Cancel pressed -----"); + fh?.SendMessage(TestMessageLevel.Informational, "Cancel pressed -----"); executor?.Cancel(); } - - public void Dispose() - { - executor?.Dispose(); - GC.SuppressFinalize(this); - } } diff --git a/testadapter/src/execution/BaseTestExecutor.cs b/testadapter/src/execution/BaseTestExecutor.cs index 34384593..1933adbd 100644 --- a/testadapter/src/execution/BaseTestExecutor.cs +++ b/testadapter/src/execution/BaseTestExecutor.cs @@ -6,42 +6,45 @@ namespace GdUnit4.TestAdapter.Execution; using System.IO; using System.Linq; +using Api; + +using Extensions; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + using Newtonsoft.Json; -using GdUnit4.Api; -using Godot; -using GdUnit4.TestAdapter.Extensions; +using Environment = System.Environment; internal abstract class BaseTestExecutor { + protected string GodotBin { get; set; } = Environment.GetEnvironmentVariable("GODOT_BIN") + ?? throw new ArgumentNullException("Godot runtime is not set! Set env 'GODOT_BIN' is missing!"); - protected string GodotBin { get; set; } = System.Environment.GetEnvironmentVariable("GODOT_BIN") - ?? throw new ArgumentNullException("Godot runtime is not set! Set env 'GODOT_BIN' is missing!"); - - protected static EventHandler ExitHandler(IFrameworkHandle frameworkHandle) => new((sender, e) => + protected static EventHandler ExitHandler(IFrameworkHandle frameworkHandle) => (sender, e) => { Console.Out.Flush(); if (sender is Process p) frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Godot ends with exit code: {p.ExitCode}"); - }); + }; - protected static DataReceivedEventHandler StdErrorProcessor(IFrameworkHandle frameworkHandle) => new((sender, args) => + protected static DataReceivedEventHandler StdErrorProcessor(IFrameworkHandle frameworkHandle) => (sender, args) => { var message = args.Data?.Trim(); if (string.IsNullOrEmpty(message)) return; // we do log errors to stdout otherwise running `dotnet test` from console will fail with exit code 1 frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Error: {message}"); - }); + }; protected static string WriteTestRunnerConfig(Dictionary> groupedTestSuites) { try - { CleanupRunnerConfigurations(); } + { + CleanupRunnerConfigurations(); + } catch (Exception) { } var fileName = $"GdUnitRunner_{Guid.NewGuid()}.cfg"; @@ -67,77 +70,9 @@ private static void CleanupRunnerConfigurations() protected static void AttachDebuggerIfNeed(IRunContext runContext, IFrameworkHandle frameworkHandle, Process process) { if (runContext.IsBeingDebugged && frameworkHandle is IFrameworkHandle2 fh2) - fh2.AttachDebuggerToProcess(pid: process.Id); + fh2.AttachDebuggerToProcess(process.Id); } - protected static DataReceivedEventHandler TestEventProcessor(IFrameworkHandle frameworkHandle, IEnumerable tests) => new((sender, args) => - { - var json = args.Data?.Trim(); - if (string.IsNullOrEmpty(json)) - return; - - if (json.StartsWith("GdUnitTestEvent:")) - { - json = json.TrimPrefix("GdUnitTestEvent:"); - var e = JsonConvert.DeserializeObject(json)!; - - switch (e.Type) - { - case TestEvent.TYPE.TESTSUITE_BEFORE: - //frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Execute Test Suite '{e.SuiteName}'"); - break; - case TestEvent.TYPE.TESTCASE_BEFORE: - { - var testCase = FindTestCase(tests, e); - if (testCase == null) - { - //frameworkHandle.SendMessage(TestMessageLevel.Error, $"TESTCASE_BEFORE: cant find test case {e.FullyQualifiedName}"); - return; - } - frameworkHandle.RecordStart(testCase); - } - break; - case TestEvent.TYPE.TESTCASE_AFTER: - { - var testCase = FindTestCase(tests, e); - if (testCase == null) - { - //frameworkHandle.SendMessage(TestMessageLevel.Error, $"TESTCASE_AFTER: cant find test case {e.FullyQualifiedName}"); - return; - } - var testResult = new TestResult(testCase) - { - DisplayName = testCase.DisplayName, - Outcome = e.AsTestOutcome(), - EndTime = DateTimeOffset.Now, - Duration = e.ElapsedInMs - }; - foreach (var report in e.Reports) - { - testResult.ErrorMessage = report.Message.RichTextNormalize(); - testResult.ErrorStackTrace = report.StackTrace; - } - frameworkHandle.RecordResult(testResult); - frameworkHandle.RecordEnd(testCase, testResult.Outcome); - } - break; - case TestEvent.TYPE.TESTSUITE_AFTER: - //frameworkHandle.SendMessage(TestMessageLevel.Informational, $"{e.AsTestOutcome()}"); - break; - case TestEvent.TYPE.INIT: - break; - case TestEvent.TYPE.STOP: - break; - default: - break; - } - return; - } - frameworkHandle.SendMessage(TestMessageLevel.Informational, $"stdout: {json}"); - }); - - private static TestCase? FindTestCase(IEnumerable tests, TestEvent e) - => tests.FirstOrDefault(t => e.FullyQualifiedName.Equals(t.FullyQualifiedName, StringComparison.Ordinal)); protected static string? LookupGodotProjectPath(string classPath) { @@ -148,6 +83,7 @@ protected static void AttachDebuggerIfNeed(IRunContext runContext, IFrameworkHan return currentDir.FullName; currentDir = currentDir.Parent; } + return null; } } diff --git a/testadapter/src/execution/ITestExecutor.cs b/testadapter/src/execution/ITestExecutor.cs index 9f441d38..be9d13fe 100644 --- a/testadapter/src/execution/ITestExecutor.cs +++ b/testadapter/src/execution/ITestExecutor.cs @@ -6,13 +6,11 @@ namespace GdUnit4.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; - internal interface ITestExecutor : IDisposable { - public const int DEFAULT_SESSION_TIMEOUT = 30000; - public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnumerable testCases); + public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IReadOnlyList testCases); public void Cancel(); } diff --git a/testadapter/src/execution/TestEventReportServer.cs b/testadapter/src/execution/TestEventReportServer.cs new file mode 100644 index 00000000..ad45c112 --- /dev/null +++ b/testadapter/src/execution/TestEventReportServer.cs @@ -0,0 +1,121 @@ +namespace GdUnit4.TestAdapter.Execution; + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Api; + +using Extensions; + +using Godot; + +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +using Newtonsoft.Json; + +internal sealed class TestEventReportServer : IDisposable, IAsyncDisposable +{ + private readonly NamedPipeServerStream server; + + public TestEventReportServer() + => server = new NamedPipeServerStream(TestAdapterReporter.PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + public async ValueTask DisposeAsync() => await server.DisposeAsync(); + + public void Dispose() => server.Dispose(); + + internal async Task Start(IFrameworkHandle frameworkHandle, IReadOnlyList tests) + { + frameworkHandle.SendMessage(TestMessageLevel.Informational, "TestEventReportServer:: Wait for connecting GdUnit4 test report client."); + await server.WaitForConnectionAsync(); + using CancellationTokenSource tokenSource = new(TimeSpan.FromMinutes(10)); + using var reader = new StreamReader(server); + while (server.IsConnected) + try + { + var json = await reader.ReadLineAsync(tokenSource.Token); + if (string.IsNullOrEmpty(json)) continue; + + ProcessTestEvent(frameworkHandle, tests, json); + } + catch (IOException) + { + frameworkHandle.SendMessage(TestMessageLevel.Informational, "TestEventReportServer:: Client disconnected."); + break; + } + catch (Exception ex) + { + if (server.IsConnected) + frameworkHandle.SendMessage(TestMessageLevel.Error, $"TestEventReportServer:: Error: {ex.Message}"); + } + } + + private void ProcessTestEvent(IFrameworkHandle frameworkHandle, IReadOnlyList tests, string json) + { + if (json.StartsWith("GdUnitTestEvent:")) + { + json = json.TrimPrefix("GdUnitTestEvent:"); + var e = JsonConvert.DeserializeObject(json)!; + + switch (e.Type) + { + case TestEvent.TYPE.TESTSUITE_BEFORE: + //frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Execute Test Suite '{e.SuiteName}'"); + break; + case TestEvent.TYPE.TESTCASE_BEFORE: + { + var testCase = FindTestCase(tests, e); + if (testCase == null) + //frameworkHandle.SendMessage(TestMessageLevel.Error, $"TESTCASE_BEFORE: cant find test case {e.FullyQualifiedName}"); + return; + frameworkHandle.RecordStart(testCase); + break; + } + case TestEvent.TYPE.TESTCASE_AFTER: + { + var testCase = FindTestCase(tests, e); + if (testCase == null) + //frameworkHandle.SendMessage(TestMessageLevel.Error, $"TESTCASE_AFTER: cant find test case {e.FullyQualifiedName}"); + return; + var testResult = new TestResult(testCase) + { + DisplayName = testCase.DisplayName, + Outcome = e.AsTestOutcome(), + EndTime = DateTimeOffset.Now, + Duration = e.ElapsedInMs + }; + foreach (var report in e.Reports) + { + testResult.ErrorMessage = report.Message.RichTextNormalize(); + testResult.ErrorStackTrace = report.StackTrace; + } + + frameworkHandle.RecordResult(testResult); + frameworkHandle.RecordEnd(testCase, testResult.Outcome); + break; + } + case TestEvent.TYPE.TESTSUITE_AFTER: + //frameworkHandle.SendMessage(TestMessageLevel.Informational, $"{e.AsTestOutcome()}"); + break; + case TestEvent.TYPE.INIT: + break; + case TestEvent.TYPE.STOP: + break; + } + + return; + } + + frameworkHandle.SendMessage(TestMessageLevel.Informational, $"TestEventReportServer:: {json}"); + } + + private static TestCase? FindTestCase(IEnumerable tests, TestEvent e) + => tests.FirstOrDefault(t => e.FullyQualifiedName.Equals(t.FullyQualifiedName, StringComparison.Ordinal)); +} diff --git a/testadapter/src/execution/TestExecutor.cs b/testadapter/src/execution/TestExecutor.cs index 91d9d24d..3fed4248 100644 --- a/testadapter/src/execution/TestExecutor.cs +++ b/testadapter/src/execution/TestExecutor.cs @@ -3,28 +3,24 @@ namespace GdUnit4.TestAdapter.Execution; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.IO; +using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; -using GdUnit4.TestAdapter.Settings; +using Settings; internal sealed class TestExecutor : BaseTestExecutor, ITestExecutor { private const string TempTestRunnerDir = "gdunit4_testadapter"; - - private Process? pProcess; private readonly GdUnit4Settings gdUnit4Settings; -#pragma warning disable IDE0052 // Remove unread private members - private int ParallelTestCount { get; set; } -#pragma warning restore IDE0052 // Remove unread private members - - private int SessionTimeOut { get; set; } + private Process? pProcess; public TestExecutor(RunConfiguration configuration, GdUnit4Settings gdUnit4Settings) { @@ -32,32 +28,65 @@ public TestExecutor(RunConfiguration configuration, GdUnit4Settings gdUnit4Setti ? 1 : configuration.MaxCpuCount; SessionTimeOut = (int)(configuration.TestSessionTimeout == 0 - ? ITestExecutor.DEFAULT_SESSION_TIMEOUT - : configuration.TestSessionTimeout); + ? ITestExecutor.DEFAULT_SESSION_TIMEOUT + : configuration.TestSessionTimeout); this.gdUnit4Settings = gdUnit4Settings; } - public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnumerable testCases) +#pragma warning disable IDE0052 // Remove unread private members + private int ParallelTestCount { get; set; } +#pragma warning restore IDE0052 // Remove unread private members + + private int SessionTimeOut { get; } + + public void Cancel() + { + lock (this) + { + Console.WriteLine("Cancel triggered"); + try + { + pProcess?.Kill(true); + pProcess?.WaitForExit(); + } + catch (Exception) + { + //frameworkHandle.SendMessage(TestMessageLevel.Error, @$"TestRunner ends with: {e.Message}"); + } + } + } + + public void Dispose() + { + pProcess?.Dispose(); + GC.SuppressFinalize(this); + } + + public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IReadOnlyList testCases) { - frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Start executing tests, {testCases.Count()} TestCases total."); + frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Start executing tests, {testCases.Count} TestCases total."); // TODO split into multiple threads by using 'ParallelTestCount' var groupedTests = testCases .GroupBy(t => t.CodeFilePath!) .ToDictionary(group => group.Key, group => group.ToList()); var workingDirectory = LookupGodotProjectPath(groupedTests.First().Key); - _ = workingDirectory ?? throw new InvalidOperationException($"Cannot determine the godot.project! The workingDirectory is not set"); + _ = workingDirectory ?? throw new InvalidOperationException("Cannot determine the godot.project! The workingDirectory is not set"); if (Directory.Exists(workingDirectory)) { Directory.SetCurrentDirectory(workingDirectory); frameworkHandle.SendMessage(TestMessageLevel.Informational, "Current directory set to: " + Directory.GetCurrentDirectory()); } + InstallTestRunnerAndBuild(frameworkHandle, workingDirectory); var configName = WriteTestRunnerConfig(groupedTests); var debugArg = runContext.IsBeingDebugged ? "-d" : ""; + using var eventServer = new TestEventReportServer(); + Task.Run(() => eventServer.Start(frameworkHandle, testCases)); + //var filteredTestCases = filterExpression != null // ? testCases.FindAll(t => filterExpression.MatchTestCase(t, (propertyName) => // { @@ -65,7 +94,7 @@ public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnume // return t.GetPropertyValue(testProperty); // }) == false) // : testCases; - var testRunnerScene = "res://gdunit4_testadapter/TestAdapterRunner.tscn";//Path.Combine(workingDirectory, @$"{temp_test_runner_dir}/TestRunner.tscn"); + var testRunnerScene = "res://gdunit4_testadapter/TestAdapterRunner.tscn"; //Path.Combine(workingDirectory, @$"{temp_test_runner_dir}/TestRunner.tscn"); var arguments = $"{debugArg} --path . {testRunnerScene} --testadapter --configfile=\"{configName}\" {gdUnit4Settings.Parameters}"; frameworkHandle.SendMessage(TestMessageLevel.Informational, @$"Run with args {arguments}"); var processStartInfo = new ProcessStartInfo(@$"{GodotBin}", arguments) @@ -80,10 +109,9 @@ public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnume WorkingDirectory = @$"{workingDirectory}" }; - using (pProcess = new() { StartInfo = processStartInfo }) + using (pProcess = new Process { StartInfo = processStartInfo }) { pProcess.EnableRaisingEvents = true; - pProcess.OutputDataReceived += TestEventProcessor(frameworkHandle, testCases); pProcess.ErrorDataReceived += StdErrorProcessor(frameworkHandle); pProcess.Exited += ExitHandler(frameworkHandle); pProcess.Start(); @@ -91,9 +119,7 @@ public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnume pProcess.BeginOutputReadLine(); AttachDebuggerIfNeed(runContext, frameworkHandle, pProcess); while (!pProcess.WaitForExit(SessionTimeOut)) - { Thread.Sleep(100); - } try { pProcess.Kill(true); @@ -107,19 +133,17 @@ public void Run(IFrameworkHandle frameworkHandle, IRunContext runContext, IEnume { File.Delete(configName); } - }; + } } private void InstallTestRunnerAndBuild(IFrameworkHandle frameworkHandle, string workingDirectory) { var destinationFolderPath = Path.Combine(workingDirectory, @$"{TempTestRunnerDir}"); if (Directory.Exists(destinationFolderPath)) - { return; - } frameworkHandle.SendMessage(TestMessageLevel.Informational, "Install GdUnit4 TestRunner"); InstallTestRunnerClasses(destinationFolderPath); - var processStartInfo = new ProcessStartInfo(@$"{GodotBin}", @$"--path . --headless --build-solutions --quit-after 20") + var processStartInfo = new ProcessStartInfo(@$"{GodotBin}", @"--path . --headless --build-solutions --quit-after 20") { RedirectStandardOutput = false, RedirectStandardError = false, @@ -127,16 +151,14 @@ private void InstallTestRunnerAndBuild(IFrameworkHandle frameworkHandle, string UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = @$"{workingDirectory}", + WorkingDirectory = @$"{workingDirectory}" }; using Process process = new() { StartInfo = processStartInfo }; - frameworkHandle.SendMessage(TestMessageLevel.Informational, @$"Rebuild ..."); + frameworkHandle.SendMessage(TestMessageLevel.Informational, @"Rebuild ..."); process.Start(); while (!process.WaitForExit(5000)) - { Thread.Sleep(100); - } try { process.Kill(true); @@ -148,56 +170,33 @@ private void InstallTestRunnerAndBuild(IFrameworkHandle frameworkHandle, string } } - public void Cancel() - { - lock (this) - { - Console.WriteLine("Cancel triggered"); - try - { - pProcess?.Kill(true); - pProcess?.WaitForExit(); - } - catch (Exception) - { - //frameworkHandle.SendMessage(TestMessageLevel.Error, @$"TestRunner ends with: {e.Message}"); - } - } - } - private static void InstallTestRunnerClasses(string destinationFolderPath) { Directory.CreateDirectory(destinationFolderPath); var srcTestRunner = """ - namespace GdUnit4.TestAdapter; + namespace GdUnit4.TestAdapter; - public partial class TestAdapterRunner : Api.TestRunner - { - public override void _Ready() - => _ = RunTests(); - } + public partial class TestAdapterRunner : Api.TestRunner + { + public override void _Ready() + => _ = RunTests(); + } - """; + """; File.WriteAllText(Path.Combine(destinationFolderPath, "TestAdapterRunner.cs"), srcTestRunner); - var srcTestRunnerScene = $""" - [gd_scene load_steps=2 format=3 uid="uid://5o7l4yufw1rw"] + var srcTestRunnerScene = """ + [gd_scene load_steps=2 format=3 uid="uid://5o7l4yufw1rw"] - [ext_resource type="Script" path="res://gdunit4_testadapter/TestAdapterRunner.cs" id="1"] + [ext_resource type="Script" path="res://gdunit4_testadapter/TestAdapterRunner.cs" id="1"] - [node name="Control" type="Control"] - layout_mode = 3 - anchors_preset = 0 - script = ExtResource("1") + [node name="Control" type="Control"] + layout_mode = 3 + anchors_preset = 0 + script = ExtResource("1") - """; + """; File.WriteAllText(Path.Combine(destinationFolderPath, "TestAdapterRunner.tscn"), srcTestRunnerScene); } - - public void Dispose() - { - pProcess?.Dispose(); - GC.SuppressFinalize(this); - } } diff --git a/testadapter/src/utilities/Utils.cs b/testadapter/src/utilities/Utils.cs index 6b8700f7..61530d4c 100644 --- a/testadapter/src/utilities/Utils.cs +++ b/testadapter/src/utilities/Utils.cs @@ -1,17 +1,15 @@ namespace GdUnit4.TestAdapter.Utilities; using System; -using System.Linq; using System.Reflection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; internal static class Utils { - - internal static bool CheckGdUnit4ApiVersion(IMessageLogger logger, Version minVersion) + internal static bool CheckGdUnit4ApiMinimumRequiredVersion(IMessageLogger logger, Version minVersion) { - var gdUnit4ApiAssembly = Assembly.Load("gdUnit4Api") ?? throw new InvalidOperationException($"No 'gdUnit4Api' is installed!"); + var gdUnit4ApiAssembly = Assembly.Load("gdUnit4Api") ?? throw new InvalidOperationException("No 'gdUnit4Api' is installed!"); var version = gdUnit4ApiAssembly.GetName().Version; logger.SendMessage(TestMessageLevel.Informational, $"CheckGdUnit4ApiVersion gdUnit4Api, Version={version}"); if (version < minVersion) @@ -19,6 +17,7 @@ internal static bool CheckGdUnit4ApiVersion(IMessageLogger logger, Version minVe logger.SendMessage(TestMessageLevel.Error, $"Wrong gdUnit4Api, Version={version} found, you need to upgrade to minimum version: '{minVersion}'"); return false; } + return true; } }