From ab957e94fccc438495ef28347f0f79f6961e8861 Mon Sep 17 00:00:00 2001 From: Mike Schulze Date: Sun, 29 Aug 2021 17:52:56 +0200 Subject: [PATCH] GD-129: Spike to enable CSharp test support --- .github/workflows/selftest-3.3.x-mono.yml | 39 +++ .github/workflows/selftest-3.3.x.yml | 9 +- addons/gdUnit3/bin/GdUnitCmdTool.gd | 32 ++- addons/gdUnit3/plugin.gd | 2 +- addons/gdUnit3/src/GdUnitStringAssert.gd | 2 +- addons/gdUnit3/src/IAssert.cs | 47 ++++ addons/gdUnit3/src/IBoolAssert.cs | 16 ++ addons/gdUnit3/src/IDoubleAssert.cs | 10 + addons/gdUnit3/src/IIntAssert.cs | 9 + addons/gdUnit3/src/INumberAssert.cs | 62 +++++ addons/gdUnit3/src/IObjectAssert.cs | 21 ++ addons/gdUnit3/src/IStringAssert.cs | 53 +++++ addons/gdUnit3/src/TestSuite.cs | 106 +++++++++ addons/gdUnit3/src/asserts/AssertBase.cs | 64 +++++ addons/gdUnit3/src/asserts/BoolAssert.cs | 28 +++ addons/gdUnit3/src/asserts/DoubleAssert.cs | 13 + .../gdUnit3/src/asserts/GdUnitAssertImpl.gd | 1 + addons/gdUnit3/src/asserts/IntAssert.cs | 13 + addons/gdUnit3/src/asserts/NumberAssert.cs | 88 +++++++ addons/gdUnit3/src/asserts/ObjectAssert.cs | 80 +++++++ addons/gdUnit3/src/asserts/StringAssert.cs | 80 +++++++ addons/gdUnit3/src/core/CsTools.cs | 64 +++++ addons/gdUnit3/src/core/GdObjects.gd | 32 ++- addons/gdUnit3/src/core/GdUnitExecutor.gd | 13 +- addons/gdUnit3/src/core/GdUnitRunner.gd | 38 +-- addons/gdUnit3/src/core/GdUnitScriptType.gd | 21 ++ addons/gdUnit3/src/core/GdUnitSingleton.gd | 5 +- addons/gdUnit3/src/core/GdUnitTools.gd | 6 + addons/gdUnit3/src/core/_TestSuiteScanner.gd | 90 ++++--- .../src/core/attributes/FuzzerAttribute.cs | 23 ++ .../src/core/attributes/TestCaseAttributes.cs | 45 ++++ .../src/core/attributes/TestSuiteAttribute.cs | 19 ++ .../gdUnit3/src/core/data/IValueProvider.cs | 12 + addons/gdUnit3/src/core/data/TestCase.cs | 60 +++++ addons/gdUnit3/src/core/event/GdUnitEvent.gd | 18 +- addons/gdUnit3/src/core/event/TestEvent.cs | 95 ++++++++ .../src/core/event/TestEventListener.cs | 9 + .../src/core/execution/AfterExecutionStage.cs | 18 ++ .../core/execution/AfterTestExecutionStage.cs | 22 ++ .../core/execution/BeforeExecutionStage.cs | 18 ++ .../execution/BeforeTestExecutionStage.cs | 21 ++ .../src/core/execution/ExecutionContext.cs | 153 ++++++++++++ .../src/core/execution/ExecutionStage.cs | 29 +++ addons/gdUnit3/src/core/execution/Executor.cs | 38 +++ .../src/core/execution/IExecutionStage.cs | 11 + .../core/execution/TestCaseExecutionStage.cs | 36 +++ .../core/execution/TestSuiteExecutionStage.cs | 37 +++ .../src/core/monitor/OrphanNodesMonitor.cs | 31 +++ addons/gdUnit3/src/core/report/TestReport.cs | 69 ++++++ .../src/core/report/TestReportCollector.cs | 36 +++ .../src/network/rpc/RPCGdUnitTestSuite.gd | 2 +- .../src/network/rpc/dtos/GdUnitResourceDto.gd | 2 +- .../src/network/rpc/dtos/GdUnitTestCaseDto.gd | 2 +- .../network/rpc/dtos/GdUnitTestSuiteDto.gd | 2 +- addons/gdUnit3/src/report/GdUnitHtmlReport.gd | 12 +- .../gdUnit3/src/report/GdUnitReportSummary.gd | 3 + .../src/report/GdUnitTestCaseReport.gd | 3 +- .../src/report/GdUnitTestSuiteReport.gd | 1 - addons/gdUnit3/src/ui/GdUnitInspector.gd | 2 +- .../src/ui/parts/InspectorTreeMainPanel.gd | 22 +- addons/gdUnit3/test/GdUnitScriptTypeTest.gd | 16 ++ .../gdUnit3/test/GdUnitTestResourceLoader.gd | 53 ++++- addons/gdUnit3/test/asserts/BoolAssertTest.cs | 77 ++++++ .../asserts/GdUnitObjectAssertImplTest.gd | 10 +- addons/gdUnit3/test/asserts/IntAssertTest.cs | 224 ++++++++++++++++++ .../gdUnit3/test/asserts/ObjectAssertTest.cs | 141 +++++++++++ .../gdUnit3/test/asserts/StringAssertTest.cs | 221 +++++++++++++++++ addons/gdUnit3/test/core/ExampleTest.cs | 44 ++++ addons/gdUnit3/test/core/GdObjectsTest.gd | 11 + .../gdUnit3/test/core/GdUnitExecutorTest.gd | 4 +- .../test/core/_TestSuiteScannerTest.gd | 19 +- .../testsuites/mono/NotATestSuite.cs | 13 + .../ui/parts/InspectorTreeMainPanelTest.gd | 2 +- gdUnit3.csproj | 6 + gdUnit3.sln | 19 ++ project.godot | 13 + runtest.cmd | 9 + 77 files changed, 2661 insertions(+), 116 deletions(-) create mode 100644 .github/workflows/selftest-3.3.x-mono.yml create mode 100644 addons/gdUnit3/src/IAssert.cs create mode 100644 addons/gdUnit3/src/IBoolAssert.cs create mode 100644 addons/gdUnit3/src/IDoubleAssert.cs create mode 100644 addons/gdUnit3/src/IIntAssert.cs create mode 100644 addons/gdUnit3/src/INumberAssert.cs create mode 100644 addons/gdUnit3/src/IObjectAssert.cs create mode 100644 addons/gdUnit3/src/IStringAssert.cs create mode 100644 addons/gdUnit3/src/TestSuite.cs create mode 100644 addons/gdUnit3/src/asserts/AssertBase.cs create mode 100644 addons/gdUnit3/src/asserts/BoolAssert.cs create mode 100644 addons/gdUnit3/src/asserts/DoubleAssert.cs create mode 100644 addons/gdUnit3/src/asserts/IntAssert.cs create mode 100644 addons/gdUnit3/src/asserts/NumberAssert.cs create mode 100644 addons/gdUnit3/src/asserts/ObjectAssert.cs create mode 100644 addons/gdUnit3/src/asserts/StringAssert.cs create mode 100644 addons/gdUnit3/src/core/CsTools.cs create mode 100644 addons/gdUnit3/src/core/GdUnitScriptType.gd create mode 100644 addons/gdUnit3/src/core/attributes/FuzzerAttribute.cs create mode 100644 addons/gdUnit3/src/core/attributes/TestCaseAttributes.cs create mode 100644 addons/gdUnit3/src/core/attributes/TestSuiteAttribute.cs create mode 100644 addons/gdUnit3/src/core/data/IValueProvider.cs create mode 100644 addons/gdUnit3/src/core/data/TestCase.cs create mode 100644 addons/gdUnit3/src/core/event/TestEvent.cs create mode 100644 addons/gdUnit3/src/core/event/TestEventListener.cs create mode 100644 addons/gdUnit3/src/core/execution/AfterExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/AfterTestExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/BeforeExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/BeforeTestExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/ExecutionContext.cs create mode 100644 addons/gdUnit3/src/core/execution/ExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/Executor.cs create mode 100644 addons/gdUnit3/src/core/execution/IExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/TestCaseExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/execution/TestSuiteExecutionStage.cs create mode 100644 addons/gdUnit3/src/core/monitor/OrphanNodesMonitor.cs create mode 100644 addons/gdUnit3/src/core/report/TestReport.cs create mode 100644 addons/gdUnit3/src/core/report/TestReportCollector.cs create mode 100644 addons/gdUnit3/test/GdUnitScriptTypeTest.gd create mode 100644 addons/gdUnit3/test/asserts/BoolAssertTest.cs create mode 100644 addons/gdUnit3/test/asserts/IntAssertTest.cs create mode 100644 addons/gdUnit3/test/asserts/ObjectAssertTest.cs create mode 100644 addons/gdUnit3/test/asserts/StringAssertTest.cs create mode 100644 addons/gdUnit3/test/core/ExampleTest.cs create mode 100644 addons/gdUnit3/test/core/resources/testsuites/mono/NotATestSuite.cs create mode 100644 gdUnit3.csproj create mode 100644 gdUnit3.sln diff --git a/.github/workflows/selftest-3.3.x-mono.yml b/.github/workflows/selftest-3.3.x-mono.yml new file mode 100644 index 00000000..ddc88a6d --- /dev/null +++ b/.github/workflows/selftest-3.3.x-mono.yml @@ -0,0 +1,39 @@ +name: Run selftest Godot 3.3.x - Mono +on: [push] + +jobs: + testing: + strategy: + matrix: + godot: [mono-3.3.1, mono-3.3.2, mono-3.3.3] + + name: GdUnit3 Selftest on Godot ${{ matrix.godot }} + runs-on: ubuntu-latest + container: + image: barichello/godot-ci:${{ matrix.godot }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + lfs: true + + - name: Compile + run: | + nuget restore + mkdir -p .mono/assemblies/Debug + cp /usr/local/bin/GodotSharp/Api/Release/* .mono/assemblies/Debug + msbuild + + - name: Run Selftes + timeout-minutes: 7 + env: + GODOT_BIN: "/usr/local/bin/godot" + shell: bash + run: ./runtest.sh --selftest + + - name: Collect Test Reports + uses: actions/upload-artifact@v2 + with: + name: Report_${{ matrix.godot }} + path: reports/** diff --git a/.github/workflows/selftest-3.3.x.yml b/.github/workflows/selftest-3.3.x.yml index 950797d4..14e1d858 100644 --- a/.github/workflows/selftest-3.3.x.yml +++ b/.github/workflows/selftest-3.3.x.yml @@ -5,8 +5,8 @@ jobs: testing: strategy: matrix: - godot: [3.3, 3.3.1, 3.3.2, 3.3.3, mono-3.3.3] - + godot: [3.3, 3.3.1, 3.3.2, 3.3.3] + name: GdUnit3 Selftest on Godot ${{ matrix.godot }} runs-on: ubuntu-latest container: @@ -20,15 +20,14 @@ jobs: - name: Setup shell: bash run: echo "GODOT_BIN=/usr/local/bin/godot" >> $GITHUB_ENV - + - name: Run Selftes shell: bash run: ./runtest.sh --selftest - + - name: Collect Test Report if: always() uses: actions/upload-artifact@v2 with: name: Report_${{ matrix.godot }} path: reports/** - diff --git a/addons/gdUnit3/bin/GdUnitCmdTool.gd b/addons/gdUnit3/bin/GdUnitCmdTool.gd index 31a625a9..2351aa8c 100644 --- a/addons/gdUnit3/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit3/bin/GdUnitCmdTool.gd @@ -15,6 +15,7 @@ class CLIRunner extends Node: const RETURN_ERROR = 100 const RETURN_WARNING = 101 + var _state = INIT var _signal_handler var _test_suites_to_process :Array @@ -24,6 +25,7 @@ class CLIRunner extends Node: var _report_max: int = DEFAULT_REPORT_COUNT var _runner_config := GdUnitRunnerConfig.new() var _console := CmdConsole.new() + var _cs_executor var _cmd_options: = CmdOptions.new([ CmdOption.new("-a, --add", "-a ", "Adds the given test suite or directory to the execution pipeline.", TYPE_STRING), @@ -49,6 +51,11 @@ class CLIRunner extends Node: _executor.disable_default_yield() # stop on first test failure to fail fast _executor.fail_fast(true) + + if GdUnitTools.is_mono_supported(): + _cs_executor = load("res://addons/gdUnit3/src/core/execution/Executor.cs").new() + _cs_executor.AddGdTestEventListener(self) + var err := _executor.connect("send_event", self, "_on_executor_event") if err != OK: push_error("Error on startup, can't connect executor for 'send_event'") @@ -67,10 +74,13 @@ class CLIRunner extends Node: _state = STOP else: # process next test suite - var test_suite := _test_suites_to_process.pop_front() as GdUnitTestSuite - var fs = _executor.execute(test_suite) - if fs is GDScriptFunctionState: - yield(fs, "completed") + var test_suite := _test_suites_to_process.pop_front() as Node + if GdObjects.is_cs_script(test_suite.get_script()): + _cs_executor.execute(test_suite) + else: + var fs = _executor.execute(test_suite) + if fs is GDScriptFunctionState: + yield(fs, "completed") set_process(true) STOP: _state = EXIT @@ -201,7 +211,7 @@ class CLIRunner extends Node: for test_suite in test_suites: skip_suite(test_suite, skipped) - func skip_suite(test_suite :GdUnitTestSuite, skipped :Dictionary) -> void: + func skip_suite(test_suite :Node, skipped :Dictionary) -> void: var skipped_suites := skipped.keys() if skipped_suites.empty(): return @@ -217,7 +227,7 @@ class CLIRunner extends Node: else: # skip tests for test_to_skip in skipped_tests: - var test_case :_TestCase = test_suite.find_node(test_to_skip, true, false) + var test_case :_TestCase = test_suite.get_test_case_by_name(test_to_skip) if test_case: test_case.skip(true) else: @@ -229,6 +239,9 @@ class CLIRunner extends Node: total += (test_suite as Node).get_child_count() return total + func PublishEvent(data) -> void: + _on_executor_event(GdUnitEvent.new().deserialize(data.AsDictionary())) + func _on_executor_event(event :GdUnitEvent): match event.type(): GdUnitEvent.INIT: @@ -244,13 +257,14 @@ class CLIRunner extends Node: _report.add_testsuite_report(GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name())) GdUnitEvent.TESTSUITE_AFTER: - _report.update_test_suite_report(event.suite_name(), event.skipped_count(), event.orphan_nodes(), event.elapsed_time()) + _report.update_test_suite_report(event.resource_path(), 0, event.orphan_nodes(), event.elapsed_time()) GdUnitEvent.TESTCASE_BEFORE: - _report.add_testcase_report(event.suite_name(), GdUnitTestCaseReport.new(event.test_name())) + _report.add_testcase_report(event.resource_path(), GdUnitTestCaseReport.new(event.resource_path(), event.test_name())) GdUnitEvent.TESTCASE_AFTER: var test_report := GdUnitTestCaseReport.new( + event.resource_path(), event.test_name(), event.is_error(), event.is_failed(), @@ -258,7 +272,7 @@ class CLIRunner extends Node: event.skipped_count(), event.reports(), event.elapsed_time()) - _report.update_testcase_report(event.suite_name(), test_report) + _report.update_testcase_report(event.resource_path(), test_report) print_status(event) func report_exit_code(report :GdUnitHtmlReport) -> int: diff --git a/addons/gdUnit3/plugin.gd b/addons/gdUnit3/plugin.gd index 322884eb..6e5bda4e 100644 --- a/addons/gdUnit3/plugin.gd +++ b/addons/gdUnit3/plugin.gd @@ -13,7 +13,7 @@ func _enter_tree(): # show possible update notification when is enabled if GdUnitSettings.is_update_notification_enabled(): _update_tool = load("res://addons/gdUnit3/src/update/GdUnitUpdate.tscn").instance() - get_parent().add_child(_update_tool) + add_child(_update_tool) # install SignalHandler singleton GdUnitSingleton.add_singleton(SignalHandler.SINGLETON_NAME, "res://addons/gdUnit3/src/core/event/SignalHandler.gd") diff --git a/addons/gdUnit3/src/GdUnitStringAssert.gd b/addons/gdUnit3/src/GdUnitStringAssert.gd index 6c7e01f9..ce5fecc3 100644 --- a/addons/gdUnit3/src/GdUnitStringAssert.gd +++ b/addons/gdUnit3/src/GdUnitStringAssert.gd @@ -51,5 +51,5 @@ func ends_with(expected: String) -> GdUnitStringAssert: return self # Verifies that the current String has the expected length by used comparator. -func has_length(lenght: int, comparator: int = Comparator.EXACTLY) -> GdUnitStringAssert: +func has_length(lenght: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert: return self diff --git a/addons/gdUnit3/src/IAssert.cs b/addons/gdUnit3/src/IAssert.cs new file mode 100644 index 00000000..16fc999f --- /dev/null +++ b/addons/gdUnit3/src/IAssert.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; + +namespace GdUnit3 +{ + + /// Main interface of all GdUnit asserts + public interface IAssert + { + + enum EXPECT : int + { + [Description("assert expects ends with success")] + SUCCESS = 0, + [Description("assert expects ends with errors")] + FAIL = 1 + } + } + + /// Base interface of all GdUnit asserts + public interface IAssertBase : IAssert + { + + /// Verifies that the current value is null. + IAssertBase IsNull(); + + /// Verifies that the current value is not null. + IAssertBase IsNotNull(); + + /// Verifies that the current value is equal to expected one. + IAssertBase IsEqual(V expected); + + /// Verifies that the current value is not equal to expected one. + IAssertBase IsNotEqual(V expected); + + /// + IAssertBase TestFail(); + + /// Verifies the failure message is equal to expected one. + IAssertBase HasFailureMessage(string expected); + + /// Verifies that the failure starts with the given value. + IAssertBase StartsWithFailureMessage(string value); + + /// Overrides the default failure message by given custom message. + IAssertBase OverrideFailureMessage(string message); + } +} diff --git a/addons/gdUnit3/src/IBoolAssert.cs b/addons/gdUnit3/src/IBoolAssert.cs new file mode 100644 index 00000000..43e96c8d --- /dev/null +++ b/addons/gdUnit3/src/IBoolAssert.cs @@ -0,0 +1,16 @@ + +namespace GdUnit3 +{ + + /// An Assertion Tool to verify boolean values + public interface IBoolAssert : IAssertBase + { + + /// Verifies that the current value is true. + IBoolAssert IsTrue(); + + /// Verifies that the current value is false. + IBoolAssert IsFalse(); + + } +} diff --git a/addons/gdUnit3/src/IDoubleAssert.cs b/addons/gdUnit3/src/IDoubleAssert.cs new file mode 100644 index 00000000..ac7763b9 --- /dev/null +++ b/addons/gdUnit3/src/IDoubleAssert.cs @@ -0,0 +1,10 @@ + +namespace GdUnit3 +{ + + /// Base interface for integer assertions. + public interface IDoubleAssert : INumberAssert + { + + } +} diff --git a/addons/gdUnit3/src/IIntAssert.cs b/addons/gdUnit3/src/IIntAssert.cs new file mode 100644 index 00000000..95267c30 --- /dev/null +++ b/addons/gdUnit3/src/IIntAssert.cs @@ -0,0 +1,9 @@ +namespace GdUnit3 +{ + + /// Base interface for integer assertions. + public interface IIntAssert : INumberAssert + { + + } +} diff --git a/addons/gdUnit3/src/INumberAssert.cs b/addons/gdUnit3/src/INumberAssert.cs new file mode 100644 index 00000000..733ade0c --- /dev/null +++ b/addons/gdUnit3/src/INumberAssert.cs @@ -0,0 +1,62 @@ +using System; + +namespace GdUnit3 +{ + + /// Base interface for number assertions. + public interface INumberAssert : IAssertBase + { + + /// Verifies that the current value is less than the given one. + public INumberAssert IsLess(V expected); + + + /// Verifies that the current value is less than or equal the given one. + public INumberAssert IsLessEqual(V expected); + + + /// Verifies that the current value is greater than the given one. + public INumberAssert IsGreater(V expected); + + + /// Verifies that the current value is greater than or equal the given one. + public INumberAssert IsGreaterEqual(V expected); + + + /// Verifies that the current value is even. + public INumberAssert IsEven(); + + + /// Verifies that the current value is odd. + public INumberAssert IsOdd(); + + + /// Verifies that the current value is negative. + public INumberAssert IsNegative(); + + + /// Verifies that the current value is not negative. + public INumberAssert IsNotNegative(); + + + /// Verifies that the current value is equal to zero. + public INumberAssert IsZero(); + + + /// Verifies that the current value is not equal to zero. + public INumberAssert IsNotZero(); + + + /// Verifies that the current value is in the given set of values. + public INumberAssert IsIn(Array expected); + + + /// Verifies that the current value is not in the given set of values. + public INumberAssert IsNotIn(Array expected); + + + /// Verifies that the current value is between the given boundaries (inclusive). + public INumberAssert IsBetween(V from, V to); + + } +} diff --git a/addons/gdUnit3/src/IObjectAssert.cs b/addons/gdUnit3/src/IObjectAssert.cs new file mode 100644 index 00000000..d2773c3a --- /dev/null +++ b/addons/gdUnit3/src/IObjectAssert.cs @@ -0,0 +1,21 @@ + +namespace GdUnit3 +{ + + /// An Assertion Tool to verify object values + public interface IObjectAssert : IAssertBase + { + // Verifies that the current value is the same as the given one. + public IObjectAssert IsSame(object expected); + + // Verifies that the current value is not the same as the given one. + public IObjectAssert IsNotSame(object expected); + + // Verifies that the current value is an instance of the given type. + public IObjectAssert IsInstanceof(); + + // Verifies that the current value is not an instance of the given type. + public IObjectAssert IsNotInstanceof(); + + } +} diff --git a/addons/gdUnit3/src/IStringAssert.cs b/addons/gdUnit3/src/IStringAssert.cs new file mode 100644 index 00000000..74dc33d0 --- /dev/null +++ b/addons/gdUnit3/src/IStringAssert.cs @@ -0,0 +1,53 @@ + +namespace GdUnit3 +{ + + /// An Assertion Tool to verify string values + public interface IStringAssert : IAssertBase + { + enum Compare + { + EQUAL, + LESS_THAN, + LESS_EQUAL, + GREATER_THAN, + GREATER_EQUAL, + BETWEEN_EQUAL, + NOT_BETWEEN_EQUAL, + } + + /// Verifies that the current String is equal to the given one, ignoring case considerations. + public IStringAssert IsEqualIgnoringCase(string expected); + + /// Verifies that the current String is not equal to the given one, ignoring case considerations. + public IStringAssert IsNotEqualIgnoringCase(string expected); + + /// Verifies that the current String is empty, it has a length of 0. + public IStringAssert IsEmpty(); + + /// Verifies that the current String is not empty, it has a length of minimum 1. + public IStringAssert IsNotEmpty(); + + /// Verifies that the current String contains the given String. + public IStringAssert Contains(string expected); + + /// Verifies that the current String does not contain the given String. + public IStringAssert NotContains(string expected); + + /// Verifies that the current String does not contain the given String, ignoring case considerations. + public IStringAssert ContainsIgnoringCase(string expected); + + /// Verifies that the current String does not contain the given String, ignoring case considerations. + public IStringAssert NotContainsIgnoringCase(string expected); + + /// Verifies that the current String starts with the given prefix. + public IStringAssert StartsWith(string expected); + + /// Verifies that the current String ends with the given suffix. + public IStringAssert EndsWith(string expected); + + /// Verifies that the current String has the expected length by used comparator. + public IStringAssert HasLength(int lenght, Compare comparator = Compare.EQUAL); + + } +} diff --git a/addons/gdUnit3/src/TestSuite.cs b/addons/gdUnit3/src/TestSuite.cs new file mode 100644 index 00000000..36484ab4 --- /dev/null +++ b/addons/gdUnit3/src/TestSuite.cs @@ -0,0 +1,106 @@ +using Godot; +using System; +using System.Diagnostics; + +namespace GdUnit3 +{ + + /** + This class is the main class to implement your unit tests
+ You have to extend and implement your test cases as described
+ e.g
+
+ For detailed instructions see HERE
+ + For example: + + + public class MyExampleTest : GdUnit3.GdUnitTestSuite + { + public void testCaseA() + { + AssertThat("value").IsEqual("value"); + } + } + + +
*/ + public abstract class TestSuite : Node + { + private String _active_test_case; + + private static Godot.Resource GdUnitTools = (Resource)GD.Load("res://addons/gdUnit3/src/core/GdUnitTools.gd").New(); + + + // current we overide it to get the correct count of tests + public int get_child_count() + { + return TestCaseCount; + } + + // A litle helper to auto freeing your created objects after test execution + public T auto_free(T obj) + { + GdUnitTools.Call("register_auto_free", obj, GetMeta("MEMORY_POOL")); + return obj; + } + + // Discard the error message triggered by a timeout (interruption). + // By default, an interrupted test is reported as an error. + // This function allows you to change the message to Success when an interrupted error is reported. + public void discard_error_interupted_by_timeout() + { + //GdUnitTools.register_expect_interupted_by_timeout(self, __active_test_case) + } + + // Creates a new directory under the temporary directory *user://tmp* + // Useful for storing data during test execution. + // The directory is automatically deleted after test suite execution + public String create_temp_dir(String relative_path) + { + //return GdUnitTools.create_temp_dir(relative_path) + return ""; + } + + // Deletes the temporary base directory + // Is called automatically after each execution of the test suite + public void clean_temp_dir() + { + //GdUnitTools.clear_tmp() + } + + public int TestCaseCount => CsTools.TestCaseCount(GetType()); + + public string ResourcePath => (GetScript() as Script).ResourcePath; + + public bool Skipped => false; + + + // === Asserts ================================================================== + public IBoolAssert AssertBool(bool current, IAssert.EXPECT expectResult = IAssert.EXPECT.SUCCESS) + { + return new BoolAssert(this, current, expectResult); + } + + public IStringAssert AssertString(string current, IAssert.EXPECT expectResult = IAssert.EXPECT.SUCCESS) + { + return new StringAssert(this, current, expectResult); + } + + public IIntAssert AssertInt(int current, IAssert.EXPECT expectResult = IAssert.EXPECT.SUCCESS) + { + return new IntAssert(this, current, expectResult); + } + + public IDoubleAssert AssertFloat(double current, IAssert.EXPECT expectResult = IAssert.EXPECT.SUCCESS) + { + return new DoubleAssert(this, current, expectResult); + } + + public IObjectAssert AssertObject(object current, IAssert.EXPECT expectResult = IAssert.EXPECT.SUCCESS) + { + return new ObjectAssert(this, current, expectResult); + } + } + +} diff --git a/addons/gdUnit3/src/asserts/AssertBase.cs b/addons/gdUnit3/src/asserts/AssertBase.cs new file mode 100644 index 00000000..4e95b79f --- /dev/null +++ b/addons/gdUnit3/src/asserts/AssertBase.cs @@ -0,0 +1,64 @@ + +namespace GdUnit3 +{ + public abstract class AssertBase : IAssertBase + { + + protected readonly Godot.Reference _delegator; + protected readonly object _current; + + protected AssertBase(Godot.Reference delegator, object current = null) + { + _delegator = delegator; + _current = current; + } + + public IAssertBase HasFailureMessage(string expected) + { + _delegator.Call("has_failure_message", expected); + return this; + } + + public IAssertBase IsEqual(V expected) + { + _delegator.Call("is_equal", expected); + return this; + } + + public IAssertBase IsNotEqual(V expected) + { + _delegator.Call("is_not_equal", expected); + return this; + } + + public IAssertBase IsNotNull() + { + _delegator.Call("is_not_null"); + return this; + } + + public IAssertBase IsNull() + { + _delegator.Call("is_null"); + return this; + } + + public IAssertBase OverrideFailureMessage(string message) + { + _delegator.Call("override_failure_message", message); + return this; + } + + public IAssertBase StartsWithFailureMessage(string value) + { + _delegator.Call("starts_with_failure_message"); + return this; + } + + public IAssertBase TestFail() + { + _delegator.Call("test_fail"); + return this; + } + } +} diff --git a/addons/gdUnit3/src/asserts/BoolAssert.cs b/addons/gdUnit3/src/asserts/BoolAssert.cs new file mode 100644 index 00000000..7b2a717c --- /dev/null +++ b/addons/gdUnit3/src/asserts/BoolAssert.cs @@ -0,0 +1,28 @@ +using Godot; + +namespace GdUnit3 +{ + public sealed class BoolAssert : AssertBase, IBoolAssert + { + private static Godot.GDScript GdUnitBoolAssertImpl = GD.Load("res://addons/gdUnit3/src/asserts/GdUnitBoolAssertImpl.gd"); + + public BoolAssert(object caller, object current, IAssert.EXPECT expectResult) + : base((Godot.Reference)GdUnitBoolAssertImpl.New(caller, current, expectResult)) + { + + } + + public IBoolAssert IsFalse() + { + _delegator.Call("is_false"); + return this; + } + + public IBoolAssert IsTrue() + { + _delegator.Call("is_true"); + return this; + } + + } +} diff --git a/addons/gdUnit3/src/asserts/DoubleAssert.cs b/addons/gdUnit3/src/asserts/DoubleAssert.cs new file mode 100644 index 00000000..8b6ae430 --- /dev/null +++ b/addons/gdUnit3/src/asserts/DoubleAssert.cs @@ -0,0 +1,13 @@ +using Godot; + +namespace GdUnit3 +{ + public sealed class DoubleAssert : NumberAssert, IDoubleAssert + { + private static Godot.GDScript AssertImpl = GD.Load("res://addons/gdUnit3/src/asserts/GdUnitFloatAssertImpl.gd"); + public DoubleAssert(object caller, object current, IAssert.EXPECT expectResult) + : base((Godot.Reference)AssertImpl.New(caller, current, expectResult), current) + { + } + } +} diff --git a/addons/gdUnit3/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit3/src/asserts/GdUnitAssertImpl.gd index ea2c9544..868705f2 100644 --- a/addons/gdUnit3/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit3/src/asserts/GdUnitAssertImpl.gd @@ -37,6 +37,7 @@ static func _get_line_number() -> int: func _init(caller :Object, current, expect_result :int = EXPECT_SUCCESS): assert(caller != null, "missing argument caller!") assert(caller.has_meta(GdUnitReportConsumer.META_PARAM), "caller must register a report consumer!") + _report_consumer = weakref(caller.get_meta(GdUnitReportConsumer.META_PARAM)) _current = current # we expect the test will fail diff --git a/addons/gdUnit3/src/asserts/IntAssert.cs b/addons/gdUnit3/src/asserts/IntAssert.cs new file mode 100644 index 00000000..c79eb852 --- /dev/null +++ b/addons/gdUnit3/src/asserts/IntAssert.cs @@ -0,0 +1,13 @@ +using Godot; + +namespace GdUnit3 +{ + public sealed class IntAssert : NumberAssert, IIntAssert + { + private static Godot.GDScript AssertImpl = GD.Load("res://addons/gdUnit3/src/asserts/GdUnitIntAssertImpl.gd"); + + public IntAssert(object caller, object current, IAssert.EXPECT expectResult) + : base((Godot.Reference)AssertImpl.New(caller, current, expectResult), current) + { } + } +} diff --git a/addons/gdUnit3/src/asserts/NumberAssert.cs b/addons/gdUnit3/src/asserts/NumberAssert.cs new file mode 100644 index 00000000..130eb393 --- /dev/null +++ b/addons/gdUnit3/src/asserts/NumberAssert.cs @@ -0,0 +1,88 @@ +using System; + +namespace GdUnit3 +{ + public class NumberAssert : AssertBase, INumberAssert + { + public NumberAssert(Godot.Reference delegator, object current) : base(delegator, current) + { } + + public INumberAssert IsBetween(V from, V to) + { + _delegator.Call("is_between", from, to); + return this; + } + + public INumberAssert IsEven() + { + _delegator.Call("is_even"); + return this; + } + + public INumberAssert IsGreater(V expected) + { + _delegator.Call("is_greater", expected); + return this; + } + + public INumberAssert IsGreaterEqual(V expected) + { + _delegator.Call("is_greater_equal", expected); + return this; + } + + public INumberAssert IsIn(Array expected) + { + _delegator.Call("is_in", expected); + return this; + } + + public INumberAssert IsLess(V expected) + { + _delegator.Call("is_less", expected); + return this; + } + + public INumberAssert IsLessEqual(V expected) + { + _delegator.Call("is_less_equal", expected); + return this; + } + + public INumberAssert IsNegative() + { + _delegator.Call("is_negative"); + return this; + } + + public INumberAssert IsNotIn(Array expected) + { + _delegator.Call("is_not_in", expected); + return this; + } + + public INumberAssert IsNotNegative() + { + _delegator.Call("is_not_negative"); + return this; + } + + public INumberAssert IsNotZero() + { + _delegator.Call("is_not_zero"); + return this; + } + + public INumberAssert IsOdd() + { + _delegator.Call("is_odd"); + return this; + } + + public INumberAssert IsZero() + { + _delegator.Call("is_zero"); + return this; + } + } +} diff --git a/addons/gdUnit3/src/asserts/ObjectAssert.cs b/addons/gdUnit3/src/asserts/ObjectAssert.cs new file mode 100644 index 00000000..93bd2f5b --- /dev/null +++ b/addons/gdUnit3/src/asserts/ObjectAssert.cs @@ -0,0 +1,80 @@ +using Godot; +using System; + +namespace GdUnit3 +{ + public sealed class ObjectAssert : AssertBase, IObjectAssert + { + private static Godot.GDScript AssertImpl = GD.Load("res://addons/gdUnit3/src/asserts/GdUnitObjectAssertImpl.gd"); + + private static Godot.GDScript GdAssertMessages = GD.Load("res://addons/gdUnit3/src/asserts/GdAssertMessages.gd"); + + private readonly Godot.Reference _messageBuilder; + + public ObjectAssert(object caller, object current, IAssert.EXPECT expectResult) + : base((Godot.Reference)AssertImpl.New(caller, current, expectResult), current) + { + _messageBuilder = GdAssertMessages.New() as Godot.Reference; + } + + + public IObjectAssert IsNotInstanceof() + { + if (_current is ExpectedType) + { + var message = String.Format("Expected not be a instance of <{0}>", typeof(ExpectedType)); + _delegator.Call("report_error", message); + return this; + } + _delegator.Call("report_success"); + return this; + } + + public IObjectAssert IsNotSame(object expected) + { + _delegator.Call("is_not_same", expected); + return this; + } + + public IObjectAssert IsSame(object expected) + { + _delegator.Call("is_same", expected); + return this; + } + + public IObjectAssert IsInstanceof() + { + if (!(_current is ExpectedType)) + { + var message = error_is_instanceof(_current != null ? _current.GetType() : null, typeof(ExpectedType)); + _delegator.Call("report_error", message); + return this; + } + _delegator.Call("report_success"); + return this; + } + + private String format_expected(string value) + { + return _messageBuilder.Call("_expected", value) as string; + } + + private String format_current(string value) + { + return _messageBuilder.Call("_current", value) as string; + } + + private String format_error(string value) + { + return _messageBuilder.Call("_error", value) as string; + } + + private string error_is_instanceof(Type current, Type expected) + { + return String.Format("{0}\n {1}\n But it was {2}", + format_error("Expected instance of:"), + format_expected(expected.ToString()), + format_current(current != null ? current.ToString() : "Null")); + } + } +} diff --git a/addons/gdUnit3/src/asserts/StringAssert.cs b/addons/gdUnit3/src/asserts/StringAssert.cs new file mode 100644 index 00000000..f0ac8c22 --- /dev/null +++ b/addons/gdUnit3/src/asserts/StringAssert.cs @@ -0,0 +1,80 @@ +using Godot; + +namespace GdUnit3 +{ + public sealed class StringAssert : AssertBase, IStringAssert + { + private static Godot.GDScript AssertImpl = GD.Load("res://addons/gdUnit3/src/asserts/GdUnitStringAssertImpl.gd"); + + public StringAssert(object caller, object current, IAssert.EXPECT expectResult) + : base((Godot.Reference)AssertImpl.New(caller, current, expectResult)) + { } + + public IStringAssert Contains(string expected) + { + _delegator.Call("contains", expected); + return this; + } + + public IStringAssert ContainsIgnoringCase(string expected) + { + _delegator.Call("contains_ignoring_case", expected); + return this; + } + + public IStringAssert EndsWith(string expected) + { + _delegator.Call("ends_with", expected); + return this; + } + + public IStringAssert HasLength(int lenght, IStringAssert.Compare comparator = IStringAssert.Compare.EQUAL) + { + _delegator.Call("has_length", lenght, comparator); + return this; + } + + public IStringAssert IsEmpty() + { + _delegator.Call("is_empty"); + return this; + } + + public IStringAssert IsEqualIgnoringCase(string expected) + { + _delegator.Call("is_equal_ignoring_case", expected); + return this; + } + + public IStringAssert IsNotEmpty() + { + _delegator.Call("is_not_empty"); + return this; + } + + public IStringAssert IsNotEqualIgnoringCase(string expected) + { + _delegator.Call("is_not_equal_ignoring_case", expected); + return this; + } + + public IStringAssert NotContains(string expected) + { + _delegator.Call("not_contains", expected); + return this; + } + + public IStringAssert NotContainsIgnoringCase(string expected) + { + _delegator.Call("not_contains_ignoring_case", expected); + return this; + } + + public IStringAssert StartsWith(string expected) + { + _delegator.Call("starts_with", expected); + return this; + } + + } +} diff --git a/addons/gdUnit3/src/core/CsTools.cs b/addons/gdUnit3/src/core/CsTools.cs new file mode 100644 index 00000000..1fdd6952 --- /dev/null +++ b/addons/gdUnit3/src/core/CsTools.cs @@ -0,0 +1,64 @@ +using Godot; +using System; +using System.Diagnostics.Contracts; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GdUnit3 +{ + public class CsTools : Reference + { + // used from GdScript side, will be remove later + public static int TestCaseCount(Type type) + { + Contract.Requires(Attribute.IsDefined(type, typeof(TestSuiteAttribute)), "The class must have TestSuiteAttribute."); + return type.GetMethods() + .Where(m => m.IsDefined(typeof(TestCaseAttribute))) + .Count(); + } + + public static IEnumerable GetTestCases(String className) + { + System.Type type = System.Type.GetType(className); + Contract.Requires(Attribute.IsDefined(type, typeof(TestSuiteAttribute)), "The class must have TestSuiteAttribute."); + return type.GetMethods() + .Where(m => m.IsDefined(typeof(TestCaseAttribute))) + .Select(mi => new TestCase(mi)) + .ToArray(); + } + + // used from GdScript side, will be remove later + public static bool IsTestSuite(String className) + { + System.Type type = System.Type.GetType(className); + if (type == null) + { + return false; + } + return Attribute.IsDefined(type, typeof(TestSuiteAttribute)); + } + + public static IEnumerable GetTestCases(Type type) + { + Contract.Requires(Attribute.IsDefined(type, typeof(TestSuiteAttribute)), "The class must have TestSuiteAttribute."); + return type.GetMethods() + .Where(m => m.IsDefined(typeof(TestCaseAttribute))) + .Select(mi => new TestCase(mi)); + } + + public static IEnumerable GetTestMethodParameters(MethodInfo methodInfo) + { + return methodInfo.GetParameters() + .SelectMany(pi => pi.GetCustomAttributesData() + .Where(attr => attr.AttributeType == typeof(FuzzerAttribute)) + .Select(attr => + { + var arguments = attr.ConstructorArguments.Select(arg => arg.Value).ToArray(); + return attr.Constructor.Invoke(arguments); + } + ) + ); + } + } +} diff --git a/addons/gdUnit3/src/core/GdObjects.gd b/addons/gdUnit3/src/core/GdObjects.gd index d5051def..d2f101a9 100644 --- a/addons/gdUnit3/src/core/GdObjects.gd +++ b/addons/gdUnit3/src/core/GdObjects.gd @@ -300,9 +300,14 @@ static func is_object(value) -> bool: static func is_script(value) -> bool: return is_object(value) and value is Script -static func is_testsuite(script :GDScript) -> bool: - if not script: - return false +static func is_test_suite(script :Script) -> bool: + if is_gd_script(script): + return _is_extends_test_suite(script) + if is_cs_script(script): + return _is_annotated_test_suite(script) + return false + +static func _is_extends_test_suite(script :Script) -> bool: var stack := [script] while not stack.empty(): var current := stack.pop_front() as Script @@ -313,6 +318,10 @@ static func is_testsuite(script :GDScript) -> bool: stack.push_back(base) return false +static func _is_annotated_test_suite(script :Script) -> bool: + var csTools = GdUnitSingleton.get_or_create_singleton("CsTools", "res://addons/gdUnit3/src/core/CsTools.cs") + return csTools.IsTestSuite(script.resource_path.get_file().replace(".cs", "")) + static func is_native_class(value) -> bool: return is_object(value) and value.to_string() != null and value.to_string().find("GDScriptNativeClass") != -1 @@ -322,6 +331,19 @@ static func is_scene(value) -> bool: static func is_scene_resource_path(value) -> bool: return value is String and value.ends_with(".tscn") +static func is_cs_script(script :Script) -> bool: + # we need to check by stringify name because on non mono Godot the class CSharpScript is not available + return str(script).find("CSharpScript") != -1 + +static func is_vs_script(script :Script) -> bool: + return script is VisualScript + +static func is_gd_script(script :Script) -> bool: + return script is GDScript + +static func is_native_script(script :Script) -> bool: + return script is NativeScript + static func is_instance(value) -> bool: if not is_object(value) or is_native_class(value): return false @@ -515,8 +537,8 @@ static func array_to_string(elements, delimiter := "\n") -> String: if formatted.length() > 0 : formatted += delimiter formatted += str(element) - if formatted.length() > 64: - return formatted + delimiter + "..." + #if formatted.length() > 64: + # return formatted + delimiter + "..." return formatted # Filters an array by given value diff --git a/addons/gdUnit3/src/core/GdUnitExecutor.gd b/addons/gdUnit3/src/core/GdUnitExecutor.gd index 2f695f78..d9b1d3c7 100644 --- a/addons/gdUnit3/src/core/GdUnitExecutor.gd +++ b/addons/gdUnit3/src/core/GdUnitExecutor.gd @@ -117,7 +117,7 @@ func suite_after(test_suite :GdUnitTestSuite) -> GDScriptFunctionState: _report_collector.clear_reports(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) return null -func test_before(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptFunctionState: +func test_before(test_suite :GdUnitTestSuite, test_case :Node) -> GDScriptFunctionState: set_stage(STAGE_TEST_CASE_BEFORE) _memory_pool.set_pool(test_suite, GdUnitMemoryPool.TEST_SETUP, true) @@ -135,7 +135,7 @@ func test_before(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptF GdUnitTools.run_auto_close() return null -func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptFunctionState: +func test_after(test_suite :GdUnitTestSuite, test_case :Node) -> GDScriptFunctionState: set_stage(STAGE_TEST_CASE_AFTER) _memory_pool.set_pool(test_suite, GdUnitMemoryPool.TEST_SETUP) @@ -153,7 +153,7 @@ func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptFu .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(test_setup_orphan_nodes))) var reports := _report_collector.get_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - var is_error := test_case.is_interupted() and not test_case.is_expect_interupted() + var is_error :bool = test_case.is_interupted() and not test_case.is_expect_interupted() var error_count := _report_collector.count_errors(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) var failure_count := _report_collector.count_failures(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) var is_warning := _report_collector.has_warnings(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) @@ -177,7 +177,7 @@ func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptFu _report_collector.clear_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) return null -func execute_test_case(test_suite :GdUnitTestSuite, test_case :_TestCase) -> GDScriptFunctionState: +func execute_test_case(test_suite :GdUnitTestSuite, test_case :Node) -> GDScriptFunctionState: _test_run_state = test_before(test_suite, test_case) if GdUnitTools.is_yielded(_test_run_state): yield(_test_run_state, "completed") @@ -243,6 +243,7 @@ func execute(test_suite :GdUnitTestSuite) -> GDScriptFunctionState: _report_collector.register_report_provider(test_suite) add_child(test_suite) + var fs = suite_before(test_suite, test_suite.get_child_count()) if GdUnitTools.is_yielded(fs): yield(fs, "completed") @@ -251,7 +252,7 @@ func execute(test_suite :GdUnitTestSuite) -> GDScriptFunctionState: for test_case_index in test_suite.get_child_count(): var test_case = test_suite.get_child(test_case_index) # only iterate over test case, we need to filter because of possible adding other child types on before() or before_test() - if not test_case is _TestCase: + if not test_case is _TestCase and not GdObjects.is_cs_script(test_case.get_script()): continue # stop on first error if fail fast enabled if _fail_fast and _total_test_failed > 0: @@ -281,7 +282,7 @@ func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: for property in test_suite.get_property_list(): var property_name = property["name"] _test_suite.set(property_name, test_suite.get(property_name)) - + # remove incomplete duplicated childs for child in _test_suite.get_children(): _test_suite.remove_child(child) diff --git a/addons/gdUnit3/src/core/GdUnitRunner.gd b/addons/gdUnit3/src/core/GdUnitRunner.gd index 7fd93f80..44c1d46e 100644 --- a/addons/gdUnit3/src/core/GdUnitRunner.gd +++ b/addons/gdUnit3/src/core/GdUnitRunner.gd @@ -7,7 +7,6 @@ signal sync_rpc_id_result_received onready var _client :GdUnitTcpClient = $GdUnitTcpClient onready var _executor :GdUnitExecutor = $GdUnitExecutor - enum { INIT, RUN, @@ -18,10 +17,12 @@ var _config := GdUnitRunnerConfig.new() var _test_suites_to_process :Array var _state = INIT var _signal_handler :SignalHandler +var _cs_executor # holds the received sync rpc result var _result :Result + func _init(): # minimize scene window on debug mode if OS.get_cmdline_args().size() == 1: @@ -30,6 +31,9 @@ func _init(): _signal_handler = GdUnitSingleton.get_or_create_singleton(SignalHandler.SINGLETON_NAME, "res://addons/gdUnit3/src/core/event/SignalHandler.gd") # store current runner instance to engine meta data to can be access in as a singleton Engine.set_meta(GDUNIT_RUNNER, self) + if GdUnitTools.is_mono_supported(): + _cs_executor = load("res://addons/gdUnit3/src/core/execution/Executor.cs").new() + _cs_executor.AddGdTestEventListener(self) func _ready(): _config.load() @@ -58,13 +62,16 @@ func _process(delta): _state = STOP else: # process next test suite - var test_suite := _test_suites_to_process.pop_front() as GdUnitTestSuite - var fs = _executor.execute(test_suite) - # is yielded than wait for completed - if GdUnitTools.is_yielded(fs): - set_process(false) - yield(fs, "completed") - set_process(true) + var test_suite = _test_suites_to_process.pop_front() + if GdObjects.is_cs_script(test_suite.get_script()): + _cs_executor.execute(test_suite) + else: + var fs = _executor.execute(test_suite) + # is yielded than wait for completed + if GdUnitTools.is_yielded(fs): + set_process(false) + yield(fs, "completed") + set_process(true) STOP: _state = EXIT # give the engine small amount time to finish the rpc @@ -99,16 +106,15 @@ func gdUnitInit() -> void: send_message("Scaned %d test suites" % _test_suites_to_process.size()) var total_count = _collect_test_case_count(_test_suites_to_process) _on_Executor_send_event(GdUnitInit.new(_test_suites_to_process.size(), total_count)) - for t in _test_suites_to_process: - var test_suite := t as GdUnitTestSuite + for test_suite in _test_suites_to_process: send_test_suite(test_suite) -func _filter_test_case(test_suites :Array, test_case_names :Array) -> void: - if test_case_names.empty(): +func _filter_test_case(test_suites :Array, includes_tests :Array) -> void: + if includes_tests.empty(): return for test_suite in test_suites: for test_case in test_suite.get_children(): - if not test_case_names.has(test_case.get_name()): + if not includes_tests.has(test_case.get_name()): test_suite.remove_child(test_case) test_case.free() @@ -122,12 +128,16 @@ func _collect_test_case_count(testSuites :Array) -> int: func send_message(message :String): _client.rpc_send(RPCMessage.of(message)) -func send_test_suite(test_suite :GdUnitTestSuite): +func send_test_suite(test_suite): _client.rpc_send(RPCGdUnitTestSuite.of(test_suite)) func _on_Executor_send_event(event :GdUnitEvent): _client.rpc_send(RPCGdUnitEvent.of(event)) +func PublishEvent(data) -> void: + var event := GdUnitEvent.new().deserialize(data.AsDictionary()) + _client.rpc_send(RPCGdUnitEvent.of(event)) + #func get_last_push_error() -> Result: # return yield(sync_rpc_id(1, "GdUnitPushErrorHandler:get_last_error"), "completed") diff --git a/addons/gdUnit3/src/core/GdUnitScriptType.gd b/addons/gdUnit3/src/core/GdUnitScriptType.gd new file mode 100644 index 00000000..0a352a0a --- /dev/null +++ b/addons/gdUnit3/src/core/GdUnitScriptType.gd @@ -0,0 +1,21 @@ +class_name GdUnitScriptType +extends Reference + +const UNKNOWN := "" +const CS := "cs" +const GD := "gd" +const NATIVE := "gdns" +const VS := "vs" + +static func type_of(script :Script) -> String: + if script == null: + return UNKNOWN + if GdObjects.is_gd_script(script): + return GD + if GdObjects.is_vs_script(script): + return VS + if GdObjects.is_native_script(script): + return NATIVE + if GdObjects.is_cs_script(script): + return CS + return UNKNOWN diff --git a/addons/gdUnit3/src/core/GdUnitSingleton.gd b/addons/gdUnit3/src/core/GdUnitSingleton.gd index 9e4e7c3a..be8aa146 100644 --- a/addons/gdUnit3/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit3/src/core/GdUnitSingleton.gd @@ -17,9 +17,10 @@ static func get_singleton(name: String) -> Object: static func add_singleton(name: String, path: String) -> Object: var singleton:Object = load(path).new() - singleton.set_name(name) + if singleton.has_method("set_name"): + singleton.set_name(name) _singletons[name] = singleton - #print_debug("Added singleton", name, singleton) + #print_debug("Added singleton ", name, " ",singleton) return singleton static func get_or_create_singleton(name: String, path: String) -> Object: diff --git a/addons/gdUnit3/src/core/GdUnitTools.gd b/addons/gdUnit3/src/core/GdUnitTools.gd index 840054d1..a22ad563 100644 --- a/addons/gdUnit3/src/core/GdUnitTools.gd +++ b/addons/gdUnit3/src/core/GdUnitTools.gd @@ -345,6 +345,10 @@ static func is_auto_free_registered(obj, pool :int) -> bool: static func is_yielded(obj) -> bool: return obj is GDScriptFunctionState and obj.is_valid() +# test is Godot mono running +static func is_mono_supported() -> bool: + return ClassDB.class_exists("CSharpScript") + # runs over all registered files and closes it static func run_auto_close(): while not _files_to_close.empty(): @@ -376,6 +380,8 @@ static func clear_push_errors() -> void: runner.clear_push_errors() static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: + prints(test_suite.get_children()) + var test_case = test_suite.find_node(test_case_name, false, false) test_case.expect_to_interupt() diff --git a/addons/gdUnit3/src/core/_TestSuiteScanner.gd b/addons/gdUnit3/src/core/_TestSuiteScanner.gd index 3467ff9a..4167e9cb 100644 --- a/addons/gdUnit3/src/core/_TestSuiteScanner.gd +++ b/addons/gdUnit3/src/core/_TestSuiteScanner.gd @@ -5,7 +5,6 @@ extends Node var _script_parser := GdScriptParser.new() var _extends_test_suite_classes := Array() - func scan_testsuite_classes() -> void: # scan and cache extends GdUnitTestSuite by class name an resource paths _extends_test_suite_classes.append("GdUnitTestSuite") @@ -17,11 +16,13 @@ func scan_testsuite_classes() -> void: _extends_test_suite_classes.append(script_meta["class"]) func scan(resource_path :String) -> Array: + scan_testsuite_classes() var base_dir := Directory.new() # if single testsuite requested if base_dir.file_exists(resource_path): - if resource_path.ends_with(".gd") and _is_test_suite(resource_path): - return [_parse_test_suite(resource_path)] + var test_suite := _parse_is_test_suite(resource_path) + if test_suite: + return [test_suite] if base_dir.open(resource_path) != OK: prints("Given directory or file does not exists:", resource_path) @@ -33,45 +34,78 @@ func _scan_test_suites(dir :Directory, collected_suites :Array) -> Array: dir.list_dir_begin(true, true) var file_name := dir.get_next() while file_name != "": - var current = dir.get_current_dir() + "/" + file_name + var resource_path = _file(dir, file_name) if dir.current_is_dir(): var sub_dir := Directory.new() - if sub_dir.open(current) == OK: + if sub_dir.open(resource_path) == OK: _scan_test_suites(sub_dir, collected_suites) else: - if _is_test_suite(current): - collected_suites.append(_parse_test_suite(current)) + var test_suite := _parse_is_test_suite(resource_path) + if test_suite: + collected_suites.append(test_suite) file_name = dir.get_next() return collected_suites -func _is_test_suite(file_name :String) -> bool: - # only scan on gd scrip files - if not file_name.ends_with(".gd"): - return false +static func _file(dir :Directory, file_name :String) -> String: + var current_dir := dir.get_current_dir() + if current_dir.ends_with("/"): + return current_dir + file_name + return current_dir + "/" + file_name + +func _parse_is_test_suite(resource_path :String) -> Node: + if not _is_script_format_supported(resource_path): + return null # exclude non test directories - if file_name.find("/test") == -1: - return false - return GdObjects.is_testsuite(ResourceLoader.load(file_name)) + if resource_path.find("/test") == -1: + return null + var script :Script = ResourceLoader.load(resource_path) + if not GdObjects.is_test_suite(script): + return null + if GdObjects.is_gd_script(script): + return _parse_test_suite(script) + if GdObjects.is_cs_script(script): + return _parse_cs_test_suite(script) + return null + +static func _is_script_format_supported(resource_path :String) -> bool: + var ext := resource_path.get_extension() + if ext == "gd": + return true + if ext == "cs" and GdUnitTools.is_mono_supported(): + return true + return false + +func _parse_cs_test_suite(script :Script) -> Node: + var test_suite = script.new() + test_suite.set_name(parse_test_suite_name(script)) + var cs_tools = load("res://addons/gdUnit3/src/core/CsTools.cs").new() + for test_case in cs_tools.GetTestCases(script.resource_path.get_file().replace(".cs", "")): + var test := _TestCase.new() + var attributes :Dictionary = test_case.attributes(); + test.configure(attributes.get("name"), attributes.get("line_number"), script.resource_path) + test_suite.add_child(test) + return test_suite + -func _parse_test_suite(resource_path :String) -> GdUnitTestSuite: - var test_suite := load(resource_path).new() as GdUnitTestSuite - test_suite.set_name(parse_test_suite_name(resource_path)) +func _parse_test_suite(script :GDScript) -> GdUnitTestSuite: + var test_suite = script.new() + test_suite.set_name(parse_test_suite_name(script)) # find all test cases as array of names - var test_case_names := _extract_test_case_names(test_suite) + var test_case_names := _extract_test_case_names(script) # add test cases to test suite and parse test case line nummber - _parse_and_add_test_cases(test_suite, resource_path, test_case_names) + _parse_and_add_test_cases(test_suite, script, test_case_names) # not all test case parsed? # we have to scan the base class to if not test_case_names.empty(): var base_script :GDScript = test_suite.get_script().get_base_script() while base_script is GDScript: - _parse_and_add_test_cases(test_suite, base_script.resource_path, test_case_names) + _parse_and_add_test_cases(test_suite, base_script, test_case_names) base_script = base_script.get_base_script() return test_suite -func _extract_test_case_names(test_suite :GdUnitTestSuite) -> PoolStringArray: +func _extract_test_case_names(script :GDScript) -> PoolStringArray: var names := PoolStringArray() - for method in test_suite.get_script().get_script_method_list(): + for method in script.get_script_method_list(): #prints(method["flags"], method["name"] ) var flags :int = method["flags"] var funcName :String = method["name"] @@ -79,15 +113,13 @@ func _extract_test_case_names(test_suite :GdUnitTestSuite) -> PoolStringArray: names.append(funcName) return names -static func parse_test_suite_name(resource_path :String) -> String: - var start := resource_path.find_last("/") - var end := resource_path.find_last(".gd") - return resource_path.substr(start, end-start) +static func parse_test_suite_name(script :Script) -> String: + return script.resource_path.get_file().replace(".gd", "").replace(".cs", "") -func _parse_and_add_test_cases(test_suite :GdUnitTestSuite, resource_path :String, test_case_names :PoolStringArray): +func _parse_and_add_test_cases(test_suite, script :GDScript, test_case_names :PoolStringArray): var test_cases_to_find = Array(test_case_names) var file := File.new() - file.open(resource_path, File.READ) + file.open(script.resource_path, File.READ) var line_number:int = 0 file.seek(0) @@ -108,7 +140,7 @@ func _parse_and_add_test_cases(test_suite :GdUnitTestSuite, resource_path :Strin var iterations = _script_parser.parse_argument(row, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ITERATION_DEFAULT_COUNT) var seed_value = _script_parser.parse_argument(row, Fuzzer.ARGUMENT_SEED, -1) var fuzzers := _script_parser.parse_fuzzers(row) - test_suite.add_child(_TestCase.new().configure(func_name, line_number, resource_path, timeout, fuzzers, iterations, seed_value)) + test_suite.add_child(_TestCase.new().configure(func_name, line_number, script.resource_path, timeout, fuzzers, iterations, seed_value)) file.close() diff --git a/addons/gdUnit3/src/core/attributes/FuzzerAttribute.cs b/addons/gdUnit3/src/core/attributes/FuzzerAttribute.cs new file mode 100644 index 00000000..b62a5239 --- /dev/null +++ b/addons/gdUnit3/src/core/attributes/FuzzerAttribute.cs @@ -0,0 +1,23 @@ + +using System; +using System.Collections.Generic; + +namespace GdUnit3 +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public class FuzzerAttribute : System.Attribute, IValueProvider + { + + public int _value; + public FuzzerAttribute(int value) + { + _value = value; + } + + public IEnumerable GetValues() + { + _value += 1; + yield return _value; + } + } +} diff --git a/addons/gdUnit3/src/core/attributes/TestCaseAttributes.cs b/addons/gdUnit3/src/core/attributes/TestCaseAttributes.cs new file mode 100644 index 00000000..38faec9a --- /dev/null +++ b/addons/gdUnit3/src/core/attributes/TestCaseAttributes.cs @@ -0,0 +1,45 @@ +using System; + +namespace GdUnit3 +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class TestCaseAttribute : Attribute + { + public int timeout = -1; + + public double seed = 1.0; + + public int iterations = 1; + + public readonly int line; + + public TestCaseAttribute([System.Runtime.CompilerServices.CallerLineNumber] int line = 0) + { + this.line = line; + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class BeforeTestAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class AfterTestAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class IgnoreUntilAttribute : Attribute + { + + public string Description + { get; set; } + + public IgnoreUntilAttribute() { } + public IgnoreUntilAttribute(string description) + { + Description = description; + } + } +} diff --git a/addons/gdUnit3/src/core/attributes/TestSuiteAttribute.cs b/addons/gdUnit3/src/core/attributes/TestSuiteAttribute.cs new file mode 100644 index 00000000..23511e63 --- /dev/null +++ b/addons/gdUnit3/src/core/attributes/TestSuiteAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace GdUnit3 +{ + [AttributeUsage(AttributeTargets.Class)] + public class TestSuiteAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class BeforeAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class AfterAttribute : Attribute + { + } +} diff --git a/addons/gdUnit3/src/core/data/IValueProvider.cs b/addons/gdUnit3/src/core/data/IValueProvider.cs new file mode 100644 index 00000000..36652ce4 --- /dev/null +++ b/addons/gdUnit3/src/core/data/IValueProvider.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; + +namespace GdUnit3 +{ + + public interface IValueProvider + { + + public IEnumerable GetValues(); + } +} diff --git a/addons/gdUnit3/src/core/data/TestCase.cs b/addons/gdUnit3/src/core/data/TestCase.cs new file mode 100644 index 00000000..e037b6e3 --- /dev/null +++ b/addons/gdUnit3/src/core/data/TestCase.cs @@ -0,0 +1,60 @@ +using Godot.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System; + +namespace GdUnit3 +{ + public sealed class TestCase : Godot.Reference, IExecutionStage + { + public TestCase(MethodInfo methodInfo) + { + this.Name = methodInfo.Name; + this.MethodInfo = methodInfo; + this.Parameters = CsTools.GetTestMethodParameters(methodInfo).ToArray(); + } + + public string Name + { get; set; } + + public string StageName() => "TestCase"; + + public TestCaseAttribute Attributes + { get => MethodInfo.GetCustomAttribute(); } + + public bool Skipped => Attribute.IsDefined(MethodInfo, typeof(IgnoreUntilAttribute)); + + public Godot.Collections.Dictionary attributes() + { + var attributes = Attributes; + return new Dictionary { + { "name", Name }, + { "line_number", attributes.line }, + { "timeout", attributes.timeout }, + { "iterations", attributes.iterations }, + { "seed", attributes.seed }, + }; + } + private IEnumerable Parameters + { get; set; } + + private MethodInfo MethodInfo + { get; set; } + + private IEnumerable ResolveParam(object input) + { + if (input is IValueProvider) + { + return (input as IValueProvider).GetValues(); + } + return new object[] { input }; + } + + public void Execute(ExecutionContext context) + { + object[] arguments = Parameters.SelectMany(ResolveParam).ToArray(); + MethodInfo.Invoke(context.TestInstance, arguments); + } + } +} diff --git a/addons/gdUnit3/src/core/event/GdUnitEvent.gd b/addons/gdUnit3/src/core/event/GdUnitEvent.gd index 1ee8a81f..1874a36c 100644 --- a/addons/gdUnit3/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit3/src/core/event/GdUnitEvent.gd @@ -128,14 +128,14 @@ func serialize() -> Dictionary: serialized["reports"] = _serialize_TestReports() return serialized -func deserialize(serialized:Dictionary) -> GdUnitEvent: - _event_type = serialized["type"] - _resource_path = serialized["resource_path"] - _suite_name = serialized["suite_name"] - _test_name = serialized["test_name"] - _total_count = serialized["total_count"] - _statistics = serialized["statistics"] - _reports = _deserialize_reports(serialized["reports"]) +func deserialize(serialized :Dictionary) -> GdUnitEvent: + _event_type = serialized.get("type", null) + _resource_path = serialized.get("resource_path", null) + _suite_name = serialized.get("suite_name", null) + _test_name = serialized.get("test_name", "unknown") + _total_count = serialized.get("total_count", 0) + _statistics = serialized.get("statistics", Dictionary()) + _reports = _deserialize_reports(serialized.get("reports",[])) return self func _serialize_TestReports() -> Array: @@ -144,7 +144,7 @@ func _serialize_TestReports() -> Array: serialized_reports.append(report.serialize()) return serialized_reports -func _deserialize_reports(reports:Array) -> Array: +func _deserialize_reports(reports :Array) -> Array: var deserialized_reports := Array() for report in reports: var test_report := GdUnitReport.new().deserialize(report) diff --git a/addons/gdUnit3/src/core/event/TestEvent.cs b/addons/gdUnit3/src/core/event/TestEvent.cs new file mode 100644 index 00000000..2c4174ab --- /dev/null +++ b/addons/gdUnit3/src/core/event/TestEvent.cs @@ -0,0 +1,95 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace GdUnit3 +{ + + public class TestEvent : Godot.Reference + { + + enum TYPE + { + INIT, + STOP, + TESTSUITE_BEFORE, + TESTSUITE_AFTER, + TESTCASE_BEFORE, + TESTCASE_AFTER, + } + const string WARNINGS = "warnings"; + const string FAILED = "failed"; + const string ERRORS = "errors"; + const string SKIPPED = "skipped"; + const string ELAPSED_TIME = "elapsed_time"; + const string ORPHAN_NODES = "orphan_nodes"; + const string ERROR_COUNT = "error_count"; + const string FAILED_COUNT = "failed_count"; + const string SKIPPED_COUNT = "skipped_count"; + + private IDictionary _data = new Dictionary(); +#nullable enable + private TestEvent(TYPE type, string resourcePath, string suiteName, string testName, int totalCount = 0, IEnumerable? statistics = null, IEnumerable? reports = null) + { + _data.Add("type", type); + _data.Add("resource_path", resourcePath); + _data.Add("suite_name", suiteName); + _data.Add("test_name", testName); + _data.Add("total_count", totalCount); + //var _statistics = statistics ?? Enumerable.Empty(); + if (statistics != null) + { + _data.Add("statistics", statistics); + } + if (reports != null) + { + var serializedReports = reports.Select(report => report.Serialize()).ToArray(); + _data.Add("reports", new Godot.Collections.Array(serializedReports)); + } + } + + public static TestEvent Before(string resourcePath, string suiteName, int totalCount) + { + return new TestEvent(TYPE.TESTSUITE_BEFORE, resourcePath, suiteName, "", totalCount); + } + + public static TestEvent After(string resourcePath, string suiteName, IEnumerable statistics) + { + return new TestEvent(TYPE.TESTSUITE_AFTER, resourcePath, suiteName, "", 0, statistics); + } + + public static TestEvent BeforeTest(string resourcePath, string suiteName, string testName) + { + return new TestEvent(TYPE.TESTCASE_BEFORE, resourcePath, suiteName, testName); + } + public static TestEvent AfterTest(string resourcePath, string suiteName, string testName, IEnumerable? statistics = null, IEnumerable? reports = null) + { + return new TestEvent(TYPE.TESTCASE_AFTER, resourcePath, suiteName, testName, 0, statistics, reports); + } +#nullable disable + + public static IDictionary BuildStatistics(int orphan_count, + bool isError, int error_count, + bool isFailure, int failure_count, + bool is_warning, + bool is_skipped, int skippedCount, + int elapsed_since_ms) + { + return new Dictionary() { + { ORPHAN_NODES, orphan_count}, + { ELAPSED_TIME, elapsed_since_ms}, + { WARNINGS, is_warning}, + { ERRORS, isError}, + { ERROR_COUNT, error_count}, + { FAILED, isFailure}, + { FAILED_COUNT, failure_count}, + { SKIPPED, is_skipped}, + { SKIPPED_COUNT, skippedCount}}; + } + + public System.Collections.IDictionary AsDictionary() + { + return _data; + } + } +} diff --git a/addons/gdUnit3/src/core/event/TestEventListener.cs b/addons/gdUnit3/src/core/event/TestEventListener.cs new file mode 100644 index 00000000..0e02d88d --- /dev/null +++ b/addons/gdUnit3/src/core/event/TestEventListener.cs @@ -0,0 +1,9 @@ + +namespace GdUnit3 +{ + + public interface ITestEventListener + { + void PublishEvent(TestEvent testEvent); + } +} diff --git a/addons/gdUnit3/src/core/execution/AfterExecutionStage.cs b/addons/gdUnit3/src/core/execution/AfterExecutionStage.cs new file mode 100644 index 00000000..0c5c0747 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/AfterExecutionStage.cs @@ -0,0 +1,18 @@ +using System; + +namespace GdUnit3 +{ + public class AfterExecutionStage : ExecutionStage + { + public AfterExecutionStage(Type type) : base("After", type) + { } + + public override void Execute(ExecutionContext context) + { + context.OrphanMonitor.Start(); + base.Execute(context); + context.OrphanMonitor.Stop(); + context.FireAfterEvent(); + } + } +} diff --git a/addons/gdUnit3/src/core/execution/AfterTestExecutionStage.cs b/addons/gdUnit3/src/core/execution/AfterTestExecutionStage.cs new file mode 100644 index 00000000..f783a77d --- /dev/null +++ b/addons/gdUnit3/src/core/execution/AfterTestExecutionStage.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; + +namespace GdUnit3 +{ + public class AfterTestExecutionStage : ExecutionStage + { + public AfterTestExecutionStage(Type type) : base("AfterTest", type) + { } + + public override void Execute(ExecutionContext context) + { + if (!context.IsSkipped()) + { + context.OrphanMonitor.Start(); + base.Execute(context); + context.OrphanMonitor.Stop(); + } + context.FireAfterTestEvent(); + } + } +} diff --git a/addons/gdUnit3/src/core/execution/BeforeExecutionStage.cs b/addons/gdUnit3/src/core/execution/BeforeExecutionStage.cs new file mode 100644 index 00000000..53f84c74 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/BeforeExecutionStage.cs @@ -0,0 +1,18 @@ +using System; + +namespace GdUnit3 +{ + public class BeforeExecutionStage : ExecutionStage + { + public BeforeExecutionStage(Type type) : base("Before", type) + { } + + public override void Execute(ExecutionContext context) + { + context.FireBeforeEvent(); + context.OrphanMonitor.Start(true); + base.Execute(context); + context.OrphanMonitor.Stop(); + } + } +} diff --git a/addons/gdUnit3/src/core/execution/BeforeTestExecutionStage.cs b/addons/gdUnit3/src/core/execution/BeforeTestExecutionStage.cs new file mode 100644 index 00000000..c4e7c33a --- /dev/null +++ b/addons/gdUnit3/src/core/execution/BeforeTestExecutionStage.cs @@ -0,0 +1,21 @@ +using System; + +namespace GdUnit3 +{ + public class BeforeTestExecutionStage : ExecutionStage + { + public BeforeTestExecutionStage(Type type) : base("BeforeTest", type) + { } + + public override void Execute(ExecutionContext context) + { + context.FireBeforeTestEvent(); + if (!context.IsSkipped()) + { + context.OrphanMonitor.Start(true); + base.Execute(context); + context.OrphanMonitor.Stop(); + } + } + } +} diff --git a/addons/gdUnit3/src/core/execution/ExecutionContext.cs b/addons/gdUnit3/src/core/execution/ExecutionContext.cs new file mode 100644 index 00000000..c453291f --- /dev/null +++ b/addons/gdUnit3/src/core/execution/ExecutionContext.cs @@ -0,0 +1,153 @@ +using System; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace GdUnit3 +{ + public sealed class ExecutionContext : IDisposable + { + public ExecutionContext(TestSuite testInstance, IEnumerable eventListeners) + { + OrphanMonitor = new OrphanNodesMonitor(); + Stopwatch = new Stopwatch(); + Stopwatch.Start(); + + TestInstance = testInstance; + EventListeners = eventListeners; + ReportCollector = new TestReportCollector(); + SubExecutionContexts = new List(); + // fake report consumer for now, will be replaced by TestEvent listener + testInstance.SetMeta("gdunit.report.consumer", ReportCollector); + } + public ExecutionContext(ExecutionContext context) : this(context.TestInstance, context.EventListeners) + { + context.SubExecutionContexts.Add(this); + Test = context.Test ?? null; + Skipped = Test?.Skipped ?? false; + CurrentIteration = Test?.Attributes.iterations ?? 0; + } + + public ExecutionContext(ExecutionContext context, TestCase testCase) : this(context.TestInstance, context.EventListeners) + { + context.SubExecutionContexts.Add(this); + Test = testCase; + CurrentIteration = testCase.Attributes.iterations; + Skipped = Test.Skipped; + } + + public OrphanNodesMonitor OrphanMonitor + { get; set; } + + public Stopwatch Stopwatch + { get; private set; } + + public TestSuite TestInstance + { get; private set; } + + private IEnumerable EventListeners + { get; set; } + +#nullable enable + private List SubExecutionContexts + { get; set; } +#nullable disable + + public TestCase Test + { get; set; } + + private bool Skipped + { get; set; } + + private int Duration + { get => (int)Stopwatch.ElapsedMilliseconds; } + + private int _iteration; + public int CurrentIteration + { + get => _iteration--; + set => _iteration = value; + } + + public TestReportCollector ReportCollector + { get; private set; } + + + public bool IsFailed() + { + return ReportCollector.Failures.Count() > 0 || SubExecutionContexts.Where(context => context.IsFailed()).Count() != 0; + } + + public bool IsError() + { + return ReportCollector.Errors.Count() > 0 || SubExecutionContexts.Where(context => context.IsError()).Count() != 0; + } + + public bool IsWarning() + { + return ReportCollector.Warnings.Count() > 0 || SubExecutionContexts.Where(context => context.IsWarning()).Count() != 0; + } + + public bool IsSkipped() => Skipped; + + private int SkippedCount() => SubExecutionContexts.Where(context => context.IsSkipped()).Count(); + + private int FailureCount() => ReportCollector.Failures.Count(); + + private int ErrorCount() => ReportCollector.Errors.Count(); + + public int OrphanCount() => SubExecutionContexts.Select(context => context.OrphanMonitor.OrphanCount()).Sum(); + + public IEnumerable BuildStatistics() + { + return TestEvent.BuildStatistics( + OrphanCount(), + IsError(), ErrorCount(), + IsFailed(), FailureCount(), + false, + IsSkipped(), SkippedCount(), + Duration); + } + + public void FireTestEvent(TestEvent e) + { + EventListeners.ToList() + .ForEach(l => l.PublishEvent(e)); + } + + public void FireBeforeEvent() + { + FireTestEvent(TestEvent.Before(TestInstance.ResourcePath, TestInstance.Name, TestInstance.TestCaseCount)); + } + + public void FireAfterEvent() + { + FireTestEvent(TestEvent.After(TestInstance.ResourcePath, TestInstance.Name, BuildStatistics())); + } + + public void FireBeforeTestEvent() + { + FireTestEvent(TestEvent.BeforeTest(TestInstance.ResourcePath, TestInstance.Name, Test.Name)); + } + + public void FireAfterTestEvent() + { + var testEvent = TestEvent.AfterTest(TestInstance.ResourcePath, TestInstance.Name, Test.Name, BuildStatistics(), ReportCollector.Reports); + FireTestEvent(testEvent); + } + + public void Dispose() + { + ReportCollector.Clear(); + SubExecutionContexts.Clear(); + Stopwatch.Stop(); + } + + public void PrintDebug(string name = "") + { + Godot.GD.PrintS(name, "test context", TestInstance.Name, Test?.Name, "error:" + IsError(), "failed:" + IsFailed(), "skipped:" + IsSkipped()); + } + } + +} diff --git a/addons/gdUnit3/src/core/execution/ExecutionStage.cs b/addons/gdUnit3/src/core/execution/ExecutionStage.cs new file mode 100644 index 00000000..1a5fb0a0 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/ExecutionStage.cs @@ -0,0 +1,29 @@ + +using System; +using System.Linq; +using System.Reflection; + +namespace GdUnit3 +{ + public abstract class ExecutionStage : IExecutionStage + { + private readonly string _name; +#nullable enable + private readonly MethodInfo? _mi; +#nullable disable + protected ExecutionStage(string name, Type type) + { + _name = name; + _mi = type + .GetMethods() + .FirstOrDefault(m => m.IsDefined(typeof(T))); + } + + public virtual void Execute(ExecutionContext context) + { + _mi?.Invoke(context.TestInstance, new object[] { }); + } + + public string StageName() => _name; + } +} diff --git a/addons/gdUnit3/src/core/execution/Executor.cs b/addons/gdUnit3/src/core/execution/Executor.cs new file mode 100644 index 00000000..512fc457 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/Executor.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace GdUnit3 +{ + public sealed class Executor : Godot.Reference + { + + private List _eventListeners = new List(); + + private class GdTestEventListenerDelegator : ITestEventListener + { + private readonly Godot.Object _listener; + + public GdTestEventListenerDelegator(Godot.Object listener) + { + _listener = listener; + } + public void PublishEvent(TestEvent testEvent) => _listener.Call("PublishEvent", testEvent); + } + public void AddGdTestEventListener(Godot.Object listener) + { + // I want to using anonymus implementation to remove the extra delegator class + _eventListeners.Add(new GdTestEventListenerDelegator(listener)); + } + + public void AddTestEventListener(ITestEventListener listener) + { + _eventListeners.Add(listener); + } + + public void execute(TestSuite testSuite) + { + var stage = new TestSuiteExecutionStage(testSuite.GetType()); + stage.Execute(new ExecutionContext(testSuite, _eventListeners)); + testSuite.Free(); + } + } +} diff --git a/addons/gdUnit3/src/core/execution/IExecutionStage.cs b/addons/gdUnit3/src/core/execution/IExecutionStage.cs new file mode 100644 index 00000000..57f318a4 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/IExecutionStage.cs @@ -0,0 +1,11 @@ +namespace GdUnit3 +{ + public interface IExecutionStage + { + + public string StageName(); + + + public void Execute(ExecutionContext context); + } +} diff --git a/addons/gdUnit3/src/core/execution/TestCaseExecutionStage.cs b/addons/gdUnit3/src/core/execution/TestCaseExecutionStage.cs new file mode 100644 index 00000000..bf582dee --- /dev/null +++ b/addons/gdUnit3/src/core/execution/TestCaseExecutionStage.cs @@ -0,0 +1,36 @@ +using System; + +namespace GdUnit3 +{ + public sealed class TestCaseExecutionStage : IExecutionStage + { + public TestCaseExecutionStage(Type type) + { + BeforeTestStage = new BeforeTestExecutionStage(type); + AfterTestStage = new AfterTestExecutionStage(type); + } + + public string StageName() => "TestCases"; + + private IExecutionStage BeforeTestStage + { get; set; } + private IExecutionStage AfterTestStage + { get; set; } + + + public void Execute(ExecutionContext context) + { + BeforeTestStage.Execute(context); + using (ExecutionContext currentContext = new ExecutionContext(context)) + { + currentContext.OrphanMonitor.Start(true); + while (!currentContext.IsSkipped() && currentContext.CurrentIteration != 0) + { + currentContext.Test.Execute(currentContext); + } + currentContext.OrphanMonitor.Stop(); + } + AfterTestStage.Execute(context); + } + } +} diff --git a/addons/gdUnit3/src/core/execution/TestSuiteExecutionStage.cs b/addons/gdUnit3/src/core/execution/TestSuiteExecutionStage.cs new file mode 100644 index 00000000..80ee36f5 --- /dev/null +++ b/addons/gdUnit3/src/core/execution/TestSuiteExecutionStage.cs @@ -0,0 +1,37 @@ +using System; + +namespace GdUnit3 +{ + public sealed class TestSuiteExecutionStage : IExecutionStage + { + public TestSuiteExecutionStage(Type type) + { + BeforeStage = new BeforeExecutionStage(type); + AfterStage = new AfterExecutionStage(type); + TestCaseStage = new TestCaseExecutionStage(type); + } + + public string StageName() => "TestSuite"; + + private IExecutionStage BeforeStage + { get; set; } + private IExecutionStage AfterStage + { get; set; } + + private IExecutionStage TestCaseStage + { get; set; } + + public void Execute(ExecutionContext context) + { + BeforeStage.Execute(context); + foreach (TestCase testCase in CsTools.GetTestCases(context.TestInstance.GetType())) + { + using (ExecutionContext currentContext = new ExecutionContext(context, testCase)) + { + TestCaseStage.Execute(currentContext); + } + } + AfterStage.Execute(context); + } + } +} diff --git a/addons/gdUnit3/src/core/monitor/OrphanNodesMonitor.cs b/addons/gdUnit3/src/core/monitor/OrphanNodesMonitor.cs new file mode 100644 index 00000000..76a0a6e7 --- /dev/null +++ b/addons/gdUnit3/src/core/monitor/OrphanNodesMonitor.cs @@ -0,0 +1,31 @@ +using static Godot.Performance; + +namespace GdUnit3 +{ + public class OrphanNodesMonitor + { + + private int _orphanNodesStart = 0; + private int _orphanCount = 0; + + public void Start(bool reset = false) + { + if (reset) + { + Reset(); + } + _orphanNodesStart = GetMonitoredOrphanCount(); + } + + public void Stop() + { + _orphanCount += GetMonitoredOrphanCount() - _orphanNodesStart; + } + + private int GetMonitoredOrphanCount() => (int)GetMonitor(Monitor.ObjectOrphanNodeCount); + + public int OrphanCount() => _orphanCount; + + public void Reset() => _orphanCount = 0; + } +} diff --git a/addons/gdUnit3/src/core/report/TestReport.cs b/addons/gdUnit3/src/core/report/TestReport.cs new file mode 100644 index 00000000..c5f9b06b --- /dev/null +++ b/addons/gdUnit3/src/core/report/TestReport.cs @@ -0,0 +1,69 @@ + +using System; +using System.Linq; +using System.Collections.Generic; + +namespace GdUnit3 +{ + public sealed class TestReport : Godot.Reference + { + [Flags] + public enum TYPE + { + SUCCESS, + WARN, + FAILURE, + ORPHAN, + TERMINATED, + INTERUPTED, + ABORT + } + + public TestReport(TYPE type, int line_number, string message) + { + Type = type; + LineNumber = line_number; + Message = message; + } + + public TYPE Type + { get; private set; } + + public int LineNumber + { get; private set; } + + public string Message + { get; private set; } + + + + private static IEnumerable ErrorTypes => new[] { TYPE.TERMINATED, TYPE.INTERUPTED, TYPE.ABORT }; + public bool IsError => ErrorTypes.Contains(Type); + public bool IsFailure => Type == TYPE.FAILURE; + + public bool IsWarning => Type == TYPE.WARN; + + public override string ToString() => $"[color=green]line [/color][color=aqua]{LineNumber}:[/color] \t {Message}"; + + public IDictionary Serialize() + { + return new Dictionary(){ + {"type" ,Type}, + {"line_number" ,LineNumber}, + {"message" ,Message} + }; + } + + public TestReport Deserialize(IDictionary serialized) + { + TYPE type = (TYPE)Enum.Parse(typeof(TYPE), (string)serialized["type"]); + int lineNumber = (int)serialized["line_number"]; + string message = (string)serialized["message"]; + return new TestReport(type, lineNumber, message); + } + } +} + + + + diff --git a/addons/gdUnit3/src/core/report/TestReportCollector.cs b/addons/gdUnit3/src/core/report/TestReportCollector.cs new file mode 100644 index 00000000..fa851eb5 --- /dev/null +++ b/addons/gdUnit3/src/core/report/TestReportCollector.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace GdUnit3 +{ + public sealed class TestReportCollector : Godot.Reference + { + + private List _reports = new List(); + public TestReportCollector() + { } + + // called by GdScript, will be removed after full gd to cs refactoring + public void consume(Godot.Resource report) + { + TestReport.TYPE type = (TestReport.TYPE)Enum.ToObject(typeof(TestReport.TYPE), (int)report.Call("type")); + Consume(new TestReport(type, (int)report.Call("line_number"), (string)report.Call("message"))); + } + + public void Consume(TestReport report) => _reports.Add(report); + + public void Clear() => _reports.Clear(); + + + public IEnumerable Reports + { get => _reports; } + + + public IEnumerable Failures => _reports.Where(r => r.IsFailure); + + public IEnumerable Errors => _reports.Where(r => r.IsError); + + public IEnumerable Warnings => _reports.Where(r => r.IsWarning); + } +} diff --git a/addons/gdUnit3/src/network/rpc/RPCGdUnitTestSuite.gd b/addons/gdUnit3/src/network/rpc/RPCGdUnitTestSuite.gd index 0c517848..a5df29f0 100644 --- a/addons/gdUnit3/src/network/rpc/RPCGdUnitTestSuite.gd +++ b/addons/gdUnit3/src/network/rpc/RPCGdUnitTestSuite.gd @@ -3,7 +3,7 @@ extends RPC var _data :Dictionary -static func of(test_suite :GdUnitTestSuite) -> RPCGdUnitTestSuite: +static func of(test_suite) -> RPCGdUnitTestSuite: var rpc = load("res://addons/gdUnit3/src/network/rpc/RPCGdUnitTestSuite.gd").new() rpc._data = GdUnitTestSuiteDto.new().serialize(test_suite) return rpc diff --git a/addons/gdUnit3/src/network/rpc/dtos/GdUnitResourceDto.gd b/addons/gdUnit3/src/network/rpc/dtos/GdUnitResourceDto.gd index 54590d96..6fb9164f 100644 --- a/addons/gdUnit3/src/network/rpc/dtos/GdUnitResourceDto.gd +++ b/addons/gdUnit3/src/network/rpc/dtos/GdUnitResourceDto.gd @@ -4,7 +4,7 @@ extends Resource var _name :String var _path :String -func serialize(resource :Object) -> Dictionary: +func serialize(resource) -> Dictionary: var serialized := Dictionary() serialized["name"] = resource.get_name() var script = resource.get_script() diff --git a/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestCaseDto.gd index 3557920c..e11f8c36 100644 --- a/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestCaseDto.gd +++ b/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestCaseDto.gd @@ -3,7 +3,7 @@ extends GdUnitResourceDto var _line_number :int = -1 -func serialize(test_case :Object) -> Dictionary: +func serialize(test_case) -> Dictionary: var serialized := .serialize(test_case) serialized["line_number"] = test_case.line_number() return serialized diff --git a/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestSuiteDto.gd b/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestSuiteDto.gd index ab581fb7..8a6cb8cc 100644 --- a/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestSuiteDto.gd +++ b/addons/gdUnit3/src/network/rpc/dtos/GdUnitTestSuiteDto.gd @@ -3,7 +3,7 @@ extends GdUnitResourceDto var _test_cases_by_name := Dictionary() -func serialize(test_suite :Object) -> Dictionary: +func serialize(test_suite :Node) -> Dictionary: var serialized := .serialize(test_suite) var test_cases := Array() serialized["test_cases"] = test_cases diff --git a/addons/gdUnit3/src/report/GdUnitHtmlReport.gd b/addons/gdUnit3/src/report/GdUnitHtmlReport.gd index ed9cbc3b..60bd4d2b 100644 --- a/addons/gdUnit3/src/report/GdUnitHtmlReport.gd +++ b/addons/gdUnit3/src/report/GdUnitHtmlReport.gd @@ -13,21 +13,21 @@ func _init(path :String): func add_testsuite_report(suite_report :GdUnitTestSuiteReport): _reports.append(suite_report) -func add_testcase_report(suite_name :String, suite_report :GdUnitTestCaseReport) -> void: +func add_testcase_report(resource_path :String, suite_report :GdUnitTestCaseReport) -> void: for report in _reports: - if report.name() == suite_name: + if report.resource_path() == resource_path: report.add_report(suite_report) -func update_test_suite_report(suite_name :String, skipped :int, orphans :int, duration :int) -> void: +func update_test_suite_report(resource_path :String, skipped :int, orphans :int, duration :int) -> void: for report in _reports: - if report.name() == suite_name: + if report.resource_path() == resource_path: report.set_duration(duration) report.set_skipped(skipped) report.set_orphans(orphans) -func update_testcase_report(suite_name :String, test_report :GdUnitTestCaseReport): +func update_testcase_report(resource_path :String, test_report :GdUnitTestCaseReport): for report in _reports: - if report.name() == suite_name: + if report.resource_path() == resource_path: report.update(test_report) func write() -> String: diff --git a/addons/gdUnit3/src/report/GdUnitReportSummary.gd b/addons/gdUnit3/src/report/GdUnitReportSummary.gd index 58a5be48..6b0213d8 100644 --- a/addons/gdUnit3/src/report/GdUnitReportSummary.gd +++ b/addons/gdUnit3/src/report/GdUnitReportSummary.gd @@ -18,6 +18,9 @@ func name() -> String: func path() -> String: return _resource_path.get_base_dir().replace("res://", "") +func resource_path() -> String: + return _resource_path + func suite_count() -> int: return _reports.size() diff --git a/addons/gdUnit3/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit3/src/report/GdUnitTestCaseReport.gd index 0a623b3d..8b0bc97c 100644 --- a/addons/gdUnit3/src/report/GdUnitTestCaseReport.gd +++ b/addons/gdUnit3/src/report/GdUnitTestCaseReport.gd @@ -3,7 +3,8 @@ extends GdUnitReportSummary var _failure_reports :Array -func _init(test_name :String, is_error :bool = false, is_failed :bool = false, orphans :int = 0, skipped :int = 0, failure_reports :Array = [], duration :int = 0): +func _init(resource_path :String, test_name :String, is_error :bool = false, is_failed :bool = false, orphans :int = 0, skipped :int = 0, failure_reports :Array = [], duration :int = 0): + _resource_path = resource_path _name = test_name _test_count = 1 _error_count = is_error diff --git a/addons/gdUnit3/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit3/src/report/GdUnitTestSuiteReport.gd index bd40f4ba..41897bad 100644 --- a/addons/gdUnit3/src/report/GdUnitTestSuiteReport.gd +++ b/addons/gdUnit3/src/report/GdUnitTestSuiteReport.gd @@ -45,7 +45,6 @@ func duration() -> int: func set_skipped(skipped :int) -> void: _skipped_count = skipped - _test_count += skipped func set_orphans(orphans :int) -> void: _orphan_count = orphans diff --git a/addons/gdUnit3/src/ui/GdUnitInspector.gd b/addons/gdUnit3/src/ui/GdUnitInspector.gd index 68da7a0a..d53057a8 100644 --- a/addons/gdUnit3/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit3/src/ui/GdUnitInspector.gd @@ -180,7 +180,7 @@ func extend_script_editor_popup(tab_container :Control) -> void: func _on_script_editor_context_menu_show(context_menu :PopupMenu): var current_script := _editor_interface.get_script_editor().get_current_script() - if GdObjects.is_testsuite(current_script): + if GdObjects.is_test_suite(current_script): context_menu.add_separator() # save menu entry index var current_index := context_menu.get_item_count() diff --git a/addons/gdUnit3/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit3/src/ui/parts/InspectorTreeMainPanel.gd index dd3ea054..1f73702f 100644 --- a/addons/gdUnit3/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit3/src/ui/parts/InspectorTreeMainPanel.gd @@ -38,7 +38,8 @@ enum STATE { WARNING, FAILED, ERROR, - ABORDED + ABORDED, + SKIPPED } const META_GDUNIT_NAME := "gdUnit_name" const META_GDUNIT_STATE := "gdUnit_state" @@ -163,6 +164,13 @@ func set_state_succeded(item :TreeItem) -> void: item.set_icon(0, ICON_TEST_SUCCESS) item.collapsed = true +func set_state_skipped(item :TreeItem) -> void: + item.set_meta(META_GDUNIT_STATE, STATE.SKIPPED) + item.set_custom_color(0, Color.lightgray) + item.set_suffix(0, "(skipped)") + item.set_icon(0, ICON_TEST_SUCCESS) + item.collapsed = false + func set_state_warnings(item :TreeItem) -> void: item.set_meta(META_GDUNIT_STATE, STATE.WARNING) item.set_custom_color(0, Color.yellow) @@ -204,20 +212,22 @@ func set_state_orphan(item :TreeItem, event: GdUnitEvent) -> void: item.set_tooltip(0, "Total <%d> orphan nodes detected." % orphan_count) if is_state_warning(item): item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) - if is_state_failed(item): + elif is_state_failed(item): item.set_icon(0, ICON_TEST_FAILED_ORPHAN) - if is_state_error(item): + elif is_state_error(item): item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) func update_state(item: TreeItem, event :GdUnitEvent) -> void: if is_state_running(item) and event.is_success(): set_state_succeded(item) else: - if event.is_warning(): + if event.is_skipped(): + set_state_skipped(item) + elif event.is_warning(): set_state_warnings(item) - if event.is_failed(): + elif event.is_failed(): set_state_failed(item) - if event.is_error(): + elif event.is_error(): set_state_error(item) for report in event.reports(): add_report(item, report) diff --git a/addons/gdUnit3/test/GdUnitScriptTypeTest.gd b/addons/gdUnit3/test/GdUnitScriptTypeTest.gd new file mode 100644 index 00000000..625e6ec6 --- /dev/null +++ b/addons/gdUnit3/test/GdUnitScriptTypeTest.gd @@ -0,0 +1,16 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitScriptTypeTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit3/src/core/GdUnitScriptType.gd' + +func test_type_of() -> void: + assert_str(GdUnitScriptType.type_of(null)).is_equal(GdUnitScriptType.UNKNOWN) + assert_str(GdUnitScriptType.type_of(ClassDB.instance("GDScript"))).is_equal(GdUnitScriptType.GD) + if GdUnitTools.is_mono_supported(): + assert_str(GdUnitScriptType.type_of(ClassDB.instance("CSharpScript"))).is_equal(GdUnitScriptType.CS) + assert_str(GdUnitScriptType.type_of(ClassDB.instance("VisualScript"))).is_equal(GdUnitScriptType.VS) + assert_str(GdUnitScriptType.type_of(ClassDB.instance("NativeScript"))).is_equal(GdUnitScriptType.NATIVE) diff --git a/addons/gdUnit3/test/GdUnitTestResourceLoader.gd b/addons/gdUnit3/test/GdUnitTestResourceLoader.gd index 9bd809e2..4ba8233a 100644 --- a/addons/gdUnit3/test/GdUnitTestResourceLoader.gd +++ b/addons/gdUnit3/test/GdUnitTestResourceLoader.gd @@ -1,18 +1,63 @@ class_name GdUnitTestResourceLoader extends Reference -static func load_test_suite(resource_path :String) -> GdUnitTestSuite: +enum { + GD_SUITE, + CS_SUITE +} + + +static func load_test_suite(resource_path :String, script_type = GD_SUITE) -> Node: + match script_type: + GD_SUITE: + return load_test_suite_gd(resource_path) + CS_SUITE: + return load_test_suite_cs(resource_path) + assert("type '%s' is not impleented" % script_type) + return null + +static func load_test_suite_gd(resource_path :String) -> Node: var script := GDScript.new() script.source_code = GdUnitTools.resource_as_string(resource_path) script.resource_path = resource_path script.reload() var test_suite :GdUnitTestSuite = GdUnitTestSuite.new() test_suite.set_script(script) - test_suite.set_name(_TestSuiteScanner.parse_test_suite_name(resource_path.replace(".resource", ".gd"))) + test_suite.set_name(resource_path.get_file().replace(".resource", "")) # complete test suite wiht parsed test cases var suite_parser := _TestSuiteScanner.new() - var test_case_names := suite_parser._extract_test_case_names(test_suite) + var test_case_names := suite_parser._extract_test_case_names(script) # add test cases to test suite and parse test case line nummber - suite_parser._parse_and_add_test_cases(test_suite, resource_path, test_case_names) + suite_parser._parse_and_add_test_cases(test_suite, script, test_case_names) suite_parser.free() return test_suite + + +static func load_test_suite_cs(resource_path :String) -> Node: + if not GdUnitTools.is_mono_supported(): + return null + var script = ClassDB.instance("CSharpScript") + script.source_code = GdUnitTools.resource_as_string(resource_path) + script.resource_path = resource_path + script.reload() + + return null + +static func load_cs_script(resource_path :String) -> Script: + if not GdUnitTools.is_mono_supported(): + return null + var script = ClassDB.instance("CSharpScript") + script.source_code = GdUnitTools.resource_as_string(resource_path) + script.resource_path = GdUnitTools.create_temp_dir("test") + "/%s" % resource_path.get_file().replace(".resource", ".cs") + Directory.new().remove(script.resource_path) + ResourceSaver.save(script.resource_path, script) + script.reload() + return script + +static func load_gd_script(resource_path :String) -> GDScript: + var script := GDScript.new() + script.source_code = GdUnitTools.resource_as_string(resource_path) + script.resource_path = resource_path.replace(".resource", ".gd") + script.reload() + return script + diff --git a/addons/gdUnit3/test/asserts/BoolAssertTest.cs b/addons/gdUnit3/test/asserts/BoolAssertTest.cs new file mode 100644 index 00000000..f37f1b44 --- /dev/null +++ b/addons/gdUnit3/test/asserts/BoolAssertTest.cs @@ -0,0 +1,77 @@ +using GdUnit3; + +[TestSuite] +public class BoolAssertTest : TestSuite +{ + [TestCase] + public void IsTrue() + { + AssertBool(true).IsTrue(); + AssertBool(false, IAssert.EXPECT.FAIL).IsTrue() + .HasFailureMessage("Expecting: 'True' but is 'False'"); + } + + [TestCase] + public void IsFalse() + { + AssertBool(false).IsFalse(); + AssertBool(true, IAssert.EXPECT.FAIL).IsFalse() + .HasFailureMessage("Expecting: 'False' but is 'True'"); + } + + [TestCase] + public void IsNull() + { + AssertBool(true, IAssert.EXPECT.FAIL) + .IsNull() + .StartsWithFailureMessage("Expecting: 'Null' but was 'True'"); + AssertBool(false, IAssert.EXPECT.FAIL) + .IsNull() + .StartsWithFailureMessage("Expecting: 'Null' but was 'False'"); + } + + [TestCase] + public void IsNotNull() + { + AssertBool(true).IsNotNull(); + AssertBool(false).IsNotNull(); + } + + [TestCase] + public void IsEqual() + { + AssertBool(true).IsEqual(true); + AssertBool(false).IsEqual(false); + AssertBool(true, IAssert.EXPECT.FAIL) + .IsEqual(false) + .HasFailureMessage("Expecting:\n 'False'\n but was\n 'True'"); + } + + [TestCase] + public void IsNotEqual() + { + AssertBool(true).IsNotEqual(false); + AssertBool(false).IsNotEqual(true); + AssertBool(true, IAssert.EXPECT.FAIL) + .IsNotEqual(true) + .HasFailureMessage("Expecting:\n 'True'\n not equal to\n 'True'"); + } + + [TestCase] + public void Fluent() + { + AssertBool(true).IsTrue() + .IsEqual(true) + .IsNotEqual(false) + .IsNotNull(); + } + + [TestCase] + public void OverrideFailureMessage() + { + AssertBool(true, IAssert.EXPECT.FAIL) + .OverrideFailureMessage("Custom failure message") + .IsNull() + .HasFailureMessage("Custom failure message"); + } +} diff --git a/addons/gdUnit3/test/asserts/GdUnitObjectAssertImplTest.gd b/addons/gdUnit3/test/asserts/GdUnitObjectAssertImplTest.gd index 14dedaa9..e454e69b 100644 --- a/addons/gdUnit3/test/asserts/GdUnitObjectAssertImplTest.gd +++ b/addons/gdUnit3/test/asserts/GdUnitObjectAssertImplTest.gd @@ -31,8 +31,12 @@ func test_is_instanceof(): assert_object(auto_free(Path.new()), GdUnitAssert.EXPECT_FAIL)\ .is_instanceof(Tree)\ .has_failure_message("Expected instance of:\n 'Tree'\n But it was 'Path'") + assert_object(null, GdUnitAssert.EXPECT_FAIL)\ + .is_instanceof(Tree)\ + .has_failure_message("Expected instance of:\n 'Tree'\n But it was 'Null'") func test_is_not_instanceof(): + assert_object(null).is_not_instanceof(Node) # engine class test assert_object(auto_free(Path.new())).is_not_instanceof(Tree) # script class test @@ -46,12 +50,6 @@ func test_is_not_instanceof(): .is_not_instanceof(Node)\ .has_failure_message("Expected not be a instance of ") -func test_is_not_instanceof_on_null_value(): - assert_object(null, GdUnitAssert.EXPECT_FAIL)\ - .is_not_null()\ - .is_instanceof(Node)\ - .has_failure_message("Expected instance of:\n 'Node'\n But it was 'Null'") - func test_is_null(): assert_object(null).is_null() # should fail because the current is not null diff --git a/addons/gdUnit3/test/asserts/IntAssertTest.cs b/addons/gdUnit3/test/asserts/IntAssertTest.cs new file mode 100644 index 00000000..ab34e796 --- /dev/null +++ b/addons/gdUnit3/test/asserts/IntAssertTest.cs @@ -0,0 +1,224 @@ +using GdUnit3; +using Godot; +using static GdUnit3.IAssert.EXPECT; + +[TestSuite] +public class IntAssertTest : TestSuite +{ + + [TestCase] + public void IsNull() + { + // should fail because the current is not null + AssertInt(23, FAIL) + .IsNull() + .StartsWithFailureMessage("Expecting: 'Null' but was '23'"); + } + + [TestCase] + public void IsNotNull() + { + AssertInt(23).IsNotNull(); + } + + [TestCase] + public void IsEqual() + { + AssertInt(23).IsEqual(23); + // this assertion fails because 23 are not equal to 42 + AssertInt(23, FAIL) + .IsEqual(42) + .HasFailureMessage("Expecting:\n '42'\n but was\n '23'"); + } + + [TestCase] + public void IsNotEqual() + { + AssertInt(23).IsNotEqual(42); + // this assertion fails because 23 are equal to 23 + AssertInt(23, FAIL) + .IsNotEqual(23) + .HasFailureMessage("Expecting:\n '23'\n not equal to\n '23'"); + } + + [TestCase] + public void IsLess() + { + AssertInt(23).IsLess(42); + AssertInt(23).IsLess(24); + // this assertion fails because 23 is not less than 23 + AssertInt(23, FAIL) + .IsLess(23) + .HasFailureMessage("Expecting to be less than:\n '23' but was '23'"); + } + + [TestCase] + public void IsLessEqual() + { + AssertInt(23).IsLessEqual(42); + AssertInt(23).IsLessEqual(23); + // this assertion fails because 23 is not less than or equal to 22 + AssertInt(23, FAIL) + .IsLessEqual(22) + .HasFailureMessage("Expecting to be less than or equal:\n '22' but was '23'"); + } + + [TestCase] + public void IsGreater() + { + AssertInt(23).IsGreater(20); + AssertInt(23).IsGreater(22); + // this assertion fails because 23 is not greater than 23 + AssertInt(23, FAIL) + .IsGreater(23) + .HasFailureMessage("Expecting to be greater than:\n '23' but was '23'"); + } + + [TestCase] + public void IsGreaterEqual() + { + AssertInt(23).IsGreaterEqual(20); + AssertInt(23).IsGreaterEqual(23); + // this assertion fails because 23 is not greater than 23 + AssertInt(23, FAIL) + .IsGreaterEqual(24) + .HasFailureMessage("Expecting to be greater than or equal:\n '24' but was '23'"); + } + + // [TestCase] + //public void _test_IsEven_fuzz(fuzzer = Fuzzers.even(-9223372036854775807, 9223372036854775807)) + //{ + // AssertInt(fuzzer.next_value()).IsEven() + //} + + [TestCase] + public void IsEven() + { + AssertInt(12).IsEven(); + AssertInt(13, FAIL) + .IsEven() + .HasFailureMessage("Expecting:\n '13' must be even"); + } + + [TestCase] + public void IsOdd() + { + AssertInt(13).IsOdd(); + AssertInt(12, FAIL) + .IsOdd() + .HasFailureMessage("Expecting:\n '12' must be odd"); + } + + [TestCase] + public void IsNegative() + { + AssertInt(-13).IsNegative(); + AssertInt(13, FAIL) + .IsNegative() + .HasFailureMessage("Expecting:\n '13' be negative"); + } + + [TestCase] + public void IsNotNegative() + { + AssertInt(13).IsNotNegative(); + AssertInt(-13, FAIL) + .IsNotNegative() + .HasFailureMessage("Expecting:\n '-13' be not negative"); + } + + [TestCase] + public void IsZero() + { + AssertInt(0).IsZero(); + // this assertion fail because the value is not zero + AssertInt(1, FAIL) + .IsZero() + .HasFailureMessage("Expecting:\n equal to 0 but is '1'"); + } + + [TestCase] + public void IsNotZero() + { + AssertInt(1).IsNotZero(); + // this assertion fail because the value is not zero + AssertInt(0, FAIL) + .IsNotZero() + .HasFailureMessage("Expecting:\n not equal to 0"); + } + + [TestCase] + public void IsIn() + { + AssertInt(5).IsIn(new int[] { 3, 4, 5, 6 }); + // this assertion fail because 7 is not in [3, 4, 5, 6] + AssertInt(7, FAIL) + .IsIn(new int[] { 3, 4, 5, 6 }) + .HasFailureMessage("Expecting:\n '7'\n is in\n '[3, 4, 5, 6]'"); + } + + [TestCase] + public void IsNotIn() + { + AssertInt(5).IsNotIn(new int[] { 3, 4, 6, 7 }); + // this assertion fail because 7 is not in [3, 4, 5, 6] + AssertInt(5, FAIL) + .IsNotIn(new int[] { 3, 4, 5, 6 }) + .HasFailureMessage("Expecting:\n '5'\n is not in\n '[3, 4, 5, 6]'"); + } + + //[Theory] + //[Fuzzer(name = "rangei", from = -20, to = 20)] + [TestCase(timeout = 1000)] + public void IsBetween() + { + int value = 10; + AssertInt(value).IsBetween(-20, 20); + } + + [TestCase] + public void IsBetweenMustFail() + { + AssertInt(-10, FAIL) + .IsBetween(-9, 0) + .HasFailureMessage("Expecting:\n '-10'\n in range between\n '-9' <> '0'"); + AssertInt(0, FAIL) + .IsBetween(1, 10) + .HasFailureMessage("Expecting:\n '0'\n in range between\n '1' <> '10'"); + AssertInt(10, FAIL) + .IsBetween(11, 21) + .HasFailureMessage("Expecting:\n '10'\n in range between\n '11' <> '21'"); + } + + + [TestCase(timeout = 20, seed = 111, iterations = 20)] + public void override_failure_message([Fuzzer(10)] int value, [Fuzzer(5)] int value2 = 0) + { + GD.PrintS("iteration", value, value2); + AssertInt(value, FAIL) + .OverrideFailureMessage("Custom failure message") + .IsNull() + .HasFailureMessage("Custom failure message"); + } + + [BeforeTest] + public void setup() + { + + } + + [AfterTest] + public void testFin() + { + + } + + [IgnoreUntil("Ignore on self test")] + [TestCase] + public void Executor() + { + new Executor().execute(this); + } + + +} diff --git a/addons/gdUnit3/test/asserts/ObjectAssertTest.cs b/addons/gdUnit3/test/asserts/ObjectAssertTest.cs new file mode 100644 index 00000000..8a5e9e4a --- /dev/null +++ b/addons/gdUnit3/test/asserts/ObjectAssertTest.cs @@ -0,0 +1,141 @@ +using Godot; +using GdUnit3; +using static GdUnit3.IAssert.EXPECT; + +[TestSuite] +public class ObjectAssertTest : TestSuite +{ + + class CustomClass + { + public class InnerClassA : Node { } + + public class InnerClassB : InnerClassA { } + + public class InnerClassC : Node { } + } + + [TestCase] + public void IsEqual() + { + AssertObject(new CubeMesh()).IsEqual(new CubeMesh()); + // should fail because the current is an CubeMesh and we expect equal to a Skin + AssertObject(new CubeMesh(), FAIL) + .IsEqual(new Skin()); + } + + [TestCase] + public void IsNotEqual() + { + AssertObject(new CubeMesh()).IsNotEqual(new Skin()); + // should fail because the current is an CubeMesh and we expect not equal to a CubeMesh + AssertObject(new CubeMesh(), FAIL) + .IsNotEqual(new CubeMesh()); + } + + [TestCase] + public void IsInstanceof() + { + // engine class test + AssertObject(auto_free(new Path())).IsInstanceof(); + AssertObject(auto_free(new Camera())).IsInstanceof(); + // script class test + // inner class test + AssertObject(auto_free(new CustomClass.InnerClassA())).IsInstanceof(); + AssertObject(auto_free(new CustomClass.InnerClassB())).IsInstanceof(); + + // should fail because the current is not a instance of `Tree` + AssertObject(auto_free(new Path()), FAIL) + .IsInstanceof() + .HasFailureMessage("Expected instance of:\n 'Godot.Tree'\n But it was 'Godot.Path'"); + AssertObject(null, FAIL) + .IsInstanceof() + .HasFailureMessage("Expected instance of:\n 'Godot.Tree'\n But it was 'Null'"); + } + + [TestCase] + public void IsNotInstanceof() + { + AssertObject(null).IsNotInstanceof(); + // engine class test + AssertObject(auto_free(new Path())).IsNotInstanceof(); + // inner class test + AssertObject(auto_free(new CustomClass.InnerClassA())).IsNotInstanceof(); + AssertObject(auto_free(new CustomClass.InnerClassB())).IsNotInstanceof(); + + // should fail because the current is not a instance of `Tree` + AssertObject(auto_free(new Path()), FAIL) + .IsNotInstanceof() + .HasFailureMessage("Expected not be a instance of "); + } + + [TestCase] + public void IsNull() + { + AssertObject(null).IsNull(); + // should fail because the current is not null + AssertObject(auto_free(new Node()), FAIL) + .IsNull() + .StartsWithFailureMessage("Expecting: 'Null' but was "); + } + + [TestCase] + public void IsNotNull() + { + AssertObject(auto_free(new Node())).IsNotNull(); + // should fail because the current is null + AssertObject(null, FAIL) + .IsNotNull() + .HasFailureMessage("Expecting: not to be 'Null'"); + } + + [TestCase] + public void IsSame() + { + var obj1 = auto_free(new Node()); + var obj2 = obj1; + var obj3 = auto_free(obj1.Duplicate()); + AssertObject(obj1).IsSame(obj1); + AssertObject(obj1).IsSame(obj2); + AssertObject(obj2).IsSame(obj1); + AssertObject(null, FAIL).IsSame(obj1); + AssertObject(obj1, FAIL).IsSame(obj3); + AssertObject(obj3, FAIL).IsSame(obj1); + AssertObject(obj3, FAIL).IsSame(obj2); + } + + [TestCase] + public void IsNotSame() + { + var obj1 = auto_free(new Node()); + var obj2 = obj1; + var obj3 = auto_free(obj1.Duplicate()); + AssertObject(null).IsNotSame(obj1); + AssertObject(obj1).IsNotSame(obj3); + AssertObject(obj3).IsNotSame(obj1); + AssertObject(obj3).IsNotSame(obj2); + + AssertObject(obj1, FAIL).IsNotSame(obj1); + AssertObject(obj1, FAIL).IsNotSame(obj2); + AssertObject(obj2, FAIL).IsNotSame(obj1); + } + + [TestCase] + public void must_fail_has_invlalid_type() + { + AssertObject(1, FAIL).HasFailureMessage("GdUnitObjectAssert inital error, unexpected type "); + AssertObject(1.3, FAIL).HasFailureMessage("GdUnitObjectAssert inital error, unexpected type "); + AssertObject(true, FAIL).HasFailureMessage("GdUnitObjectAssert inital error, unexpected type "); + AssertObject("foo", FAIL).HasFailureMessage("GdUnitObjectAssert inital error, unexpected type "); + } + + [TestCase] + public void OverrideFailureMessage() + { + AssertObject(auto_free(new Node()), FAIL) + .OverrideFailureMessage("Custom failure message") + .IsNull() + .HasFailureMessage("Custom failure message"); + } + +} diff --git a/addons/gdUnit3/test/asserts/StringAssertTest.cs b/addons/gdUnit3/test/asserts/StringAssertTest.cs new file mode 100644 index 00000000..d62d58d7 --- /dev/null +++ b/addons/gdUnit3/test/asserts/StringAssertTest.cs @@ -0,0 +1,221 @@ + +using GdUnit3; +using static GdUnit3.IAssert.EXPECT; +using static GdUnit3.IStringAssert.Compare; + +[TestSuite] +public class StringAssertTest : TestSuite +{ + [TestCase] + public void IsNull() + { + AssertString(null).IsNull(); + // should fail because the current is not null + AssertString("abc", FAIL) + .IsNull() + .StartsWithFailureMessage("Expecting: 'Null' but was 'abc'"); + } + + [TestCase] + public void IsNotNull() + { + AssertString("abc").IsNotNull(); + // should fail because the current is null + AssertString(null, FAIL) + .IsNotNull() + .HasFailureMessage("Expecting: not to be 'Null'"); + } + + [TestCase] + public void IsEqual() + { + AssertString("This is a test message").IsEqual("This is a test message"); + AssertString("This is a test message", FAIL) + .IsEqual("This is a test Message") + .HasFailureMessage("Expecting:\n 'This is a test Message'\n but was\n 'This is a test Mmessage'"); + } + + [TestCase] + public void IsEqualIgnoringCase() + { + AssertString("This is a test message").IsEqualIgnoringCase("This is a test Message"); + AssertString("This is a test message", FAIL) + .IsEqualIgnoringCase("This is a Message") + .HasFailureMessage("Expecting:\n 'This is a Message'\n but was\n 'This is a test Mmessage' (ignoring case)"); + } + + [TestCase] + public void IsNotEqual() + { + AssertString("This is a test message").IsNotEqual("This is a test Message"); + AssertString("This is a test message", FAIL) + .IsNotEqual("This is a test message") + .HasFailureMessage("Expecting:\n 'This is a test message'\n not equal to\n 'This is a test message'"); + } + + [TestCase] + public void IsNotEqualIgnoringCase() + { + AssertString("This is a test message").IsNotEqualIgnoringCase("This is a Message"); + AssertString("This is a test message", FAIL) + .IsNotEqualIgnoringCase("This is a test Message") + .HasFailureMessage("Expecting:\n 'This is a test Message'\n not equal to\n 'This is a test message'"); + } + + [TestCase] + public void IsEmpty() + { + AssertString("").IsEmpty(); + // should fail because the current value is not empty it contains a space + AssertString(" ", FAIL) + .IsEmpty() + .HasFailureMessage("Expecting:\n must be empty but was\n ' '"); + AssertString("abc", FAIL) + .IsEmpty() + .HasFailureMessage("Expecting:\n must be empty but was\n 'abc'"); + } + + [TestCase] + public void IsNotEmpty() + { + AssertString(" ").IsNotEmpty(); + AssertString(" ").IsNotEmpty(); + AssertString("abc").IsNotEmpty(); + // should fail because current is empty + AssertString("", FAIL) + .IsNotEmpty() + .HasFailureMessage("Expecting:\n must not be empty"); + } + + [TestCase] + public void Contains() + { + AssertString("This is a test message").Contains("a test"); + // must fail because of camel case difference + AssertString("This is a test message", FAIL) + .Contains("a Test") + .HasFailureMessage("Expecting:\n 'This is a test message'\n do contains\n 'a Test'"); + } + + [TestCase] + public void notContains() + { + AssertString("This is a test message").NotContains("a tezt"); + AssertString("This is a test message", FAIL) + .NotContains("a test") + .HasFailureMessage("Expecting:\n 'This is a test message'\n not do contain\n 'a test'"); + } + + [TestCase] + public void ContainsIgnoringCase() + { + AssertString("This is a test message").ContainsIgnoringCase("a Test"); + AssertString("This is a test message", FAIL) + .ContainsIgnoringCase("a Tesd") + .HasFailureMessage("Expecting:\n 'This is a test message'\n contains\n 'a Tesd'\n (ignoring case)"); + } + + [TestCase] + public void NotContainsIgnoringCase() + { + AssertString("This is a test message").NotContainsIgnoringCase("a Tezt"); + AssertString("This is a test message", FAIL) + .NotContainsIgnoringCase("a Test") + .HasFailureMessage("Expecting:\n 'This is a test message'\n not do contains\n 'a Test'\n (ignoring case)"); + } + + [TestCase] + public void StartsWith() + { + AssertString("This is a test message").StartsWith("This is"); + AssertString("This is a test message", FAIL) + .StartsWith("This iss") + .HasFailureMessage("Expecting:\n 'This is a test message'\n to start with\n 'This iss'"); + AssertString("This is a test message", FAIL) + .StartsWith("this is") + .HasFailureMessage("Expecting:\n 'This is a test message'\n to start with\n 'this is'"); + AssertString("This is a test message", FAIL) + .StartsWith("test") + .HasFailureMessage("Expecting:\n 'This is a test message'\n to start with\n 'test'"); + } + + [TestCase] + public void EndsWith() + { + AssertString("This is a test message").EndsWith("test message"); + AssertString("This is a test message", FAIL) + .EndsWith("tes message") + .HasFailureMessage("Expecting:\n 'This is a test message'\n to end with\n 'tes message'"); + AssertString("This is a test message", FAIL) + .EndsWith("a test") + .HasFailureMessage("Expecting:\n 'This is a test message'\n to end with\n 'a test'"); + } + + [TestCase] + public void HasLenght() + { + AssertString("This is a test message").HasLength(22); + AssertString("").HasLength(0); + AssertString("This is a test message", FAIL) + .HasLength(23) + .HasFailureMessage("Expecting size:\n '23' but was '22' in\n 'This is a test message'"); + } + + [TestCase] + public void HasLenghtLessThan() + { + AssertString("This is a test message").HasLength(23, LESS_THAN); + AssertString("This is a test message").HasLength(42, LESS_THAN); + AssertString("This is a test message", FAIL) + .HasLength(22, LESS_THAN) + .HasFailureMessage("Expecting size to be less than:\n '22' but was '22' in\n 'This is a test message'"); + } + + [TestCase] + public void HasLenghtLessEqual() + { + AssertString("This is a test message").HasLength(22, LESS_EQUAL); + AssertString("This is a test message").HasLength(23, LESS_EQUAL); + AssertString("This is a test message", FAIL) + .HasLength(21, LESS_EQUAL) + .HasFailureMessage("Expecting size to be less than or equal:\n '21' but was '22' in\n 'This is a test message'"); + } + + [TestCase] + public void HasLenghtGreaterThan() + { + AssertString("This is a test message").HasLength(21, GREATER_THAN); + AssertString("This is a test message", FAIL) + .HasLength(22, GREATER_THAN) + .HasFailureMessage("Expecting size to be greater than:\n '22' but was '22' in\n 'This is a test message'"); + } + + [TestCase] + public void HasLenghtGreaterEqual() + { + AssertString("This is a test message").HasLength(21, GREATER_EQUAL); + AssertString("This is a test message").HasLength(22, GREATER_EQUAL); + AssertString("This is a test message", FAIL) + .HasLength(23, GREATER_EQUAL) + .HasFailureMessage("Expecting size to be greater than or equal:\n '23' but was '22' in\n 'This is a test message'"); + } + + [TestCase] + public void Fluent() + { + AssertString("value a").HasLength(7) + .IsNotEqual("a") + .IsEqual("value a") + .IsNotNull(); + } + + [TestCase] + public void OverrideFailureMessage() + { + AssertString("", FAIL) + .OverrideFailureMessage("Custom failure message") + .IsNull() + .HasFailureMessage("Custom failure message"); + } + +} diff --git a/addons/gdUnit3/test/core/ExampleTest.cs b/addons/gdUnit3/test/core/ExampleTest.cs new file mode 100644 index 00000000..7becf0ed --- /dev/null +++ b/addons/gdUnit3/test/core/ExampleTest.cs @@ -0,0 +1,44 @@ +using Godot; +using GdUnit3; + + +[TestSuite] +public class ExampleTest : TestSuite +{ + [Before] + public void Before() + { + GD.PrintS("calling Before"); + } + + [After] + public void After() + { + GD.PrintS("calling After"); + } + + [BeforeTest] + public void BeforeTest() + { + GD.PrintS("calling BeforeTest"); + } + + [AfterTest] + public void AfterTest() + { + GD.PrintS("calling AfterTest"); + } + + [TestCase] + public void TestFoo() + { + AssertBool(true).IsEqual(true); + } + + [TestCase] + public void TestBar() + { + AssertBool(true).IsEqual(true); + } + +} diff --git a/addons/gdUnit3/test/core/GdObjectsTest.gd b/addons/gdUnit3/test/core/GdObjectsTest.gd index d4ffb949..4ab91abb 100644 --- a/addons/gdUnit3/test/core/GdObjectsTest.gd +++ b/addons/gdUnit3/test/core/GdObjectsTest.gd @@ -551,3 +551,14 @@ func test_all_types() -> void: GdObjects.TYPE_VOID, GdObjects.TYPE_VARARG, ]) + +func test_is_test_suite() -> void: + assert_bool(GdObjects.is_test_suite(GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit3/test/core/ResultTest.gd"))).is_true() + if GdUnitTools.is_mono_supported(): + assert_bool(GdObjects.is_test_suite(GdUnitTestResourceLoader.load_cs_script("res://addons/gdUnit3/test/core/ExampleTest.cs"))).is_true() + assert_bool(GdObjects.is_test_suite(GdUnitTestResourceLoader.load_cs_script("res://addons/gdUnit3/test/core/resources/testsuites/mono/NotATestSuite.cs"))).is_false() + # currently not supported + assert_bool(GdObjects.is_test_suite(NativeScript.new())).is_false() + assert_bool(GdObjects.is_test_suite(PluginScript.new())).is_false() + assert_bool(GdObjects.is_test_suite(VisualScript.new())).is_false() + diff --git a/addons/gdUnit3/test/core/GdUnitExecutorTest.gd b/addons/gdUnit3/test/core/GdUnitExecutorTest.gd index 0ce528d4..5748f290 100644 --- a/addons/gdUnit3/test/core/GdUnitExecutorTest.gd +++ b/addons/gdUnit3/test/core/GdUnitExecutorTest.gd @@ -13,13 +13,13 @@ func before(): Engine.get_main_loop().root.add_child(_executor) _executor.connect("send_event_debug", self, "_on_executor_event") -func resource(resource_path :String) -> GdUnitTestSuite: +func resource(resource_path :String) -> Node: return GdUnitTestResourceLoader.load_test_suite(resource_path) func _on_executor_event(event :GdUnitEvent) -> void: _events.append(event) -func execute(test_suite :GdUnitTestSuite, enable_orphan_detection := true): +func execute(test_suite :Node, enable_orphan_detection := true): yield(get_tree(), "idle_frame") _events.clear() _executor._memory_pool.configure(enable_orphan_detection) diff --git a/addons/gdUnit3/test/core/_TestSuiteScannerTest.gd b/addons/gdUnit3/test/core/_TestSuiteScannerTest.gd index 04e2d142..512d3084 100644 --- a/addons/gdUnit3/test/core/_TestSuiteScannerTest.gd +++ b/addons/gdUnit3/test/core/_TestSuiteScannerTest.gd @@ -125,11 +125,14 @@ func test_build_test_suite_path() -> void: func test_parse_and_add_test_cases() -> void: var default_time := GdUnitSettings.test_timeout() - var scanner = auto_free(_TestSuiteScanner.new()) + var scanner :_TestSuiteScanner = auto_free(_TestSuiteScanner.new()) + # fake a test suite var test_suite :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) - var script_path := "res://addons/gdUnit3/test/core/resources/test_script_with_arguments.gd" + var script := GDScript.new() + script.resource_path = "res://addons/gdUnit3/test/core/resources/test_script_with_arguments.gd" + test_suite.set_script(script) var test_case_names := PoolStringArray(["test_no_args", "test_with_timeout", "test_with_fuzzer", "test_with_fuzzer_iterations", "test_with_multible_fuzzers"]) - scanner._parse_and_add_test_cases(test_suite, script_path, test_case_names) + scanner._parse_and_add_test_cases(test_suite, test_suite.get_script(), test_case_names) assert_array(test_suite.get_children())\ .extractv(extr("get_name"), extr("timeout"), extr("fuzzers"), extr("iterations"))\ .contains_exactly([ @@ -166,3 +169,13 @@ func test_scan_by_inheritance_class_path() -> void: # finally free all scaned test suites for ts in test_suites: ts.free() + +func test_is_script_format_supported() -> void: + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.gd")).is_true() + if GdUnitTools.is_mono_supported(): + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.cs")).is_true() + else: + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.cs")).is_false() + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.gdns")).is_false() + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.vs")).is_false() + assert_bool(_TestSuiteScanner._is_script_format_supported("res://exampe.tres")).is_false() diff --git a/addons/gdUnit3/test/core/resources/testsuites/mono/NotATestSuite.cs b/addons/gdUnit3/test/core/resources/testsuites/mono/NotATestSuite.cs new file mode 100644 index 00000000..517d393f --- /dev/null +++ b/addons/gdUnit3/test/core/resources/testsuites/mono/NotATestSuite.cs @@ -0,0 +1,13 @@ +using Godot; +using GdUnit3; + +// will be ignored becaus of missing `[TestSuite]` animation +public class NotATestSuite : TestSuite +{ + + [TestCase] + public void TestFoo() + { + AssertBool(true).IsEqual(false); + } +} diff --git a/addons/gdUnit3/test/ui/parts/InspectorTreeMainPanelTest.gd b/addons/gdUnit3/test/ui/parts/InspectorTreeMainPanelTest.gd index 0576d637..bc3ed593 100644 --- a/addons/gdUnit3/test/ui/parts/InspectorTreeMainPanelTest.gd +++ b/addons/gdUnit3/test/ui/parts/InspectorTreeMainPanelTest.gd @@ -30,7 +30,7 @@ func after_test(): _inspector.free() -static func toDto(test_suite :GdUnitTestSuite) -> GdUnitTestSuiteDto: +static func toDto(test_suite :Node) -> GdUnitTestSuiteDto: var dto := GdUnitTestSuiteDto.new() return dto.deserialize(dto.serialize(test_suite)) as GdUnitTestSuiteDto diff --git a/gdUnit3.csproj b/gdUnit3.csproj new file mode 100644 index 00000000..af6af65f --- /dev/null +++ b/gdUnit3.csproj @@ -0,0 +1,6 @@ + + + net472 + preview + + diff --git a/gdUnit3.sln b/gdUnit3.sln new file mode 100644 index 00000000..993157f3 --- /dev/null +++ b/gdUnit3.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gdUnit3", "gdUnit3.csproj", "{1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {1F7492EE-4E0C-47ED-8D6F-FFF9FC4DCDB6}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal diff --git a/project.godot b/project.godot index 03df93b7..97d11d12 100644 --- a/project.godot +++ b/project.godot @@ -635,6 +635,16 @@ _global_script_classes=[ { "path": "res://addons/gdUnit3/test/core/GdUnitSceneRunnerTest.gd" }, { "base": "Reference", +"class": "GdUnitScriptType", +"language": "GDScript", +"path": "res://addons/gdUnit3/src/core/GdUnitScriptType.gd" +}, { +"base": "GdUnitTestSuite", +"class": "GdUnitScriptTypeTest", +"language": "GDScript", +"path": "res://addons/gdUnit3/test/GdUnitScriptTypeTest.gd" +}, { +"base": "Reference", "class": "GdUnitSettings", "language": "GDScript", "path": "res://addons/gdUnit3/src/core/GdUnitSettings.gd" @@ -1100,6 +1110,8 @@ _global_script_class_icons={ "GdUnitRunnerConfigTest": "", "GdUnitSceneRunner": "", "GdUnitSceneRunnerTest": "", +"GdUnitScriptType": "", +"GdUnitScriptTypeTest": "", "GdUnitSettings": "", "GdUnitSettingsTest": "", "GdUnitSingleton": "", @@ -1192,6 +1204,7 @@ enabled=PoolStringArray( "res://addons/gdUnit3/plugin.cfg" ) report/assert/verbose_errors=false report/assert/verbose_warnings=false +settings/update_notification_enabled=false [network] diff --git a/runtest.cmd b/runtest.cmd index 785accc0..0d071946 100644 --- a/runtest.cmd +++ b/runtest.cmd @@ -7,6 +7,15 @@ IF NOT DEFINED GODOT_BIN ( EXIT /b -1 ) +REM scan if Godot mono used and compile c# classes +for /f "tokens=5 delims=. " %%i in ('%GODOT_BIN% --version') do set GODOT_TYPE=%%i +IF "%GODOT_TYPE%" == "mono" ( + ECHO "Godot mono detected" + ECHO Compiling c# classes ... Please Wait + %GODOT_BIN% --build-solutions --no-window -q --quiet + ECHO done +) + %GODOT_BIN% --no-window -s -d .\addons\gdUnit3\bin\GdUnitCmdTool.gd %* SET exit_code=%errorlevel% %GODOT_BIN% --no-window --quiet -s -d .\addons\gdUnit3\bin\GdUnitCopyLog.gd %*