From bdce8b969c0b88d5f5b783c6065ad657d367e0e1 Mon Sep 17 00:00:00 2001 From: Minimata Date: Mon, 26 Jan 2026 09:05:55 +0100 Subject: [PATCH] reinstalling GDUnit from assetlib --- addons/gdUnit4/LICENSE | 21 + addons/gdUnit4/bin/GdUnitCmdTool.gd | 21 + addons/gdUnit4/bin/GdUnitCmdTool.gd.uid | 1 + addons/gdUnit4/bin/GdUnitCopyLog.gd | 167 ++++ addons/gdUnit4/bin/GdUnitCopyLog.gd.uid | 1 + addons/gdUnit4/plugin.cfg | 7 + addons/gdUnit4/plugin.gd | 110 +++ addons/gdUnit4/plugin.gd.uid | 1 + addons/gdUnit4/runtest.cmd | 62 ++ addons/gdUnit4/runtest.sh | 62 ++ addons/gdUnit4/src/Comparator.gd | 12 + addons/gdUnit4/src/Comparator.gd.uid | 1 + addons/gdUnit4/src/Fuzzers.gd | 80 ++ addons/gdUnit4/src/Fuzzers.gd.uid | 1 + addons/gdUnit4/src/GdUnitArrayAssert.gd | 122 +++ addons/gdUnit4/src/GdUnitArrayAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitAssert.gd | 47 ++ addons/gdUnit4/src/GdUnitAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitAwaiter.gd | 72 ++ addons/gdUnit4/src/GdUnitAwaiter.gd.uid | 1 + addons/gdUnit4/src/GdUnitBoolAssert.gd | 35 + addons/gdUnit4/src/GdUnitBoolAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitConstants.gd | 10 + addons/gdUnit4/src/GdUnitConstants.gd.uid | 1 + addons/gdUnit4/src/GdUnitDictionaryAssert.gd | 79 ++ .../gdUnit4/src/GdUnitDictionaryAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitFailureAssert.gd | 52 ++ addons/gdUnit4/src/GdUnitFailureAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitFileAssert.gd | 38 + addons/gdUnit4/src/GdUnitFileAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitFloatAssert.gd | 75 ++ addons/gdUnit4/src/GdUnitFloatAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitFuncAssert.gd | 42 + addons/gdUnit4/src/GdUnitFuncAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitGodotErrorAssert.gd | 59 ++ .../gdUnit4/src/GdUnitGodotErrorAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitIntAssert.gd | 79 ++ addons/gdUnit4/src/GdUnitIntAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitObjectAssert.gd | 51 ++ addons/gdUnit4/src/GdUnitObjectAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitResultAssert.gd | 51 ++ addons/gdUnit4/src/GdUnitResultAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitSceneRunner.gd | 325 ++++++++ addons/gdUnit4/src/GdUnitSceneRunner.gd.uid | 1 + addons/gdUnit4/src/GdUnitSignalAssert.gd | 46 ++ addons/gdUnit4/src/GdUnitSignalAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitStringAssert.gd | 71 ++ addons/gdUnit4/src/GdUnitStringAssert.gd.uid | 1 + addons/gdUnit4/src/GdUnitTestSuite.gd.uid | 1 + addons/gdUnit4/src/GdUnitTuple.gd | 28 + addons/gdUnit4/src/GdUnitTuple.gd.uid | 1 + addons/gdUnit4/src/GdUnitValueExtractor.gd | 9 + .../gdUnit4/src/GdUnitValueExtractor.gd.uid | 1 + addons/gdUnit4/src/GdUnitVectorAssert.gd | 55 ++ addons/gdUnit4/src/GdUnitVectorAssert.gd.uid | 1 + .../src/asserts/CallBackValueProvider.gd | 25 + .../src/asserts/CallBackValueProvider.gd.uid | 1 + .../src/asserts/DefaultValueProvider.gd | 13 + .../src/asserts/DefaultValueProvider.gd.uid | 1 + .../gdUnit4/src/asserts/GdAssertMessages.gd | 692 ++++++++++++++++ .../src/asserts/GdAssertMessages.gd.uid | 1 + addons/gdUnit4/src/asserts/GdAssertReports.gd | 54 ++ .../src/asserts/GdAssertReports.gd.uid | 1 + .../src/asserts/GdUnitArrayAssertImpl.gd.uid | 1 + .../gdUnit4/src/asserts/GdUnitAssertImpl.gd | 80 ++ .../src/asserts/GdUnitAssertImpl.gd.uid | 1 + .../gdUnit4/src/asserts/GdUnitAssertions.gd | 68 ++ .../src/asserts/GdUnitAssertions.gd.uid | 1 + .../src/asserts/GdUnitBoolAssertImpl.gd | 87 ++ .../src/asserts/GdUnitBoolAssertImpl.gd.uid | 1 + .../asserts/GdUnitDictionaryAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitFailureAssertImpl.gd | 136 ++++ .../asserts/GdUnitFailureAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitFileAssertImpl.gd | 116 +++ .../src/asserts/GdUnitFileAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitFloatAssertImpl.gd | 159 ++++ .../src/asserts/GdUnitFloatAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitFuncAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitGodotErrorAssertImpl.gd | 141 ++++ .../asserts/GdUnitGodotErrorAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitIntAssertImpl.gd | 166 ++++ .../src/asserts/GdUnitIntAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitObjectAssertImpl.gd | 166 ++++ .../src/asserts/GdUnitObjectAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitResultAssertImpl.gd | 128 +++ .../src/asserts/GdUnitResultAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitSignalAssertImpl.gd | 143 ++++ .../src/asserts/GdUnitSignalAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitStringAssertImpl.gd | 208 +++++ .../src/asserts/GdUnitStringAssertImpl.gd.uid | 1 + .../src/asserts/GdUnitVectorAssertImpl.gd | 187 +++++ .../src/asserts/GdUnitVectorAssertImpl.gd.uid | 1 + addons/gdUnit4/src/asserts/ValueProvider.gd | 10 + .../gdUnit4/src/asserts/ValueProvider.gd.uid | 1 + addons/gdUnit4/src/cmd/CmdArgumentParser.gd | 62 ++ .../gdUnit4/src/cmd/CmdArgumentParser.gd.uid | 1 + addons/gdUnit4/src/cmd/CmdCommand.gd | 27 + addons/gdUnit4/src/cmd/CmdCommand.gd.uid | 1 + addons/gdUnit4/src/cmd/CmdCommandHandler.gd | 136 ++++ .../gdUnit4/src/cmd/CmdCommandHandler.gd.uid | 1 + addons/gdUnit4/src/cmd/CmdOption.gd | 61 ++ addons/gdUnit4/src/cmd/CmdOption.gd.uid | 1 + addons/gdUnit4/src/cmd/CmdOptions.gd | 31 + addons/gdUnit4/src/cmd/CmdOptions.gd.uid | 1 + addons/gdUnit4/src/core/GdArrayTools.gd | 127 +++ addons/gdUnit4/src/core/GdArrayTools.gd.uid | 1 + addons/gdUnit4/src/core/GdDiffTool.gd | 224 +++++ addons/gdUnit4/src/core/GdDiffTool.gd.uid | 1 + addons/gdUnit4/src/core/GdObjects.gd | 726 +++++++++++++++++ addons/gdUnit4/src/core/GdObjects.gd.uid | 1 + addons/gdUnit4/src/core/GdUnit4Version.gd | 65 ++ addons/gdUnit4/src/core/GdUnit4Version.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitFileAccess.gd | 232 ++++++ .../gdUnit4/src/core/GdUnitFileAccess.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitProperty.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitResult.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitRunnerConfig.gd | 126 +++ .../src/core/GdUnitRunnerConfig.gd.uid | 1 + .../gdUnit4/src/core/GdUnitSceneRunnerImpl.gd | 622 ++++++++++++++ .../src/core/GdUnitSceneRunnerImpl.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitSettings.gd | 435 ++++++++++ addons/gdUnit4/src/core/GdUnitSettings.gd.uid | 1 + .../gdUnit4/src/core/GdUnitSignalAwaiter.gd | 81 ++ .../src/core/GdUnitSignalAwaiter.gd.uid | 1 + .../gdUnit4/src/core/GdUnitSignalCollector.gd | 129 +++ .../src/core/GdUnitSignalCollector.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitSignals.gd | 118 +++ addons/gdUnit4/src/core/GdUnitSignals.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitSingleton.gd | 56 ++ .../gdUnit4/src/core/GdUnitSingleton.gd.uid | 1 + .../src/core/GdUnitTestResourceLoader.gd.uid | 1 + .../src/core/GdUnitTestSuiteBuilder.gd | 20 + .../src/core/GdUnitTestSuiteBuilder.gd.uid | 1 + .../src/core/GdUnitTestSuiteScanner.gd.uid | 1 + addons/gdUnit4/src/core/GdUnitTools.gd | 144 ++++ addons/gdUnit4/src/core/GdUnitTools.gd.uid | 1 + .../gdUnit4/src/core/GodotVersionFixures.gd | 11 + .../src/core/GodotVersionFixures.gd.uid | 1 + addons/gdUnit4/src/core/LocalTime.gd | 114 +++ addons/gdUnit4/src/core/LocalTime.gd.uid | 1 + addons/gdUnit4/src/core/_TestCase.gd | 243 ++++++ addons/gdUnit4/src/core/_TestCase.gd.uid | 1 + .../gdUnit4/src/core/assets/touch-button.png | 3 + .../src/core/assets/touch-button.png.import | 8 +- .../src/core/attributes/TestCaseAttribute.gd | 76 ++ .../core/attributes/TestCaseAttribute.gd.uid | 1 + .../gdUnit4/src/core/command/GdUnitCommand.gd | 41 + .../src/core/command/GdUnitCommand.gd.uid | 1 + .../src/core/command/GdUnitCommandHandler.gd | 408 ++++++++++ .../core/command/GdUnitCommandHandler.gd.uid | 1 + .../src/core/command/GdUnitShortcut.gd | 52 ++ .../src/core/command/GdUnitShortcut.gd.uid | 1 + .../src/core/command/GdUnitShortcutAction.gd | 40 + .../core/command/GdUnitShortcutAction.gd.uid | 1 + .../gdUnit4/src/core/discovery/GdUnitGUID.gd | 46 ++ .../src/core/discovery/GdUnitGUID.gd.uid | 1 + .../src/core/discovery/GdUnitTestCase.gd.uid | 1 + .../discovery/GdUnitTestDiscoverGuard.gd.uid | 1 + .../core/discovery/GdUnitTestDiscoverSink.gd | 13 + .../discovery/GdUnitTestDiscoverSink.gd.uid | 1 + .../core/discovery/GdUnitTestDiscoverer.gd | 171 ++++ .../discovery/GdUnitTestDiscoverer.gd.uid | 1 + addons/gdUnit4/src/core/event/GdUnitEvent.gd | 206 +++++ .../gdUnit4/src/core/event/GdUnitEvent.gd.uid | 1 + .../gdUnit4/src/core/event/GdUnitEventInit.gd | 6 + .../src/core/event/GdUnitEventInit.gd.uid | 1 + .../gdUnit4/src/core/event/GdUnitEventStop.gd | 6 + .../src/core/event/GdUnitEventStop.gd.uid | 1 + .../core/event/GdUnitEventTestDiscoverEnd.gd | 19 + .../event/GdUnitEventTestDiscoverEnd.gd.uid | 1 + .../event/GdUnitEventTestDiscoverStart.gd | 6 + .../event/GdUnitEventTestDiscoverStart.gd.uid | 1 + .../src/core/event/GdUnitSessionClose.gd | 6 + .../src/core/event/GdUnitSessionClose.gd.uid | 1 + .../src/core/event/GdUnitSessionStart.gd | 6 + .../src/core/event/GdUnitSessionStart.gd.uid | 1 + .../core/execution/GdUnitExecutionContext.gd | 269 ++++++ .../execution/GdUnitExecutionContext.gd.uid | 1 + .../core/execution/GdUnitMemoryObserver.gd | 135 ++++ .../execution/GdUnitMemoryObserver.gd.uid | 1 + .../execution/GdUnitTestReportCollector.gd | 62 ++ .../GdUnitTestReportCollector.gd.uid | 1 + .../core/execution/GdUnitTestSuiteExecutor.gd | 48 ++ .../execution/GdUnitTestSuiteExecutor.gd.uid | 1 + .../stages/GdUnitTestCaseAfterStage.gd | 22 + .../stages/GdUnitTestCaseAfterStage.gd.uid | 1 + .../stages/GdUnitTestCaseBeforeStage.gd | 19 + .../stages/GdUnitTestCaseBeforeStage.gd.uid | 1 + .../stages/GdUnitTestCaseExecutionStage.gd | 37 + .../GdUnitTestCaseExecutionStage.gd.uid | 1 + .../stages/GdUnitTestSuiteAfterStage.gd | 29 + .../stages/GdUnitTestSuiteAfterStage.gd.uid | 1 + .../stages/GdUnitTestSuiteBeforeStage.gd | 14 + .../stages/GdUnitTestSuiteBeforeStage.gd.uid | 1 + .../stages/GdUnitTestSuiteExecutionStage.gd | 147 ++++ .../GdUnitTestSuiteExecutionStage.gd.uid | 1 + .../execution/stages/IGdUnitExecutionStage.gd | 39 + .../stages/IGdUnitExecutionStage.gd.uid | 1 + .../GdUnitTestCaseFuzzedExecutionStage.gd | 52 ++ .../GdUnitTestCaseFuzzedExecutionStage.gd.uid | 1 + .../fuzzed/GdUnitTestCaseFuzzedTestStage.gd | 55 ++ .../GdUnitTestCaseFuzzedTestStage.gd.uid | 1 + .../GdUnitTestCaseSingleExecutionStage.gd | 53 ++ .../GdUnitTestCaseSingleExecutionStage.gd.uid | 1 + .../single/GdUnitTestCaseSingleTestStage.gd | 11 + .../GdUnitTestCaseSingleTestStage.gd.uid | 1 + .../GdUnitBaseReporterTestSessionHook.gd | 78 ++ .../GdUnitBaseReporterTestSessionHook.gd.uid | 1 + .../GdUnitHtmlReporterTestSessionHook.gd | 9 + .../GdUnitHtmlReporterTestSessionHook.gd.uid | 1 + .../src/core/hooks/GdUnitTestSessionHook.gd | 111 +++ .../core/hooks/GdUnitTestSessionHook.gd.uid | 1 + .../hooks/GdUnitTestSessionHookService.gd | 191 +++++ .../hooks/GdUnitTestSessionHookService.gd.uid | 1 + .../hooks/GdUnitXMLReporterTestSessionHook.gd | 11 + .../GdUnitXMLReporterTestSessionHook.gd.uid | 1 + .../src/core/parse/GdClassDescriptor.gd | 25 + .../src/core/parse/GdClassDescriptor.gd.uid | 1 + .../src/core/parse/GdDefaultValueDecoder.gd | 290 +++++++ .../core/parse/GdDefaultValueDecoder.gd.uid | 1 + .../src/core/parse/GdFunctionArgument.gd.uid | 1 + .../src/core/parse/GdFunctionDescriptor.gd | 286 +++++++ .../core/parse/GdFunctionDescriptor.gd.uid | 1 + .../parse/GdFunctionParameterSetResolver.gd | 188 +++++ .../GdFunctionParameterSetResolver.gd.uid | 1 + .../gdUnit4/src/core/parse/GdScriptParser.gd | 764 ++++++++++++++++++ .../src/core/parse/GdScriptParser.gd.uid | 1 + .../src/core/parse/GdUnitExpressionRunner.gd | 74 ++ .../core/parse/GdUnitExpressionRunner.gd.uid | 1 + .../parse/GdUnitTestParameterSetResolver.gd | 163 ++++ .../GdUnitTestParameterSetResolver.gd.uid | 1 + .../src/core/report/GdUnitReport.gd.uid | 1 + .../core/runners/GdUnitTestCIRunner.gd.uid | 1 + .../src/core/runners/GdUnitTestRunner.gd | 87 ++ .../src/core/runners/GdUnitTestRunner.gd.uid | 1 + .../src/core/runners/GdUnitTestRunner.tscn | 34 + .../src/core/runners/GdUnitTestSession.gd | 169 ++++ .../src/core/runners/GdUnitTestSession.gd.uid | 1 + .../runners/GdUnitTestSessionRunner.gd.uid | 1 + .../GdUnitTestSuiteDefaultTemplate.gd | 36 + .../GdUnitTestSuiteDefaultTemplate.gd.uid | 1 + .../test_suite/GdUnitTestSuiteTemplate.gd.uid | 1 + .../src/core/thread/GdUnitThreadContext.gd | 66 ++ .../core/thread/GdUnitThreadContext.gd.uid | 1 + .../src/core/thread/GdUnitThreadManager.gd | 64 ++ .../core/thread/GdUnitThreadManager.gd.uid | 1 + .../writers/GdUnitCSIMessageWriter.gd.uid | 1 + .../src/core/writers/GdUnitMessageWriter.gd | 214 +++++ .../core/writers/GdUnitMessageWriter.gd.uid | 1 + .../writers/GdUnitRichTextMessageWriter.gd | 115 +++ .../GdUnitRichTextMessageWriter.gd.uid | 1 + .../src/dotnet/GdUnit4CSharpApi.cs.uid | 1 + .../src/dotnet/GdUnit4CSharpApiLoader.gd.uid | 1 + addons/gdUnit4/src/doubler/CallableDoubler.gd | 156 ++++ .../src/doubler/CallableDoubler.gd.uid | 1 + .../gdUnit4/src/doubler/GdFunctionDoubler.gd | 4 + .../src/doubler/GdFunctionDoubler.gd.uid | 1 + .../gdUnit4/src/doubler/GdUnitClassDoubler.gd | 119 +++ .../src/doubler/GdUnitClassDoubler.gd.uid | 1 + .../doubler/GdUnitFunctionDoublerBuilder.gd | 336 ++++++++ .../GdUnitFunctionDoublerBuilder.gd.uid | 1 + .../src/doubler/GdUnitMockFunctionDoubler.gd | 10 + .../doubler/GdUnitMockFunctionDoubler.gd.uid | 1 + .../src/doubler/GdUnitObjectInteractions.gd | 53 ++ .../doubler/GdUnitObjectInteractions.gd.uid | 1 + .../GdUnitObjectInteractionsVerifier.gd | 84 ++ .../GdUnitObjectInteractionsVerifier.gd.uid | 1 + .../src/doubler/GdUnitSpyFunctionDoubler.gd | 8 + .../doubler/GdUnitSpyFunctionDoubler.gd.uid | 1 + .../extractors/GdUnitFuncValueExtractor.gd | 73 ++ .../GdUnitFuncValueExtractor.gd.uid | 1 + addons/gdUnit4/src/fuzzers/BoolFuzzer.gd | 24 + addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid | 1 + addons/gdUnit4/src/fuzzers/FloatFuzzer.gd | 37 + addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid | 1 + addons/gdUnit4/src/fuzzers/Fuzzer.gd | 80 ++ addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid | 1 + addons/gdUnit4/src/fuzzers/IntFuzzer.gd | 77 ++ addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid | 1 + .../gdUnit4/src/fuzzers/StringFuzzer.gd.uid | 1 + addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd | 45 ++ .../gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid | 1 + addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd | 48 ++ .../gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid | 1 + .../src/matchers/AnyArgumentMatcher.gd | 11 + .../src/matchers/AnyArgumentMatcher.gd.uid | 1 + .../matchers/AnyBuildInTypeArgumentMatcher.gd | 50 ++ .../AnyBuildInTypeArgumentMatcher.gd.uid | 1 + .../src/matchers/AnyClazzArgumentMatcher.gd | 32 + .../matchers/AnyClazzArgumentMatcher.gd.uid | 1 + .../src/matchers/ChainedArgumentMatcher.gd | 22 + .../matchers/ChainedArgumentMatcher.gd.uid | 1 + .../src/matchers/EqualsArgumentMatcher.gd | 22 + .../src/matchers/EqualsArgumentMatcher.gd.uid | 1 + .../src/matchers/GdUnitArgumentMatcher.gd | 13 + .../src/matchers/GdUnitArgumentMatcher.gd.uid | 1 + .../src/matchers/GdUnitArgumentMatchers.gd | 42 + .../matchers/GdUnitArgumentMatchers.gd.uid | 1 + addons/gdUnit4/src/mocking/GdUnitMock.gd | 45 ++ addons/gdUnit4/src/mocking/GdUnitMock.gd.uid | 1 + .../src/mocking/GdUnitMockBuilder.gd.uid | 1 + .../gdUnit4/src/mocking/GdUnitMockImpl.gd.uid | 1 + addons/gdUnit4/src/monitor/ErrorLogEntry.gd | 72 ++ .../gdUnit4/src/monitor/ErrorLogEntry.gd.uid | 1 + addons/gdUnit4/src/monitor/GdUnitMonitor.gd | 24 + .../gdUnit4/src/monitor/GdUnitMonitor.gd.uid | 1 + .../src/monitor/GdUnitOrphanNodesMonitor.gd | 27 + .../monitor/GdUnitOrphanNodesMonitor.gd.uid | 1 + .../src/monitor/GodotGdErrorMonitor.gd | 103 +++ .../src/monitor/GodotGdErrorMonitor.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitServer.gd | 42 + .../gdUnit4/src/network/GdUnitServer.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitServer.tscn | 10 + .../src/network/GdUnitServerConstants.gd | 6 + .../src/network/GdUnitServerConstants.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitTask.gd | 25 + addons/gdUnit4/src/network/GdUnitTask.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitTcpClient.gd | 124 +++ .../src/network/GdUnitTcpClient.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitTcpNode.gd | 73 ++ .../gdUnit4/src/network/GdUnitTcpNode.gd.uid | 1 + addons/gdUnit4/src/network/GdUnitTcpServer.gd | 129 +++ .../src/network/GdUnitTcpServer.gd.uid | 1 + addons/gdUnit4/src/network/rpc/RPC.gd | 37 + addons/gdUnit4/src/network/rpc/RPC.gd.uid | 1 + .../src/network/rpc/RPCClientConnect.gd.uid | 1 + .../src/network/rpc/RPCClientDisconnect.gd | 13 + .../network/rpc/RPCClientDisconnect.gd.uid | 1 + .../gdUnit4/src/network/rpc/RPCGdUnitEvent.gd | 14 + .../src/network/rpc/RPCGdUnitEvent.gd.uid | 1 + addons/gdUnit4/src/network/rpc/RPCMessage.gd | 18 + .../gdUnit4/src/network/rpc/RPCMessage.gd.uid | 1 + .../src/reporters/GdUnitReportSummary.gd | 202 +++++ .../src/reporters/GdUnitReportSummary.gd.uid | 1 + .../src/reporters/GdUnitReportWriter.gd | 12 + .../src/reporters/GdUnitReportWriter.gd.uid | 1 + .../src/reporters/GdUnitTestCaseReport.gd | 47 ++ .../src/reporters/GdUnitTestCaseReport.gd.uid | 1 + .../src/reporters/GdUnitTestReporter.gd | 112 +++ .../src/reporters/GdUnitTestReporter.gd.uid | 1 + .../src/reporters/GdUnitTestSuiteReport.gd | 96 +++ .../reporters/GdUnitTestSuiteReport.gd.uid | 1 + .../console/GdUnitConsoleTestReporter.gd | 234 ++++++ .../console/GdUnitConsoleTestReporter.gd.uid | 1 + .../src/reporters/html/GdUnitByPathReport.gd | 60 ++ .../reporters/html/GdUnitByPathReport.gd.uid | 1 + .../src/reporters/html/GdUnitHtmlPatterns.gd | 199 +++++ .../reporters/html/GdUnitHtmlPatterns.gd.uid | 1 + .../reporters/html/GdUnitHtmlReportWriter.gd | 72 ++ .../html/GdUnitHtmlReportWriter.gd.uid | 1 + .../html/template/css/breadcrumb.css | 66 ++ .../src/reporters/html/template/css/logo.png | 3 + .../reporters/html/template/css/styles.css | 475 +++++++++++ .../html/template/folder_report.html | 122 +++ .../src/reporters/html/template/index.html | 164 ++++ .../reporters/html/template/suite_report.html | 177 ++++ .../src/reporters/xml/JUnitXmlReportWriter.gd | 143 ++++ .../reporters/xml/JUnitXmlReportWriter.gd.uid | 1 + .../gdUnit4/src/reporters/xml/XmlElement.gd | 69 ++ .../src/reporters/xml/XmlElement.gd.uid | 1 + addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd | 154 ++++ .../gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid | 1 + addons/gdUnit4/src/spy/GdUnitSpyImpl.gd | 46 ++ addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid | 1 + addons/gdUnit4/src/ui/GdUnitConsole.gd | 91 +++ addons/gdUnit4/src/ui/GdUnitConsole.gd.uid | 1 + addons/gdUnit4/src/ui/GdUnitConsole.tscn | 64 ++ addons/gdUnit4/src/ui/GdUnitFonts.gd.uid | 1 + addons/gdUnit4/src/ui/GdUnitInspector.gd | 31 + addons/gdUnit4/src/ui/GdUnitInspector.gd.uid | 1 + addons/gdUnit4/src/ui/GdUnitInspector.tscn | 71 ++ .../src/ui/GdUnitInspectorTreeConstants.gd | 31 + .../ui/GdUnitInspectorTreeConstants.gd.uid | 1 + addons/gdUnit4/src/ui/GdUnitUiTools.gd | 151 ++++ addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid | 1 + addons/gdUnit4/src/ui/ScriptEditorControls.gd | 100 +++ .../src/ui/ScriptEditorControls.gd.uid | 1 + .../EditorFileSystemContextMenuHandler.gd | 79 ++ .../EditorFileSystemContextMenuHandler.gd.uid | 1 + .../EditorFileSystemContextMenuHandlerV44.gdx | 47 ++ .../src/ui/menu/GdUnitContextMenuItem.gd | 69 ++ .../src/ui/menu/GdUnitContextMenuItem.gd.uid | 1 + .../ui/menu/ScriptEditorContextMenuHandler.gd | 81 ++ .../ScriptEditorContextMenuHandler.gd.uid | 1 + .../ScriptEditorContextMenuHandlerV44.gdx | 33 + .../gdUnit4/src/ui/parts/InspectorMonitor.gd | 54 ++ .../src/ui/parts/InspectorMonitor.gd.uid | 1 + .../src/ui/parts/InspectorMonitor.tscn | 94 +++ .../src/ui/parts/InspectorProgressBar.gd | 49 ++ .../src/ui/parts/InspectorProgressBar.gd.uid | 1 + .../src/ui/parts/InspectorProgressBar.tscn | 33 + .../src/ui/parts/InspectorStatusBar.gd | 216 +++++ .../src/ui/parts/InspectorStatusBar.gd.uid | 1 + .../src/ui/parts/InspectorStatusBar.tscn | 477 +++++++++++ .../gdUnit4/src/ui/parts/InspectorToolBar.gd | 130 +++ .../src/ui/parts/InspectorToolBar.gd.uid | 1 + .../src/ui/parts/InspectorToolBar.tscn | 212 +++++ .../ui/parts/InspectorTreeMainPanel.gd.uid | 1 + .../src/ui/parts/InspectorTreePanel.tscn | 273 +++++++ .../src/ui/settings/GdUnitInputCapture.gd | 56 ++ .../src/ui/settings/GdUnitInputCapture.gd.uid | 1 + .../src/ui/settings/GdUnitInputCapture.tscn | 36 + .../ui/settings/GdUnitSettingsDialog.gd.uid | 1 + .../src/ui/settings/GdUnitSettingsDialog.tscn | 349 ++++++++ .../src/ui/settings/GdUnitSettingsTabHooks.gd | 255 ++++++ .../ui/settings/GdUnitSettingsTabHooks.gd.uid | 1 + .../ui/settings/GdUnitSettingsTabHooks.tscn | 148 ++++ addons/gdUnit4/src/ui/settings/logo.png | 3 + .../gdUnit4/src/ui/settings/logo.png.import | 8 +- .../src/ui/templates/TestSuiteTemplate.gd.uid | 1 + .../src/ui/templates/TestSuiteTemplate.tscn | 127 +++ addons/gdUnit4/src/update/GdMarkDownReader.gd | 405 ++++++++++ .../src/update/GdMarkDownReader.gd.uid | 1 + addons/gdUnit4/src/update/GdUnitPatch.gd | 20 + addons/gdUnit4/src/update/GdUnitPatch.gd.uid | 1 + addons/gdUnit4/src/update/GdUnitPatcher.gd | 75 ++ .../gdUnit4/src/update/GdUnitPatcher.gd.uid | 1 + addons/gdUnit4/src/update/GdUnitUpdate.gd | 305 +++++++ addons/gdUnit4/src/update/GdUnitUpdate.gd.uid | 1 + addons/gdUnit4/src/update/GdUnitUpdate.tscn | 100 +++ .../gdUnit4/src/update/GdUnitUpdateClient.gd | 98 +++ .../src/update/GdUnitUpdateClient.gd.uid | 1 + .../gdUnit4/src/update/GdUnitUpdateNotify.gd | 206 +++++ .../src/update/GdUnitUpdateNotify.gd.uid | 1 + .../src/update/GdUnitUpdateNotify.tscn | 97 +++ .../src/update/assets/border_bottom.png | 3 + .../update/assets/border_bottom.png.import | 8 +- .../gdUnit4/src/update/assets/border_top.png | 3 + .../src/update/assets/border_top.png.import | 8 +- addons/gdUnit4/src/update/assets/dot1.png | 3 + .../gdUnit4/src/update/assets/dot1.png.import | 8 +- addons/gdUnit4/src/update/assets/dot2.png | 3 + .../gdUnit4/src/update/assets/dot2.png.import | 8 +- addons/gdUnit4/src/update/assets/embedded.png | 3 + .../src/update/assets/embedded.png.import | 8 +- .../src/update/assets/horizontal-line2.png | 3 + .../update/assets/horizontal-line2.png.import | 8 +- project.godot | 2 +- 438 files changed, 22833 insertions(+), 17 deletions(-) diff --git a/addons/gdUnit4/LICENSE b/addons/gdUnit4/LICENSE index e69de29b..8c60d132 100644 --- a/addons/gdUnit4/LICENSE +++ b/addons/gdUnit4/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mike Schulze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index e69de29b..cae9138b 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -0,0 +1,21 @@ +#!/usr/bin/env -S godot -s +extends SceneTree + + +var _cli_runner: GdUnitTestCIRunner + + +func _initialize() -> void: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + _cli_runner = GdUnitTestCIRunner.new() + root.add_child(_cli_runner) + + +# do not use print statements on _finalize it results in random crashes +func _finalize() -> void: + queue_delete(_cli_runner) + if OS.is_stdout_verbose(): + prints("Finallize ..") + prints("-Orphan nodes report-----------------------") + Window.print_orphan_nodes() + prints("Finallize .. done") diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid index e69de29b..9e751a61 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid @@ -0,0 +1 @@ +uid://do2c2faoehm61 diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd index e69de29b..8b228051 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -0,0 +1,167 @@ +#!/usr/bin/env -S godot -s +extends MainLoop + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +# gdlint: disable=max-line-length +const LOG_FRAME_TEMPLATE = """ + + + + + + Godot Logging + + + + +
+${content} +
+ + +""" + +const NO_LOG_MESSAGE = """ +

No logging available!

+
+

In order for logging to take place, you must activate the Activate file logging option in the project settings.

+

You can enable the logging under: +Project Settings > Debug > File Logging > Enable File Logging in the project settings.

+""" + +#warning-ignore-all:return_value_discarded +var _cmd_options := CmdOptions.new([ + CmdOption.new( + "-rd, --report-directory", + "-rd ", + "Specifies the output directory in which the reports are to be written. The default is res://reports/.", + TYPE_STRING, + true + ) + ]) + + +var _report_root_path: String +var _current_report_path: String +var _debug_cmd_args := PackedStringArray() + + +func _init() -> void: + set_report_directory(GdUnitFileAccess.current_dir() + "reports") + set_current_report_path() + + +func _process(_delta: float) -> bool: + # check if reports exists + if not reports_available(): + prints("no reports found") + return true + + # only process if godot logging is enabled + if not GdUnitSettings.is_log_enabled(): + write_report(NO_LOG_MESSAGE, "") + return true + + # parse possible custom report path, + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") + # ignore erros and exit quitly + if cmd_parser.parse(get_cmdline_args(), true).is_error(): + return true + CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory) + + var godot_log_file := scan_latest_godot_log() + var result := read_log_file_content(godot_log_file) + if result.is_error(): + write_report(result.error_message(), godot_log_file) + return true + write_report(result.value_as_string(), godot_log_file) + return true + + +func set_current_report_path() -> void: + # scan for latest report directory + var iteration := GdUnitFileAccess.find_last_path_index( + _report_root_path, GdUnitConstants.REPORT_DIR_PREFIX + ) + _current_report_path = "%s/%s%d" % [_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX, iteration] + + +func set_report_directory(path: String) -> void: + _report_root_path = path + + +func get_log_report_html() -> String: + return _current_report_path + "/godot_report_log.html" + + +func reports_available() -> bool: + return DirAccess.dir_exists_absolute(_report_root_path) + + +func scan_latest_godot_log() -> String: + var path := GdUnitSettings.get_log_path().get_base_dir() + var files_sorted := Array() + for file in GdUnitFileAccess.scan_dir(path): + var file_name := "%s/%s" % [path, file] + files_sorted.append(file_name) + # sort by name, the name contains the timestamp so we sort at the end by timestamp + files_sorted.sort() + return files_sorted.back() + + +func read_log_file_content(log_file: String) -> GdUnitResult: + var file := FileAccess.open(log_file, FileAccess.READ) + if file == null: + return GdUnitResult.error( + "Can't find log file '%s'. Error: %s" + % [log_file, error_string(FileAccess.get_open_error())] + ) + var content := "
" + file.get_as_text()
+	# patch out console format codes
+	for color_index in range(0, 256):
+		var to_replace := "[38;5;%dm" % color_index
+		content = content.replace(to_replace, "")
+	content += "
" + content = content\ + .replace("", "")\ + .replace(GdUnitCSIMessageWriter.CSI_BOLD, "")\ + .replace(GdUnitCSIMessageWriter.CSI_ITALIC, "")\ + .replace(GdUnitCSIMessageWriter.CSI_UNDERLINE, "") + return GdUnitResult.success(content) + + +func write_report(content: String, godot_log_file: String) -> GdUnitResult: + var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE) + if file == null: + return GdUnitResult.error( + "Can't open to write '%s'. Error: %s" + % [get_log_report_html(), error_string(FileAccess.get_open_error())] + ) + var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content) + file.store_string(report_html) + _update_index_html(godot_log_file) + return GdUnitResult.success(file) + + +func _update_index_html(godot_log_file: String) -> void: + var index_path := "%s/index.html" % _current_report_path + var index_file := FileAccess.open(index_path, FileAccess.READ_WRITE) + if index_file == null: + push_error( + "Can't add log path '%s' to `%s`. Error: %s" + % [godot_log_file, index_path, error_string(FileAccess.get_open_error())] + ) + return + var content := index_file.get_as_text()\ + .replace("${log_report}", get_log_report_html())\ + .replace("${godot_log_file}", godot_log_file) + # overide it + index_file.seek(0) + index_file.store_string(content) + + +func get_cmdline_args() -> PackedStringArray: + if _debug_cmd_args.is_empty(): + return OS.get_cmdline_args() + return _debug_cmd_args diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid index e69de29b..d5b381ef 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid @@ -0,0 +1 @@ +uid://bretpek2ehht4 diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg index e69de29b..ca818277 100644 --- a/addons/gdUnit4/plugin.cfg +++ b/addons/gdUnit4/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="gdUnit4" +description="Unit Testing Framework for Godot Scripts" +author="Mike Schulze" +version="6.0.3" +script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index e69de29b..822c4d56 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -0,0 +1,110 @@ +@tool +extends EditorPlugin + +# We need to define manually the slot id's, to be downwards compatible +const CONTEXT_SLOT_FILESYSTEM: int = 1 # EditorContextMenuPlugin.CONTEXT_SLOT_FILESYSTEM +const CONTEXT_SLOT_SCRIPT_EDITOR: int = 2 # EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR + +var _gd_inspector: Control +var _gd_console: Control +var _gd_filesystem_context_menu: Variant +var _gd_scripteditor_context_menu: Variant + + +func _enter_tree() -> void: + + var inferred_declaration: int = ProjectSettings.get_setting("debug/gdscript/warnings/inferred_declaration") + var exclude_addons: bool = ProjectSettings.get_setting("debug/gdscript/warnings/exclude_addons") + if !exclude_addons and inferred_declaration != 0: + printerr("GdUnit4: 'inferred_declaration' is set to Warning/Error!") + printerr("GdUnit4 is not 'inferred_declaration' save, you have to excluded addons (debug/gdscript/warnings/exclude_addons)") + printerr("Loading GdUnit4 Plugin failed.") + return + + if check_running_in_test_env(): + @warning_ignore("return_value_discarded") + GdUnitCSIMessageWriter.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!") + return + + if Engine.get_version_info().hex < 0x40500: + prints("This GdUnit4 plugin version '%s' requires Godot version '4.5' or higher to run." % GdUnit4Version.current()) + return + GdUnitSettings.setup() + # Install the GdUnit Inspector + _gd_inspector = (load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn") as PackedScene).instantiate() + _add_context_menus() + add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector) + # Install the GdUnit Console + _gd_console = (load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn") as PackedScene).instantiate() + var control: Control = add_control_to_bottom_panel(_gd_console, "gdUnitConsole") + @warning_ignore("unsafe_method_access") + await _gd_console.setup_update_notification(control) + if GdUnit4CSharpApiLoader.is_api_loaded(): + prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) + else: + prints("No GdUnit4Net found.") + # Connect to be notified for script changes to be able to discover new tests + GdUnitTestDiscoverGuard.instance() + @warning_ignore("return_value_discarded") + resource_saved.connect(_on_resource_saved) + prints("Loading GdUnit4 Plugin success") + + +func _exit_tree() -> void: + if check_running_in_test_env(): + return + if is_instance_valid(_gd_inspector): + remove_control_from_docks(_gd_inspector) + _gd_inspector.free() + _remove_context_menus() + if is_instance_valid(_gd_console): + remove_control_from_bottom_panel(_gd_console) + _gd_console.free() + var gdUnitTools: GDScript = load("res://addons/gdUnit4/src/core/GdUnitTools.gd") + @warning_ignore("unsafe_method_access") + gdUnitTools.dispose_all(true) + prints("Unload GdUnit4 Plugin success") + + +func check_running_in_test_env() -> bool: + var args: PackedStringArray = OS.get_cmdline_args() + args.append_array(OS.get_cmdline_user_args()) + return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import") + + +func _add_context_menus() -> void: + if Engine.get_version_info().hex >= 0x40400: + # With Godot 4.4 we have to use the 'add_context_menu_plugin' to register editor context menus + _gd_filesystem_context_menu = _preload_gdx_script("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx") + call_deferred("add_context_menu_plugin", CONTEXT_SLOT_FILESYSTEM, _gd_filesystem_context_menu) + # the CONTEXT_SLOT_SCRIPT_EDITOR is adding to the script panel instead of script editor see https://github.com/godotengine/godot/pull/100556 + #_gd_scripteditor_context_menu = _preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx") + #call_deferred("add_context_menu_plugin", CONTEXT_SLOT_SCRIPT_EDITOR, _gd_scripteditor_context_menu) + # so we use the old hacky way to add the context menu + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new()) + else: + # TODO Delete it if the minimum requirement for the plugin is set to Godot 4.4. + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd").new()) + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new()) + + +func _remove_context_menus() -> void: + if is_instance_valid(_gd_filesystem_context_menu): + call_deferred("remove_context_menu_plugin", _gd_filesystem_context_menu) + if is_instance_valid(_gd_scripteditor_context_menu): + call_deferred("remove_context_menu_plugin", _gd_scripteditor_context_menu) + + +func _preload_gdx_script(script_path: String) -> Variant: + var script: GDScript = GDScript.new() + script.source_code = GdUnitFileAccess.resource_as_string(script_path) + script.take_over_path(script_path) + var err :Error = script.reload() + if err != OK: + push_error("Can't create context menu %s, error: %s" % [script_path, error_string(err)]) + return script.new() + + +func _on_resource_saved(resource: Resource) -> void: + if resource is Script: + await GdUnitTestDiscoverGuard.instance().discover(resource as Script) diff --git a/addons/gdUnit4/plugin.gd.uid b/addons/gdUnit4/plugin.gd.uid index e69de29b..5e8143a9 100644 --- a/addons/gdUnit4/plugin.gd.uid +++ b/addons/gdUnit4/plugin.gd.uid @@ -0,0 +1 @@ +uid://bc4fimf6ynr5d diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd index e69de29b..ad5da4d9 100644 --- a/addons/gdUnit4/runtest.cmd +++ b/addons/gdUnit4/runtest.cmd @@ -0,0 +1,62 @@ +@echo off +setlocal enabledelayedexpansion + +:: Initialize variables +set "godot_binary=" +set "filtered_args=" + +:: Process all arguments +set "i=0" +:parse_args +if "%~1"=="" goto end_parse_args + +if "%~1"=="--godot_binary" ( + set "godot_binary=%~2" + shift + shift +) else ( + set "filtered_args=!filtered_args! %~1" + shift +) +goto parse_args +:end_parse_args + +:: If --godot_binary wasn't provided, fallback to environment variable +if "!godot_binary!"=="" ( + set "godot_binary=%GODOT_BIN%" +) + +:: Check if we have a godot_binary value from any source +if "!godot_binary!"=="" ( + echo Godot binary path is not specified. + echo Please either: + echo - Set the environment variable: set GODOT_BIN=C:\path\to\godot.exe + echo - Or use the --godot_binary argument: --godot_binary C:\path\to\godot.exe + exit /b 1 +) + +:: Check if the Godot binary exists +if not exist "!godot_binary!" ( + echo Error: The specified Godot binary '!godot_binary!' does not exist. + exit /b 1 +) + +:: Get Godot version and check if it's a mono build +for /f "tokens=*" %%i in ('"!godot_binary!" --version') do set GODOT_VERSION=%%i +echo !GODOT_VERSION! | findstr /I "mono" >nul +if !errorlevel! equ 0 ( + echo Godot .NET detected + echo Compiling c# classes ... Please Wait + dotnet build --debug + echo done !errorlevel! +) + +:: Run the tests with the filtered arguments +"!godot_binary!" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd !filtered_args! +set exit_code=%ERRORLEVEL% +echo Run tests ends with %exit_code% + +:: Run the copy log command +"!godot_binary!" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd !filtered_args! > nul +set exit_code2=%ERRORLEVEL% +exit /b %exit_code% diff --git a/addons/gdUnit4/runtest.sh b/addons/gdUnit4/runtest.sh index e69de29b..f0269efb 100644 --- a/addons/gdUnit4/runtest.sh +++ b/addons/gdUnit4/runtest.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Check for command-line argument +godot_binary="" +filtered_args="" + +# Process all arguments with a more compatible approach +while [ $# -gt 0 ]; do + if [ "$1" = "--godot_binary" ] && [ $# -gt 1 ]; then + # Get the next argument as the value + godot_binary="$2" + shift 2 + else + # Keep non-godot_binary arguments for passing to Godot + filtered_args="$filtered_args $1" + shift + fi +done + +# If --godot_binary wasn't provided, fallback to environment variable +if [ -z "$godot_binary" ]; then + godot_binary="$GODOT_BIN" +fi + +# Check if we have a godot_binary value from any source +if [ -z "$godot_binary" ]; then + echo "Godot binary path is not specified." + echo "Please either:" + echo " - Set the environment variable: export GODOT_BIN=/path/to/godot" + echo " - Or use the --godot_binary argument: --godot_binary /path/to/godot" + exit 1 +fi + +# Check if the Godot binary exists and is executable +if [ ! -f "$godot_binary" ]; then + echo "Error: The specified Godot binary '$godot_binary' does not exist." + exit 1 +fi + +if [ ! -x "$godot_binary" ]; then + echo "Error: The specified Godot binary '$godot_binary' is not executable." + exit 1 +fi + +# Get Godot version and check if it's a .NET build +GODOT_VERSION=$("$godot_binary" --version) +if echo "$GODOT_VERSION" | grep -i "mono" > /dev/null; then + echo "Godot .NET detected" + echo "Compiling c# classes ... Please Wait" + dotnet build --debug + echo "done $?" +fi + +# Run the tests with the filtered arguments +"$godot_binary" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $filtered_args +exit_code=$? +echo "Run tests ends with $exit_code" + +# Run the copy log command +"$godot_binary" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd $filtered_args > /dev/null +exit_code2=$? +exit $exit_code diff --git a/addons/gdUnit4/src/Comparator.gd b/addons/gdUnit4/src/Comparator.gd index e69de29b..096088a6 100644 --- a/addons/gdUnit4/src/Comparator.gd +++ b/addons/gdUnit4/src/Comparator.gd @@ -0,0 +1,12 @@ +class_name Comparator +extends Resource + +enum { + EQUAL, + LESS_THAN, + LESS_EQUAL, + GREATER_THAN, + GREATER_EQUAL, + BETWEEN_EQUAL, + NOT_BETWEEN_EQUAL, +} diff --git a/addons/gdUnit4/src/Comparator.gd.uid b/addons/gdUnit4/src/Comparator.gd.uid index e69de29b..73ac82a9 100644 --- a/addons/gdUnit4/src/Comparator.gd.uid +++ b/addons/gdUnit4/src/Comparator.gd.uid @@ -0,0 +1 @@ +uid://buiskkw1yyuw3 diff --git a/addons/gdUnit4/src/Fuzzers.gd b/addons/gdUnit4/src/Fuzzers.gd index e69de29b..c8689df3 100644 --- a/addons/gdUnit4/src/Fuzzers.gd +++ b/addons/gdUnit4/src/Fuzzers.gd @@ -0,0 +1,80 @@ +## Factory class providing convenient static methods to create various fuzzer instances.[br] +## +## Fuzzers is a utility class that simplifies the creation of different fuzzer types +## for testing purposes. It provides static factory methods that create pre-configured +## fuzzers with sensible defaults, making it easier to set up fuzz testing in your +## test suites without manually instantiating each fuzzer type.[br] +## +## This class acts as a central access point for all fuzzer types, improving code +## readability and reducing boilerplate in test cases.[br] +## +## @tutorial(Fuzzing Testing): https://en.wikipedia.org/wiki/Fuzzing +class_name Fuzzers +extends Resource + + +## Generates random strings with length between [param min_length] and +## [param max_length] (inclusive), using characters from [param charset]. +## See [StringFuzzer] for detailed documentation and examples. +static func rand_str(min_length: int, max_length: int, charset := StringFuzzer.DEFAULT_CHARSET) -> StringFuzzer: + return StringFuzzer.new(min_length, max_length, charset) + + +## Creates a [BoolFuzzer] for generating random boolean values.[br] +## +## See [BoolFuzzer] for detailed documentation and examples. +static func boolean() -> BoolFuzzer: + return BoolFuzzer.new() + + +## Creates an [IntFuzzer] for generating random integers within a range.[br] +## +## Generates random integers between [param from] and [param to] (inclusive) +## using [constant IntFuzzer.NORMAL] mode. +## See [IntFuzzer] for detailed documentation and examples. +static func rangei(from: int, to: int) -> IntFuzzer: + return IntFuzzer.new(from, to) + + +## Creates a [FloatFuzzer] for generating random floats within a range.[br] +## +## Generates random float values between [param from] and [param to] (inclusive). +## See [FloatFuzzer] for detailed documentation and examples. +static func rangef(from: float, to: float) -> FloatFuzzer: + return FloatFuzzer.new(from, to) + + +## Creates a [Vector2Fuzzer] for generating random 2D vectors within a range.[br] +## +## Generates random Vector2 values where each component is bounded by +## [param from] and [param to] (inclusive). +## See [Vector2Fuzzer] for detailed documentation and examples. +static func rangev2(from: Vector2, to: Vector2) -> Vector2Fuzzer: + return Vector2Fuzzer.new(from, to) + + +## Creates a [Vector3Fuzzer] for generating random 3D vectors within a range.[br] +## +## Generates random Vector3 values where each component is bounded by +## [param from] and [param to] (inclusive). +## See [Vector3Fuzzer] for detailed documentation and examples. +static func rangev3(from: Vector3, to: Vector3) -> Vector3Fuzzer: + return Vector3Fuzzer.new(from, to) + + +## Creates an [IntFuzzer] that generates only even integers.[br] +## +## Generates random even integers between [param from] and [param to] (inclusive) +## using [constant IntFuzzer.EVEN] mode. +## See [IntFuzzer] for detailed documentation about even number generation. +static func eveni(from: int, to: int) -> IntFuzzer: + return IntFuzzer.new(from, to, IntFuzzer.EVEN) + + +## Creates an [IntFuzzer] that generates only odd integers.[br] +## +## Generates random odd integers between [param from] and [param to] (inclusive) +## using [constant IntFuzzer.ODD] mode. +## See [IntFuzzer] for detailed documentation about odd number generation. +static func oddi(from: int, to: int) -> IntFuzzer: + return IntFuzzer.new(from, to, IntFuzzer.ODD) diff --git a/addons/gdUnit4/src/Fuzzers.gd.uid b/addons/gdUnit4/src/Fuzzers.gd.uid index e69de29b..1d752c61 100644 --- a/addons/gdUnit4/src/Fuzzers.gd.uid +++ b/addons/gdUnit4/src/Fuzzers.gd.uid @@ -0,0 +1 @@ +uid://drfioswpw8u2u diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd b/addons/gdUnit4/src/GdUnitArrayAssert.gd index e69de29b..eeb7ca6b 100644 --- a/addons/gdUnit4/src/GdUnitArrayAssert.gd +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd @@ -0,0 +1,122 @@ +## An Assertion Tool to verify array values +@abstract class_name GdUnitArrayAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitArrayAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitArrayAssert + + +## Verifies that the current Array is equal to the given one. +@abstract func is_equal(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array is equal to the given one, ignoring case considerations. +@abstract func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array is not equal to the given one. +@abstract func is_not_equal(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array is not equal to the given one, ignoring case considerations. +@abstract func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitArrayAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitArrayAssert + + +## Verifies that the current Array is empty, it has a size of 0. +@abstract func is_empty() -> GdUnitArrayAssert + + +## Verifies that the current Array is not empty, it has a size of minimum 1. +@abstract func is_not_empty() -> GdUnitArrayAssert + + +## Verifies that the current Array is the same. [br] +## Compares the current by object reference equals +@abstract func is_same(expected: Variant) -> GdUnitArrayAssert + + +## Verifies that the current Array is NOT the same. [br] +## Compares the current by object reference equals +@abstract func is_not_same(expected: Variant) -> GdUnitArrayAssert + + +## Verifies that the current Array has a size of given value. +@abstract func has_size(expectd: int) -> GdUnitArrayAssert + + +## Verifies that the current Array contains the given values, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same] +@abstract func contains(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly] +@abstract func contains_exactly(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order] +@abstract func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array contains the given values, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains] +@abstract func contains_same(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains_exactly] +@abstract func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order] +@abstract func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array do NOT contains the given values, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same] +## [b]Example:[/b] +## [codeblock] +## # will succeed +## assert_array([1, 2, 3, 4, 5]).not_contains(6) +## # will fail +## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6) +## [/codeblock] +@abstract func not_contains(...expected: Array) -> GdUnitArrayAssert + + +## Verifies that the current Array do NOT contains the given values, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method not_contains] +## [b]Example:[/b] +## [codeblock] +## # will succeed +## assert_array([1, 2, 3, 4, 5]).not_contains(6) +## # will fail +## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6) +## [/codeblock] +@abstract func not_contains_same(...expected: Array) -> GdUnitArrayAssert + + +## Extracts all values by given function name and optional arguments into a new ArrayAssert. +## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values +@abstract func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert + + +## Extracts all values by given extractor's into a new ArrayAssert. +## If the elements not extractable than the value is converted to `"n.a"`, expecting null values +## -- The argument type is Array[GdUnitValueExtractor] +@abstract func extractv(...extractors: Array) -> GdUnitArrayAssert diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid index e69de29b..bf8d079e 100644 --- a/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid @@ -0,0 +1 @@ +uid://byeulsiqvaugq diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd index e69de29b..41382d9f 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -0,0 +1,47 @@ +## Base interface of all GdUnit asserts +@abstract class_name GdUnitAssert +extends RefCounted + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitAssert + + +## Verifies that the current value is equal to expected one. +@abstract func is_equal(expected: Variant) -> GdUnitAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitAssert + + +## Overrides the default failure message by given custom message.[br] +## This function allows you to replace the automatically generated failure message with a more specific +## or user-friendly message that better describes the test failure context.[br] +## Usage: +## [codeblock] +## # Override with custom context-specific message +## func test_player_inventory(): +## assert_that(player.get_item_count("sword"))\ +## .override_failure_message("Player should have exactly one sword")\ +## .is_equal(1) +## [/codeblock] +@abstract func override_failure_message(message: String) -> GdUnitAssert + + +## Appends a custom message to the failure message.[br] +## This can be used to add additional information to the generated failure message +## while keeping the original assertion details for better debugging context.[br] +## Usage: +## [codeblock] +## # Add context to existing failure message +## func test_player_health(): +## assert_that(player.health)\ +## .append_failure_message("Player was damaged by: %s" % last_damage_source)\ +## .is_greater(0) +## [/codeblock] +@abstract func append_failure_message(message: String) -> GdUnitAssert diff --git a/addons/gdUnit4/src/GdUnitAssert.gd.uid b/addons/gdUnit4/src/GdUnitAssert.gd.uid index e69de29b..aa0a9cf2 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitAssert.gd.uid @@ -0,0 +1 @@ +uid://bmy2nu4w22wia diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd index e69de29b..51385e88 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -0,0 +1,72 @@ +class_name GdUnitAwaiter +extends RefCounted + + +# Waits for a specified signal in an interval of 50ms sent from the , and terminates with an error after the specified timeout has elapsed. +# source: the object from which the signal is emitted +# signal_name: signal name +# args: the expected signal arguments as an array +# timeout: the timeout in ms, default is set to 2000ms +func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + # fail fast if the given source instance invalid + var assert_that := GdUnitAssertImpl.new(signal_name) + var line_number := GdUnitAssertions.get_line_number() + if not is_instance_valid(source): + @warning_ignore("return_value_discarded") + assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await (Engine.get_main_loop() as SceneTree).process_frame + # fail fast if the given source instance invalid + if not is_instance_valid(source): + @warning_ignore("return_value_discarded") + assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await await_idle_frame() + var awaiter := GdUnitSignalAwaiter.new(timeout_millis) + var value :Variant = await awaiter.on_signal(source, signal_name, args) + if awaiter.is_interrupted(): + var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + @warning_ignore("return_value_discarded") + assert_that.report_error(failure, line_number) + return value + + +# Waits for a specified signal sent from the between idle frames and aborts with an error after the specified timeout has elapsed +# source: the object from which the signal is emitted +# signal_name: signal name +# args: the expected signal arguments as an array +# timeout: the timeout in ms, default is set to 2000ms +func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + var line_number := GdUnitAssertions.get_line_number() + # fail fast if the given source instance invalid + if not is_instance_valid(source): + @warning_ignore("return_value_discarded") + GdUnitAssertImpl.new(signal_name)\ + .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await await_idle_frame() + var awaiter := GdUnitSignalAwaiter.new(timeout_millis, true) + var value :Variant = await awaiter.on_signal(source, signal_name, args) + if awaiter.is_interrupted(): + var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + @warning_ignore("return_value_discarded") + GdUnitAssertImpl.new(signal_name).report_error(failure, line_number) + return value + + +# Waits for for a given amount of milliseconds +# example: +# # waits for 100ms +# await GdUnitAwaiter.await_millis(myNode, 100).completed +# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out +func await_millis(milliSec :int) -> void: + var timer :Timer = Timer.new() + timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id()) + (Engine.get_main_loop() as SceneTree).root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + timer.start(milliSec / 1000.0) + await timer.timeout + timer.queue_free() + + +# Waits until the next idle frame +func await_idle_frame() -> void: + await (Engine.get_main_loop() as SceneTree).process_frame diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd.uid b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid index e69de29b..5eda3099 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd.uid +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid @@ -0,0 +1 @@ +uid://c1jp2le4lldby diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd b/addons/gdUnit4/src/GdUnitBoolAssert.gd index e69de29b..714f8fc5 100644 --- a/addons/gdUnit4/src/GdUnitBoolAssert.gd +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd @@ -0,0 +1,35 @@ +## An Assertion Tool to verify boolean values +@abstract class_name GdUnitBoolAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitBoolAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitBoolAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitBoolAssert + + +## Verifies that the current value is not equal to the given one. +@abstract func is_not_equal(expected: Variant) -> GdUnitBoolAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitBoolAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitBoolAssert + + +## Verifies that the current value is true. +@abstract func is_true() -> GdUnitBoolAssert + + +## Verifies that the current value is false. +@abstract func is_false() -> GdUnitBoolAssert diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid index e69de29b..7e402923 100644 --- a/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid @@ -0,0 +1 @@ +uid://bftfpffmfb1il diff --git a/addons/gdUnit4/src/GdUnitConstants.gd b/addons/gdUnit4/src/GdUnitConstants.gd index e69de29b..e43c75ab 100644 --- a/addons/gdUnit4/src/GdUnitConstants.gd +++ b/addons/gdUnit4/src/GdUnitConstants.gd @@ -0,0 +1,10 @@ +class_name GdUnitConstants +extends RefCounted + +const NO_ARG :Variant = "<--null-->" + +const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures" + +## The maximum number of report history files to store +const DEFAULT_REPORT_HISTORY_COUNT = 20 +const REPORT_DIR_PREFIX = "report_" diff --git a/addons/gdUnit4/src/GdUnitConstants.gd.uid b/addons/gdUnit4/src/GdUnitConstants.gd.uid index e69de29b..bee2c812 100644 --- a/addons/gdUnit4/src/GdUnitConstants.gd.uid +++ b/addons/gdUnit4/src/GdUnitConstants.gd.uid @@ -0,0 +1 @@ +uid://dkap7kpfh2bhg diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd index e69de29b..45cc62a4 100644 --- a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd @@ -0,0 +1,79 @@ +## An Assertion Tool to verify dictionary +@abstract class_name GdUnitDictionaryAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitDictionaryAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is equal to the given one, ignoring order. +@abstract func is_equal(expected: Variant) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is not equal to the given one, ignoring order. +@abstract func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitDictionaryAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is empty, it has a size of 0. +@abstract func is_empty() -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is not empty, it has a size of minimum 1. +@abstract func is_not_empty() -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is the same. [br] +## Compares the current by object reference equals +@abstract func is_same(expected: Variant) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary is NOT the same. [br] +## Compares the current by object reference equals +@abstract func is_not_same(expected: Variant) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary has a size of given value. +@abstract func has_size(expected: int) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary contains the given key(s).[br] +## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys] +@abstract func contains_keys(...expected: Array) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary contains the given key and value.[br] +## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value] +@abstract func contains_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary not contains the given key(s).[br] +## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys] +@abstract func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary contains the given key(s).[br] +## The keys are compared by object reference, for deep parameter comparision use [method contains_keys] +@abstract func contains_same_keys(expected: Array) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary contains the given key and value.[br] +## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value] +@abstract func contains_same_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert + + +## Verifies that the current dictionary not contains the given key(s). +## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys] +@abstract func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid index e69de29b..45ba6f27 100644 --- a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid @@ -0,0 +1 @@ +uid://8s1lymhdvlpu diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd b/addons/gdUnit4/src/GdUnitFailureAssert.gd index e69de29b..6fec1910 100644 --- a/addons/gdUnit4/src/GdUnitFailureAssert.gd +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd @@ -0,0 +1,52 @@ +## An assertion tool to verify GDUnit asserts. +## This assert is for internal use only, to verify that failed asserts work as expected. +@abstract class_name GdUnitFailureAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFailureAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFailureAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFailureAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFailureAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFailureAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFailureAssert + + +## Verifies if the executed assert was successful +@abstract func is_success() -> GdUnitFailureAssert + + +## Verifies if the executed assert has failed +@abstract func is_failed() -> GdUnitFailureAssert + + +## Verifies the failure line is equal to expected one. +@abstract func has_line(expected: int) -> GdUnitFailureAssert + + +## Verifies the failure message is equal to expected one. +@abstract func has_message(expected: String) -> GdUnitFailureAssert + + +## Verifies that the failure message starts with the expected message. +@abstract func starts_with_message(expected: String) -> GdUnitFailureAssert + + +## Verifies that the failure message contains the expected message. +@abstract func contains_message(expected: String) -> GdUnitFailureAssert diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid index e69de29b..204f1438 100644 --- a/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid @@ -0,0 +1 @@ +uid://x54vf4fue301 diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd b/addons/gdUnit4/src/GdUnitFileAssert.gd index e69de29b..771da906 100644 --- a/addons/gdUnit4/src/GdUnitFileAssert.gd +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd @@ -0,0 +1,38 @@ +@abstract class_name GdUnitFileAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFileAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFileAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFileAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFileAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFileAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFileAssert + + +@abstract func is_file() -> GdUnitFileAssert + + +@abstract func exists() -> GdUnitFileAssert + + +@abstract func is_script() -> GdUnitFileAssert + + +@abstract func contains_exactly(expected_rows :Array) -> GdUnitFileAssert diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd.uid b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid index e69de29b..d79b837d 100644 --- a/addons/gdUnit4/src/GdUnitFileAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid @@ -0,0 +1 @@ +uid://vt1hx0i6pg4h diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd index e69de29b..2695ab0e 100644 --- a/addons/gdUnit4/src/GdUnitFloatAssert.gd +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd @@ -0,0 +1,75 @@ +## An Assertion Tool to verify float values +@abstract class_name GdUnitFloatAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFloatAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFloatAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFloatAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFloatAssert + + +## Verifies that the current and expected value are approximately equal. +@abstract func is_equal_approx(expected: float, approx: float) -> GdUnitFloatAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFloatAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFloatAssert + + +## Verifies that the current value is less than the given one. +@abstract func is_less(expected: float) -> GdUnitFloatAssert + + +## Verifies that the current value is less than or equal the given one. +@abstract func is_less_equal(expected: float) -> GdUnitFloatAssert + + +## Verifies that the current value is greater than the given one. +@abstract func is_greater(expected: float) -> GdUnitFloatAssert + + +## Verifies that the current value is greater than or equal the given one. +@abstract func is_greater_equal(expected: float) -> GdUnitFloatAssert + + +## Verifies that the current value is negative. +@abstract func is_negative() -> GdUnitFloatAssert + + +## Verifies that the current value is not negative. +@abstract func is_not_negative() -> GdUnitFloatAssert + + +## Verifies that the current value is equal to zero. +@abstract func is_zero() -> GdUnitFloatAssert + + +## Verifies that the current value is not equal to zero. +@abstract func is_not_zero() -> GdUnitFloatAssert + + +## Verifies that the current value is in the given set of values. +@abstract func is_in(expected: Array) -> GdUnitFloatAssert + + +## Verifies that the current value is not in the given set of values. +@abstract func is_not_in(expected: Array) -> GdUnitFloatAssert + + +## Verifies that the current value is between the given boundaries (inclusive). +@abstract func is_between(from: float, to: float) -> GdUnitFloatAssert diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid index e69de29b..4f3ff1e4 100644 --- a/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid @@ -0,0 +1 @@ +uid://l487wamffax1 diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd index e69de29b..e8a49c51 100644 --- a/addons/gdUnit4/src/GdUnitFuncAssert.gd +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd @@ -0,0 +1,42 @@ +## An Assertion Tool to verify function callback values +@abstract class_name GdUnitFuncAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFuncAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFuncAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFuncAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFuncAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFuncAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFuncAssert + + +## Verifies that the current value is true. +@abstract func is_true() -> GdUnitFuncAssert + + +## Verifies that the current value is false. +@abstract func is_false() -> GdUnitFuncAssert + + +## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br] +## e.g.[br] +## do wait until 5s the function `is_state` is returns 10 [br] +## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code] +@abstract func wait_until(timeout: int) -> GdUnitFuncAssert diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid index e69de29b..f8c037d4 100644 --- a/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid @@ -0,0 +1 @@ +uid://bvvptcdhi1g14 diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd index e69de29b..01711f9a 100644 --- a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd @@ -0,0 +1,59 @@ +## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error(). +@abstract class_name GdUnitGodotErrorAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitGodotErrorAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitGodotErrorAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitGodotErrorAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitGodotErrorAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitGodotErrorAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitGodotErrorAssert + + +## Verifies if the executed code runs without any runtime errors +## Usage: +## [codeblock] +## await assert_error().is_success() +## [/codeblock] +@abstract func is_success() -> GdUnitGodotErrorAssert + + +## Verifies if the executed code runs into a runtime error +## Usage: +## [codeblock] +## await assert_error().is_runtime_error() +## [/codeblock] +@abstract func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert + + +## Verifies if the executed code has a push_warning() used +## Usage: +## [codeblock] +## await assert_error().is_push_warning() +## [/codeblock] +@abstract func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert + + +## Verifies if the executed code has a push_error() used +## Usage: +## [codeblock] +## await assert_error().is_push_error() +## [/codeblock] +@abstract func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid index e69de29b..bdad5847 100644 --- a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid @@ -0,0 +1 @@ +uid://bwkv3a1hhdt88 diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd index e69de29b..05eb9223 100644 --- a/addons/gdUnit4/src/GdUnitIntAssert.gd +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd @@ -0,0 +1,79 @@ +## An Assertion Tool to verify integer values +@abstract class_name GdUnitIntAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitIntAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitIntAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitIntAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitIntAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitIntAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitIntAssert + + +## Verifies that the current value is less than the given one. +@abstract func is_less(expected: int) -> GdUnitIntAssert + + +## Verifies that the current value is less than or equal the given one. +@abstract func is_less_equal(expected: int) -> GdUnitIntAssert + + +## Verifies that the current value is greater than the given one. +@abstract func is_greater(expected: int) -> GdUnitIntAssert + + +## Verifies that the current value is greater than or equal the given one. +@abstract func is_greater_equal(expected: int) -> GdUnitIntAssert + + +## Verifies that the current value is even. +@abstract func is_even() -> GdUnitIntAssert + + +## Verifies that the current value is odd. +@abstract func is_odd() -> GdUnitIntAssert + + +## Verifies that the current value is negative. +@abstract func is_negative() -> GdUnitIntAssert + + +## Verifies that the current value is not negative. +@abstract func is_not_negative() -> GdUnitIntAssert + + +## Verifies that the current value is equal to zero. +@abstract func is_zero() -> GdUnitIntAssert + + +## Verifies that the current value is not equal to zero. +@abstract func is_not_zero() -> GdUnitIntAssert + + +## Verifies that the current value is in the given set of values. +@abstract func is_in(expected: Array) -> GdUnitIntAssert + + +## Verifies that the current value is not in the given set of values. +@abstract func is_not_in(expected: Array) -> GdUnitIntAssert + + +## Verifies that the current value is between the given boundaries (inclusive). +@abstract func is_between(from: int, to: int) -> GdUnitIntAssert diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd.uid b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid index e69de29b..968a7b3d 100644 --- a/addons/gdUnit4/src/GdUnitIntAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid @@ -0,0 +1 @@ +uid://ghuy35olsym1 diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd b/addons/gdUnit4/src/GdUnitObjectAssert.gd index e69de29b..9d7e76ea 100644 --- a/addons/gdUnit4/src/GdUnitObjectAssert.gd +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd @@ -0,0 +1,51 @@ +## An Assertion Tool to verify Object values +@abstract class_name GdUnitObjectAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitObjectAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitObjectAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitObjectAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitObjectAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitObjectAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitObjectAssert + + +## Verifies that the current object is the same as the given one. +@abstract func is_same(expected: Variant) -> GdUnitObjectAssert + + +## Verifies that the current object is not the same as the given one. +@abstract func is_not_same(expected: Variant) -> GdUnitObjectAssert + + +## Verifies that the current object is an instance of the given type. +@abstract func is_instanceof(type: Variant) -> GdUnitObjectAssert + + +## Verifies that the current object is not an instance of the given type. +@abstract func is_not_instanceof(type: Variant) -> GdUnitObjectAssert + + +## Checks whether the current object inherits from the specified type. +@abstract func is_inheriting(type: Variant) -> GdUnitObjectAssert + + +## Checks whether the current object does NOT inherit from the specified type. +@abstract func is_not_inheriting(type: Variant) -> GdUnitObjectAssert diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid index e69de29b..1ace27e5 100644 --- a/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid @@ -0,0 +1 @@ +uid://dmunl8xg53sym diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd b/addons/gdUnit4/src/GdUnitResultAssert.gd index e69de29b..01eb8800 100644 --- a/addons/gdUnit4/src/GdUnitResultAssert.gd +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd @@ -0,0 +1,51 @@ +## An Assertion Tool to verify Results +@abstract class_name GdUnitResultAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitResultAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitResultAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitResultAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitResultAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitResultAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitResultAssert + + +## Verifies that the result is ends up with empty +@abstract func is_empty() -> GdUnitResultAssert + + +## Verifies that the result is ends up with success +@abstract func is_success() -> GdUnitResultAssert + + +## Verifies that the result is ends up with warning +@abstract func is_warning() -> GdUnitResultAssert + + +## Verifies that the result is ends up with error +@abstract func is_error() -> GdUnitResultAssert + + +## Verifies that the result contains the given message +@abstract func contains_message(expected: String) -> GdUnitResultAssert + + +## Verifies that the result contains the given value +@abstract func is_value(expected: Variant) -> GdUnitResultAssert diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd.uid b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid index e69de29b..1ac97d4a 100644 --- a/addons/gdUnit4/src/GdUnitResultAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid @@ -0,0 +1 @@ +uid://b4n45twg8y2ar diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd index e69de29b..6b11918d 100644 --- a/addons/gdUnit4/src/GdUnitSceneRunner.gd +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd @@ -0,0 +1,325 @@ +## The Scene Runner is a tool used for simulating interactions on a scene. +## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames. +## This tool is typically used for integration testing a scene. +@abstract class_name GdUnitSceneRunner +extends RefCounted + + +## Simulates that an action has been pressed.[br] +## [member action] : the action e.g. [code]"ui_up"[/code][br] +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner + + +## Simulates that an action is pressed.[br] +## [member action] : the action e.g. [code]"ui_up"[/code][br] +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner + + +## Simulates that an action has been released.[br] +## [member action] : the action e.g. [code]"ui_up"[/code][br] +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner + + +## Simulates that a key has been pressed.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +## [codeblock] +## func test_key_presssed(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_key_pressed(KEY_SPACE) +## [/codeblock] +@abstract func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner + + +## Simulates that a key is pressed.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +@abstract func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner + + +## Simulates that a key has been released.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +@abstract func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner + + +## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br] +## [member position] : The absolute position in pixels as Vector2 +@abstract func set_mouse_position(position: Vector2) -> GdUnitSceneRunner + + +## Returns the mouse's position in this Viewport using the coordinate system of this Viewport. +@abstract func get_mouse_position() -> Vector2 + + +## Gets the current global mouse position of the current window +@abstract func get_global_mouse_position() -> Vector2 + + +## Simulates a mouse moved to final position.[br] +## [member position] : The final mouse position +@abstract func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner + + +## Simulates a mouse move to the relative coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br] +## [br] +## [member relative] : The relative position, indicating the mouse position offset.[br] +## [member time] : The time to move the mouse by the relative position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_move_mouse(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_mouse_move_relative(Vector2(100,100)) +## [/codeblock] +@abstract func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner + + +## Simulates a mouse move to the absolute coordinates.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br] +## [br] +## [member position] : The final position of the mouse.[br] +## [member time] : The time to move the mouse to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_move_mouse(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_mouse_move_absolute(Vector2(100,100)) +## [/codeblock] +@abstract func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner + + +## Simulates a mouse button pressed.[br] +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member double_click] : Set to true to simulate a double-click +@abstract func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner + + +## Simulates a mouse button press (holding)[br] +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member double_click] : Set to true to simulate a double-click +@abstract func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner + + +## Simulates a mouse button released.[br] +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +@abstract func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner + + +## Simulates a screen touch is pressed.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@abstract func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner + + +## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@abstract func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner + + +## Simulates a screen touch is released.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@abstract func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner + + +## Simulates a touch drag and drop event to a relative position.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member relative] : The relative position, indicating the drag&drop position offset.[br] +## [member time] : The time to move to the relative position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at final at 150,50 relative (50,50 + 100,0) +## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0)) +## [/codeblock] +@abstract func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner + + +## Simulates a touch screen drop to the absolute coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The final position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at 100,50 +## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50)) +## [/codeblock] +@abstract func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner + + +## Simulates a complete drag and drop event from one position to another.[br] +## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The drag start position, indicating the drag position.[br] +## [member drop_position] : The drop position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 and drop it at 100,50 +## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50)) +## [/codeblock] +@abstract func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner + + +## Simulates a touch screen drag event to given position.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The drag start position, indicating the drag position.[br] +@abstract func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner + + +## Returns the actual position of the touchscreen drag position by given index. +## [member index] : The touch index in the case of a multi-touch event.[br] +@abstract func get_screen_touch_drag_position(index: int) -> Vector2 + + +## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br] +## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life, +## whilst a value of 0.5 means the game moves at half the regular speed. +## [member time_factor] : A float representing the simulation speed.[br] +## - Default is 1.0, meaning the simulation runs at normal speed.[br] +## - A value of 2.0 means the simulation runs twice as fast as real time.[br] +## - A value of 0.5 means the simulation runs at half the regular speed.[br] +@abstract func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner + + +## Simulates scene processing for a certain number of frames.[br] +## [member frames] : amount of frames to process[br] +## [member delta_milli] : the time delta between a frame in milliseconds +@abstract func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner + + +## Simulates scene processing until the given signal is emitted by the scene.[br] +## [member signal_name] : the signal to stop the simulation[br] +## [member args] : optional signal arguments to be matched for stop[br] +@abstract func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner + + +## Simulates scene processing until the given signal is emitted by the given object.[br] +## [member source] : the object that should emit the signal[br] +## [member signal_name] : the signal to stop the simulation[br] +## [member args] : optional signal arguments to be matched for stop +@abstract func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner + + +## Waits for all input events to be processed by flushing any buffered input events +## and then awaiting a full cycle of both the process and physics frames.[br] +## [br] +## This is typically used to ensure that any simulated or queued inputs are fully +## processed before proceeding with the next steps in the scene.[br] +## It's essential for reliable input simulation or when synchronizing logic based +## on inputs.[br] +## +## Usage Example: +## [codeblock] +## await await_input_processed() # Ensure all inputs are processed before continuing +## [/codeblock] +@abstract func await_input_processed() -> void + + +## The await_func function pauses execution until a specified function in the scene returns a value.[br] +## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br] +## [member func_name] : The name of the function to wait for.[br] +## [member args] : Optional function arguments +## [br] +## Usage Example: +## [codeblock] +## # Waits for 'calculate_score' function and verifies the result is equal to 100. +## await_func("calculate_score").is_equal(100) +## [/codeblock] +@abstract func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert + + +## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br] +## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br] +## [member source] : The object where implements the function.[br] +## [member func_name] : The name of the function to wait for.[br] +## [member args] : optional function arguments +## [br] +## Usage Example: +## [codeblock] +## # Waits for 'calculate_score' function and verifies the result is equal to 100. +## var my_instance := ScoreCalculator.new() +## await_func(my_instance, "calculate_score").is_equal(100) +## [/codeblock] +@abstract func await_func_on(source: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert + + +## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br] +## [member signal_name] : The name of the signal to wait for[br] +## [member args] : The signal arguments as an array[br] +## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing +@abstract func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void + + +## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br] +## [member source] : the object from which the signal is emitted[br] +## [member signal_name] : The name of the signal to wait for[br] +## [member args] : The signal arguments as an array[br] +## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing +@abstract func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void + + +## Restores the scene window to a windowed mode and brings it to the foreground.[br] +## This ensures that the scene is visible and active during testing, making it easier to observe and interact with. +@abstract func move_window_to_foreground() -> GdUnitSceneRunner + + +## Minimizes the scene window to a windowed mode and brings it to the background.[br] +## This ensures that the scene is hidden during testing. +@abstract func move_window_to_background() -> GdUnitSceneRunner + + +## Return the current value of the property with the name .[br] +## [member name] : name of property[br] +## [member return] : the value of the property +@abstract func get_property(name: String) -> Variant + + +## Set the value of the property with the name .[br] +## [member name] : name of property[br] +## [member value] : value of property[br] +## [member return] : true|false depending on valid property name. +@abstract func set_property(name: String, value: Variant) -> bool + + +## executes the function specified by in the scene and returns the result.[br] +## [member name] : the name of the function to execute[br] +## [member args] : optional function arguments[br] +## [member return] : the function result +@abstract func invoke(name: String, ...args: Array) -> Variant + + +## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br] +## [member name] : the name of the node to find[br] +## [member recursive] : enables/disables seraching recursive[br] +## [member return] : the node if find otherwise null +@abstract func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node + + +## Access to current running scene +@abstract func scene() -> Node diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid index e69de29b..a3745db0 100644 --- a/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid @@ -0,0 +1 @@ +uid://dn20c5e8kb3q3 diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd index e69de29b..bb975e89 100644 --- a/addons/gdUnit4/src/GdUnitSignalAssert.gd +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd @@ -0,0 +1,46 @@ +## An Assertion Tool to verify for emitted signals until a waiting time +@abstract class_name GdUnitSignalAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitSignalAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitSignalAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitSignalAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitSignalAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitSignalAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitSignalAssert + + +## Verifies that given signal is emitted until waiting time +@abstract func is_emitted(name: String, args := []) -> GdUnitSignalAssert + + +## Verifies that given signal is NOT emitted until waiting time +@abstract func is_not_emitted(name: String, args := []) -> GdUnitSignalAssert + + +## Verifies the signal exists checked the emitter +@abstract func is_signal_exists(name: String) -> GdUnitSignalAssert + + +## Sets the assert signal timeout in ms, if the time over a failure is reported.[br] +## e.g.[br] +## do wait until 5s the instance has emitted the signal `signal_a`[br] +## [code]assert_signal(instance).wait_until(5000).is_emitted("signal_a")[/code] +@abstract func wait_until(timeout: int) -> GdUnitSignalAssert diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid index e69de29b..1674748f 100644 --- a/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid @@ -0,0 +1 @@ +uid://572nse6u4l86 diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd b/addons/gdUnit4/src/GdUnitStringAssert.gd index e69de29b..2de698bd 100644 --- a/addons/gdUnit4/src/GdUnitStringAssert.gd +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd @@ -0,0 +1,71 @@ +## An Assertion Tool to verify String values +@abstract class_name GdUnitStringAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitStringAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitStringAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitStringAssert + + +## Verifies that the current String is equal to the given one, ignoring case considerations. +@abstract func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitStringAssert + + +## Verifies that the current String is not equal to the given one, ignoring case considerations. +@abstract func is_not_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitStringAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitStringAssert + + +## Verifies that the current String is empty, it has a length of 0. +@abstract func is_empty() -> GdUnitStringAssert + + +## Verifies that the current String is not empty, it has a length of minimum 1. +@abstract func is_not_empty() -> GdUnitStringAssert + + +## Verifies that the current String contains the given String. +@abstract func contains(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String does not contain the given String. +@abstract func not_contains(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String does not contain the given String, ignoring case considerations. +@abstract func contains_ignoring_case(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String does not contain the given String, ignoring case considerations. +@abstract func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String starts with the given prefix. +@abstract func starts_with(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String ends with the given suffix. +@abstract func ends_with(expected: String) -> GdUnitStringAssert + + +## Verifies that the current String has the expected length by used comparator. +@abstract func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd.uid b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid index e69de29b..0d26dde3 100644 --- a/addons/gdUnit4/src/GdUnitStringAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid @@ -0,0 +1 @@ +uid://ip241g801xri diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd.uid b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid index e69de29b..2dc80ed3 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd.uid +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid @@ -0,0 +1 @@ +uid://cgbfa4cflb5nl diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd index e69de29b..6c910023 100644 --- a/addons/gdUnit4/src/GdUnitTuple.gd +++ b/addons/gdUnit4/src/GdUnitTuple.gd @@ -0,0 +1,28 @@ +## A tuple implementation to hold two or many values +class_name GdUnitTuple +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG + +var __values :Array = Array() + + +func _init(arg0:Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG) -> void: + __values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + + +func values() -> Array: + return __values + + +func _to_string() -> String: + return "tuple(%s)" % str(__values) diff --git a/addons/gdUnit4/src/GdUnitTuple.gd.uid b/addons/gdUnit4/src/GdUnitTuple.gd.uid index e69de29b..0a8b36ec 100644 --- a/addons/gdUnit4/src/GdUnitTuple.gd.uid +++ b/addons/gdUnit4/src/GdUnitTuple.gd.uid @@ -0,0 +1 @@ +uid://mjqw2uww51fk diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd b/addons/gdUnit4/src/GdUnitValueExtractor.gd index e69de29b..1a344454 100644 --- a/addons/gdUnit4/src/GdUnitValueExtractor.gd +++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd @@ -0,0 +1,9 @@ +## This is the base interface for value extraction +class_name GdUnitValueExtractor +extends RefCounted + + +## Extracts a value by given implementation +func extract_value(value :Variant) -> Variant: + push_error("Uninplemented func 'extract_value'") + return value diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid index e69de29b..40cc1111 100644 --- a/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid +++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid @@ -0,0 +1 @@ +uid://2dylh01qtb66 diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd b/addons/gdUnit4/src/GdUnitVectorAssert.gd index e69de29b..c186cba2 100644 --- a/addons/gdUnit4/src/GdUnitVectorAssert.gd +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd @@ -0,0 +1,55 @@ +## An Assertion Tool to verify Vector values +@abstract class_name GdUnitVectorAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitVectorAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitVectorAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current and expected value are approximately equal. +@abstract func is_equal_approx(expected: Variant, approx: Variant) -> GdUnitVectorAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitVectorAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitVectorAssert + + +## Verifies that the current value is less than the given one. +@abstract func is_less(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is less than or equal the given one. +@abstract func is_less_equal(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is greater than the given one. +@abstract func is_greater(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is greater than or equal the given one. +@abstract func is_greater_equal(expected: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is between the given boundaries (inclusive). +@abstract func is_between(from: Variant, to: Variant) -> GdUnitVectorAssert + + +## Verifies that the current value is not between the given boundaries (inclusive). +@abstract func is_not_between(from: Variant, to: Variant) -> GdUnitVectorAssert diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid index e69de29b..a1926b28 100644 --- a/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid @@ -0,0 +1 @@ +uid://bcx6bgypklb3e diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd index e69de29b..6be4b3ee 100644 --- a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd +++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd @@ -0,0 +1,25 @@ +# a value provider unsing a callback to get `next` value from a certain function +class_name CallBackValueProvider +extends ValueProvider + +var _cb :Callable +var _args :Array + + +func _init(instance :Object, func_name :String, args :Array = Array(), force_error := true) -> void: + _cb = Callable(instance, func_name); + _args = args + if force_error and not _cb.is_valid(): + push_error("Can't find function '%s' checked instance %s" % [func_name, instance]) + + +func get_value() -> Variant: + if not _cb.is_valid(): + return null + if _args.is_empty(): + return await _cb.call() + return await _cb.callv(_args) + + +func dispose() -> void: + _cb = Callable() diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid index e69de29b..50e1e8a7 100644 --- a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid @@ -0,0 +1 @@ +uid://r43u2usutiss diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd index e69de29b..2f828fa2 100644 --- a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd +++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd @@ -0,0 +1,13 @@ +# default value provider, simple returns the initial value +class_name DefaultValueProvider +extends ValueProvider + +var _value: Variant + + +func _init(value: Variant) -> void: + _value = value + + +func get_value() -> Variant: + return _value diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid index e69de29b..cd08d1f8 100644 --- a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid @@ -0,0 +1 @@ +uid://coauynw7rnsij diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd index e69de29b..f9bc7aa5 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -0,0 +1,692 @@ +class_name GdAssertMessages +extends Resource + +const WARN_COLOR = "#EFF883" +const ERROR_COLOR = "#CD5C5C" +const VALUE_COLOR = "#1E90FF" +const SUB_COLOR := Color(1, 0, 0, .15) +const ADD_COLOR := Color(0, 1, 0, .15) + + +# Dictionary of control characters and their readable representations +const CONTROL_CHARS = { + "\n": "", # Line Feed + "\r": "", # Carriage Return + "\t": "", # Tab + "\b": "", # Backspace + "\f": "", # Form Feed + "\v": "", # Vertical Tab + "\a": "", # Bell + "": "" # Escape +} + + +static func format_dict(value :Variant) -> String: + if not value is Dictionary: + return str(value) + + var dict_value: Dictionary = value + if dict_value.is_empty(): + return "{ }" + var as_rows := var_to_str(value).split("\n") + for index in range( 1, as_rows.size()-1): + as_rows[index] = " " + as_rows[index] + as_rows[-1] = " " + as_rows[-1] + return "\n".join(as_rows) + + +# improved version of InputEvent as text +static func input_event_as_text(event :InputEvent) -> String: + var text := "" + if event is InputEventKey: + var key_event := event as InputEventKey + text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [ + event.as_text(), key_event.pressed, key_event.keycode, key_event.physical_keycode] + else: + text += event.as_text() + if event is InputEventMouse: + var mouse_event := event as InputEventMouse + text += ", global_position %s" % mouse_event.global_position + if event is InputEventWithModifiers: + var mouse_event := event as InputEventWithModifiers + text += ", shift=%s, alt=%s, control=%s, meta=%s, command=%s" % [ + mouse_event.shift_pressed, + mouse_event.alt_pressed, + mouse_event.ctrl_pressed, + mouse_event.meta_pressed, + mouse_event.command_or_control_autoremap] + return text + + +static func _colored_string_div(characters: String) -> String: + return colored_array_div(characters.to_utf32_buffer().to_int32_array()) + + +static func colored_array_div(characters: PackedInt32Array) -> String: + if characters.is_empty(): + return "" + var result := PackedInt32Array() + var index := 0 + var missing_chars := PackedInt32Array() + var additional_chars := PackedInt32Array() + + while index < characters.size(): + var character := characters[index] + match character: + GdDiffTool.DIV_ADD: + index += 1 + @warning_ignore("return_value_discarded") + additional_chars.append(characters[index]) + GdDiffTool.DIV_SUB: + index += 1 + @warning_ignore("return_value_discarded") + missing_chars.append(characters[index]) + _: + if not missing_chars.is_empty(): + result.append_array(format_chars(missing_chars, SUB_COLOR)) + missing_chars = PackedInt32Array() + if not additional_chars.is_empty(): + result.append_array(format_chars(additional_chars, ADD_COLOR)) + additional_chars = PackedInt32Array() + @warning_ignore("return_value_discarded") + result.append(character) + index += 1 + + result.append_array(format_chars(missing_chars, SUB_COLOR)) + result.append_array(format_chars(additional_chars, ADD_COLOR)) + return result.to_byte_array().get_string_from_utf32() + + +static func _typed_value(value :Variant) -> String: + return GdDefaultValueDecoder.decode(value) + + +static func _warning(error :String) -> String: + return "[color=%s]%s[/color]" % [WARN_COLOR, error] + + +static func _error(error :String) -> String: + return "[color=%s]%s[/color]" % [ERROR_COLOR, error] + + +static func _nerror(number :Variant) -> String: + match typeof(number): + TYPE_INT: + return "[color=%s]%d[/color]" % [ERROR_COLOR, number] + TYPE_FLOAT: + return "[color=%s]%f[/color]" % [ERROR_COLOR, number] + _: + return "[color=%s]%s[/color]" % [ERROR_COLOR, str(number)] + + +static func _colored_value(value :Variant) -> String: + match typeof(value): + TYPE_STRING, TYPE_STRING_NAME: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(str(value))] + TYPE_INT: + return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value] + TYPE_FLOAT: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + TYPE_COLOR: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + TYPE_OBJECT: + if value == null: + return "'[color=%s][/color]'" % [VALUE_COLOR] + if value is InputEvent: + var ie: InputEvent = value + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(ie)] + var obj_value: Object = value + if obj_value.has_method("_to_string"): + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)] + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, obj_value.get_class()] + TYPE_DICTIONARY: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)] + _: + if GdArrayTools.is_array_type(value): + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, value] + + + +static func _index_report_as_table(index_reports :Array) -> String: + var table := "[table=3]$cells[/table]" + var header := "[cell][right][b]$text[/b][/right]\t[/cell]" + var cell := "[cell][right]$text[/right]\t[/cell]" + var cells := header.replace("$text", "Index") + header.replace("$text", "Current") + header.replace("$text", "Expected") + for report :Variant in index_reports: + var index :String = str(report["index"]) + var current :String = str(report["current"]) + var expected :String = str(report["expected"]) + cells += cell.replace("$text", index) + cell.replace("$text", current) + cell.replace("$text", expected) + return table.replace("$cells", cells) + + +static func orphan_detected_on_suite_setup(count :int) -> String: + return "%s\n Detected <%d> orphan nodes during test suite setup stage! [b]Check before() and after()![/b]" % [ + _warning("WARNING:"), count] + + +static func orphan_detected_on_test_setup(count :int) -> String: + return "%s\n Detected <%d> orphan nodes during test setup! [b]Check before_test() and after_test()![/b]" % [ + _warning("WARNING:"), count] + + +static func orphan_detected_on_test(count :int) -> String: + return "%s\n Detected <%d> orphan nodes during test execution!" % [ + _warning("WARNING:"), count] + + +static func fuzzer_interuped(iterations: int, error: String) -> String: + return "%s %s %s\n %s" % [ + _error("Found an error after"), + _colored_value(iterations + 1), + _error("test iterations"), + error] + + +static func test_timeout(timeout :int) -> String: + return "%s\n %s" % [_error("Timeout !"), _colored_value("Test timed out after %s" % LocalTime.elapsed(timeout))] + + +# gdlint:disable = mixed-tabs-and-spaces +static func test_suite_skipped(hint :String, skip_count :int) -> String: + return """ + %s + Skipped %s tests + Reason: %s + """.dedent().trim_prefix("\n")\ + % [_error("The Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)] + + +static func test_skipped(hint :String) -> String: + return """ + %s + Reason: %s + """.dedent().trim_prefix("\n")\ + % [_error("This test is skipped!"), _colored_value(hint)] + + +static func error_not_implemented() -> String: + return _error("Test not implemented!") + + +static func error_is_null(current :Variant) -> String: + return "%s %s but was %s" % [_error("Expecting:"), _colored_value(null), _colored_value(current)] + + +static func error_is_not_null() -> String: + return "%s %s" % [_error("Expecting: not to be"), _colored_value(null)] + + +static func error_equal(current :Variant, expected :Variant, index_reports :Array = []) -> String: + var report := """ + %s + %s + but was + %s""".dedent().trim_prefix("\n") % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + if not index_reports.is_empty(): + report += "\n\n%s\n%s" % [_error("Differences found:"), _index_report_as_table(index_reports)] + return report + + +static func error_not_equal(current :Variant, expected :Variant) -> String: + return "%s\n %s\n not equal to\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_not_equal_case_insensetiv(current :Variant, expected :Variant) -> String: + return "%s\n %s\n not equal to (case insensitiv)\n %s" % [ + _error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_is_empty(current :Variant) -> String: + return "%s\n must be empty but was\n %s" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_empty() -> String: + return "%s\n must not be empty" % [_error("Expecting:")] + + +static func error_is_same(current :Variant, expected :Variant) -> String: + return "%s\n %s\n to refer to the same object\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +@warning_ignore("unused_parameter") +static func error_not_same(_current :Variant, expected :Variant) -> String: + return "%s\n %s" % [_error("Expecting not same:"), _colored_value(expected)] + + +static func error_not_same_error(current :Variant, expected :Variant) -> String: + return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)] + + +static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String: + return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\ + _colored_value(expected.or_else(null)), _colored_value(current.or_else(null))] + + +# -- Boolean Assert specific messages ----------------------------------------------------- +static func error_is_true(current :Variant) -> String: + return "%s %s but is %s" % [_error("Expecting:"), _colored_value(true), _colored_value(current)] + + +static func error_is_false(current :Variant) -> String: + return "%s %s but is %s" % [_error("Expecting:"), _colored_value(false), _colored_value(current)] + + +# - Integer/Float Assert specific messages ----------------------------------------------------- + +static func error_is_even(current :Variant) -> String: + return "%s\n %s must be even" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_odd(current :Variant) -> String: + return "%s\n %s must be odd" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_negative(current :Variant) -> String: + return "%s\n %s be negative" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_negative(current :Variant) -> String: + return "%s\n %s be not negative" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_zero(current :Variant) -> String: + return "%s\n equal to 0 but is %s" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_zero() -> String: + return "%s\n not equal to 0" % [_error("Expecting:")] + + +static func error_is_wrong_type(current_type :Variant.Type, expected_type :Variant.Type) -> String: + return "%s\n Expecting type %s but is %s" % [ + _error("Unexpected type comparison:"), + _colored_value(GdObjects.type_as_string(current_type)), + _colored_value(GdObjects.type_as_string(expected_type))] + + +static func error_is_value(operation :int, current :Variant, expected :Variant, expected2 :Variant = null) -> String: + match operation: + Comparator.EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting:"), _colored_value(expected), _nerror(current)] + Comparator.LESS_THAN: + return "%s\n %s but was '%s'" % [_error("Expecting to be less than:"), _colored_value(expected), _nerror(current)] + Comparator.LESS_EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting to be less than or equal:"), _colored_value(expected), _nerror(current)] + Comparator.GREATER_THAN: + return "%s\n %s but was '%s'" % [_error("Expecting to be greater than:"), _colored_value(expected), _nerror(current)] + Comparator.GREATER_EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting to be greater than or equal:"), _colored_value(expected), _nerror(current)] + Comparator.BETWEEN_EQUAL: + return "%s\n %s\n in range between\n %s <> %s" % [ + _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)] + Comparator.NOT_BETWEEN_EQUAL: + return "%s\n %s\n not in range between\n %s <> %s" % [ + _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)] + return "TODO create expected message" + + +static func error_is_in(current :Variant, expected :Array) -> String: + return "%s\n %s\n is in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))] + + +static func error_is_not_in(current :Variant, expected :Array) -> String: + return "%s\n %s\n is not in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))] + + +# - StringAssert --------------------------------------------------------------------------------- +static func error_equal_ignoring_case(current :Variant, expected :Variant) -> String: + return "%s\n %s\n but was\n %s (ignoring case)" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_contains(current :Variant, expected :Variant) -> String: + return "%s\n %s\n do contains\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_not_contains(current :Variant, expected :Variant) -> String: + return "%s\n %s\n not do contain\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_contains_ignoring_case(current :Variant, expected :Variant) -> String: + return "%s\n %s\n contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_not_contains_ignoring_case(current :Variant, expected :Variant) -> String: + return "%s\n %s\n not do contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_starts_with(current :Variant, expected :Variant) -> String: + return "%s\n %s\n to start with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_ends_with(current :Variant, expected :Variant) -> String: + return "%s\n %s\n to end with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String: + @warning_ignore("unsafe_method_access") + var current_length :Variant = current.length() if current != null else null + match compare_operator: + Comparator.EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size:"), _colored_value(expected), _nerror(current_length), _colored_value(current)] + Comparator.LESS_THAN: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be less than:"), _colored_value(expected), _nerror(current_length), _colored_value(current)] + Comparator.LESS_EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be less than or equal:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + Comparator.GREATER_THAN: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be greater than:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + Comparator.GREATER_EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be greater than or equal:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + return "TODO create expected message" + + +# - ArrayAssert specific messgaes --------------------------------------------------- + +static func error_arr_contains(current: Variant, expected: Variant, not_expect: Variant, not_found: Variant, by_reference: bool) -> String: + var failure_message := "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:" + var error := "%s\n %s\n do contains (in any order)\n %s" % [ + _error(failure_message), _colored_value(current), _colored_value(expected)] + if not is_empty(not_expect): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] + return error + + +static func error_arr_contains_exactly( + current: Variant, + expected: Variant, + not_expect: Variant, + not_found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String: + var failure_message := ( + "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME exactly elements:" + ) + if is_empty(not_expect) and is_empty(not_found): + var arr_current: Array = current + var arr_expected: Array = expected + var diff := _find_first_diff(arr_current, arr_expected) + return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [ + _error(failure_message), _colored_value(current), _colored_value(expected), diff] + + var error := "%s\n %s\n do contains (in same order)\n %s" % [ + _error(failure_message), _colored_value(current), _colored_value(expected)] + if not is_empty(not_expect): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] + return error + + +static func error_arr_contains_exactly_in_any_order( + current: Variant, + expected: Variant, + not_expect: Variant, + not_found: Variant, + compare_mode: GdObjects.COMPARE_MODE) -> String: + + var failure_message := ( + "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME exactly elements:" + ) + var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [ + _error(failure_message), _colored_value(current), _colored_value(expected)] + if not is_empty(not_expect): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] + return error + + +static func error_arr_not_contains(current: Variant, expected: Variant, found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String: + var failure_message := "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:" + var error := "%s\n %s\n do not contains\n %s" % [ + _error(failure_message), _colored_value(current), _colored_value(expected)] + if not is_empty(found): + error += "\n but found elements:\n %s" % _colored_value(found) + return error + + +# - DictionaryAssert specific messages ---------------------------------------------- +static func error_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME keys:" + ) + return "%s\n %s\n to contains:\n %s\n but can't find key's:\n %s" % [ + _error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)] + + +static func error_not_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting NOT contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting NOT contains SAME keys" + ) + return "%s\n %s\n do not contains:\n %s\n but contains key's:\n %s" % [ + _error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)] + + +static func error_contains_key_value(key :Variant, value :Variant, current_value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting contains key and value:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME key and value:" + ) + return "%s\n %s : %s\n but contains\n %s : %s" % [ + _error(failure), _colored_value(key), _colored_value(value), _colored_value(key), _colored_value(current_value)] + + +# - ResultAssert specific errors ---------------------------------------------------- +static func error_result_is_empty(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.EMPTY) + + +static func error_result_is_success(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.SUCCESS) + + +static func error_result_is_warning(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.WARN) + + +static func error_result_is_error(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.ERROR) + + +static func error_result_has_message(current :String, expected :String) -> String: + return "%s\n %s\n but was\n %s." % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_result_has_message_on_success(expected :String) -> String: + return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)] + + +static func error_result_is_value(current :Variant, expected :Variant) -> String: + return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)] + + +static func _result_error_message(current :GdUnitResult, expected_type :int) -> String: + if current == null: + return _error("Expecting the result must be a %s but was ." % result_type(expected_type)) + if current.is_success(): + return _error("Expecting the result must be a %s but was SUCCESS." % result_type(expected_type)) + var error := "Expecting the result must be a %s but was %s:" % [result_type(expected_type), result_type(current._state)] + return "%s\n %s" % [_error(error), _colored_value(result_message(current))] + + +static func error_interrupted(func_name :String, expected :Variant, elapsed :String) -> String: + func_name = humanized(func_name) + if expected == null: + return "%s %s but timed out after %s" % [_error("Expected:"), func_name, elapsed] + return "%s %s %s but timed out after %s" % [_error("Expected:"), func_name, _colored_value(expected), elapsed] + + +static func error_wait_signal(signal_name :String, args :Array, elapsed :String) -> String: + if args.is_empty(): + return "%s %s but timed out after %s" % [ + _error("Expecting emit signal:"), _colored_value(signal_name + "()"), elapsed] + return "%s %s but timed out after %s" % [ + _error("Expecting emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed] + + +static func error_signal_emitted(signal_name :String, args :Array, elapsed :String) -> String: + if args.is_empty(): + return "%s %s but is emitted after %s" % [ + _error("Expecting do not emit signal:"), _colored_value(signal_name + "()"), elapsed] + return "%s %s but is emitted after %s" % [ + _error("Expecting do not emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed] + + +static func error_await_signal_on_invalid_instance(source :Variant, signal_name :String, args :Array) -> String: + return "%s\n await_signal_on(%s, %s, %s)" % [ + _error("Invalid source! Can't await on signal:"), _colored_value(source), signal_name, args] + + +static func result_type(type :int) -> String: + match type: + GdUnitResult.SUCCESS: return "SUCCESS" + GdUnitResult.WARN: return "WARNING" + GdUnitResult.ERROR: return "ERROR" + GdUnitResult.EMPTY: return "EMPTY" + return "UNKNOWN" + + +static func result_message(result :GdUnitResult) -> String: + match result._state: + GdUnitResult.SUCCESS: return "" + GdUnitResult.WARN: return result.warn_message() + GdUnitResult.ERROR: return result.error_message() + GdUnitResult.EMPTY: return "" + return "UNKNOWN" +# ----------------------------------------------------------------------------------- + +# - Spy|Mock specific errors ---------------------------------------------------- +static func error_no_more_interactions(summary :Dictionary) -> String: + var interactions := PackedStringArray() + for args :Array in summary.keys(): + var times :int = summary[args] + @warning_ignore("return_value_discarded") + interactions.append(_format_arguments(args, times)) + return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)] + + +static func error_validate_interactions(current_interactions: Dictionary, expected_interactions: Dictionary) -> String: + var collected_interactions := PackedStringArray() + for args: Array in current_interactions.keys(): + var times: int = current_interactions[args] + @warning_ignore("return_value_discarded") + collected_interactions.append(_format_arguments(args, times)) + + var arguments: Array = expected_interactions.keys()[0] + var interactions: int = expected_interactions.values()[0] + var expected_interaction := _format_arguments(arguments, interactions) + return "%s\n%s\n%s\n%s" % [ + _error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(collected_interactions)] + + +static func _format_arguments(args :Array, times :int) -> String: + var fname :String = args[0] + var fargs := args.slice(1) as Array + var typed_args := _to_typed_args(fargs) + var fsignature := _colored_value("%s(%s)" % [fname, ", ".join(typed_args)]) + return " %s %d time's" % [fsignature, times] + + +static func _to_typed_args(args :Array) -> PackedStringArray: + var typed := PackedStringArray() + for arg :Variant in args: + @warning_ignore("return_value_discarded") + typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg))) + return typed + + +static func _format_arg(arg :Variant) -> String: + if arg is InputEvent: + var ie: InputEvent = arg + return input_event_as_text(ie) + return str(arg) + + +static func _find_first_diff(left :Array, right :Array) -> String: + for index in left.size(): + var l :Variant = left[index] + var r :Variant = "" if index >= right.size() else right[index] + if not GdObjects.equals(l, r): + return "at position %s\n '%s' vs '%s'" % [_colored_value(index), _typed_value(l), _typed_value(r)] + return "" + + +static func error_has_size(current :Variant, expected: int) -> String: + @warning_ignore("unsafe_method_access") + var current_size :Variant = null if current == null else current.size() + return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)] + + +static func error_contains_exactly(current: Array, expected: Array) -> String: + return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)] + + +static func format_chars(characters: PackedInt32Array, type: Color) -> PackedInt32Array: + if characters.size() == 0:# or characters[0] == 10: + return characters + + # Replace each control character with its readable form + var formatted_text := characters.to_byte_array().get_string_from_utf32() + for control_char: String in CONTROL_CHARS: + var replace_text: String = CONTROL_CHARS[control_char] + formatted_text = formatted_text.replace(control_char, replace_text) + + # Handle special ASCII control characters (0x00-0x1F, 0x7F) + var ascii_text := "" + for i in formatted_text.length(): + var character := formatted_text[i] + var code := character.unicode_at(0) + if code < 0x20 and not CONTROL_CHARS.has(character): # Control characters not handled above + ascii_text += "<0x%02X>" % code + elif code == 0x7F: # DEL character + ascii_text += "" + else: + ascii_text += character + + var message := "[bgcolor=#%s][color=white]%s[/color][/bgcolor]" % [ + type.to_html(), + ascii_text + ] + + var result := PackedInt32Array() + result.append_array(message.to_utf32_buffer().to_int32_array()) + return result + + +static func format_invalid(value :String) -> String: + return "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [SUB_COLOR.to_html(), value] + + +static func humanized(value :String) -> String: + return value.replace("_", " ") + + +static func build_failure_message(failure :String, additional_failure_message: String, custom_failure_message: String) -> String: + var message := failure if custom_failure_message.is_empty() else custom_failure_message + if additional_failure_message.is_empty(): + return message + return """ + %s + [color=LIME_GREEN][b]Additional info:[/b][/color] + %s""".dedent().trim_prefix("\n") % [message, additional_failure_message] + + +static func is_empty(value: Variant) -> bool: + var arry_value: Array = value + return arry_value != null and arry_value.is_empty() diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid index e69de29b..ea0b25ef 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid @@ -0,0 +1 @@ +uid://vl7cfc01g5wl diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd index e69de29b..06b72edb 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -0,0 +1,54 @@ +class_name GdAssertReports +extends RefCounted + +const LAST_ERROR = "last_assert_error_message" +const LAST_ERROR_LINE = "last_assert_error_line" + + +static func report_success() -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(false) + GdAssertReports.set_last_error_line_number(-1) + Engine.remove_meta(LAST_ERROR) + + +static func report_warning(message :String, line_number :int) -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(false) + send_report(GdUnitReport.new().create(GdUnitReport.WARN, line_number, message)) + + +static func report_error(message:String, line_number :int) -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(true) + GdAssertReports.set_last_error_line_number(line_number) + Engine.set_meta(LAST_ERROR, message) + # if we expect to fail we handle as success test + if _do_expect_assert_failing(): + return + send_report(GdUnitReport.new().create(GdUnitReport.FAILURE, line_number, message)) + + +static func reset_last_error_line_number() -> void: + Engine.remove_meta(LAST_ERROR_LINE) + + +static func set_last_error_line_number(line_number :int) -> void: + Engine.set_meta(LAST_ERROR_LINE, line_number) + + +static func get_last_error_line_number() -> int: + if Engine.has_meta(LAST_ERROR_LINE): + return Engine.get_meta(LAST_ERROR_LINE) + return -1 + + +static func _do_expect_assert_failing() -> bool: + if Engine.has_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES): + return Engine.get_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES) + return false + + +static func current_failure() -> String: + return Engine.get_meta(LAST_ERROR) + + +static func send_report(report :GdUnitReport) -> void: + GdUnitThreadManager.get_current_context().get_execution_context().add_report(report) diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid index e69de29b..e1b6b858 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid @@ -0,0 +1 @@ +uid://brxvavm3ml0om diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid index e69de29b..e49cbab9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://bx7cehfdh2x4w diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd index e69de29b..9f578c4d 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -0,0 +1,80 @@ +class_name GdUnitAssertImpl +extends GdUnitAssert + + +var _current :Variant +var _current_failure_message :String = "" +var _custom_failure_message :String = "" +var _additional_failure_message: String = "" + + +func _init(current :Variant) -> void: + _current = current + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + GdAssertReports.reset_last_error_line_number() + + + +func failure_message() -> String: + return _current_failure_message + + +func current_value() -> Variant: + return _current + + +func report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func report_error(failure :String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + GdAssertReports.set_last_error_line_number(line_number) + _current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, line_number) + Engine.set_meta("GD_TEST_FAILURE", true) + return self + + +func do_fail() -> GdUnitAssert: + return report_error(GdAssertMessages.error_not_implemented()) + + +func override_failure_message(message: String) -> GdUnitAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitAssert: + _additional_failure_message = message + return self + + +func is_null() -> GdUnitAssert: + var current :Variant = current_value() + if current != null: + return report_error(GdAssertMessages.error_is_null(current)) + return report_success() + + +func is_not_null() -> GdUnitAssert: + var current :Variant = current_value() + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + return report_success() + + +func is_equal(expected: Variant) -> GdUnitAssert: + var current: Variant = current_value() + if not GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_equal(current, expected)) + return report_success() + + +func is_not_equal(expected: Variant) -> GdUnitAssert: + var current: Variant = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid index e69de29b..e60fa2ed 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cq38mcld2thyl diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd index e69de29b..a5b53c17 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -0,0 +1,68 @@ +# Preloads all GdUnit assertions +class_name GdUnitAssertions +extends RefCounted + + +@warning_ignore("return_value_discarded") +func _init() -> void: + # preload all gdunit assertions to speedup testsuite loading time + # gdlint:disable=private-method-call + @warning_ignore_start("return_value_discarded") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd") + @warning_ignore_restore("return_value_discarded") + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +# gdlint:disable=function-name +static func __lazy_load(script_path :String) -> GDScript: + return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + +static func validate_value_type(value :Variant, type :Variant.Type) -> bool: + return value == null or typeof(value) == type + + +# Scans the current stack trace for the root cause to extract the line number +static func get_line_number() -> int: + var stack_trace := get_stack() + if stack_trace == null or stack_trace.is_empty(): + return -1 + for index in stack_trace.size(): + var stack_info :Dictionary = stack_trace[index] + var function :String = stack_info.get("function") + # we catch helper asserts to skip over to return the correct line number + if function.begins_with("assert_"): + continue + if function.begins_with("test_"): + return stack_info.get("line") + var source :String = stack_info.get("source") + if source.is_empty() \ + or source.begins_with("user://") \ + or source.ends_with("GdUnitAssert.gd") \ + or source.ends_with("GdUnitAssertions.gd") \ + or source.ends_with("AssertImpl.gd") \ + or source.ends_with("GdUnitTestSuite.gd") \ + or source.ends_with("GdUnitSceneRunnerImpl.gd") \ + or source.ends_with("GdUnitObjectInteractions.gd") \ + or source.ends_with("GdUnitObjectInteractionsVerifier.gd") \ + or source.ends_with("GdUnitAwaiter.gd"): + continue + return stack_info.get("line") + return -1 diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid index e69de29b..a8600308 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid @@ -0,0 +1 @@ +uid://61d7pdgldg0r diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd index e69de29b..2fc0ce43 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -0,0 +1,87 @@ +extends GdUnitBoolAssert + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_BOOL): + @warning_ignore("return_value_discarded") + report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +func is_true() -> GdUnitBoolAssert: + if current_value() != true: + return report_error(GdAssertMessages.error_is_true(current_value())) + return report_success() + + +func is_false() -> GdUnitBoolAssert: + if current_value() == true || current_value() == null: + return report_error(GdAssertMessages.error_is_false(current_value())) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid index e69de29b..76e9a3a0 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cxndss6mdq7de diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid index e69de29b..10a744ef 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dqrp7csbeyvon diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd index e69de29b..198624c6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -0,0 +1,136 @@ +extends GdUnitFailureAssert + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _is_failed := false +var _failure_message: String +var _current_failure_message := "" +var _custom_failure_message := "" +var _additional_failure_message := "" + + +func _set_do_expect_fail(enabled :bool = true) -> void: + Engine.set_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES, enabled) + + +func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAssert: + # do not report any failure from the original assertion we want to test + _set_do_expect_fail(true) + var thread_context := GdUnitThreadManager.get_current_context() + thread_context.set_assert(null) + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed) + # execute the given assertion as callable + if do_await: + await assertion.call() + else: + assertion.call() + _set_do_expect_fail(false) + # get the assert instance from current tread context + var current_assert := thread_context.get_assert() + if not is_instance_of(current_assert, GdUnitAssert): + _is_failed = true + _failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'" + return self + @warning_ignore("unsafe_method_access") + _failure_message = current_assert.failure_message() + return self + + +func execute(assertion :Callable) -> GdUnitFailureAssert: + @warning_ignore("return_value_discarded") + execute_and_await(assertion, false) + return self + + +func _on_test_failed(value :bool) -> void: + _is_failed = value + + +func is_equal(_expected: Variant) -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_not_equal(_expected: Variant) -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_null() -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_not_null() -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func override_failure_message(message: String) -> GdUnitFailureAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitFailureAssert: + _additional_failure_message = message + return self + + +func is_success() -> GdUnitFailureAssert: + if _is_failed: + return _report_error("Expect: assertion ends successfully.") + return self + + +func is_failed() -> GdUnitFailureAssert: + if not _is_failed: + return _report_error("Expect: assertion fails.") + return self + + +func has_line(expected :int) -> GdUnitFailureAssert: + var current := GdAssertReports.get_last_error_line_number() + if current != expected: + return _report_error("Expect: to failed on line '%d'\n but was '%d'." % [expected, current]) + return self + + +func has_message(expected :String) -> GdUnitFailureAssert: + @warning_ignore("return_value_discarded") + is_failed() + var expected_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(expected)) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) + if current_error != expected_error: + var diffs := GdDiffTool.string_diff(current_error, expected_error) + var current := GdAssertMessages.colored_array_div(diffs[1]) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return self + + +func contains_message(expected :String) -> GdUnitFailureAssert: + var expected_error := GdUnitTools.normalize_text(expected) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) + if not current_error.contains(expected_error): + var diffs := GdDiffTool.string_diff(current_error, expected_error) + var current := GdAssertMessages.colored_array_div(diffs[1]) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return self + + +func starts_with_message(expected :String) -> GdUnitFailureAssert: + var expected_error := GdUnitTools.normalize_text(expected) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) + if current_error.find(expected_error) != 0: + var diffs := GdDiffTool.string_diff(current_error, expected_error) + var current := GdAssertMessages.colored_array_div(diffs[1]) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return self + + +func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, line_number) + return self + + +func _report_success() -> GdUnitFailureAssert: + GdAssertReports.report_success() + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid index e69de29b..61645532 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cbrj7dsr235i0 diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd index e69de29b..c4f9570e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -0,0 +1,116 @@ +extends GdUnitFileAssert + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_STRING): + @warning_ignore("return_value_discarded") + report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> String: + return _base.current_value() + + +func report_success() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +func is_file() -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Is not a file '%s', error code %s" % [current, FileAccess.get_open_error()]) + return report_success() + + +func exists() -> GdUnitFileAssert: + var current := current_value() + if not FileAccess.file_exists(current): + return report_error("The file '%s' not exists" %current) + return report_success() + + +func is_script() -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()]) + + var script := load(current) + if not script is GDScript: + return report_error("The file '%s' is not a GdScript" % current) + return report_success() + + +func contains_exactly(expected_rows: Array) -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()]) + + var script: GDScript = load(current) + if script is GDScript: + var source_code := GdScriptParser.to_unix_format(script.source_code) + var rows := Array(source_code.split("\n")) + @warning_ignore("return_value_discarded") + GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows) + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid index e69de29b..7141b12b 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://2s6h0titid8y diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd index e69de29b..83d7e05e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -0,0 +1,159 @@ +extends GdUnitFloatAssert + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_FLOAT): + @warning_ignore("return_value_discarded") + report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +@warning_ignore("shadowed_global_identifier") +func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert: + return is_between(expected-approx, expected+approx) + + +func is_less(expected :float) -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :float) -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :float) -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :float) -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_negative() -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current >= 0.0: + return report_error(GdAssertMessages.error_is_negative(current)) + return report_success() + + +func is_not_negative() -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current < 0.0: + return report_error(GdAssertMessages.error_is_not_negative(current)) + return report_success() + + +func is_zero() -> GdUnitFloatAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or not is_equal_approx(0.00000000, current as float): + return report_error(GdAssertMessages.error_is_zero(current)) + return report_success() + + +func is_not_zero() -> GdUnitFloatAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or is_equal_approx(0.00000000, current as float): + return report_error(GdAssertMessages.error_is_not_zero()) + return report_success() + + +func is_in(expected :Array) -> GdUnitFloatAssert: + var current :Variant = current_value() + if not expected.has(current): + return report_error(GdAssertMessages.error_is_in(current, expected)) + return report_success() + + +func is_not_in(expected :Array) -> GdUnitFloatAssert: + var current :Variant = current_value() + if expected.has(current): + return report_error(GdAssertMessages.error_is_not_in(current, expected)) + return report_success() + + +func is_between(from :float, to :float) -> GdUnitFloatAssert: + var current :Variant = current_value() + if current == null or current < from or current > to: + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid index e69de29b..a5add1a5 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dvce6xeybbh1i diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid index e69de29b..dbde7c4f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://c2jdw0vv5nldq diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd index e69de29b..fc010db3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -0,0 +1,141 @@ +extends GdUnitGodotErrorAssert + +var _current_failure_message := "" +var _custom_failure_message := "" +var _additional_failure_message := "" +var _callable: Callable + + +func _init(callable: Callable) -> void: + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + GdAssertReports.reset_last_error_line_number() + _callable = callable + + +func _execute() -> Array[ErrorLogEntry]: + # execute the given code and monitor for runtime errors + if _callable == null or not _callable.is_valid(): + @warning_ignore("return_value_discarded") + _report_error("Invalid Callable '%s'" % _callable) + else: + await _callable.call() + return await _error_monitor().scan(true) + + +func _error_monitor() -> GodotGdErrorMonitor: + return GdUnitThreadManager.get_current_context().get_execution_context().error_monitor + + +func failure_message() -> String: + return _current_failure_message + + +func _report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func _report_error(error_message: String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, line_number) + return self + + +func _has_log_entry(log_entries: Array[ErrorLogEntry], type: ErrorLogEntry.TYPE, error: Variant) -> bool: + for entry in log_entries: + if entry._type == type and GdObjects.equals(entry._message, error): + # Erase the log entry we already handled it by this assertion, otherwise it will report at twice + _error_monitor().erase_log_entry(entry) + return true + return false + + +func _to_list(log_entries: Array[ErrorLogEntry]) -> String: + if log_entries.is_empty(): + return "no errors" + if log_entries.size() == 1: + return log_entries[0]._message + var value := "" + for entry in log_entries: + value += "'%s'\n" % entry._message + return value + + +func is_null() -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_not_null() -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_equal(_expected: Variant) -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_not_equal(_expected: Variant) -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func override_failure_message(message: String) -> GdUnitGodotErrorAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitGodotErrorAssert: + _additional_failure_message = message + return self + + +func is_success() -> GdUnitGodotErrorAssert: + var log_entries := await _execute() + if log_entries.is_empty(): + return _report_success() + return _report_error(""" + Expecting: no error's are ocured. + but found: '%s' + """.dedent().trim_prefix("\n") % _to_list(log_entries)) + + +func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error) + if result.is_error(): + return _report_error(result.error_message()) + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.SCRIPT_ERROR, expected_error): + return _report_success() + return _report_error(""" + Expecting: a runtime error is triggered. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) + + +func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_warning) + if result.is_error(): + return _report_error(result.error_message()) + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_WARNING, expected_warning): + return _report_success() + return _report_error(""" + Expecting: push_warning() is called. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)]) + + +func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error) + if result.is_error(): + return _report_error(result.error_message()) + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_ERROR, expected_error): + return _report_success() + return _report_error(""" + Expecting: push_error() is called. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid index e69de29b..6da674e1 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cyi6ooahncq7q diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd index e69de29b..bdee249e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -0,0 +1,166 @@ +extends GdUnitIntAssert + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_INT): + @warning_ignore("return_value_discarded") + report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +func is_less(expected :int) -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :int) -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :int) -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :int) -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_even() -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current % 2 != 0: + return report_error(GdAssertMessages.error_is_even(current)) + return report_success() + + +func is_odd() -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current % 2 == 0: + return report_error(GdAssertMessages.error_is_odd(current)) + return report_success() + + +func is_negative() -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current >= 0: + return report_error(GdAssertMessages.error_is_negative(current)) + return report_success() + + +func is_not_negative() -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current < 0: + return report_error(GdAssertMessages.error_is_not_negative(current)) + return report_success() + + +func is_zero() -> GdUnitIntAssert: + var current :Variant = current_value() + if current != 0: + return report_error(GdAssertMessages.error_is_zero(current)) + return report_success() + + +func is_not_zero() -> GdUnitIntAssert: + var current :Variant= current_value() + if current == 0: + return report_error(GdAssertMessages.error_is_not_zero()) + return report_success() + + +func is_in(expected :Array) -> GdUnitIntAssert: + var current :Variant = current_value() + if not expected.has(current): + return report_error(GdAssertMessages.error_is_in(current, expected)) + return report_success() + + +func is_not_in(expected :Array) -> GdUnitIntAssert: + var current :Variant = current_value() + if expected.has(current): + return report_error(GdAssertMessages.error_is_not_in(current, expected)) + return report_success() + + +func is_between(from :int, to :int) -> GdUnitIntAssert: + var current :Variant = current_value() + if current == null or current < from or current > to: + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid index e69de29b..2424b41a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://j4mpmwm2hw61 diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd index e69de29b..955e1ef3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -0,0 +1,166 @@ +extends GdUnitObjectAssert + +var _base: GdUnitAssertImpl + + +func _init(current: Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if (current != null + and (GdUnitAssertions.validate_value_type(current, TYPE_BOOL) + or GdUnitAssertions.validate_value_type(current, TYPE_INT) + or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT) + or GdUnitAssertions.validate_value_type(current, TYPE_STRING))): + @warning_ignore("return_value_discarded") + report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event: int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error: String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_equal(expected: Variant) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +func is_null() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +@warning_ignore("shadowed_global_identifier") +func is_same(expected: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if not is_same(current, expected): + return report_error(GdAssertMessages.error_is_same(current, expected)) + return report_success() + + +func is_not_same(expected: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if is_same(current, expected): + return report_error(GdAssertMessages.error_not_same(current, expected)) + return report_success() + + +func is_instanceof(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if current == null or not is_instance_of(current, type): + var result_expected := GdObjects.extract_class_name(type) + var result_current := GdObjects.extract_class_name(current) + return report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected)) + return report_success() + + +func is_not_instanceof(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if is_instance_of(current, type): + var result := GdObjects.extract_class_name(type) + if result.is_success(): + return report_error("Expected not be a instance of <%s>" % str(result.value())) + + push_error("Internal ERROR: %s" % result.error_message()) + return self + return report_success() + + +## Checks whether the current object inherits from the specified type. +func is_inheriting(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if not is_instance_of(current, TYPE_OBJECT): + return report_error("Expected '%s' to inherit from at least Object." % str(current)) + var result := _inherits(current, type) + if result.is_success(): + return report_success() + return report_error(result.error_message()) + + +## Checks whether the current object does NOT inherit from the specified type. +func is_not_inheriting(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if not is_instance_of(current, TYPE_OBJECT): + return report_error("Expected '%s' to inherit from at least Object." % str(current)) + var result := _inherits(current, type) + if result.is_success(): + return report_error("Expected type to not inherit from <%s>" % _extract_class_type(type)) + return report_success() + + +func _inherits(current: Variant, type: Variant) -> GdUnitResult: + var type_as_string := _extract_class_type(type) + if type_as_string == "Object": + return GdUnitResult.success("") + + var obj: Object = current + for p in obj.get_property_list(): + var clazz_name :String = p["name"] + if p["usage"] == PROPERTY_USAGE_CATEGORY and clazz_name == p["hint_string"] and clazz_name == type_as_string: + return GdUnitResult.success("") + var script: Script = obj.get_script() + if script != null: + while script != null: + var result := GdObjects.extract_class_name(script) + if result.is_success() and result.value() == type_as_string: + return GdUnitResult.success("") + script = script.get_base_script() + return GdUnitResult.error("Expected type to inherit from <%s>" % type_as_string) + + +func _extract_class_type(type: Variant) -> String: + if type is String: + return type + var result := GdObjects.extract_class_name(type) + if result.is_error(): + return "" + return result.value() diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid index e69de29b..5db4c8b3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://bm6qm58a0dacq diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index e69de29b..98a6768f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -0,0 +1,128 @@ +extends GdUnitResultAssert + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not validate_value_type(current): + @warning_ignore("return_value_discarded") + report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func validate_value_type(value :Variant) -> bool: + return value == null or value is GdUnitResult + + +func current_value() -> GdUnitResult: + return _base.current_value() + + +func report_success() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitResultAssert: + return is_value(expected) + + +func is_not_equal(expected: Variant) -> GdUnitResultAssert: + var result := current_value() + var value :Variant = null if result == null else result.value() + if GdObjects.equals(value, expected): + return report_error(GdAssertMessages.error_not_equal(value, expected)) + return report_success() + + +func is_empty() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_empty(): + return report_error(GdAssertMessages.error_result_is_empty(result)) + return report_success() + + +func is_success() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_success(): + return report_error(GdAssertMessages.error_result_is_success(result)) + return report_success() + + +func is_warning() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_warn(): + return report_error(GdAssertMessages.error_result_is_warning(result)) + return report_success() + + +func is_error() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_error(): + return report_error(GdAssertMessages.error_result_is_error(result)) + return report_success() + + +func contains_message(expected :String) -> GdUnitResultAssert: + var result := current_value() + if result == null: + return report_error(GdAssertMessages.error_result_has_message("", expected)) + if result.is_success(): + return report_error(GdAssertMessages.error_result_has_message_on_success(expected)) + if result.is_error() and result.error_message() != expected: + return report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected)) + if result.is_warn() and result.warn_message() != expected: + return report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected)) + return report_success() + + +func is_value(expected: Variant) -> GdUnitResultAssert: + var result := current_value() + var value :Variant = null if result == null else result.value() + if not GdObjects.equals(value, expected): + return report_error(GdAssertMessages.error_result_is_value(value, expected)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid index e69de29b..6d1ed11d 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://b0dlq6jyjcvps diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd index e69de29b..6f5878c8 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -0,0 +1,143 @@ +extends GdUnitSignalAssert + +const DEFAULT_TIMEOUT := 2000 + +var _signal_collector :GdUnitSignalCollector +var _emitter :Object +var _current_failure_message :String = "" +var _custom_failure_message :String = "" +var _additional_failure_message: String = "" +var _line_number := -1 +var _timeout := DEFAULT_TIMEOUT +var _interrupted := false + + +func _init(emitter :Object) -> void: + # save the actual assert instance on the current thread context + var context := GdUnitThreadManager.get_current_context() + context.set_assert(self) + _signal_collector = context.get_signal_collector() + _line_number = GdUnitAssertions.get_line_number() + _emitter = emitter + GdAssertReports.reset_last_error_line_number() + + +func _notification(what :int) -> void: + if what == NOTIFICATION_PREDELETE: + _interrupted = true + if is_instance_valid(_emitter): + _signal_collector.unregister_emitter(_emitter) + _emitter = null + + +func report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func report_warning(message :String) -> GdUnitAssert: + GdAssertReports.report_warning(message, GdUnitAssertions.get_line_number()) + return self + + +func report_error(failure :String) -> GdUnitAssert: + _current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, _line_number) + return self + + +func failure_message() -> String: + return _current_failure_message + + +func override_failure_message(message: String) -> GdUnitSignalAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitSignalAssert: + _additional_failure_message = message + return self + + +func wait_until(timeout := 2000) -> GdUnitSignalAssert: + if timeout <= 0: + @warning_ignore("return_value_discarded") + report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!") + _timeout = DEFAULT_TIMEOUT + else: + _timeout = timeout + return self + + +func is_null() -> GdUnitSignalAssert: + if _emitter != null: + return report_error(GdAssertMessages.error_is_null(_emitter)) + return report_success() + + +func is_not_null() -> GdUnitSignalAssert: + if _emitter == null: + return report_error(GdAssertMessages.error_is_not_null()) + return report_success() + + +func is_equal(_expected: Variant) -> GdUnitSignalAssert: + return report_error("Not implemented") + + +func is_not_equal(_expected: Variant) -> GdUnitSignalAssert: + return report_error("Not implemented") + + +# Verifies the signal exists checked the emitter +func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: + if not _emitter.has_signal(signal_name): + @warning_ignore("return_value_discarded") + report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()]) + return self + + +# Verifies that given signal is emitted until waiting time +func is_emitted(name :String, args := []) -> GdUnitSignalAssert: + _line_number = GdUnitAssertions.get_line_number() + return await _wail_until_signal(name, args, false) + + +# Verifies that given signal is NOT emitted until waiting time +func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: + _line_number = GdUnitAssertions.get_line_number() + return await _wail_until_signal(name, args, true) + + +func _wail_until_signal(signal_name :String, expected_args :Array, expect_not_emitted: bool) -> GdUnitSignalAssert: + if _emitter == null: + return report_error("Can't wait for signal checked a NULL object.") + # first verify the signal is defined + if not _emitter.has_signal(signal_name): + return report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()]) + _signal_collector.register_emitter(_emitter) + var time_scale := Engine.get_time_scale() + var timer := Timer.new() + (Engine.get_main_loop() as SceneTree).root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + @warning_ignore("return_value_discarded") + timer.timeout.connect(func on_timeout() -> void: _interrupted = true) + timer.start((_timeout/1000.0)*time_scale) + var is_signal_emitted := false + while not _interrupted and not is_signal_emitted: + await (Engine.get_main_loop() as SceneTree).process_frame + if is_instance_valid(_emitter): + is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args) + if is_signal_emitted and expect_not_emitted: + @warning_ignore("return_value_discarded") + report_error(GdAssertMessages.error_signal_emitted(signal_name, expected_args, LocalTime.elapsed(int(_timeout-timer.time_left*1000)))) + + if _interrupted and not expect_not_emitted: + @warning_ignore("return_value_discarded") + report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout))) + timer.free() + if is_instance_valid(_emitter): + _signal_collector.reset_received_signals(_emitter, signal_name, expected_args) + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid index e69de29b..0feeb0f2 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dlh37yc086vr5 diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd index e69de29b..cdbcdff8 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -0,0 +1,208 @@ +extends GdUnitStringAssert + +var _base: GdUnitAssertImpl + + +func _init(current :Variant) -> void: + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if current != null and typeof(current) != TYPE_STRING and typeof(current) != TYPE_STRING_NAME: + @warning_ignore("return_value_discarded") + report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func failure_message() -> String: + return _base.failure_message() + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func override_failure_message(message: String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message: String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitStringAssert: + return _is_equal(expected, false, GdAssertMessages.error_equal) + + +func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert: + return _is_equal(expected, true, GdAssertMessages.error_equal_ignoring_case) + + +@warning_ignore_start("unsafe_call_argument") +func _is_equal(expected: Variant, ignore_case: bool, message_cb: Callable) -> GdUnitStringAssert: + var current: Variant = current_value() + if current == null: + return report_error(message_cb.call(current, expected)) + var cur_value := str(current) + if not GdObjects.equals(cur_value, expected, ignore_case): + var exp_value := str(expected) + if contains_bbcode(cur_value): + # mask user bbcode + # https://docs.godotengine.org/en/4.5/tutorials/ui/bbcode_in_richtextlabel.html#handling-user-input-safely + return report_error(message_cb.call(cur_value.replace("[", "[lb]"), exp_value.replace("[", "[lb]"))) + var diffs := GdDiffTool.string_diff(cur_value, exp_value) + var formatted_current := GdAssertMessages.colored_array_div(diffs[1]) + return report_error(message_cb.call(formatted_current, exp_value)) + return report_success() +@warning_ignore_restore("unsafe_call_argument") + + +func is_not_equal(expected: Variant) -> GdUnitStringAssert: + var current: Variant = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert: + var current :Variant = current_value() + if GdObjects.equals(current, expected, true): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +func is_empty() -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or not (current as String).is_empty(): + return report_error(GdAssertMessages.error_is_empty(current)) + return report_success() + + +func is_not_empty() -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or (current as String).is_empty(): + return report_error(GdAssertMessages.error_is_not_empty()) + return report_success() + + +func contains(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or (current as String).find(expected) == -1: + return report_error(GdAssertMessages.error_contains(current, expected)) + return report_success() + + +func not_contains(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current != null and (current as String).find(expected) != -1: + return report_error(GdAssertMessages.error_not_contains(current, expected)) + return report_success() + + +func contains_ignoring_case(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or (current as String).findn(expected) == -1: + return report_error(GdAssertMessages.error_contains_ignoring_case(current, expected)) + return report_success() + + +func not_contains_ignoring_case(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current != null and (current as String).findn(expected) != -1: + return report_error(GdAssertMessages.error_not_contains_ignoring_case(current, expected)) + return report_success() + + +func starts_with(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + @warning_ignore("unsafe_cast") + if current == null or (current as String).find(expected) != 0: + return report_error(GdAssertMessages.error_starts_with(current, expected)) + return report_success() + + +func ends_with(expected :String) -> GdUnitStringAssert: + var current :Variant = current_value() + if current == null: + return report_error(GdAssertMessages.error_ends_with(current, expected)) + @warning_ignore("unsafe_cast") + var find :int = (current as String).length() - expected.length() + @warning_ignore("unsafe_cast") + if (current as String).rfind(expected) != find: + return report_error(GdAssertMessages.error_ends_with(current, expected)) + return report_success() + + +# gdlint:disable=max-returns +func has_length(expected :int, comparator := Comparator.EQUAL) -> GdUnitStringAssert: + var current :Variant = current_value() + if current == null: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + var str_current: String = current + match comparator: + Comparator.EQUAL: + if str_current.length() != expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) + Comparator.LESS_THAN: + if str_current.length() >= expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) + Comparator.LESS_EQUAL: + if str_current.length() > expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) + Comparator.GREATER_THAN: + if str_current.length() <= expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) + Comparator.GREATER_EQUAL: + if str_current.length() < expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) + _: + return report_error("Comparator '%d' not implemented!" % comparator) + return report_success() + + +func contains_bbcode(value: String) -> bool: + var rtl := RichTextLabel.new() + rtl.bbcode_enabled = true + rtl.parse_bbcode(value) + var has_bbcode := rtl.get_parsed_text() != value + rtl.free() + return has_bbcode diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid index e69de29b..ba34078a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dxqvilchqqeta diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd index e69de29b..fbc031a4 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -0,0 +1,187 @@ +extends GdUnitVectorAssert + +var _base: GdUnitAssertImpl +var _current_type: int +var _type_check: bool + +func _init(current: Variant, type_check := true) -> void: + _type_check = type_check + _base = GdUnitAssertImpl.new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not _validate_value_type(current): + @warning_ignore("return_value_discarded") + report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current)) + _current_type = typeof(current) + + +func _notification(event :int) -> void: + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func _validate_value_type(value :Variant) -> bool: + return ( + value == null + or typeof(value) in [ + TYPE_VECTOR2, + TYPE_VECTOR2I, + TYPE_VECTOR3, + TYPE_VECTOR3I, + TYPE_VECTOR4, + TYPE_VECTOR4I + ] + ) + + +func _validate_is_vector_type(value :Variant) -> bool: + var type := typeof(value) + if type == _current_type or _current_type == TYPE_NIL: + return true + @warning_ignore("return_value_discarded") + report_error(GdAssertMessages.error_is_wrong_type(_current_type, type)) + return false + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base.failure_message() + + +func override_failure_message(message: String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + + +func append_failure_message(message :String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + + +func is_null() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitVectorAssert: + if _type_check and not _validate_is_vector_type(expected): + return self + @warning_ignore("return_value_discarded") + _base.is_equal(expected) + return self + + +func is_not_equal(expected: Variant) -> GdUnitVectorAssert: + if _type_check and not _validate_is_vector_type(expected): + return self + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self + + +@warning_ignore("shadowed_global_identifier") +func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected) or not _validate_is_vector_type(approx): + return self + var current :Variant = current_value() + var from :Variant = expected - approx + var to :Variant = expected + approx + if current == null or (not _is_equal_approx(current, from, to)): + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() + + +func _is_equal_approx(current :Variant, from :Variant, to :Variant) -> bool: + match typeof(current): + TYPE_VECTOR2, TYPE_VECTOR2I: + return ((current.x >= from.x and current.y >= from.y) + and (current.x <= to.x and current.y <= to.y)) + TYPE_VECTOR3, TYPE_VECTOR3I: + return ((current.x >= from.x and current.y >= from.y and current.z >= from.z) + and (current.x <= to.x and current.y <= to.y and current.z <= to.z)) + TYPE_VECTOR4, TYPE_VECTOR4I: + return ((current.x >= from.x and current.y >= from.y and current.z >= from.z and current.w >= from.w) + and (current.x <= to.x and current.y <= to.y and current.z <= to.z and current.w <= to.w)) + _: + push_error("Missing implementation '_is_equal_approx' for vector type %s" % typeof(current)) + return false + + +func is_less(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current :Variant = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current :Variant = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current :Variant = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current :Variant = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(from) or not _validate_is_vector_type(to): + return self + var current :Variant = current_value() + if current == null or not (current >= from and current <= to): + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() + + +func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(from) or not _validate_is_vector_type(to): + return self + var current :Variant = current_value() + if (current != null and current >= from and current <= to): + return report_error(GdAssertMessages.error_is_value(Comparator.NOT_BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid index e69de29b..c0a7e13a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://r4avfcakvscw diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd b/addons/gdUnit4/src/asserts/ValueProvider.gd index e69de29b..be01f70b 100644 --- a/addons/gdUnit4/src/asserts/ValueProvider.gd +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd @@ -0,0 +1,10 @@ +# base interface for assert value provider +class_name ValueProvider +extends RefCounted + +func get_value() -> Variant: + return null + + +func dispose() -> void: + pass diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd.uid b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid index e69de29b..a34788ef 100644 --- a/addons/gdUnit4/src/asserts/ValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid @@ -0,0 +1 @@ +uid://8y15b6ts3kss diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd index e69de29b..aa023194 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd @@ -0,0 +1,62 @@ +class_name CmdArgumentParser +extends RefCounted + +var _options :CmdOptions +var _tool_name :String +var _parsed_commands :Dictionary = Dictionary() + + +func _init(p_options :CmdOptions, p_tool_name :String) -> void: + _options = p_options + _tool_name = p_tool_name + + +func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult: + _parsed_commands.clear() + + # parse until first program argument + while not args.is_empty(): + var arg :String = args.pop_front() + if arg.find(_tool_name) != -1: + break + + if args.is_empty(): + return GdUnitResult.empty() + + # now parse all arguments + while not args.is_empty(): + var cmd :String = args.pop_front() + var option := _options.get_option(cmd) + + if option: + if _parse_cmd_arguments(option, args) == -1: + return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command()) + elif not ignore_unknown_cmd: + return GdUnitResult.error("Unknown '%s' command!" % cmd) + return GdUnitResult.success(_parsed_commands.values()) + + +func options() -> CmdOptions: + return _options + + +func _parse_cmd_arguments(option: CmdOption, args: Array) -> int: + var command_name := option.short_command() + var command: CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name)) + + if option.has_argument(): + if not option.is_argument_optional() and args.is_empty(): + return -1 + if _is_next_value_argument(args): + var value: String = args.pop_front() + command.add_argument(value) + elif not option.is_argument_optional(): + return -1 + _parsed_commands[command_name] = command + return 0 + + +func _is_next_value_argument(args: PackedStringArray) -> bool: + if args.is_empty(): + return false + return _options.get_option(args[0]) == null diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid index e69de29b..f0bc4b67 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid @@ -0,0 +1 @@ +uid://d4hd3vc50jltg diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd b/addons/gdUnit4/src/cmd/CmdCommand.gd index e69de29b..92e8c1fe 100644 --- a/addons/gdUnit4/src/cmd/CmdCommand.gd +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd @@ -0,0 +1,27 @@ +class_name CmdCommand +extends RefCounted + +var _name: String +var _arguments: PackedStringArray + + +func _init(p_name :String, p_arguments := []) -> void: + _name = p_name + _arguments = PackedStringArray(p_arguments) + + +func name() -> String: + return _name + + +func arguments() -> PackedStringArray: + return _arguments + + +func add_argument(arg :String) -> void: + @warning_ignore("return_value_discarded") + _arguments.append(arg) + + +func _to_string() -> String: + return "%s:%s" % [_name, ", ".join(_arguments)] diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd.uid b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid index e69de29b..f087f8c7 100644 --- a/addons/gdUnit4/src/cmd/CmdCommand.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid @@ -0,0 +1 @@ +uid://w4mr1j0k0l diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd index e69de29b..2a7ed553 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd @@ -0,0 +1,136 @@ +class_name CmdCommandHandler +extends RefCounted + +const CB_SINGLE_ARG = 0 +const CB_MULTI_ARGS = 1 +const NO_CB := Callable() + +var _cmd_options :CmdOptions +# holds the command callbacks by key::String and value: [, ]:Array +# Dictionary[String, Array[Callback] +var _command_cbs :Dictionary + + + +func _init(cmd_options: CmdOptions) -> void: + _cmd_options = cmd_options + + +# register a callback function for given command +# cmd_name short name of the command +# fr_arg a funcref to a function with a single argument +func register_cb(cmd_name: String, cb: Callable) -> CmdCommandHandler: + var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB]) + if registered_cb[CB_SINGLE_ARG]: + push_error("A function for command '%s' is already registered!" % cmd_name) + return self + + if not _validate_cb_signature(cb, TYPE_STRING): + push_error( + ("The callback '%s:%s' for command '%s' has invalid function signature. " + +"The callback signature must be 'func name(value: PackedStringArray)'") + % [cb.get_object().get_class(), cb.get_method(), cmd_name]) + return null + + registered_cb[CB_SINGLE_ARG] = cb + _command_cbs[cmd_name] = registered_cb + return self + + +# register a callback function for given command +# cb a funcref to a function with a variable number of arguments but expects all parameters to be passed via a single Array. +func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler: + var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB]) + if registered_cb[CB_MULTI_ARGS]: + push_error("A function for command '%s' is already registered!" % cmd_name) + return self + + if not _validate_cb_signature(cb, TYPE_PACKED_STRING_ARRAY): + push_error( + ("The callback '%s:%s' for command '%s' has invalid function signature. " + +"The callback signature must be 'func name(value: PackedStringArray)'") + % [cb.get_object().get_class(), cb.get_method(), cmd_name]) + return null + + registered_cb[CB_MULTI_ARGS] = cb + _command_cbs[cmd_name] = registered_cb + return self + + +func _validate() -> GdUnitResult: + var errors := PackedStringArray() + # Dictionary[StringName, String] + var registered_cbs := Dictionary() + + for cmd_name in _command_cbs.keys() as Array[String]: + var cb: Callable = (_command_cbs[cmd_name][CB_SINGLE_ARG] + if _command_cbs[cmd_name][CB_SINGLE_ARG] + else _command_cbs[cmd_name][CB_MULTI_ARGS]) + if cb != NO_CB and not cb.is_valid(): + @warning_ignore("return_value_discarded") + errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name) + if _cmd_options.get_option(cmd_name) == null: + @warning_ignore("return_value_discarded") + errors.append("The command '%s' is unknown, verify your CmdOptions!" % cmd_name) + # verify for multiple registered command callbacks + if cb != NO_CB: + var cb_method := cb.get_method() + if registered_cbs.has(cb_method): + var already_registered_cmd :String = registered_cbs[cb_method] + @warning_ignore("return_value_discarded") + errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd]) + else: + registered_cbs[cb_method] = cmd_name + if errors.is_empty(): + return GdUnitResult.success(true) + return GdUnitResult.error("\n".join(errors)) + + +func execute(commands: Array[CmdCommand]) -> GdUnitResult: + var result := _validate() + if result.is_error(): + return result + for cmd in commands: + var cmd_name := cmd.name() + if _command_cbs.has(cmd_name): + var cb_s: Callable = _command_cbs.get(cmd_name)[CB_SINGLE_ARG] + var arguments := cmd.arguments() + var cmd_option := _cmd_options.get_option(cmd_name) + + if arguments.is_empty(): + cb_s.call() + elif arguments.size() > 1: + var cb_m: Callable = _command_cbs.get(cmd_name)[CB_MULTI_ARGS] + cb_m.call(arguments) + else: + if cmd_option.type() == TYPE_BOOL: + cb_s.call(true if arguments[0] == "true" else false) + else: + cb_s.call(arguments[0]) + + return GdUnitResult.success(true) + + +func _validate_cb_signature(cb: Callable, arg_type: int) -> bool: + for m in cb.get_object().get_method_list(): + if m["name"] == cb.get_method(): + @warning_ignore("unsafe_cast") + return _validate_func_arguments(m["args"] as Array, arg_type) + return true + + +func _validate_func_arguments(arguments: Array, arg_type: int) -> bool: + # validate we have a single argument + if arguments.size() > 1: + return false + # a cb with no arguments is also valid + if arguments.size() == 0: + return true + # validate argument type + var arg: Dictionary = arguments[0] + @warning_ignore("unsafe_cast") + if arg["usage"] as int == PROPERTY_USAGE_NIL_IS_VARIANT: + return true + if arg["type"] != arg_type: + return false + return true diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid index e69de29b..a1a2ddc6 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid @@ -0,0 +1 @@ +uid://ccm3ivfiaf3i7 diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd b/addons/gdUnit4/src/cmd/CmdOption.gd index e69de29b..a4982de2 100644 --- a/addons/gdUnit4/src/cmd/CmdOption.gd +++ b/addons/gdUnit4/src/cmd/CmdOption.gd @@ -0,0 +1,61 @@ +class_name CmdOption +extends RefCounted + + +var _commands :PackedStringArray +var _help :String +var _description :String +var _type :int +var _arg_optional :bool = false + + +# constructs a command option by given arguments +# commands : a string with comma separated list of available commands begining with the short form +# help: a help text show howto use +# description: a full description of the command +# type: the argument type +# arg_optional: defines of the argument optional +func _init(p_commands :String, p_help :String, p_description :String, p_type :int = TYPE_NIL, p_arg_optional :bool = false) -> void: + _commands = p_commands.replace(" ", "").replace("\t", "").split(",") + _help = p_help + _description = p_description + _type = p_type + _arg_optional = p_arg_optional + + +func commands() -> PackedStringArray: + return _commands + + +func short_command() -> String: + return _commands[0] + + +func help() -> String: + return _help + + +func description() -> String: + return _description + + +func type() -> int: + return _type + + +func is_argument_optional() -> bool: + return _arg_optional + + +func has_argument() -> bool: + return _type != TYPE_NIL + + +func describe() -> String: + if help().is_empty(): + return " %-32s %s \n" % [commands(), description()] + return " %-32s %s \n %-32s %s\n" % [commands(), description(), "", help()] + + +func _to_string() -> String: + return describe() diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd.uid b/addons/gdUnit4/src/cmd/CmdOption.gd.uid index e69de29b..0b077447 100644 --- a/addons/gdUnit4/src/cmd/CmdOption.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdOption.gd.uid @@ -0,0 +1 @@ +uid://ccnb2ah35atho diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd b/addons/gdUnit4/src/cmd/CmdOptions.gd index e69de29b..c6105298 100644 --- a/addons/gdUnit4/src/cmd/CmdOptions.gd +++ b/addons/gdUnit4/src/cmd/CmdOptions.gd @@ -0,0 +1,31 @@ +class_name CmdOptions +extends RefCounted + + +var _default_options :Array[CmdOption] +var _advanced_options :Array[CmdOption] + + +func _init(p_options :Array[CmdOption] = [], p_advanced_options :Array[CmdOption] = []) -> void: + # default help options + _default_options = p_options + _advanced_options = p_advanced_options + + +func default_options() -> Array[CmdOption]: + return _default_options + + +func advanced_options() -> Array[CmdOption]: + return _advanced_options + + +func options() -> Array[CmdOption]: + return default_options() + advanced_options() + + +func get_option(cmd :String) -> CmdOption: + for option in options(): + if Array(option.commands()).has(cmd): + return option + return null diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd.uid b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid index e69de29b..6d1112bf 100644 --- a/addons/gdUnit4/src/cmd/CmdOptions.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid @@ -0,0 +1 @@ +uid://0p8udx4tdwol diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd index e69de29b..74f0e175 100644 --- a/addons/gdUnit4/src/core/GdArrayTools.gd +++ b/addons/gdUnit4/src/core/GdArrayTools.gd @@ -0,0 +1,127 @@ +## Small helper tool to work with Godot Arrays +class_name GdArrayTools +extends RefCounted + + +const max_elements := 32 +const ARRAY_TYPES := [ + TYPE_ARRAY, + TYPE_PACKED_BYTE_ARRAY, + TYPE_PACKED_INT32_ARRAY, + TYPE_PACKED_INT64_ARRAY, + TYPE_PACKED_FLOAT32_ARRAY, + TYPE_PACKED_FLOAT64_ARRAY, + TYPE_PACKED_STRING_ARRAY, + TYPE_PACKED_VECTOR2_ARRAY, + TYPE_PACKED_VECTOR3_ARRAY, + TYPE_PACKED_VECTOR4_ARRAY, + TYPE_PACKED_COLOR_ARRAY +] + + +static func is_array_type(value: Variant) -> bool: + return is_type_array(typeof(value)) + + +static func is_type_array(type :int) -> bool: + return type in ARRAY_TYPES + + +## Filters an array by given value[br] +## If the given value not an array it returns null, will remove all occurence of given value. +static func filter_value(array: Variant, value: Variant) -> Variant: + if not is_array_type(array): + return null + + @warning_ignore("unsafe_method_access") + var filtered_array: Variant = array.duplicate() + @warning_ignore("unsafe_method_access") + var index: int = filtered_array.find(value) + while index != -1: + @warning_ignore("unsafe_method_access") + filtered_array.remove_at(index) + @warning_ignore("unsafe_method_access") + index = filtered_array.find(value) + return filtered_array + + +## Groups an array by a custom key selector +## The function should take an item and return the group key +static func group_by(array: Array, key_selector: Callable) -> Dictionary: + var result := {} + + for item: Variant in array: + var group_key: Variant = key_selector.call(item) + var values: Array = result.get_or_add(group_key, []) + values.append(item) + + return result + + +## Erases a value from given array by using equals(l,r) to find the element to erase +static func erase_value(array :Array, value :Variant) -> void: + for element :Variant in array: + if GdObjects.equals(element, value): + array.erase(element) + + +## Scans for the array build in type on a untyped array[br] +## Returns the buildin type by scan all values and returns the type if all values has the same type. +## If the values has different types TYPE_VARIANT is returend +static func scan_typed(array :Array) -> int: + if array.is_empty(): + return TYPE_NIL + var actual_type := GdObjects.TYPE_VARIANT + for value :Variant in array: + var current_type := typeof(value) + if not actual_type in [GdObjects.TYPE_VARIANT, current_type]: + return GdObjects.TYPE_VARIANT + actual_type = current_type + return actual_type + + +## Converts given array into a string presentation.[br] +## This function is different to the original Godot str() implementation. +## The string presentaion contains fullquallified typed informations. +##[br] +## Examples: +## [codeblock] +## # will result in PackedString(["a", "b"]) +## GdArrayTools.as_string(PackedStringArray("a", "b")) +## # will result in PackedString(["a", "b"]) +## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN)) +## [/codeblock] +static func as_string(elements: Variant, encode_value := true) -> String: + var delemiter := ", " + if elements == null: + return "" + @warning_ignore("unsafe_cast") + if (elements as Array).is_empty(): + return "" + var prefix := _typeof_as_string(elements) if encode_value else "" + var formatted := "" + var index := 0 + for element :Variant in elements: + if max_elements != -1 and index > max_elements: + return prefix + "[" + formatted + delemiter + "...]" + if formatted.length() > 0 : + formatted += delemiter + formatted += GdDefaultValueDecoder.decode(element) if encode_value else str(element) + index += 1 + return prefix + "[" + formatted + "]" + + +static func has_same_content(current: Array, other: Array) -> bool: + if current.size() != other.size(): return false + for element: Variant in current: + if not other.has(element): return false + if current.count(element) != other.count(element): return false + return true + + +static func _typeof_as_string(value :Variant) -> String: + var type := typeof(value) + # for untyped array we retun empty string + if type == TYPE_ARRAY: + return "" + return GdObjects.typeof_as_string(value) diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd.uid b/addons/gdUnit4/src/core/GdArrayTools.gd.uid index e69de29b..98d9d57d 100644 --- a/addons/gdUnit4/src/core/GdArrayTools.gd.uid +++ b/addons/gdUnit4/src/core/GdArrayTools.gd.uid @@ -0,0 +1 @@ +uid://bk60ywsj4ekp7 diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd index e69de29b..5131df71 100644 --- a/addons/gdUnit4/src/core/GdDiffTool.gd +++ b/addons/gdUnit4/src/core/GdDiffTool.gd @@ -0,0 +1,224 @@ +# Myers' Diff Algorithm implementation +# Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers +class_name GdDiffTool +extends RefCounted + + +const DIV_ADD :int = 214 +const DIV_SUB :int = 215 + + +class Edit: + enum Type { EQUAL, INSERT, DELETE } + var type: Type + var character: int + + func _init(t: Type, chr: int) -> void: + type = t + character = chr + + +# Main entry point - returns [ldiff, rdiff] +static func string_diff(left: Variant, right: Variant) -> Array[PackedInt32Array]: + var lb := PackedInt32Array() if left == null else str(left).to_utf32_buffer().to_int32_array() + var rb := PackedInt32Array() if right == null else str(right).to_utf32_buffer().to_int32_array() + + # Early exit for identical strings + if lb == rb: + return [lb.duplicate(), rb.duplicate()] + + var edits := _myers_diff(lb, rb) + return _edits_to_diff_format(edits) + + +# Core Myers' algorithm +static func _myers_diff(a: PackedInt32Array, b: PackedInt32Array) -> Array[Edit]: + var n := a.size() + var m := b.size() + var max_d := n + m + + # V array stores the furthest reaching x coordinate for each k-line + # We need indices from -max_d to max_d, so we offset by max_d + var v := PackedInt32Array() + v.resize(2 * max_d + 1) + v.fill(-1) + v[max_d + 1] = 0 # k=1 starts at x=0 + + var trace := [] # Store V arrays for each d to backtrack later + + # Find the edit distance + for d in range(0, max_d + 1): + # Store current V for backtracking + trace.append(v.duplicate()) + + for k in range(-d, d + 1, 2): + var k_offset := k + max_d + + # Decide whether to move down or right + var x: int + if k == -d or (k != d and v[k_offset - 1] < v[k_offset + 1]): + x = v[k_offset + 1] # Move down (insert from b) + else: + x = v[k_offset - 1] + 1 # Move right (delete from a) + + var y := x - k + + # Follow diagonal as far as possible (matching characters) + while x < n and y < m and a[x] == b[y]: + x += 1 + y += 1 + + v[k_offset] = x + + # Check if we've reached the end + if x >= n and y >= m: + return _backtrack(a, b, trace, d, max_d) + + # Should never reach here for valid inputs + return [] + + +# Backtrack through the edit graph to build the edit script +static func _backtrack(a: PackedInt32Array, b: PackedInt32Array, trace: Array, d: int, max_d: int) -> Array[Edit]: + var edits: Array[Edit] = [] + var x := a.size() + var y := b.size() + + # Walk backwards through each d value + for depth in range(d, -1, -1): + var v: PackedInt32Array = trace[depth] + var k := x - y + var k_offset := k + max_d + + # Determine previous k + var prev_k: int + if k == -depth or (k != depth and v[k_offset - 1] < v[k_offset + 1]): + prev_k = k + 1 + else: + prev_k = k - 1 + + var prev_k_offset := prev_k + max_d + var prev_x := v[prev_k_offset] + var prev_y := prev_x - prev_k + + # Extract diagonal (equal) characters + while x > prev_x and y > prev_y: + x -= 1 + y -= 1 + #var char_array := PackedInt32Array([a[x]]) + edits.insert(0, Edit.new(Edit.Type.EQUAL, a[x])) + + # Record the edit operation + if depth > 0: + if x == prev_x: + # Insert from b + y -= 1 + #var char_array := PackedInt32Array([b[y]]) + edits.insert(0, Edit.new(Edit.Type.INSERT, b[y])) + else: + # Delete from a + x -= 1 + #var char_array := PackedInt32Array([a[x]]) + edits.insert(0, Edit.new(Edit.Type.DELETE, a[x])) + + return edits + + +# Convert edit script to the DIV_ADD/DIV_SUB format +static func _edits_to_diff_format(edits: Array[Edit]) -> Array[PackedInt32Array]: + var ldiff := PackedInt32Array() + var rdiff := PackedInt32Array() + + for edit in edits: + match edit.type: + Edit.Type.EQUAL: + ldiff.append(edit.character) + rdiff.append(edit.character) + Edit.Type.INSERT: + ldiff.append(DIV_ADD) + ldiff.append(edit.character) + rdiff.append(DIV_SUB) + rdiff.append(edit.character) + Edit.Type.DELETE: + ldiff.append(DIV_SUB) + ldiff.append(edit.character) + rdiff.append(DIV_ADD) + rdiff.append(edit.character) + + return [ldiff, rdiff] + + +# prototype +static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray: + var text1Words := text1.split(" ") + var text2Words := text2.split(" ") + var text1WordCount := text1Words.size() + var text2WordCount := text2Words.size() + var solutionMatrix := Array() + for i in text1WordCount+1: + var ar := Array() + for n in text2WordCount+1: + ar.append(0) + solutionMatrix.append(ar) + + for i in range(text1WordCount-1, 0, -1): + for j in range(text2WordCount-1, 0, -1): + if text1Words[i] == text2Words[j]: + solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1; + else: + solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]); + + var i := 0 + var j := 0 + var lcsResultList := PackedStringArray(); + while (i < text1WordCount && j < text2WordCount): + if text1Words[i] == text2Words[j]: + @warning_ignore("return_value_discarded") + lcsResultList.append(text2Words[j]) + i += 1 + j += 1 + else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]): + i += 1 + else: + j += 1 + return lcsResultList + + +static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String: + var stringBuffer := "" + if text1 == null and lcsList == null: + return stringBuffer + + var text1Words := text1.split(" ") + var text2Words := text2.split(" ") + var i := 0 + var j := 0 + var word1LastIndex := 0 + var word2LastIndex := 0 + for k in lcsList.size(): + while i < text1Words.size() and j < text2Words.size(): + if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]: + stringBuffer += "" + lcsList[k] + " " + word1LastIndex = i + 1 + word2LastIndex = j + 1 + i = text1Words.size() + j = text2Words.size() + + else: if text1Words[i] != lcsList[k]: + while i < text1Words.size() and text1Words[i] != lcsList[k]: + stringBuffer += "" + text1Words[i] + " " + i += 1 + else: if text2Words[j] != lcsList[k]: + while j < text2Words.size() and text2Words[j] != lcsList[k]: + stringBuffer += "" + text2Words[j] + " " + j += 1 + i = word1LastIndex + j = word2LastIndex + + while word1LastIndex < text1Words.size(): + stringBuffer += "" + text1Words[word1LastIndex] + " " + word1LastIndex += 1 + while word2LastIndex < text2Words.size(): + stringBuffer += "" + text2Words[word2LastIndex] + " " + word2LastIndex += 1 + return stringBuffer diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd.uid b/addons/gdUnit4/src/core/GdDiffTool.gd.uid index e69de29b..89041b66 100644 --- a/addons/gdUnit4/src/core/GdDiffTool.gd.uid +++ b/addons/gdUnit4/src/core/GdDiffTool.gd.uid @@ -0,0 +1 @@ +uid://b5sli0lem5xca diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index e69de29b..279e5188 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -0,0 +1,726 @@ +# This is a helper class to compare two objects by equals +class_name GdObjects +extends Resource + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +# introduced with Godot 4.3.beta1 +const TYPE_VOID = 1000 +const TYPE_VARARG = 1001 +const TYPE_VARIANT = 1002 +const TYPE_FUNC = 1003 +const TYPE_FUZZER = 1004 +# missing Godot types +const TYPE_NODE = 2001 +const TYPE_CONTROL = 2002 +const TYPE_CANVAS = 2003 +const TYPE_ENUM = 2004 + + +const TYPE_AS_STRING_MAPPINGS := { + TYPE_NIL: "null", + TYPE_BOOL: "bool", + TYPE_INT: "int", + TYPE_FLOAT: "float", + TYPE_STRING: "String", + TYPE_VECTOR2: "Vector2", + TYPE_VECTOR2I: "Vector2i", + TYPE_RECT2: "Rect2", + TYPE_RECT2I: "Rect2i", + TYPE_VECTOR3: "Vector3", + TYPE_VECTOR3I: "Vector3i", + TYPE_TRANSFORM2D: "Transform2D", + TYPE_VECTOR4: "Vector4", + TYPE_VECTOR4I: "Vector4i", + TYPE_PLANE: "Plane", + TYPE_QUATERNION: "Quaternion", + TYPE_AABB: "AABB", + TYPE_BASIS: "Basis", + TYPE_TRANSFORM3D: "Transform3D", + TYPE_PROJECTION: "Projection", + TYPE_COLOR: "Color", + TYPE_STRING_NAME: "StringName", + TYPE_NODE_PATH: "NodePath", + TYPE_RID: "RID", + TYPE_OBJECT: "Object", + TYPE_CALLABLE: "Callable", + TYPE_SIGNAL: "Signal", + TYPE_DICTIONARY: "Dictionary", + TYPE_ARRAY: "Array", + TYPE_PACKED_BYTE_ARRAY: "PackedByteArray", + TYPE_PACKED_INT32_ARRAY: "PackedInt32Array", + TYPE_PACKED_INT64_ARRAY: "PackedInt64Array", + TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array", + TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array", + TYPE_PACKED_STRING_ARRAY: "PackedStringArray", + TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array", + TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array", + TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array", + TYPE_PACKED_COLOR_ARRAY: "PackedColorArray", + TYPE_VOID: "void", + TYPE_VARARG: "VarArg", + TYPE_FUNC: "Func", + TYPE_FUZZER: "Fuzzer", + TYPE_VARIANT: "Variant" +} + + +class EditorNotifications: + # NOTE: Hardcoding to avoid runtime errors in exported projects when editor + # classes are not available. These values are unlikely to change. + # See: EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED + const NOTIFICATION_EDITOR_SETTINGS_CHANGED := 10000 + + +const NOTIFICATION_AS_STRING_MAPPINGS := { + TYPE_OBJECT: { + Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE", + Object.NOTIFICATION_PREDELETE: "PREDELETE", + EditorNotifications.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED", + }, + TYPE_NODE: { + Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE", + Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE", + Node.NOTIFICATION_CHILD_ORDER_CHANGED: "CHILD_ORDER_CHANGED", + Node.NOTIFICATION_READY: "READY", + Node.NOTIFICATION_PAUSED: "PAUSED", + Node.NOTIFICATION_UNPAUSED: "UNPAUSED", + Node.NOTIFICATION_PHYSICS_PROCESS: "PHYSICS_PROCESS", + Node.NOTIFICATION_PROCESS: "PROCESS", + Node.NOTIFICATION_PARENTED: "PARENTED", + Node.NOTIFICATION_UNPARENTED: "UNPARENTED", + Node.NOTIFICATION_SCENE_INSTANTIATED: "INSTANCED", + Node.NOTIFICATION_DRAG_BEGIN: "DRAG_BEGIN", + Node.NOTIFICATION_DRAG_END: "DRAG_END", + Node.NOTIFICATION_PATH_RENAMED: "PATH_CHANGED", + Node.NOTIFICATION_INTERNAL_PROCESS: "INTERNAL_PROCESS", + Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS: "INTERNAL_PHYSICS_PROCESS", + Node.NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE", + Node.NOTIFICATION_WM_MOUSE_ENTER: "WM_MOUSE_ENTER", + Node.NOTIFICATION_WM_MOUSE_EXIT: "WM_MOUSE_EXIT", + Node.NOTIFICATION_APPLICATION_FOCUS_IN: "WM_FOCUS_IN", + Node.NOTIFICATION_APPLICATION_FOCUS_OUT: "WM_FOCUS_OUT", + #Node.NOTIFICATION_WM_QUIT_REQUEST: "WM_QUIT_REQUEST", + Node.NOTIFICATION_WM_GO_BACK_REQUEST: "WM_GO_BACK_REQUEST", + Node.NOTIFICATION_WM_WINDOW_FOCUS_OUT: "WM_UNFOCUS_REQUEST", + Node.NOTIFICATION_OS_MEMORY_WARNING: "OS_MEMORY_WARNING", + Node.NOTIFICATION_TRANSLATION_CHANGED: "TRANSLATION_CHANGED", + Node.NOTIFICATION_WM_ABOUT: "WM_ABOUT", + Node.NOTIFICATION_CRASH: "CRASH", + Node.NOTIFICATION_OS_IME_UPDATE: "OS_IME_UPDATE", + Node.NOTIFICATION_APPLICATION_RESUMED: "APP_RESUMED", + Node.NOTIFICATION_APPLICATION_PAUSED: "APP_PAUSED", + Node3D.NOTIFICATION_TRANSFORM_CHANGED: "TRANSFORM_CHANGED", + Node3D.NOTIFICATION_ENTER_WORLD: "ENTER_WORLD", + Node3D.NOTIFICATION_EXIT_WORLD: "EXIT_WORLD", + Node3D.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED", + Skeleton3D.NOTIFICATION_UPDATE_SKELETON: "UPDATE_SKELETON", + CanvasItem.NOTIFICATION_DRAW: "DRAW", + CanvasItem.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED", + CanvasItem.NOTIFICATION_ENTER_CANVAS: "ENTER_CANVAS", + CanvasItem.NOTIFICATION_EXIT_CANVAS: "EXIT_CANVAS", + #Popup.NOTIFICATION_POST_POPUP: "POST_POPUP", + #Popup.NOTIFICATION_POPUP_HIDE: "POPUP_HIDE", + }, + TYPE_CONTROL : { + Object.NOTIFICATION_PREDELETE: "PREDELETE", + Container.NOTIFICATION_SORT_CHILDREN: "SORT_CHILDREN", + Control.NOTIFICATION_RESIZED: "RESIZED", + Control.NOTIFICATION_MOUSE_ENTER: "MOUSE_ENTER", + Control.NOTIFICATION_MOUSE_EXIT: "MOUSE_EXIT", + Control.NOTIFICATION_FOCUS_ENTER: "FOCUS_ENTER", + Control.NOTIFICATION_FOCUS_EXIT: "FOCUS_EXIT", + Control.NOTIFICATION_THEME_CHANGED: "THEME_CHANGED", + #Control.NOTIFICATION_MODAL_CLOSE: "MODAL_CLOSE", + Control.NOTIFICATION_SCROLL_BEGIN: "SCROLL_BEGIN", + Control.NOTIFICATION_SCROLL_END: "SCROLL_END", + } +} + + +enum COMPARE_MODE { + OBJECT_REFERENCE, + PARAMETER_DEEP_TEST +} + + +# prototype of better object to dictionary +static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary: + if obj == null: + return {} + var clazz_name := obj.get_class() + var dict := Dictionary() + var clazz_path := "" + + if is_instance_valid(obj) and obj.get_script() != null: + var script: Script = obj.get_script() + # handle build-in scripts + if script.resource_path != null and script.resource_path.contains(".tscn"): + var path_elements := script.resource_path.split(".tscn") + clazz_name = path_elements[0].get_file() + clazz_path = script.resource_path + else: + var d := inst_to_dict(obj) + clazz_path = d["@path"] + if d["@subpath"] != NodePath(""): + clazz_name = d["@subpath"] + dict["@inner_class"] = true + else: + clazz_name = clazz_path.get_file().replace(".gd", "") + dict["@path"] = clazz_path + + for property in obj.get_property_list(): + var property_name :String = property["name"] + var property_type :int = property["type"] + var property_value :Variant = obj.get(property_name) + if property_value is GDScript or property_value is Callable or property_value is RegEx: + continue + if (property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE|PROPERTY_USAGE_DEFAULT + and not property["usage"] & PROPERTY_USAGE_CATEGORY + and not property["usage"] == 0): + if property_type == TYPE_OBJECT: + # prevent recursion + if hashed_objects.has(obj): + dict[property_name] = str(property_value) + continue + hashed_objects[obj] = true + @warning_ignore("unsafe_cast") + dict[property_name] = obj2dict(property_value as Object, hashed_objects) + else: + dict[property_name] = property_value + if obj is Node: + var childrens :Array = (obj as Node).get_children() + dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects)) + if obj is TreeItem: + var childrens :Array = (obj as TreeItem).get_children() + dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects)) + + return {"%s" % clazz_name : dict} + + +static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0) + + +static func equals_sorted(obj_a: Array[Variant], obj_b: Array[Variant], case_sensitive: bool = false, compare_mode: COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + var a: Array[Variant] = obj_a.duplicate() + var b: Array[Variant] = obj_b.duplicate() + a.sort() + b.sort() + return equals(a, b, case_sensitive, compare_mode) + + +static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool: + var type_a := typeof(obj_a) + var type_b := typeof(obj_b) + if stack_depth > 32: + prints("stack_depth", stack_depth, deep_stack) + push_error("GdUnit equals has max stack deep reached!") + return false + + # use argument matcher if requested + if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher: + @warning_ignore("unsafe_cast") + return (obj_a as GdUnitArgumentMatcher).is_match(obj_b) + if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher: + @warning_ignore("unsafe_cast") + return (obj_b as GdUnitArgumentMatcher).is_match(obj_a) + + stack_depth += 1 + # fast fail is different types + if not _is_type_equivalent(type_a, type_b): + return false + # is same instance + if obj_a == obj_b: + return true + # handle null values + if obj_a == null and obj_b != null: + return false + if obj_b == null and obj_a != null: + return false + + match type_a: + TYPE_OBJECT: + if deep_stack.has(obj_a) or deep_stack.has(obj_b): + return true + deep_stack.append(obj_a) + deep_stack.append(obj_b) + if compare_mode == COMPARE_MODE.PARAMETER_DEEP_TEST: + # fail fast + if not is_instance_valid(obj_a) or not is_instance_valid(obj_b): + return false + @warning_ignore("unsafe_method_access") + if obj_a.get_class() != obj_b.get_class(): + return false + @warning_ignore("unsafe_cast") + var a := obj2dict(obj_a as Object) + @warning_ignore("unsafe_cast") + var b := obj2dict(obj_b as Object) + return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth) + return obj_a == obj_b + + TYPE_ARRAY: + @warning_ignore("unsafe_method_access") + if obj_a.size() != obj_b.size(): + return false + @warning_ignore("unsafe_method_access") + for index :int in obj_a.size(): + if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth): + return false + return true + + TYPE_DICTIONARY: + @warning_ignore("unsafe_method_access") + if obj_a.size() != obj_b.size(): + return false + @warning_ignore("unsafe_method_access") + for key :Variant in obj_a.keys(): + @warning_ignore("unsafe_method_access") + var value_a :Variant = obj_a[key] if obj_a.has(key) else null + @warning_ignore("unsafe_method_access") + var value_b :Variant = obj_b[key] if obj_b.has(key) else null + if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth): + return false + return true + + TYPE_STRING: + if case_sensitive: + @warning_ignore("unsafe_method_access") + return obj_a.to_lower() == obj_b.to_lower() + else: + return obj_a == obj_b + return obj_a == obj_b + + +@warning_ignore("shadowed_variable_base_class") +static func notification_as_string(instance :Variant, notification :int) -> String: + var error := "Unknown notification: '%s' at instance: %s" % [notification, instance] + if instance is Node and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].has(notification): + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error) + if instance is Control and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].has(notification): + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].get(notification, error) + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_OBJECT].get(notification, error) + + +static func string_to_type(value :String) -> int: + for type :int in TYPE_AS_STRING_MAPPINGS.keys(): + if TYPE_AS_STRING_MAPPINGS.get(type) == value: + return type + return TYPE_NIL + + +static func to_camel_case(value :String) -> String: + var p := to_pascal_case(value) + if not p.is_empty(): + p[0] = p[0].to_lower() + return p + + +static func to_pascal_case(value :String) -> String: + return value.capitalize().replace(" ", "") + + +@warning_ignore("return_value_discarded") +static func to_snake_case(value :String) -> String: + var result := PackedStringArray() + for ch in value: + var lower_ch := ch.to_lower() + if ch != lower_ch and result.size() > 1: + result.append('_') + result.append(lower_ch) + return ''.join(result) + + +static func is_snake_case(value :String) -> bool: + for ch in value: + if ch == '_': + continue + if ch == ch.to_upper(): + return false + return true + + +static func type_as_string(type :int) -> String: + if type < TYPE_MAX: + return type_string(type) + return TYPE_AS_STRING_MAPPINGS.get(type, "Variant") + + +static func typeof_as_string(value :Variant) -> String: + return TYPE_AS_STRING_MAPPINGS.get(typeof(value), "Unknown type") + + +static func all_types() -> PackedInt32Array: + return PackedInt32Array(TYPE_AS_STRING_MAPPINGS.keys()) + + +static func string_as_typeof(type_name :String) -> int: + var type :Variant = TYPE_AS_STRING_MAPPINGS.find_key(type_name) + return type if type != null else TYPE_VARIANT + + +static func is_primitive_type(value :Variant) -> bool: + return typeof(value) in [TYPE_BOOL, TYPE_STRING, TYPE_STRING_NAME, TYPE_INT, TYPE_FLOAT] + + +static func _is_type_equivalent(type_a :int, type_b :int) -> bool: + # don't test for TYPE_STRING_NAME equivalenz + if type_a == TYPE_STRING_NAME or type_b == TYPE_STRING_NAME: + return true + if GdUnitSettings.is_strict_number_type_compare(): + return type_a == type_b + return ( + (type_a == TYPE_FLOAT and type_b == TYPE_INT) + or (type_a == TYPE_INT and type_b == TYPE_FLOAT) + or type_a == type_b) + + +static func is_engine_type(value :Variant) -> bool: + if value is GDScript or value is ScriptExtension: + return false + var obj: Object = value + if is_instance_valid(obj) and obj.has_method("is_class"): + return obj.is_class("GDScriptNativeClass") + return false + + +static func is_type(value :Variant) -> bool: + # is an build-in type + if typeof(value) != TYPE_OBJECT: + return false + # is a engine class type + if is_engine_type(value): + return true + # is a custom class type + @warning_ignore("unsafe_cast") + if value is GDScript and (value as GDScript).can_instantiate(): + return true + return false + + +static func _is_same(left :Variant, right :Variant) -> bool: + var left_type := -1 if left == null else typeof(left) + var right_type := -1 if right == null else typeof(right) + + # if typ different can't be the same + if left_type != right_type: + return false + if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT: + @warning_ignore("unsafe_cast") + return (left as Object).get_instance_id() == (right as Object).get_instance_id() + return equals(left, right) + + +static func is_object(value :Variant) -> bool: + return typeof(value) == TYPE_OBJECT + + +static func is_script(value :Variant) -> bool: + return is_object(value) and value is Script + + +static func is_native_class(value :Variant) -> bool: + return is_object(value) and is_engine_type(value) + + +static func is_scene(value :Variant) -> bool: + return is_object(value) and value is PackedScene + + +static func is_scene_resource_path(value :Variant) -> bool: + @warning_ignore("unsafe_cast") + return value is String and (value as String).ends_with(".tscn") + + +static func is_singleton(value: Variant) -> bool: + if not is_instance_valid(value) or is_native_class(value): + return false + for name in Engine.get_singleton_list(): + @warning_ignore("unsafe_cast") + if (value as Object).is_class(name): + return true + return false + + +static func is_instance(value :Variant) -> bool: + if not is_instance_valid(value) or is_native_class(value): + return false + @warning_ignore("unsafe_cast") + if is_script(value) and (value as Script).get_instance_base_type() == "": + return true + if is_scene(value): + return true + @warning_ignore("unsafe_cast") + return not (value as Object).has_method('new') and not (value as Object).has_method('instance') + + +# only object form type Node and attached filename +static func is_instance_scene(instance :Variant) -> bool: + if instance is Node: + var node: Node = instance + return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty() + return false + + +static func can_be_instantiate(obj :Variant) -> bool: + if not obj or is_engine_type(obj): + return false + @warning_ignore("unsafe_cast") + return (obj as Object).has_method("new") + + +static func create_instance(clazz :Variant) -> GdUnitResult: + match typeof(clazz): + TYPE_OBJECT: + # test is given clazz already an instance + if is_instance(clazz): + return GdUnitResult.success(clazz) + @warning_ignore("unsafe_method_access") + return GdUnitResult.success(clazz.new()) + TYPE_STRING: + var clazz_name: String = clazz + if ClassDB.class_exists(clazz_name): + if Engine.has_singleton(clazz_name): + return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz_name) + if not ClassDB.can_instantiate(clazz_name): + return GdUnitResult.error("Can't instance Engine class '%s'." % clazz_name) + return GdUnitResult.success(ClassDB.instantiate(clazz_name)) + else: + var clazz_path :String = extract_class_path(clazz_name)[0] + if not FileAccess.file_exists(clazz_path): + return GdUnitResult.error("Class '%s' not found." % clazz_name) + var script: GDScript = load(clazz_path) + if script != null: + return GdUnitResult.success(script.new()) + else: + return GdUnitResult.error("Can't create instance for '%s'." % clazz_name) + return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz)) + + +## We do dispose 'GDScriptFunctionState' in a kacky style because the class is not visible anymore +static func dispose_function_state(func_state: Variant) -> void: + if func_state != null and str(func_state).contains("GDScriptFunctionState"): + @warning_ignore("unsafe_method_access") + func_state.completed.emit() + + +@warning_ignore("return_value_discarded") +static func extract_class_path(clazz :Variant) -> PackedStringArray: + var clazz_path := PackedStringArray() + if clazz is String: + @warning_ignore("unsafe_cast") + clazz_path.append(clazz as String) + return clazz_path + if is_instance(clazz): + # is instance a script instance? + var script: GDScript = clazz.script + if script != null: + return extract_class_path(script) + return clazz_path + + if clazz is GDScript: + var script: GDScript = clazz + if not script.resource_path.is_empty(): + clazz_path.append(script.resource_path) + return clazz_path + # if not found we go the expensive way and extract the path form the script by creating an instance + var arg_list := build_function_default_arguments(script, "_init") + var instance: Object = script.callv("new", arg_list) + var clazz_info := inst_to_dict(instance) + GdUnitTools.free_instance(instance) + @warning_ignore("unsafe_cast") + clazz_path.append(clazz_info["@path"] as String) + if clazz_info.has("@subpath"): + var sub_path :String = clazz_info["@subpath"] + if not sub_path.is_empty(): + var sub_paths := sub_path.split("/") + clazz_path += sub_paths + return clazz_path + return clazz_path + + +static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> String: + var base_clazz := clazz_path[0] + # return original class name if engine class + if ClassDB.class_exists(base_clazz): + return base_clazz + var clazz_name := to_pascal_case(base_clazz.get_basename().get_file()) + for path_index in range(1, clazz_path.size()): + clazz_name += "." + clazz_path[path_index] + return clazz_name + + +static func extract_class_name(clazz :Variant) -> GdUnitResult: + if clazz == null: + return GdUnitResult.error("Can't extract class name form a null value.") + + if is_instance(clazz): + # is instance a script instance? + var script: GDScript = clazz.script + if script != null: + return extract_class_name(script) + @warning_ignore("unsafe_cast") + return GdUnitResult.success((clazz as Object).get_class()) + + # extract name form full qualified class path + if clazz is String: + var clazz_name: String = clazz + if ClassDB.class_exists(clazz_name): + return GdUnitResult.success(clazz_name) + var source_script :GDScript = load(clazz_name) + clazz_name = GdScriptParser.new().get_class_name(source_script) + return GdUnitResult.success(to_pascal_case(clazz_name)) + + if is_primitive_type(clazz): + return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) + + if is_script(clazz): + @warning_ignore("unsafe_cast") + if (clazz as Script).resource_path.is_empty(): + var class_path := extract_class_name_from_class_path(extract_class_path(clazz)) + return GdUnitResult.success(class_path); + return extract_class_name(clazz.resource_path) + + # need to create an instance for a class typ the extract the class name + @warning_ignore("unsafe_method_access") + var instance :Variant = clazz.new() + if instance == null: + return GdUnitResult.error("Can't create a instance for class '%s'" % str(clazz)) + var result := extract_class_name(instance) + @warning_ignore("return_value_discarded") + GdUnitTools.free_instance(instance) + return result + + +static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStringArray) -> PackedStringArray: + var inner_classes := PackedStringArray() + + if ClassDB.class_exists(clazz_name): + return inner_classes + var script :GDScript = load(script_path[0]) + var map := script.get_script_constant_map() + for key :String in map.keys(): + var value :Variant = map.get(key) + if value is GDScript: + var class_path := extract_class_path(value) + @warning_ignore("return_value_discarded") + inner_classes.append(class_path[1]) + return inner_classes + + +static func extract_class_functions(clazz_name :String, script_path :PackedStringArray) -> Array: + if ClassDB.class_get_method_list(clazz_name): + return ClassDB.class_get_method_list(clazz_name) + + if not FileAccess.file_exists(script_path[0]): + return Array() + var script :GDScript = load(script_path[0]) + if script is GDScript: + # if inner class on class path we have to load the script from the script_constant_map + if script_path.size() == 2 and script_path[1] != "": + var inner_classes := script_path[1] + var map := script.get_script_constant_map() + script = map[inner_classes] + var clazz_functions :Array = script.get_method_list() + var base_clazz :String = script.get_instance_base_type() + if base_clazz: + return extract_class_functions(base_clazz, script_path) + return clazz_functions + return Array() + + +# scans all registert script classes for given +# if the class is public in the global space than return true otherwise false +# public class means the script class is defined by 'class_name ' +static func is_public_script_class(clazz_name :String) -> bool: + var script_classes:Array[Dictionary] = ProjectSettings.get_global_class_list() + for class_info in script_classes: + if class_info.has("class"): + if class_info["class"] == clazz_name: + return true + return false + + +static func build_function_default_arguments(script :GDScript, func_name :String) -> Array: + var arg_list := Array() + for func_sig in script.get_script_method_list(): + if func_sig["name"] == func_name: + var args :Array[Dictionary] = func_sig["args"] + for arg in args: + var value_type :int = arg["type"] + var default_value :Variant = default_value_by_type(value_type) + arg_list.append(default_value) + return arg_list + return arg_list + + +static func default_value_by_type(type :int) -> Variant: + assert(type < TYPE_MAX) + assert(type >= 0) + + match type: + TYPE_NIL: return null + TYPE_BOOL: return false + TYPE_INT: return 0 + TYPE_FLOAT: return 0.0 + TYPE_STRING: return "" + TYPE_VECTOR2: return Vector2.ZERO + TYPE_VECTOR2I: return Vector2i.ZERO + TYPE_VECTOR3: return Vector3.ZERO + TYPE_VECTOR3I: return Vector3i.ZERO + TYPE_VECTOR4: return Vector4.ZERO + TYPE_VECTOR4I: return Vector4i.ZERO + TYPE_RECT2: return Rect2() + TYPE_RECT2I: return Rect2i() + TYPE_TRANSFORM2D: return Transform2D() + TYPE_PLANE: return Plane() + TYPE_QUATERNION: return Quaternion() + TYPE_AABB: return AABB() + TYPE_BASIS: return Basis() + TYPE_TRANSFORM3D: return Transform3D() + TYPE_COLOR: return Color() + TYPE_NODE_PATH: return NodePath() + TYPE_RID: return RID() + TYPE_OBJECT: return null + TYPE_CALLABLE: return Callable() + TYPE_ARRAY: return [] + TYPE_DICTIONARY: return {} + TYPE_PACKED_BYTE_ARRAY: return PackedByteArray() + TYPE_PACKED_COLOR_ARRAY: return PackedColorArray() + TYPE_PACKED_INT32_ARRAY: return PackedInt32Array() + TYPE_PACKED_INT64_ARRAY: return PackedInt64Array() + TYPE_PACKED_FLOAT32_ARRAY: return PackedFloat32Array() + TYPE_PACKED_FLOAT64_ARRAY: return PackedFloat64Array() + TYPE_PACKED_STRING_ARRAY: return PackedStringArray() + TYPE_PACKED_VECTOR2_ARRAY: return PackedVector2Array() + TYPE_PACKED_VECTOR3_ARRAY: return PackedVector3Array() + + push_error("Can't determine a default value for type: '%s', Please create a Bug issue and attach the stacktrace please." % type) + return null + + +static func find_nodes_by_class(root: Node, cls: String, recursive: bool = false) -> Array[Node]: + if not recursive: + return _find_nodes_by_class_no_rec(root, cls) + return _find_nodes_by_class(root, cls) + + +static func _find_nodes_by_class_no_rec(parent: Node, cls: String) -> Array[Node]: + var result :Array[Node] = [] + for ch in parent.get_children(): + if ch.get_class() == cls: + result.append(ch) + return result + + +static func _find_nodes_by_class(root: Node, cls: String) -> Array[Node]: + var result :Array[Node] = [] + var stack :Array[Node] = [root] + while stack: + var node :Node = stack.pop_back() + if node.get_class() == cls: + result.append(node) + for ch in node.get_children(): + stack.push_back(ch) + return result diff --git a/addons/gdUnit4/src/core/GdObjects.gd.uid b/addons/gdUnit4/src/core/GdObjects.gd.uid index e69de29b..fd8f6d6e 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd.uid +++ b/addons/gdUnit4/src/core/GdObjects.gd.uid @@ -0,0 +1 @@ +uid://b7ldhc4ryfh1v diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd index e69de29b..3e6a334e 100644 --- a/addons/gdUnit4/src/core/GdUnit4Version.gd +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd @@ -0,0 +1,65 @@ +class_name GdUnit4Version +extends RefCounted + +const VERSION_PATTERN = "[center][color=#9887c4]gd[/color][color=#7a57d6]Unit[/color][color=#9887c4]4[/color] [color=#9887c4]${version}[/color][/center]" + +var _major :int +var _minor :int +var _patch :int + + +func _init(major :int, minor :int, patch :int) -> void: + _major = major + _minor = minor + _patch = patch + + +static func parse(value :String) -> GdUnit4Version: + var regex := RegEx.new() + @warning_ignore("return_value_discarded") + regex.compile("[a-zA-Z:,-]+") + var cleaned := regex.sub(value, "", true) + var parts := cleaned.split(".") + var major := parts[0].to_int() + var minor := parts[1].to_int() + var patch := parts[2].to_int() if parts.size() > 2 else 0 + return GdUnit4Version.new(major, minor, patch) + + +static func current() -> GdUnit4Version: + var config := ConfigFile.new() + @warning_ignore("return_value_discarded") + config.load('addons/gdUnit4/plugin.cfg') + @warning_ignore("unsafe_cast") + return parse(config.get_value('plugin', 'version') as String) + + +func equals(other :GdUnit4Version) -> bool: + return _major == other._major and _minor == other._minor and _patch == other._patch + + +func is_greater(other :GdUnit4Version) -> bool: + if _major > other._major: + return true + if _major == other._major and _minor > other._minor: + return true + return _major == other._major and _minor == other._minor and _patch > other._patch + + +static func init_version_label(label :Control) -> void: + var config := ConfigFile.new() + @warning_ignore("return_value_discarded") + config.load('addons/gdUnit4/plugin.cfg') + var version :String = config.get_value('plugin', 'version') + if label is RichTextLabel: + (label as RichTextLabel).text = VERSION_PATTERN.replace('${version}', version) + else: + (label as Label).text = "gdUnit4 " + version + + +func _to_string() -> String: + return "v%d.%d.%d" % [_major, _minor, _patch] + + +func documentation_version() -> String: + return "v%d.%d.x" % [_major, _minor] diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd.uid b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid index e69de29b..2b7462fe 100644 --- a/addons/gdUnit4/src/core/GdUnit4Version.gd.uid +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid @@ -0,0 +1 @@ +uid://bbaqjhpbxce3u diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd index e69de29b..f6db5b4a 100644 --- a/addons/gdUnit4/src/core/GdUnitFileAccess.gd +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd @@ -0,0 +1,232 @@ +class_name GdUnitFileAccess +extends RefCounted + +const GDUNIT_TEMP := "user://tmp" + + +static func current_dir() -> String: + return ProjectSettings.globalize_path("res://") + + +static func clear_tmp() -> void: + delete_directory(GDUNIT_TEMP) + + +# Creates a new file under +static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: + var file_path := create_temp_dir(relative_path) + "/" + file_name + var file := FileAccess.open(file_path, mode) + if file == null: + push_error("Error creating temporary file at: %s, %s" % [file_path, error_string(FileAccess.get_open_error())]) + return file + + +static func temp_dir() -> String: + if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) + return GDUNIT_TEMP + + +static func create_temp_dir(folder_name :String) -> String: + var new_folder := temp_dir() + "/" + folder_name + if not DirAccess.dir_exists_absolute(new_folder): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(new_folder) + return new_folder + + +static func copy_file(from_file :String, to_dir :String) -> GdUnitResult: + var dir := DirAccess.open(to_dir) + if dir != null: + var to_file := to_dir + "/" + from_file.get_file() + prints("Copy %s to %s" % [from_file, to_file]) + var error := dir.copy(from_file, to_file) + if error != OK: + return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_string(error)]) + return GdUnitResult.success(to_file) + return GdUnitResult.error("Directory not found: " + to_dir) + + +static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool: + if not DirAccess.dir_exists_absolute(from_dir): + push_error("Source directory not found '%s'" % from_dir) + return false + + # check if destination exists + if not DirAccess.dir_exists_absolute(to_dir): + # create it + var err := DirAccess.make_dir_recursive_absolute(to_dir) + if err != OK: + push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)]) + return false + var source_dir := DirAccess.open(from_dir) + var dest_dir := DirAccess.open(to_dir) + if source_dir != null: + @warning_ignore("return_value_discarded") + source_dir.list_dir_begin() + var next := "." + + while next != "": + next = source_dir.get_next() + if next == "" or next == "." or next == "..": + continue + var source := source_dir.get_current_dir() + "/" + next + var dest := dest_dir.get_current_dir() + "/" + next + if source_dir.current_is_dir(): + if recursive: + @warning_ignore("return_value_discarded") + copy_directory(source + "/", dest, recursive) + continue + var err := source_dir.copy(source, dest) + if err != OK: + push_error("Error checked copy file '%s' to '%s'" % [source, dest]) + return false + + return true + else: + push_error("Directory not found: " + from_dir) + return false + + +static func delete_directory(path :String, only_content := false) -> void: + var dir := DirAccess.open(path) + if dir != null: + dir.include_hidden = true + @warning_ignore("return_value_discarded") + dir.list_dir_begin() + var file_name := "." + while file_name != "": + file_name = dir.get_next() + if file_name.is_empty() or file_name == "." or file_name == "..": + continue + var next := path + "/" +file_name + if dir.current_is_dir(): + delete_directory(next) + else: + # delete file + var err := dir.remove(next) + if err: + push_error("Delete %s failed: %s" % [next, error_string(err)]) + if not only_content: + var err := dir.remove(path) + if err: + push_error("Delete %s failed: %s" % [path, error_string(err)]) + + +static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int: + var dir := DirAccess.open(path) + if dir == null: + return 0 + var deleted := 0 + @warning_ignore("return_value_discarded") + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + if next.begins_with(prefix): + var current_index := next.split("_")[1].to_int() + if current_index <= index: + deleted += 1 + delete_directory(path + "/" + next) + return deleted + + +# scans given path for sub directories by given prefix and returns the highest index numer +# e.g. +static func find_last_path_index(path :String, prefix :String) -> int: + var dir := DirAccess.open(path) + if dir == null: + return 0 + var last_iteration := 0 + @warning_ignore("return_value_discarded") + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + if next.begins_with(prefix): + var iteration := next.split("_")[1].to_int() + if iteration > last_iteration: + last_iteration = iteration + return last_iteration + + +static func as_resource_path(value: String) -> String: + if value.begins_with("res://"): + return value + return "res://" + value.trim_prefix("//").trim_prefix("/").trim_suffix("/") + + +static func scan_dir(path :String) -> PackedStringArray: + var dir := DirAccess.open(path) + if dir == null or not dir.dir_exists(path): + return PackedStringArray() + var content := PackedStringArray() + dir.include_hidden = true + @warning_ignore("return_value_discarded") + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + @warning_ignore("return_value_discarded") + content.append(next) + return content + + +static func resource_as_array(resource_path :String) -> PackedStringArray: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file == null: + push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())]) + return PackedStringArray() + var file_content := PackedStringArray() + while not file.eof_reached(): + @warning_ignore("return_value_discarded") + file_content.append(file.get_line()) + return file_content + + +static func resource_as_string(resource_path :String) -> String: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file == null: + push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())]) + return "" + return file.get_as_text(true) + + +static func make_qualified_path(path :String) -> String: + if path.begins_with("res://"): + return path + if path.begins_with("//"): + return path.replace("//", "res://") + if path.begins_with("/"): + return "res:/" + path + return path + + +static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult: + var zip: ZIPReader = ZIPReader.new() + var err := zip.open(zip_package) + if err != OK: + return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + var zip_entries: PackedStringArray = zip.get_files() + # Get base path and step over archive folder + var archive_path := zip_entries[0] + zip_entries.remove_at(0) + + for zip_entry in zip_entries: + var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") + if zip_entry.ends_with("/"): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(new_file_path) + continue + var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) + file.store_buffer(zip.read_file(zip_entry)) + @warning_ignore("return_value_discarded") + zip.close() + return GdUnitResult.success(dest_path) diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid index e69de29b..14695c19 100644 --- a/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid @@ -0,0 +1 @@ +uid://dflqb5germp5n diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd.uid b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid index e69de29b..de104010 100644 --- a/addons/gdUnit4/src/core/GdUnitProperty.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid @@ -0,0 +1 @@ +uid://cqndh0nuu8ltx diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd.uid b/addons/gdUnit4/src/core/GdUnitResult.gd.uid index e69de29b..2835c400 100644 --- a/addons/gdUnit4/src/core/GdUnitResult.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitResult.gd.uid @@ -0,0 +1 @@ +uid://cnvq3nb61ei76 diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd index e69de29b..9f36354d 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -0,0 +1,126 @@ +class_name GdUnitRunnerConfig +extends Resource + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const CONFIG_VERSION = "5.0" +const VERSION = "version" +const TESTS = "tests" +const SERVER_PORT = "server_port" +const EXIT_FAIL_FAST = "exit_on_first_fail" + +const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg" + +var _config := { + VERSION : CONFIG_VERSION, + # a set of directories or testsuite paths as key and a optional set of testcases as values + + TESTS : Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase), + + # the port of running test server for this session + SERVER_PORT : -1 + } + + +func version() -> String: + return _config[VERSION] + + +func clear() -> GdUnitRunnerConfig: + _config[TESTS] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + return self + + +func set_server_port(port: int) -> GdUnitRunnerConfig: + _config[SERVER_PORT] = port + return self + + +func server_port() -> int: + return _config.get(SERVER_PORT, -1) + + +func add_test_cases(tests: Array[GdUnitTestCase]) -> GdUnitRunnerConfig: + test_cases().append_array(tests) + return self + + +func test_cases() -> Array[GdUnitTestCase]: + return _config.get(TESTS, []) + + +func save_config(path: String = CONFIG_FILE) -> GdUnitResult: + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + var error := FileAccess.get_open_error() + return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)]) + + var to_save := { + VERSION : CONFIG_VERSION, + SERVER_PORT : _config.get(SERVER_PORT), + TESTS : Array() + } + + var tests: Array = to_save.get(TESTS) + for test in test_cases(): + tests.append(inst_to_dict(test)) + file.store_string(JSON.stringify(to_save, "\t")) + return GdUnitResult.success(path) + + +func load_config(path: String = CONFIG_FILE) -> GdUnitResult: + if not FileAccess.file_exists(path): + return GdUnitResult.warn("Can't find test runner configuration '%s'! Please select a test to run." % path) + var file := FileAccess.open(path, FileAccess.READ) + if file == null: + var error := FileAccess.get_open_error() + return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, error_string(error)]) + var content := file.get_as_text() + if not content.is_empty() and content[0] == '{': + # Parse as json + var test_json_conv := JSON.new() + var error := test_json_conv.parse(content) + if error != OK: + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + var config: Dictionary = test_json_conv.get_data() + if not config.has(VERSION): + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + + var default: Array[Dictionary] = Array([], TYPE_DICTIONARY, "", null) + var tests_as_json: Array = config.get(TESTS, default) + _config = config + _config[TESTS] = convert_test_json_to_test_cases(tests_as_json) + + + fix_value_types() + return GdUnitResult.success(path) + + +func convert_test_json_to_test_cases(jsons: Array) -> Array[GdUnitTestCase]: + if jsons.is_empty(): + return [] + var tests := jsons.map(func(d: Dictionary) -> GdUnitTestCase: + var test: GdUnitTestCase = dict_to_inst(d) + # we need o covert manually to the corect type becaus JSON do not handle typed values + test.guid = GdUnitGUID.new(str(d["guid"])) + test.attribute_index = test.attribute_index as int + test.line_number = test.line_number as int + return test + ) + return Array(tests, TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + +func fix_value_types() -> void: + # fix float value to int json stores all numbers as float + var server_port_: int = _config.get(SERVER_PORT, -1) + _config[SERVER_PORT] = server_port_ + + +func convert_Array_to_PackedStringArray(data: Dictionary) -> void: + for key in data.keys() as Array[String]: + var values :Array = data[key] + data[key] = PackedStringArray(values) + + +func _to_string() -> String: + return str(_config) diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid index e69de29b..60443d84 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid @@ -0,0 +1 @@ +uid://ltvpkh3ayklf diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index e69de29b..f2718537 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -0,0 +1,622 @@ +# This class provides a runner for scense to simulate interactions like keyboard or mouse +class_name GdUnitSceneRunnerImpl +extends GdUnitSceneRunner + + +var GdUnitFuncAssertImpl: GDScript = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + +# mapping of mouse buttons and his masks +const MAP_MOUSE_BUTTON_MASKS := { + MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT, + MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT, + MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE, + # https://github.com/godotengine/godot/issues/73632 + MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1), + MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1), + MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1, + MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2, +} + +var _is_disposed := false +var _current_scene: Node = null +var _awaiter: GdUnitAwaiter = GdUnitAwaiter.new() +var _verbose: bool +var _simulate_start_time: LocalTime +var _last_input_event: InputEvent = null +var _mouse_button_on_press := [] +var _key_on_press := [] +var _action_on_press := [] +var _curent_mouse_position: Vector2 +# holds the touch position for each touch index +# { index: int = position: Vector2} +var _current_touch_position: Dictionary = {} +# holds the curretn touch drag position +var _current_touch_drag_position: Vector2 = Vector2.ZERO + +# time factor settings +var _time_factor := 1.0 +var _saved_iterations_per_second: float +var _scene_auto_free := false + + +func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> void: + _verbose = p_verbose + _saved_iterations_per_second = Engine.get_physics_ticks_per_second() + @warning_ignore("return_value_discarded") + set_time_factor(1) + # handle scene loading by resource path + if typeof(p_scene) == TYPE_STRING: + @warning_ignore("unsafe_cast") + if !ResourceLoader.exists(p_scene as String): + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene) + return + if !str(p_scene).ends_with(".tscn") and !str(p_scene).ends_with(".scn") and !str(p_scene).begins_with("uid://"): + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene) + return + @warning_ignore("unsafe_cast") + _current_scene = (load(p_scene as String) as PackedScene).instantiate() + _scene_auto_free = true + else: + # verify we have a node instance + if not p_scene is Node: + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene) + return + _current_scene = p_scene + if _current_scene == null: + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: Scene must be not null!") + return + + _scene_tree().root.add_child(_current_scene) + # do finally reset all open input events when the scene is removed + @warning_ignore("return_value_discarded") + _scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void: + if child == _current_scene: + # we need to disable the processing to avoid input flush buffer errors + _current_scene.process_mode = Node.PROCESS_MODE_DISABLED + _reset_input_to_default() + ) + _simulate_start_time = LocalTime.now() + # we need to set inital a valid window otherwise the warp_mouse() is not handled + move_window_to_foreground() + + # set inital mouse pos to 0,0 + var max_iteration_to_wait := 0 + while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100: + Input.warp_mouse(Vector2.ZERO) + max_iteration_to_wait += 1 + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and is_instance_valid(self): + # reset time factor to normal + __deactivate_time_factor() + if is_instance_valid(_current_scene): + move_window_to_background() + _scene_tree().root.remove_child(_current_scene) + # do only free scenes instanciated by this runner + if _scene_auto_free: + _current_scene.free() + _is_disposed = true + _current_scene = null + + +func _scene_tree() -> SceneTree: + return Engine.get_main_loop() as SceneTree + + +func await_input_processed() -> void: + if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame + + +@warning_ignore("return_value_discarded") +func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner: + simulate_action_press(action, event_index) + simulate_action_release(action, event_index) + return self + + +func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner: + __print_current_focus() + var event := InputEventAction.new() + event.pressed = true + event.action = action + event.event_index = event_index + _action_on_press.append(action) + return _handle_input_event(event) + + +func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner: + __print_current_focus() + var event := InputEventAction.new() + event.pressed = false + event.action = action + event.event_index = event_index + _action_on_press.erase(action) + return _handle_input_event(event) + + +@warning_ignore("return_value_discarded") +func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + simulate_key_press(key_code, shift_pressed, ctrl_pressed) + await _scene_tree().process_frame + simulate_key_release(key_code, shift_pressed, ctrl_pressed) + return self + + +func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + __print_current_focus() + var event := InputEventKey.new() + event.pressed = true + event.keycode = key_code as Key + event.physical_keycode = key_code as Key + event.unicode = key_code + event.alt_pressed = key_code == KEY_ALT + event.shift_pressed = shift_pressed or key_code == KEY_SHIFT + event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL + _apply_input_modifiers(event) + _key_on_press.append(key_code) + return _handle_input_event(event) + + +func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + __print_current_focus() + var event := InputEventKey.new() + event.pressed = false + event.keycode = key_code as Key + event.physical_keycode = key_code as Key + event.unicode = key_code + event.alt_pressed = key_code == KEY_ALT + event.shift_pressed = shift_pressed or key_code == KEY_SHIFT + event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL + _apply_input_modifiers(event) + _key_on_press.erase(key_code) + return _handle_input_event(event) + + +func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner: + var event := InputEventMouseMotion.new() + event.position = pos + event.global_position = get_global_mouse_position() + _apply_input_modifiers(event) + return _handle_input_event(event) + + +func get_mouse_position() -> Vector2: + if _last_input_event is InputEventMouse: + return (_last_input_event as InputEventMouse).position + var current_scene := scene() + if current_scene != null: + return current_scene.get_viewport().get_mouse_position() + return Vector2.ZERO + + +func get_global_mouse_position() -> Vector2: + return (Engine.get_main_loop() as SceneTree).root.get_mouse_position() + + +func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner: + var event := InputEventMouseMotion.new() + event.position = position + event.relative = position - get_mouse_position() + event.global_position = get_global_mouse_position() + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + return _handle_input_event(event) + + +@warning_ignore("return_value_discarded") +func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var tween := _scene_tree().create_tween() + _curent_mouse_position = get_mouse_position() + var final_position := _curent_mouse_position + relative + tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type) + tween.play() + + while not get_mouse_position().is_equal_approx(final_position): + simulate_mouse_move(_curent_mouse_position) + await _scene_tree().process_frame + return self + + +@warning_ignore("return_value_discarded") +func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var tween := _scene_tree().create_tween() + _curent_mouse_position = get_mouse_position() + tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type) + tween.play() + + while not get_mouse_position().is_equal_approx(position): + simulate_mouse_move(_curent_mouse_position) + await _scene_tree().process_frame + return self + + +@warning_ignore("return_value_discarded") +func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: + simulate_mouse_button_press(button_index, double_click) + simulate_mouse_button_release(button_index) + return self + + +func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: + var event := InputEventMouseButton.new() + event.button_index = button_index + event.pressed = true + event.double_click = double_click + _apply_input_mouse_position(event) + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + _mouse_button_on_press.append(button_index) + return _handle_input_event(event) + + +func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner: + var event := InputEventMouseButton.new() + event.button_index = button_index + event.pressed = false + _apply_input_mouse_position(event) + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + _mouse_button_on_press.erase(button_index) + return _handle_input_event(event) + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + simulate_screen_touch_press(index, position, double_tap) + simulate_screen_touch_release(index) + return self + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the touch the mouse events + set_mouse_position(position) + simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + # push touch press event at position + var event := InputEventScreenTouch.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = position + event.double_tap = double_tap + event.pressed = true + _current_scene.get_viewport().push_input(event) + # save current drag position by index + _current_touch_position[index] = position + return self + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the touch the mouse events + simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + # push touch release event at position + var event := InputEventScreenTouch.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = get_screen_touch_drag_position(index) + event.pressed = false + event.double_tap = (_last_input_event as InputEventScreenTouch).double_tap if _last_input_event is InputEventScreenTouch else double_tap + _current_scene.get_viewport().push_input(event) + return self + + +func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var current_position: Vector2 = _current_touch_position[index] + return await _do_touch_drag_at(index, current_position + relative, time, trans_type) + + +func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + return await _do_touch_drag_at(index, position, time, trans_type) + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + simulate_screen_touch_press(index, position) + return await _do_touch_drag_at(index, drop_position, time, trans_type) + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + simulate_mouse_move(position) + var event := InputEventScreenDrag.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = position + event.relative = _get_screen_touch_drag_position_or_default(index, position) - position + event.velocity = event.relative / _scene_tree().root.get_process_delta_time() + event.pressure = 1.0 + _current_touch_position[index] = position + _current_scene.get_viewport().push_input(event) + return self + + +func get_screen_touch_drag_position(index: int) -> Vector2: + if _current_touch_position.has(index): + return _current_touch_position[index] + push_error("No touch drag position for index '%d' is set!" % index) + return Vector2.ZERO + + +func is_emulate_mouse_from_touch() -> bool: + return ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch", true) + + +func _get_screen_touch_drag_position_or_default(index: int, default_position: Vector2) -> Vector2: + if _current_touch_position.has(index): + return _current_touch_position[index] + return default_position + + +@warning_ignore("return_value_discarded") +func _do_touch_drag_at(index: int, drag_position: Vector2, time: float, trans_type: Tween.TransitionType) -> GdUnitSceneRunner: + # start draging + var event := InputEventScreenDrag.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = get_screen_touch_drag_position(index) + event.pressure = 1.0 + _current_touch_drag_position = event.position + + var tween := _scene_tree().create_tween() + tween.tween_property(self, "_current_touch_drag_position", drag_position, time).set_trans(trans_type) + tween.play() + + while not _current_touch_drag_position.is_equal_approx(drag_position): + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the drag the mouse move events + simulate_mouse_move(event.position) + # send touche drag event to new position + event.relative = _current_touch_drag_position - event.position + event.velocity = event.relative / _scene_tree().root.get_process_delta_time() + event.position = _current_touch_drag_position + _current_scene.get_viewport().push_input(event) + await _scene_tree().process_frame + + # finaly drop it + if is_emulate_mouse_from_touch(): + simulate_mouse_move(drag_position) + simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + var touch_drop_event := InputEventScreenTouch.new() + touch_drop_event.window_id = event.window_id + touch_drop_event.index = event.index + touch_drop_event.position = drag_position + touch_drop_event.pressed = false + _current_scene.get_viewport().push_input(touch_drop_event) + await _scene_tree().process_frame + return self + + +func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner: + _time_factor = min(9.0, time_factor) + __activate_time_factor() + __print("set time factor: %f" % _time_factor) + __print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor)) + return self + + +func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner: + var time_shift_frames :int = max(1, frames / _time_factor) + for frame in time_shift_frames: + if delta_milli == -1: + await _scene_tree().process_frame + else: + await _scene_tree().create_timer(delta_milli * 0.001).timeout + return self + + +func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner: + await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000) + return self + + +func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner: + await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) + return self + + +func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert: + return GdUnitFuncAssertImpl.new(scene(), func_name, args) + + +func await_func_on(instance: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert: + return GdUnitFuncAssertImpl.new(instance, func_name, args) + + +func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void: + await _awaiter.await_signal_on(scene(), signal_name, args, timeout) + + +func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void: + await _awaiter.await_signal_on(source, signal_name, args, timeout) + + +func move_window_to_foreground() -> GdUnitSceneRunner: + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_move_to_foreground() + return self + + +func move_window_to_background() -> GdUnitSceneRunner: + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + return self + + +func _property_exists(name: String) -> bool: + return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name) + + +func get_property(name: String) -> Variant: + if not _property_exists(name): + return "The property '%s' not exist checked loaded scene." % name + return scene().get(name) + + +func set_property(name: String, value: Variant) -> bool: + if not _property_exists(name): + push_error("The property named '%s' cannot be set, it does not exist!" % name) + return false; + scene().set(name, value) + return true + + +func invoke(name: String, ...args: Array) -> Variant: + if scene().has_method(name): + return await scene().callv(name, args) + return "The method '%s' not exist checked loaded scene." % name + + +func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node: + return scene().find_child(name, recursive, owned) + + +func _scene_name() -> String: + var scene_script :GDScript = scene().get_script() + var scene_name :String = scene().get_name() + if not scene_script: + return scene_name + if not scene_name.begins_with("@"): + return scene_name + return scene_script.resource_name.get_basename() + + +func __activate_time_factor() -> void: + Engine.set_time_scale(_time_factor) + Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int) + + +func __deactivate_time_factor() -> void: + Engine.set_time_scale(1) + Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int) + + +# copy over current active modifiers +func _apply_input_modifiers(event: InputEvent) -> void: + if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers: + var last_input_event := _last_input_event as InputEventWithModifiers + var _event := event as InputEventWithModifiers + _event.meta_pressed = _event.meta_pressed or last_input_event.meta_pressed + _event.alt_pressed = _event.alt_pressed or last_input_event.alt_pressed + _event.shift_pressed = _event.shift_pressed or last_input_event.shift_pressed + _event.ctrl_pressed = _event.ctrl_pressed or last_input_event.ctrl_pressed + # this line results into reset the control_pressed state!!! + #event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap + + +# copy over current active mouse mask and combine with curren mask +func _apply_input_mouse_mask(event: InputEvent) -> void: + # first apply last mask + if _last_input_event is InputEventMouse and event is InputEventMouse: + (event as InputEventMouse).button_mask |= (_last_input_event as InputEventMouse).button_mask + if event is InputEventMouseButton: + var _event := event as InputEventMouseButton + var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(_event.get_button_index(), 0) + if _event.is_pressed(): + _event.button_mask |= button_mask + else: + _event.button_mask ^= button_mask + + +# copy over last mouse position if need +func _apply_input_mouse_position(event: InputEvent) -> void: + if _last_input_event is InputEventMouse and event is InputEventMouseButton: + (event as InputEventMouseButton).position = (_last_input_event as InputEventMouse).position + + +## handle input action via Input modifieres +func _handle_actions(event: InputEventAction) -> bool: + if not InputMap.event_is_action(event, event.action, true): + return false + __print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()]) + if event.is_pressed(): + Input.action_press(event.action, event.get_strength()) + else: + Input.action_release(event.action) + return true + + +# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work +@warning_ignore("return_value_discarded") +func _handle_input_event(event: InputEvent) -> GdUnitSceneRunner: + if event is InputEventMouse: + Input.warp_mouse((event as InputEventMouse).position as Vector2) + Input.parse_input_event(event) + + if event is InputEventAction: + _handle_actions(event as InputEventAction) + + var current_scene := scene() + if is_instance_valid(current_scene): + # do not flush events if node processing disabled otherwise we run into errors at tree removed + if _current_scene.process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() + __print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()]) + if(current_scene.has_method("_gui_input")): + (current_scene as Control)._gui_input(event) + if(current_scene.has_method("_unhandled_input")): + current_scene._unhandled_input(event) + current_scene.get_viewport().set_input_as_handled() + + # save last input event needs to be merged with next InputEventMouseButton + _last_input_event = event + return self + + +@warning_ignore("return_value_discarded") +func _reset_input_to_default() -> void: + # reset all mouse button to inital state if need + for m_button :int in _mouse_button_on_press.duplicate(): + if Input.is_mouse_button_pressed(m_button): + simulate_mouse_button_release(m_button) + _mouse_button_on_press.clear() + + for key_scancode :int in _key_on_press.duplicate(): + if Input.is_key_pressed(key_scancode): + simulate_key_release(key_scancode) + _key_on_press.clear() + + for action :String in _action_on_press.duplicate(): + if Input.is_action_pressed(action): + simulate_action_release(action) + _action_on_press.clear() + + if is_instance_valid(_current_scene) and _current_scene.process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() + _last_input_event = null + + +func __print(message: String) -> void: + if _verbose: + prints(message) + + +func __print_current_focus() -> void: + if not _verbose: + return + var focused_node := scene().get_viewport().gui_get_focus_owner() + if focused_node: + prints(" focus checked %s" % focused_node) + else: + prints(" no focus set") + + +func scene() -> Node: + if is_instance_valid(_current_scene): + return _current_scene + if not _is_disposed: + push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.") + return null diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid index e69de29b..152eeef5 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid @@ -0,0 +1 @@ +uid://7a566a4kfreu diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd index e69de29b..f6bff127 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -0,0 +1,435 @@ +@tool +class_name GdUnitSettings +extends RefCounted + + +const MAIN_CATEGORY = "gdunit4" +# Common Settings +const COMMON_SETTINGS = MAIN_CATEGORY + "/settings" + +const GROUP_COMMON = COMMON_SETTINGS + "/common" +const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled" +const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes" + +const GROUP_HOOKS = MAIN_CATEGORY + "/hooks" +const SESSION_HOOKS = GROUP_HOOKS + "/session_hooks" + +const GROUP_TEST = COMMON_SETTINGS + "/test" +const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds" +const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" +const TEST_SUITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" +const TEST_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery" +const TEST_FLAKY_CHECK = GROUP_TEST + "/flaky_check_enable" +const TEST_FLAKY_MAX_RETRIES = GROUP_TEST + "/flaky_max_retries" + + +# Report Setiings +const REPORT_SETTINGS = MAIN_CATEGORY + "/report" +const GROUP_GODOT = REPORT_SETTINGS + "/godot" +const REPORT_PUSH_ERRORS = GROUP_GODOT + "/push_error" +const REPORT_SCRIPT_ERRORS = GROUP_GODOT + "/script_error" +const REPORT_ORPHANS = REPORT_SETTINGS + "/verbose_orphans" +const GROUP_ASSERT = REPORT_SETTINGS + "/assert" +const REPORT_ASSERT_WARNINGS = GROUP_ASSERT + "/verbose_warnings" +const REPORT_ASSERT_ERRORS = GROUP_ASSERT + "/verbose_errors" +const REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE = GROUP_ASSERT + "/strict_number_type_compare" + +# Godot debug stdout/logging settings +const CATEGORY_LOGGING := "debug/file_logging/" +const STDOUT_ENABLE_TO_FILE = CATEGORY_LOGGING + "enable_file_logging" +const STDOUT_WITE_TO_FILE = CATEGORY_LOGGING + "log_path" + + +# GdUnit Templates +const TEMPLATES = MAIN_CATEGORY + "/templates" +const TEMPLATES_TS = TEMPLATES + "/testsuite" +const TEMPLATE_TS_GD = TEMPLATES_TS + "/GDScript" +const TEMPLATE_TS_CS = TEMPLATES_TS + "/CSharpScript" + + +# UI Setiings +const UI_SETTINGS = MAIN_CATEGORY + "/ui" +const GROUP_UI_INSPECTOR = UI_SETTINGS + "/inspector" +const INSPECTOR_NODE_COLLAPSE = GROUP_UI_INSPECTOR + "/node_collapse" +const INSPECTOR_TREE_VIEW_MODE = GROUP_UI_INSPECTOR + "/tree_view_mode" +const INSPECTOR_TREE_SORT_MODE = GROUP_UI_INSPECTOR + "/tree_sort_mode" + + +# Shortcut Setiings +const SHORTCUT_SETTINGS = MAIN_CATEGORY + "/Shortcuts" +const GROUP_SHORTCUT_INSPECTOR = SHORTCUT_SETTINGS + "/inspector" +const SHORTCUT_INSPECTOR_RERUN_TEST = GROUP_SHORTCUT_INSPECTOR + "/rerun_test" +const SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_debug" +const SHORTCUT_INSPECTOR_RUN_TEST_OVERALL = GROUP_SHORTCUT_INSPECTOR + "/run_test_overall" +const SHORTCUT_INSPECTOR_RUN_TEST_STOP = GROUP_SHORTCUT_INSPECTOR + "/run_test_stop" + +const GROUP_SHORTCUT_EDITOR = SHORTCUT_SETTINGS + "/editor" +const SHORTCUT_EDITOR_RUN_TEST = GROUP_SHORTCUT_EDITOR + "/run_test" +const SHORTCUT_EDITOR_RUN_TEST_DEBUG = GROUP_SHORTCUT_EDITOR + "/run_test_debug" +const SHORTCUT_EDITOR_CREATE_TEST = GROUP_SHORTCUT_EDITOR + "/create_test" + +const GROUP_SHORTCUT_FILESYSTEM = SHORTCUT_SETTINGS + "/filesystem" +const SHORTCUT_FILESYSTEM_RUN_TEST = GROUP_SHORTCUT_FILESYSTEM + "/run_test" +const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_test_debug" + + +# Toolbar Setiings +const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar" +const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall" + +# Feature flags +const GROUP_FEATURE = MAIN_CATEGORY + "/feature" + + +# defaults +# server connection timeout in minutes +const DEFAULT_SERVER_TIMEOUT :int = 30 +# test case runtime timeout in seconds +const DEFAULT_TEST_TIMEOUT :int = 60*5 +# the folder to create new test-suites +const DEFAULT_TEST_LOOKUP_FOLDER := "test" + +# help texts +const HELP_TEST_LOOKUP_FOLDER := "Subfolder where test suites are located (or empty to use source folder directly)" + +enum NAMING_CONVENTIONS { + AUTO_DETECT, + SNAKE_CASE, + PASCAL_CASE, +} + + +const _VALUE_SET_SEPARATOR = "\f" # ASCII Form-feed character (AKA page break) + + +static func setup() -> void: + create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Show notification if new gdUnit4 version is found") + # test settings + create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Server connection timeout in minutes") + create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Test case runtime timeout in seconds") + create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER) + create_property_if_need(TEST_SUITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention to use when generating testsuites", NAMING_CONVENTIONS.keys()) + create_property_if_need(TEST_DISCOVER_ENABLED, false, "Automatically detect new tests in test lookup folders at runtime") + create_property_if_need(TEST_FLAKY_CHECK, false, "Rerun tests on failure and mark them as FLAKY") + create_property_if_need(TEST_FLAKY_MAX_RETRIES, 3, "Sets the number of retries for rerunning a flaky test") + # report settings + create_property_if_need(REPORT_PUSH_ERRORS, false, "Report push_error() as failure") + create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Report script errors as failure") + create_property_if_need(REPORT_ORPHANS, true, "Report orphaned nodes after tests finish") + create_property_if_need(REPORT_ASSERT_ERRORS, true, "Report assertion failures as errors") + create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Report assertion failures as warnings") + create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Compare number values strictly by type (real vs int)") + # inspector + create_property_if_need(INSPECTOR_NODE_COLLAPSE, true, + "Close testsuite node after a successful test run.") + create_property_if_need(INSPECTOR_TREE_VIEW_MODE, GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE, + "Inspector panel presentation mode", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys()) + create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED, + "Inspector panel sorting mode", GdUnitInspectorTreeConstants.SORT_MODE.keys()) + create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false, + "Show 'Run overall Tests' button in the inspector toolbar") + create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use") + create_shortcut_properties_if_need() + create_property_if_need(SESSION_HOOKS, {} as Dictionary[String,bool]) + migrate_properties() + + +static func migrate_properties() -> void: + var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder" + if get_property(TEST_ROOT_FOLDER) != null: + migrate_property(TEST_ROOT_FOLDER,\ + TEST_LOOKUP_FOLDER,\ + DEFAULT_TEST_LOOKUP_FOLDER,\ + HELP_TEST_LOOKUP_FOLDER,\ + func(value :Variant) -> String: return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value) + + +static func create_shortcut_properties_if_need() -> void: + # inspector + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun the most recently executed tests") + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun the most recently executed tests (Debug mode)") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug mode)") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stop the current test execution") + # script editor + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Run the currently selected test") + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Run the currently selected test (Debug mode).") + create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Create a new test case for the currently selected function") + # filesystem + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file") + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file (Debug)") + + +static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void: + if not ProjectSettings.has_setting(name): + #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)]) + ProjectSettings.set_setting(name, default) + + ProjectSettings.set_initial_value(name, default) + help = help if value_set.is_empty() else "%s%s%s" % [help, _VALUE_SET_SEPARATOR, value_set] + set_help(name, default, help) + + +static func set_help(property_name :String, value :Variant, help :String) -> void: + ProjectSettings.add_property_info({ + "name": property_name, + "type": typeof(value), + "hint": PROPERTY_HINT_TYPE_STRING, + "hint_string": help + }) + + +static func get_setting(name :String, default :Variant) -> Variant: + if ProjectSettings.has_setting(name): + return ProjectSettings.get_setting(name) + return default + + +static func is_update_notification_enabled() -> bool: + if ProjectSettings.has_setting(UPDATE_NOTIFICATION_ENABLED): + return ProjectSettings.get_setting(UPDATE_NOTIFICATION_ENABLED) + return false + + +static func set_update_notification(enable :bool) -> void: + ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable) + @warning_ignore("return_value_discarded") + ProjectSettings.save() + + +static func get_log_path() -> String: + return ProjectSettings.get_setting(STDOUT_WITE_TO_FILE) + + +static func set_log_path(path :String) -> void: + ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true) + ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path) + @warning_ignore("return_value_discarded") + ProjectSettings.save() + + +static func get_session_hooks() -> Dictionary[String, bool]: + var property := get_property(SESSION_HOOKS) + if property == null: + return {} + var hooks: Dictionary[String, bool] = property.value() + return hooks + + +static func set_session_hooks(hooks: Dictionary[String, bool]) -> void: + var property := get_property(SESSION_HOOKS) + property.set_value(hooks) + update_property(property) + + +static func set_inspector_tree_sort_mode(sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void: + var property := get_property(INSPECTOR_TREE_SORT_MODE) + property.set_value(sort_mode) + update_property(property) + + +static func get_inspector_tree_sort_mode() -> GdUnitInspectorTreeConstants.SORT_MODE: + var property := get_property(INSPECTOR_TREE_SORT_MODE) + return property.value() if property != null else GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED + + +static func set_inspector_tree_view_mode(tree_view_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void: + var property := get_property(INSPECTOR_TREE_VIEW_MODE) + property.set_value(tree_view_mode) + update_property(property) + + +static func get_inspector_tree_view_mode() -> GdUnitInspectorTreeConstants.TREE_VIEW_MODE: + var property := get_property(INSPECTOR_TREE_VIEW_MODE) + return property.value() if property != null else GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE + + +# the configured server connection timeout in ms +static func server_timeout() -> int: + return get_setting(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT) * 60 * 1000 + + +# the configured test case timeout in ms +static func test_timeout() -> int: + return get_setting(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT) * 1000 + + +# the root folder to store/generate test-suites +static func test_root_folder() -> String: + return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER) + + +static func is_verbose_assert_warnings() -> bool: + return get_setting(REPORT_ASSERT_WARNINGS, true) + + +static func is_verbose_assert_errors() -> bool: + return get_setting(REPORT_ASSERT_ERRORS, true) + + +static func is_verbose_orphans() -> bool: + return get_setting(REPORT_ORPHANS, true) + + +static func is_strict_number_type_compare() -> bool: + return get_setting(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + + +static func is_report_push_errors() -> bool: + return get_setting(REPORT_PUSH_ERRORS, false) + + +static func is_report_script_errors() -> bool: + return get_setting(REPORT_SCRIPT_ERRORS, true) + + +static func is_inspector_node_collapse() -> bool: + return get_setting(INSPECTOR_NODE_COLLAPSE, true) + + +static func is_inspector_toolbar_button_show() -> bool: + return get_setting(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, true) + + +static func is_test_discover_enabled() -> bool: + return get_setting(TEST_DISCOVER_ENABLED, false) + + +static func is_test_flaky_check_enabled() -> bool: + return get_setting(TEST_FLAKY_CHECK, false) + + +static func is_feature_enabled(feature: String) -> bool: + return get_setting(feature, false) + + +static func get_flaky_max_retries() -> int: + return get_setting(TEST_FLAKY_MAX_RETRIES, 3) + + +static func set_test_discover_enabled(enable :bool) -> void: + var property := get_property(TEST_DISCOVER_ENABLED) + property.set_value(enable) + update_property(property) + + +static func is_log_enabled() -> bool: + return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE) + + +static func list_settings(category: String) -> Array[GdUnitProperty]: + var settings: Array[GdUnitProperty] = [] + for property in ProjectSettings.get_property_list(): + var property_name :String = property["name"] + if property_name.begins_with(category): + settings.append(build_property(property_name, property)) + return settings + + +static func extract_value_set_from_help(value :String) -> PackedStringArray: + var split_value := value.split(_VALUE_SET_SEPARATOR) + if not split_value.size() > 1: + return PackedStringArray() + + var regex := RegEx.new() + @warning_ignore("return_value_discarded") + regex.compile("\\[(.+)\\]") + var matches := regex.search_all(split_value[1]) + if matches.is_empty(): + return PackedStringArray() + var values: String = matches[0].get_string(1) + return values.replacen(" ", "").replacen("\"", "").split(",", false) + + +static func extract_help_text(value :String) -> String: + return value.split(_VALUE_SET_SEPARATOR)[0] + + +static func update_property(property :GdUnitProperty) -> Variant: + var current_value :Variant = ProjectSettings.get_setting(property.name()) + if current_value != property.value(): + var error :Variant = validate_property_value(property) + if error != null: + return error + ProjectSettings.set_setting(property.name(), property.value()) + GdUnitSignals.instance().gdunit_settings_changed.emit(property) + _save_settings() + return null + + +static func reset_property(property :GdUnitProperty) -> void: + ProjectSettings.set_setting(property.name(), property.default()) + GdUnitSignals.instance().gdunit_settings_changed.emit(property) + _save_settings() + + +static func validate_property_value(property :GdUnitProperty) -> Variant: + match property.name(): + TEST_LOOKUP_FOLDER: + return validate_lookup_folder(property.value_as_string()) + _: return null + + +static func validate_lookup_folder(value :String) -> Variant: + if value.is_empty() or value == "/": + return null + if value.contains("res:"): + return "Test Lookup Folder: do not allowed to contains 'res://'" + if not value.is_valid_filename(): + return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)" + return null + + +static func save_property(name :String, value :Variant) -> void: + ProjectSettings.set_setting(name, value) + _save_settings() + + +static func _save_settings() -> void: + var err := ProjectSettings.save() + if err != OK: + push_error("Save GdUnit4 settings failed : %s" % error_string(err)) + return + + +static func has_property(name :String) -> bool: + return ProjectSettings.get_property_list().any(func(property :Dictionary) -> bool: return property["name"] == name) + + +static func get_property(name :String) -> GdUnitProperty: + for property in ProjectSettings.get_property_list(): + var property_name :String = property["name"] + if property_name == name: + return build_property(name, property) + return null + + +static func build_property(property_name: String, property: Dictionary) -> GdUnitProperty: + var value: Variant = ProjectSettings.get_setting(property_name) + var value_type: int = property["type"] + var default: Variant = ProjectSettings.property_get_revert(property_name) + var help: String = property["hint_string"] + var value_set := extract_value_set_from_help(help) + return GdUnitProperty.new(property_name, value_type, value, default, extract_help_text(help), value_set) + + +static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void: + var property := get_property(old_property) + if property == null: + prints("Migration not possible, property '%s' not found" % old_property) + return + var value :Variant = converter.call(property.value()) if converter.is_valid() else property.value() + ProjectSettings.set_setting(new_property, value) + ProjectSettings.set_initial_value(new_property, default_value) + set_help(new_property, value, help) + ProjectSettings.clear(old_property) + prints("Successfully migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value]) + + +static func dump_to_tmp() -> void: + @warning_ignore("return_value_discarded") + ProjectSettings.save_custom("user://project_settings.godot") + + +static func restore_dump_from_tmp() -> void: + @warning_ignore("return_value_discarded") + DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot") diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid index e69de29b..cb7e30e1 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid @@ -0,0 +1 @@ +uid://coby4unvmd3eh diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd index e69de29b..528e133f 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd @@ -0,0 +1,81 @@ +class_name GdUnitSignalAwaiter +extends RefCounted + +signal signal_emitted(action :Variant) + +const NO_ARG :Variant = GdUnitConstants.NO_ARG + +var _wait_on_idle_frame := false +var _interrupted := false +var _time_left :float = 0 +var _timeout_millis :int + + +func _init(timeout_millis :int, wait_on_idle_frame := false) -> void: + _timeout_millis = timeout_millis + _wait_on_idle_frame = wait_on_idle_frame + + +func _on_signal_emmited( + arg0 :Variant = NO_ARG, + arg1 :Variant = NO_ARG, + arg2 :Variant = NO_ARG, + arg3 :Variant = NO_ARG, + arg4 :Variant = NO_ARG, + arg5 :Variant = NO_ARG, + arg6 :Variant = NO_ARG, + arg7 :Variant = NO_ARG, + arg8 :Variant = NO_ARG, + arg9 :Variant = NO_ARG) -> void: + var signal_args :Variant = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + signal_emitted.emit(signal_args) + + +func is_interrupted() -> bool: + return _interrupted + + +func elapsed_time() -> float: + return _time_left + + +func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant: + # register checked signal to wait for + @warning_ignore("return_value_discarded") + source.connect(signal_name, _on_signal_emmited) + # install timeout timer + var scene_tree := Engine.get_main_loop() as SceneTree + var timer := Timer.new() + scene_tree.root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + @warning_ignore("return_value_discarded") + timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED) + timer.start(_timeout_millis * 0.001 * Engine.get_time_scale()) + + # holds the emited value + var value :Variant + # wait for signal is emitted or a timeout is happen + while true: + value = await signal_emitted + if _interrupted: + break + if not (value is Array): + value = [value] + if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args): + break + await scene_tree.process_frame + + source.disconnect(signal_name, _on_signal_emmited) + _time_left = timer.time_left + timer.queue_free() + await scene_tree.process_frame + @warning_ignore("unsafe_cast") + if value is Array and (value as Array).size() == 1: + return value[0] + return value + + +func _do_interrupt() -> void: + _interrupted = true + signal_emitted.emit(null) diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid index e69de29b..8eaf88b7 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid @@ -0,0 +1 @@ +uid://ckx5jnr3ip6vp diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd index e69de29b..d15d3843 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -0,0 +1,129 @@ +# It connects to all signals of given emitter and collects received signals and arguments +# The collected signals are cleand finally when the emitter is freed. +class_name GdUnitSignalCollector +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG +const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] + +# { +# emitter : { +# signal_name : [signal_args], +# ... +# } +# } +var _collected_signals :Dictionary = {} + + +func clear() -> void: + for emitter :Object in _collected_signals.keys(): + if is_instance_valid(emitter): + unregister_emitter(emitter) + + +# connect to all possible signals defined by the emitter +# prepares the signal collection to store received signals and arguments +func register_emitter(emitter: Object, force_recreate := false) -> void: + if is_instance_valid(emitter): + # check emitter is already registerd + if _collected_signals.has(emitter): + if not force_recreate: + return + # If the flag recreate is set to true, emitters that are already registered must be deregistered before recreating, + # otherwise signals that have already been collected will be evaluated. + unregister_emitter(emitter) + + _collected_signals[emitter] = Dictionary() + # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. + if emitter is Node and !(emitter as Node).tree_exiting.is_connected(unregister_emitter): + (emitter as Node).tree_exiting.connect(unregister_emitter.bind(emitter)) + # connect to all signals of the emitter we want to collect + for signal_def in emitter.get_signal_list(): + var signal_name :String = signal_def["name"] + # set inital collected to empty + if not is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name] = Array() + if SIGNAL_BLACK_LIST.find(signal_name) != -1: + continue + if !emitter.is_connected(signal_name, _on_signal_emmited): + var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + if err != OK: + push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) + + +# unregister all acquired resources/connections, otherwise it ends up in orphans +# is called when the emitter is removed from the parent +func unregister_emitter(emitter :Object) -> void: + if is_instance_valid(emitter): + for signal_def in emitter.get_signal_list(): + var signal_name :String = signal_def["name"] + if emitter.is_connected(signal_name, _on_signal_emmited): + emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + @warning_ignore("return_value_discarded") + _collected_signals.erase(emitter) + + +# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements +func _on_signal_emmited( + arg0 :Variant= NO_ARG, + arg1 :Variant= NO_ARG, + arg2 :Variant= NO_ARG, + arg3 :Variant= NO_ARG, + arg4 :Variant= NO_ARG, + arg5 :Variant= NO_ARG, + arg6 :Variant= NO_ARG, + arg7 :Variant= NO_ARG, + arg8 :Variant= NO_ARG, + arg9 :Variant= NO_ARG, + arg10 :Variant= NO_ARG, + arg11 :Variant= NO_ARG) -> void: + var signal_args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) + # extract the emitter and signal_name from the last two arguments (see line 61 where is added) + var signal_name :String = signal_args.pop_back() + var emitter :Object = signal_args.pop_back() + #prints("_on_signal_emmited:", emitter, signal_name, signal_args) + if is_signal_collecting(emitter, signal_name): + @warning_ignore("unsafe_cast") + (_collected_signals[emitter][signal_name] as Array).append(signal_args) + + +func reset_received_signals(emitter: Object, signal_name: String, signal_args: Array) -> void: + #_debug_signal_list("before claer"); + if _collected_signals.has(emitter): + var signals_by_emitter :Dictionary = _collected_signals[emitter] + if signals_by_emitter.has(signal_name): + var received_args: Array = _collected_signals[emitter][signal_name] + # We iterate backwarts over to received_args to remove matching args. + # This will avoid array corruption see comment on `erase` otherwise we need a timeconsuming duplicate before + for arg_pos: int in range(received_args.size()-1, -1, -1): + var arg: Variant = received_args[arg_pos] + if GdObjects.equals(arg, signal_args): + received_args.remove_at(arg_pos) + #_debug_signal_list("after claer"); + + +func is_signal_collecting(emitter: Object, signal_name: String) -> bool: + @warning_ignore("unsafe_cast") + return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name) + + +func match(emitter :Object, signal_name :String, args :Array) -> bool: + #prints("match", signal_name, _collected_signals[emitter][signal_name]); + if _collected_signals.is_empty() or not _collected_signals.has(emitter): + return false + for received_args :Variant in _collected_signals[emitter][signal_name]: + #prints("testing", signal_name, received_args, "vs", args) + if GdObjects.equals(received_args, args): + return true + return false + + +func _debug_signal_list(message :String) -> void: + prints("-----", message, "-------") + prints("senders {") + for emitter :Object in _collected_signals: + prints("\t", emitter) + for signal_name :String in _collected_signals[emitter]: + var args :Variant = _collected_signals[emitter][signal_name] + prints("\t\t", signal_name, args) + prints("}") diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid index e69de29b..7f51e4f0 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid @@ -0,0 +1 @@ +uid://cm0rbs8vhdhd1 diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd index e69de29b..53aafe99 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -0,0 +1,118 @@ +class_name GdUnitSignals +extends RefCounted +## Singleton class that handles GdUnit's signal communication.[br] +## [br] +## This class manages all signals used to communicate test events, discovery, and status changes.[br] +## It uses a singleton pattern stored in Engine metadata to ensure a single instance.[br] +## [br] +## Signals are grouped by purpose:[br] +## - Client connection handling[br] +## - Test execution events[br] +## - Test discovery events[br] +## - Settings and status updates[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Connect to test discovery +## GdUnitSignals.instance().gdunit_test_discovered.connect(self._on_test_discovered) +## +## # Emit test event +## GdUnitSignals.instance().gdunit_event.emit(test_event) +## [/codeblock] + + +## Emitted when a client connects to the GdUnit server.[br] +## [param client_id] The ID of the connected client. +@warning_ignore("unused_signal") +signal gdunit_client_connected(client_id: int) + + +## Emitted when a client disconnects from the GdUnit server.[br] +## [param client_id] The ID of the disconnected client. +@warning_ignore("unused_signal") +signal gdunit_client_disconnected(client_id: int) + + +## Emitted when a client terminates unexpectedly. +@warning_ignore("unused_signal") +signal gdunit_client_terminated() + + +## Emitted when a test execution event occurs.[br] +## [param event] The test event containing details about test execution. +@warning_ignore("unused_signal") +signal gdunit_event(event: GdUnitEvent) + + +## Emitted for test debug events during execution.[br] +## [param event] The debug event containing test execution details. +@warning_ignore("unused_signal") +signal gdunit_event_debug(event: GdUnitEvent) + + +## Emitted to broadcast a general message.[br] +## [param message] The message to broadcast. +@warning_ignore("unused_signal") +signal gdunit_message(message: String) + + +## Emitted to update test failure status.[br] +## [param is_failed] Whether the test has failed. +@warning_ignore("unused_signal") +signal gdunit_set_test_failed(is_failed: bool) + + +## Emitted when a GdUnit setting changes.[br] +## [param property] The property that was changed. +@warning_ignore("unused_signal") +signal gdunit_settings_changed(property: GdUnitProperty) + +## Called when a new test case is discovered during the discovery process. +## Custom implementations should connect to this signal and store the discovered test case as needed.[br] +## [param test_case] The discovered test case instance to be processed. +@warning_ignore("unused_signal") +signal gdunit_test_discover_added(test_case: GdUnitTestCase) + + +## Emitted when a test case is deleted.[br] +## [param test_case] The test case that was deleted. +@warning_ignore("unused_signal") +signal gdunit_test_discover_deleted(test_case: GdUnitTestCase) + + +## Emitted when a test case is modified.[br] +## [param test_case] The test case that was modified. +@warning_ignore("unused_signal") +signal gdunit_test_discover_modified(test_case: GdUnitTestCase) + + +const META_KEY := "GdUnitSignals" + + +## Returns the singleton instance of GdUnitSignals.[br] +## Creates a new instance if none exists.[br] +## [br] +## Returns: The GdUnitSignals singleton instance. +static func instance() -> GdUnitSignals: + if Engine.has_meta(META_KEY): + return Engine.get_meta(META_KEY) + var instance_ := GdUnitSignals.new() + Engine.set_meta(META_KEY, instance_) + return instance_ + + +## Cleans up the singleton instance and disconnects all signals.[br] +## [br] +## Should be called when GdUnit is shutting down or needs to reset.[br] +## Ensures proper cleanup of signal connections and resources. +static func dispose() -> void: + var signals := instance() + # cleanup connected signals + for signal_ in signals.get_signal_list(): + @warning_ignore("unsafe_cast") + for connection in signals.get_signal_connection_list(signal_["name"] as StringName): + var _signal: Signal = connection["signal"] + var _callable: Callable = connection["callable"] + _signal.disconnect(_callable) + signals = null + Engine.remove_meta(META_KEY) diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd.uid b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid index e69de29b..cf97f408 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid @@ -0,0 +1 @@ +uid://kj16fg0hf6kn diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd index e69de29b..b8e08cc3 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -0,0 +1,56 @@ +################################################################################ +# Provides access to a global accessible singleton +# +# This is a workarount to the existing auto load singleton because of some bugs +# around plugin handling +################################################################################ +class_name GdUnitSingleton +extends Object + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MEATA_KEY := "GdUnitSingletons" + + +static func instance(name: String, clazz: Callable) -> Variant: + if Engine.has_meta(name): + return Engine.get_meta(name) + var singleton: Variant = clazz.call() + if is_instance_of(singleton, RefCounted): + @warning_ignore("unsafe_cast") + push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()]) + return + + Engine.set_meta(name, singleton) + GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton]) + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + @warning_ignore("return_value_discarded") + singletons.append(name) + Engine.set_meta(MEATA_KEY, singletons) + return singleton + + +static func unregister(p_singleton: String, use_call_deferred: bool = false) -> void: + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + if singletons.has(p_singleton): + GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton); + var index := singletons.find(p_singleton) + singletons.remove_at(index) + var instance_: Object = Engine.get_meta(p_singleton) + GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) + @warning_ignore("return_value_discarded") + GdUnitTools.free_instance(instance_, use_call_deferred) + Engine.remove_meta(p_singleton) + GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) + Engine.set_meta(MEATA_KEY, singletons) + + +static func dispose(use_call_deferred: bool = false) -> void: + # use a copy because unregister is modify the singletons array + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + GdUnitTools.prints_verbose("----------------------------------------------------------------") + GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons) + for singleton in PackedStringArray(singletons): + unregister(singleton, use_call_deferred) + Engine.remove_meta(MEATA_KEY) + GdUnitTools.prints_verbose("----------------------------------------------------------------") diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid index e69de29b..00c4d141 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid @@ -0,0 +1 @@ +uid://4sujouo3vf6d diff --git a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid index e69de29b..cb57e902 100644 --- a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid @@ -0,0 +1 @@ +uid://ierjyaem56m3 diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd index e69de29b..97a49b00 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -0,0 +1,20 @@ +class_name GdUnitTestSuiteBuilder +extends RefCounted + + +static func create(source :Script, line_number :int) -> GdUnitResult: + var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) + # we need to save and close the testsuite and source if is current opened before modify + @warning_ignore("return_value_discarded") + ScriptEditorControls.save_an_open_script(source.resource_path) + @warning_ignore("return_value_discarded") + ScriptEditorControls.save_an_open_script(test_suite_path, true) + if source.get_class() == "CSharpScript": + return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) + var parser := GdScriptParser.new() + var lines := source.source_code.split("\n") + var current_line := lines[line_number] + var func_name := parser.parse_func_name(current_line) + if func_name.is_empty(): + return GdUnitResult.error("No function found at line: %d." % line_number) + return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid index e69de29b..66f5e56a 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid @@ -0,0 +1 @@ +uid://dthfh16tl5wqc diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid index e69de29b..0f927d51 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid @@ -0,0 +1 @@ +uid://bju0nt1bgsc2s diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index e69de29b..0742896a 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -0,0 +1,144 @@ +extends RefCounted + + +static var _richtext_normalize: RegEx + + +static func normalize_text(text :String) -> String: + return text.replace("\r", ""); + + +static func richtext_normalize(input :String) -> String: + if _richtext_normalize == null: + _richtext_normalize = to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") + return _richtext_normalize.sub(input, "", true).replace("\r", "") + + +static func to_regex(pattern :String) -> RegEx: + var regex := RegEx.new() + var err := regex.compile(pattern) + if err != OK: + push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)]) + return regex + + +static func prints_verbose(message :String) -> void: + if OS.is_stdout_verbose(): + prints(message) + + +static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool: + if instance is Array: + var as_array: Array = instance + for element: Variant in as_array: + @warning_ignore("return_value_discarded") + free_instance(element) + as_array.clear() + return true + # do not free an already freed instance + if not is_instance_valid(instance): + return false + # do not free a class refernece + @warning_ignore("unsafe_cast") + if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): + return false + if is_stdout_verbose: + print_verbose("GdUnit4:gc():free instance ", instance) + @warning_ignore("unsafe_cast") + release_double(instance as Object) + if instance is RefCounted: + @warning_ignore("unsafe_cast") + (instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE) + # If scene runner freed we explicit await all inputs are processed + if instance is GdUnitSceneRunnerImpl: + @warning_ignore("unsafe_cast") + await (instance as GdUnitSceneRunnerImpl).await_input_processed() + return true + else: + if instance is Timer: + var timer: Timer = instance + timer.stop() + if use_call_deferred: + timer.call_deferred("free") + else: + timer.free() + await (Engine.get_main_loop() as SceneTree).process_frame + return true + + @warning_ignore("unsafe_cast") + if instance is Node and (instance as Node).get_parent() != null: + var node: Node = instance + if is_stdout_verbose: + print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node) + if use_call_deferred: + node.get_parent().remove_child.call_deferred(node) + #instance.call_deferred("set_owner", null) + else: + node.get_parent().remove_child(node) + if is_stdout_verbose: + print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance) + if use_call_deferred: + @warning_ignore("unsafe_cast") + (instance as Object).call_deferred("free") + else: + @warning_ignore("unsafe_cast") + (instance as Object).free() + return !is_instance_valid(instance) + + +static func _release_connections(instance :Object) -> void: + if is_instance_valid(instance): + # disconnect from all connected signals to force freeing, otherwise it ends up in orphans + for connection in instance.get_incoming_connections(): + var signal_ :Signal = connection["signal"] + var callable_ :Callable = connection["callable"] + #prints(instance, connection) + #prints("signal", signal_.get_name(), signal_.get_object()) + #prints("callable", callable_.get_object()) + if instance.has_signal(signal_.get_name()) and instance.is_connected(signal_.get_name(), callable_): + #prints("disconnect signal", signal_.get_name(), callable_) + instance.disconnect(signal_.get_name(), callable_) + release_timers() + + +static func release_timers() -> void: + # we go the new way to hold all gdunit timers in group 'GdUnitTimers' + var scene_tree := Engine.get_main_loop() as SceneTree + if scene_tree.root == null: + return + for node :Node in scene_tree.root.get_children(): + if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): + if is_instance_valid(node): + scene_tree.root.remove_child.call_deferred(node) + (node as Timer).stop() + node.queue_free() + + +# the finally cleaup unfreed resources and singletons +static func dispose_all(use_call_deferred :bool = false) -> void: + release_timers() + GdUnitSingleton.dispose(use_call_deferred) + GdUnitSignals.dispose() + + +# if instance an mock or spy we need manually freeing the self reference +static func release_double(instance :Object) -> void: + if instance.has_method("__release_double"): + instance.call("__release_double") + + + +static func find_test_case(test_suite: Node, test_case_name: String, index := -1) -> _TestCase: + for test_case: _TestCase in test_suite.get_children(): + if test_case.test_name() == test_case_name: + if index != -1: + if test_case._test_case.attribute_index != index: + continue + return test_case + return null + + +static func register_expect_interupted_by_timeout(test_suite: Node, test_case_name: String) -> void: + var test_case := find_test_case(test_suite, test_case_name) + if test_case: + test_case.expect_to_interupt() diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd.uid b/addons/gdUnit4/src/core/GdUnitTools.gd.uid index e69de29b..7fb9578c 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTools.gd.uid @@ -0,0 +1 @@ +uid://d05qgv6uu477i diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd index e69de29b..52993251 100644 --- a/addons/gdUnit4/src/core/GodotVersionFixures.gd +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -0,0 +1,11 @@ +## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version +class_name GodotVersionFixures +extends RefCounted + + +# handle global_position fixed by https://github.com/godotengine/godot/pull/88473 +static func set_event_global_position(event: InputEventMouseMotion, global_position: Vector2) -> void: + if Engine.get_version_info().hex >= 0x40202 or Engine.get_version_info().hex == 0x40104: + event.global_position = event.position + else: + event.global_position = global_position diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid index e69de29b..38732b1e 100644 --- a/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid @@ -0,0 +1 @@ +uid://dehxycxsj68ev diff --git a/addons/gdUnit4/src/core/LocalTime.gd b/addons/gdUnit4/src/core/LocalTime.gd index e69de29b..fabaaf6f 100644 --- a/addons/gdUnit4/src/core/LocalTime.gd +++ b/addons/gdUnit4/src/core/LocalTime.gd @@ -0,0 +1,114 @@ +# This class provides Date/Time functionallity to Godot +class_name LocalTime +extends Resource + +enum TimeUnit { + DEFAULT = 0, + MILLIS = 1, + SECOND = 2, + MINUTE = 3, + HOUR = 4, + DAY = 5, + MONTH = 6, + YEAR = 7 +} + +const SECONDS_PER_MINUTE:int = 60 +const MINUTES_PER_HOUR:int = 60 +const HOURS_PER_DAY:int = 24 +const MILLIS_PER_SECOND:int = 1000 +const MILLIS_PER_MINUTE:int = MILLIS_PER_SECOND * SECONDS_PER_MINUTE +const MILLIS_PER_HOUR:int = MILLIS_PER_MINUTE * MINUTES_PER_HOUR + +var _time :int +var _hour :int +var _minute :int +var _second :int +var _millisecond :int + + +static func now() -> LocalTime: + return LocalTime.new(_get_system_time_msecs()) + + +static func of_unix_time(time_ms :int) -> LocalTime: + return LocalTime.new(time_ms) + + +static func local_time(hours :int, minutes :int, seconds :int, milliseconds :int) -> LocalTime: + return LocalTime.new(MILLIS_PER_HOUR * hours\ + + MILLIS_PER_MINUTE * minutes\ + + MILLIS_PER_SECOND * seconds\ + + milliseconds) + + +func elapsed_since() -> String: + return LocalTime.elapsed(LocalTime._get_system_time_msecs() - _time) + + +func elapsed_since_ms() -> int: + return LocalTime._get_system_time_msecs() - _time + + +func plus(time_unit :TimeUnit, value :int) -> LocalTime: + var addValue:int = 0 + match time_unit: + TimeUnit.MILLIS: + addValue = value + TimeUnit.SECOND: + addValue = value * MILLIS_PER_SECOND + TimeUnit.MINUTE: + addValue = value * MILLIS_PER_MINUTE + TimeUnit.HOUR: + addValue = value * MILLIS_PER_HOUR + @warning_ignore("return_value_discarded") + _init(_time + addValue) + return self + + +static func elapsed(p_time_ms :int) -> String: + var local_time_ := LocalTime.new(p_time_ms) + if local_time_._hour > 0: + return "%dh %dmin %ds %dms" % [local_time_._hour, local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._minute > 0: + return "%dmin %ds %dms" % [local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._second > 0: + return "%ds %dms" % [local_time_._second, local_time_._millisecond] + return "%dms" % local_time_._millisecond + + +# create from epoch timestamp in ms +func _init(time: int) -> void: + _time = time + @warning_ignore("integer_division") + _hour = (time / MILLIS_PER_HOUR) % 24 + @warning_ignore("integer_division") + _minute = (time / MILLIS_PER_MINUTE) % 60 + @warning_ignore("integer_division") + _second = (time / MILLIS_PER_SECOND) % 60 + _millisecond = time % 1000 + + +func hour() -> int: + return _hour + + +func minute() -> int: + return _minute + + +func second() -> int: + return _second + + +func millis() -> int: + return _millisecond + + +func _to_string() -> String: + return "%02d:%02d:%02d.%03d" % [_hour, _minute, _second, _millisecond] + + +# wraper to old OS.get_system_time_msecs() function +static func _get_system_time_msecs() -> int: + return Time.get_unix_time_from_system() * 1000 as int diff --git a/addons/gdUnit4/src/core/LocalTime.gd.uid b/addons/gdUnit4/src/core/LocalTime.gd.uid index e69de29b..5fc94e3d 100644 --- a/addons/gdUnit4/src/core/LocalTime.gd.uid +++ b/addons/gdUnit4/src/core/LocalTime.gd.uid @@ -0,0 +1 @@ +uid://dmta1h7ndfnko diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd index e69de29b..58409e79 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -0,0 +1,243 @@ +class_name _TestCase +extends Node + +signal completed() + + +var _test_case: GdUnitTestCase +var _attribute: TestCaseAttribute +var _current_iteration: int = -1 +var _expect_to_interupt := false +var _timer: Timer +var _interupted: bool = false +var _failed := false +var _parameter_set_resolver: GdUnitTestParameterSetResolver +var _is_disposed := false +var _func_state: Variant + + +func _init(test_case: GdUnitTestCase, attribute: TestCaseAttribute, fd: GdFunctionDescriptor) -> void: + _test_case = test_case + _attribute = attribute + set_function_descriptor(fd) + + +func execute(p_test_parameter := Array(), p_iteration := 0) -> void: + _failure_received(false) + _current_iteration = p_iteration - 1 + if _current_iteration == - 1: + _set_failure_handler() + set_timeout() + + if is_parameterized(): + execute_parameterized() + elif not p_test_parameter.is_empty(): + update_fuzzers(p_test_parameter, p_iteration) + _execute_test_case(test_name(), p_test_parameter) + else: + _execute_test_case(test_name(), []) + await completed + + +func execute_parameterized() -> void: + _failure_received(false) + set_timeout() + + # Resolve parameter set at runtime to include runtime variables + var test_parameters := await _resolve_test_parameters(_test_case.attribute_index) + if test_parameters.is_empty(): + return + + await _execute_test_case(test_name(), test_parameters) + + +func _resolve_test_parameters(attribute_index: int) -> Array: + var result := _parameter_set_resolver.load_parameter_sets(get_parent()) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + # validate the parameter set + var parameter_sets: Array = result.value() + result = _parameter_set_resolver.validate(parameter_sets, attribute_index) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + @warning_ignore("unsafe_method_access") + var test_parameters: Array = parameter_sets[attribute_index].duplicate() + # We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used. + # This prevents objects in the argument list from being unnecessarily re-instantiated. + test_parameters.append([]) + + return test_parameters + + +func dispose() -> void: + if _is_disposed: + return + _is_disposed = true + Engine.remove_meta("GD_TEST_FAILURE") + stop_timer() + _remove_failure_handler() + _attribute.fuzzers.clear() + + +@warning_ignore("shadowed_variable_base_class", "redundant_await") +func _execute_test_case(name: String, test_parameter: Array) -> void: + # save the function state like GDScriptFunctionState to dispose at test timeout to prevent orphan state + _func_state = get_parent().callv(name, test_parameter) + await _func_state + # needs at least on await otherwise it breaks the awaiting chain + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + + +func update_fuzzers(input_values: Array, iteration: int) -> void: + for fuzzer :Variant in input_values: + if fuzzer is Fuzzer: + fuzzer._iteration_index = iteration + 1 + + +func set_timeout() -> void: + if is_instance_valid(_timer): + return + var time: float = _attribute.timeout / 1000.0 + _timer = Timer.new() + add_child(_timer) + _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) + @warning_ignore("return_value_discarded") + _timer.timeout.connect(do_interrupt, CONNECT_DEFERRED) + _timer.set_one_shot(true) + _timer.set_wait_time(time) + _timer.set_autostart(false) + _timer.start() + + +func do_interrupt() -> void: + _interupted = true + # We need to dispose manually the function state here + GdObjects.dispose_function_state(_func_state) + if not is_expect_interupted(): + var execution_context:= GdUnitThreadManager.get_current_context().get_execution_context() + if is_fuzzed(): + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout"))) + else: + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(_attribute.timeout))) + completed.emit() + + +func _set_failure_handler() -> void: + if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received) + + +func _remove_failure_handler() -> void: + if GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + GdUnitSignals.instance().gdunit_set_test_failed.disconnect(_failure_received) + + +func _failure_received(is_failed: bool) -> void: + # is already failed? + if _failed: + return + _failed = is_failed + Engine.set_meta("GD_TEST_FAILURE", is_failed) + + +func stop_timer() -> void: + # finish outstanding timeouts + if is_instance_valid(_timer): + _timer.stop() + _timer.call_deferred("free") + _timer = null + + +func expect_to_interupt() -> void: + _expect_to_interupt = true + + +func is_interupted() -> bool: + return _interupted + + +func is_expect_interupted() -> bool: + return _expect_to_interupt + + +func is_parameterized() -> bool: + return _parameter_set_resolver.is_parameterized() + + +func is_skipped() -> bool: + return _attribute.is_skipped + + +func skip_info() -> String: + return _attribute.skip_reason + + +func id() -> GdUnitGUID: + return _test_case.guid + + +func test_name() -> String: + return _test_case.test_name + + +@warning_ignore("native_method_override") +func get_name() -> StringName: + return _test_case.test_name + + +func line_number() -> int: + return _test_case.line_number + + +func iterations() -> int: + return _attribute.fuzzer_iterations + + +func seed_value() -> int: + return _attribute.test_seed + + +func is_fuzzed() -> bool: + return not _attribute.fuzzers.is_empty() + + +func fuzzer_arguments() -> Array[GdFunctionArgument]: + return _attribute.fuzzers + + +func script_path() -> String: + return _test_case.source_file + + +func ResourcePath() -> String: + return _test_case.source_file + + +func generate_seed() -> void: + if _attribute.test_seed != -1: + seed(_attribute.test_seed) + + +func do_skip(skipped: bool, reason: String="") -> void: + _attribute.is_skipped = skipped + _attribute.skip_reason = reason + + +func set_function_descriptor(fd: GdFunctionDescriptor) -> void: + _parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd) + + +func _to_string() -> String: + return "%s :%d (%dms)" % [get_name(), _test_case.line_number, _attribute.timeout] diff --git a/addons/gdUnit4/src/core/_TestCase.gd.uid b/addons/gdUnit4/src/core/_TestCase.gd.uid index e69de29b..8642fad1 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd.uid +++ b/addons/gdUnit4/src/core/_TestCase.gd.uid @@ -0,0 +1 @@ +uid://cb2lkpvh0liiv diff --git a/addons/gdUnit4/src/core/assets/touch-button.png b/addons/gdUnit4/src/core/assets/touch-button.png index e69de29b..de424599 100644 --- a/addons/gdUnit4/src/core/assets/touch-button.png +++ b/addons/gdUnit4/src/core/assets/touch-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e709c40067fec99c3b59c78a5a364da8c326a94c49a56337a46c98976d3db4cb +size 3292 diff --git a/addons/gdUnit4/src/core/assets/touch-button.png.import b/addons/gdUnit4/src/core/assets/touch-button.png.import index 9e459618..7f469c3a 100644 --- a/addons/gdUnit4/src/core/assets/touch-button.png.import +++ b/addons/gdUnit4/src/core/assets/touch-button.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://b8nasq23r33s3" -valid=false +uid="uid://csgvrbao53xmv" +path="res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/core/assets/touch-button.png" +dest_files=["res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"] [params] diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd index e69de29b..cf7a2b97 100644 --- a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd @@ -0,0 +1,76 @@ +class_name TestCaseAttribute +extends Resource +## Holds configuration and metadata for individual test cases.[br] +## [br] +## This class defines test behaviors and properties such as:[br] +## - Test timeouts[br] +## - Skip conditions[br] +## - Fuzzing parameters[br] +## - Random seed values[br] + + +## When set, no specific timeout value is configured and test will use the [code]test_timeout[/code][br] +## value from [GdUnitSettings]. +const DEFAULT_TIMEOUT := -1 + + +## The maximum time in milliseconds for test completion.[br] +## The test fails if execution exceeds this duration.[br] +## [br] +## When set to [constant DEFAULT_TIMEOUT], uses the value from [method GdUnitSettings.test_timeout]. +var timeout: int = DEFAULT_TIMEOUT: + set(value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + # get the default timeout from the settings + timeout = GdUnitSettings.test_timeout() + return timeout + + +## The seed used for random number generation in the test.[br] +## Ensures reproducible results for randomized test scenarios.[br] +## A value of -1 indicates no specific seed is set. +var test_seed: int = -1 + + +## Controls whether this test should be skipped during execution.[br] +## Useful for temporarily disabling tests without removing them. +var is_skipped := false + + +## Documents why the test is being skipped.[br] +## [br] +## Should explain the reason for skipping and ideally include:[br] +## - Why the test was disabled[br] +## - Under what conditions it should be re-enabled[br] +## - Any related issues or tickets +var skip_reason := "Unknown" + + +## Number of iterations to run when using fuzzers.[br] +## [br] +## Fuzzers generate random test data to help find edge cases.[br] +## Higher values provide better coverage but increase test duration. +var fuzzer_iterations: int = Fuzzer.ITERATION_DEFAULT_COUNT + + +## Array of fuzzer configurations for test parameters.[br] +## [br] +## Each [GdFunctionArgument] defines how random test data[br] +## should be generated for a particular parameter. +var fuzzers: Array[GdFunctionArgument] = [] + + +# There is a bug in `duplicate` see https://github.com/godotengine/godot/issues/98644 +# we need in addition to overwrite default values with the source values +@warning_ignore("native_method_override") +func clone() -> Resource: + var copy: TestCaseAttribute = TestCaseAttribute.new() + copy.timeout = timeout + copy.test_seed = test_seed + copy.is_skipped = is_skipped + copy.skip_reason = skip_reason + copy.fuzzer_iterations = fuzzer_iterations + copy.fuzzers = fuzzers.duplicate() + return copy diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid index e69de29b..85dc262e 100644 --- a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid @@ -0,0 +1 @@ +uid://d2bres53mgxnw diff --git a/addons/gdUnit4/src/core/command/GdUnitCommand.gd b/addons/gdUnit4/src/core/command/GdUnitCommand.gd index e69de29b..659b6a3f 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommand.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommand.gd @@ -0,0 +1,41 @@ +class_name GdUnitCommand +extends RefCounted + + +func _init(p_name :String, p_is_enabled: Callable, p_runnable: Callable, p_shortcut :GdUnitShortcut.ShortCut = GdUnitShortcut.ShortCut.NONE) -> void: + assert(p_name != null, "(%s) missing parameter 'name'" % p_name) + assert(p_is_enabled != null, "(%s) missing parameter 'is_enabled'" % p_name) + assert(p_runnable != null, "(%s) missing parameter 'runnable'" % p_name) + assert(p_shortcut != null, "(%s) missing parameter 'shortcut'" % p_name) + self.name = p_name + self.is_enabled = p_is_enabled + self.shortcut = p_shortcut + self.runnable = p_runnable + + +var name: String: + set(value): + name = value + get: + return name + + +var shortcut: GdUnitShortcut.ShortCut: + set(value): + shortcut = value + get: + return shortcut + + +var is_enabled: Callable: + set(value): + is_enabled = value + get: + return is_enabled + + +var runnable: Callable: + set(value): + runnable = value + get: + return runnable diff --git a/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid index e69de29b..1df21a92 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid @@ -0,0 +1 @@ +uid://crmuuvbqy4shs diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd index e69de29b..1f64e2ad 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -0,0 +1,408 @@ +class_name GdUnitCommandHandler +extends Object + +signal gdunit_runner_start() +signal gdunit_runner_stop(client_id :int) + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const CMD_RUN_OVERALL = "Debug Overall TestSuites" +const CMD_RUN_TESTCASE = "Run TestCases" +const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)" +const CMD_RUN_TESTSUITE = "Run TestSuites" +const CMD_RUN_TESTSUITE_DEBUG = "Run TestSuites (Debug)" +const CMD_RERUN_TESTS = "ReRun Tests" +const CMD_RERUN_TESTS_DEBUG = "ReRun Tests (Debug)" +const CMD_STOP_TEST_RUN = "Stop Test Run" +const CMD_CREATE_TESTCASE = "Create TestCase" + +const SETTINGS_SHORTCUT_MAPPING := { + "N/A" : GdUnitShortcut.ShortCut.NONE, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST : GdUnitShortcut.ShortCut.RERUN_TESTS, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL : GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP : GdUnitShortcut.ShortCut.STOP_TEST_RUN, + GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE, + GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG, + GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST : GdUnitShortcut.ShortCut.CREATE_TEST, + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTSUITE, + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG +} + +const CommandMapping := { + GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL, + GdUnitShortcut.ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE, + GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG, + GdUnitShortcut.ShortCut.RUN_TESTSUITE: GdUnitCommandHandler.CMD_RUN_TESTSUITE, + GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG, + GdUnitShortcut.ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS, + GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG, + GdUnitShortcut.ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN, + GdUnitShortcut.ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE, +} + +# the current test runner config +var _runner_config := GdUnitRunnerConfig.new() + +# holds the current connected gdUnit runner client id +var _client_id: int +# if no debug mode we have an process id +var _current_runner_process_id: int = 0 +# hold is current an test running +var _is_running: bool = false +# holds if the current running tests started in debug mode +var _running_debug_mode: bool + +var _commands := {} +var _shortcuts := {} + + +static func instance() -> GdUnitCommandHandler: + return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new()) + + +@warning_ignore("return_value_discarded") +func _init() -> void: + assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING) + + GdUnitSignals.instance().gdunit_event.connect(_on_event) + GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + # preload previous test execution + @warning_ignore("return_value_discarded") + _runner_config.load_config() + + init_shortcuts() + var is_running := func(_script :Script) -> bool: return _is_running + var is_not_running := func(_script :Script) -> bool: return !_is_running + register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL)) + register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE)) + register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG)) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false), GdUnitShortcut.ShortCut.RUN_TESTSUITE)) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true), GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG)) + register_command(GdUnitCommand.new(CMD_RERUN_TESTS, is_not_running, cmd_run.bind(false), GdUnitShortcut.ShortCut.RERUN_TESTS)) + register_command(GdUnitCommand.new(CMD_RERUN_TESTS_DEBUG, is_not_running, cmd_run.bind(true), GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG)) + register_command(GdUnitCommand.new(CMD_CREATE_TESTCASE, is_not_running, cmd_create_test, GdUnitShortcut.ShortCut.CREATE_TEST)) + register_command(GdUnitCommand.new(CMD_STOP_TEST_RUN, is_running, cmd_stop.bind(_client_id), GdUnitShortcut.ShortCut.STOP_TEST_RUN)) + + # schedule discover tests if enabled and running inside the editor + if Engine.is_editor_hint() and GdUnitSettings.is_test_discover_enabled(): + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(5) + @warning_ignore("return_value_discarded") + timer.timeout.connect(cmd_discover_tests) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + _commands.clear() + _shortcuts.clear() + + +func _do_process() -> void: + check_test_run_stopped_manually() + + +# is checking if the user has press the editor stop scene +func check_test_run_stopped_manually() -> void: + if is_test_running_but_stop_pressed(): + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Test Runner scene was stopped manually, force stopping the current test run!") + cmd_stop(_client_id) + + +func is_test_running_but_stop_pressed() -> bool: + return _running_debug_mode and _is_running and not EditorInterface.is_playing_scene() + + +func assert_shortcut_mappings(mappings: Dictionary) -> void: + for shortcut: int in GdUnitShortcut.ShortCut.values(): + assert(mappings.values().has(shortcut), "missing settings mapping for shortcut '%s'!" % GdUnitShortcut.ShortCut.keys()[shortcut]) + + +func init_shortcuts() -> void: + for shortcut: int in GdUnitShortcut.ShortCut.values(): + if shortcut == GdUnitShortcut.ShortCut.NONE: + continue + var property_name: String = SETTINGS_SHORTCUT_MAPPING.find_key(shortcut) + var property := GdUnitSettings.get_property(property_name) + var keys := GdUnitShortcut.default_keys(shortcut) + if property != null: + keys = property.value() + var inputEvent := create_shortcut_input_even(keys) + register_shortcut(shortcut, inputEvent) + + +func create_shortcut_input_even(key_codes: PackedInt32Array) -> InputEventKey: + var inputEvent := InputEventKey.new() + inputEvent.pressed = true + for key_code in key_codes: + match key_code: + KEY_ALT: + inputEvent.alt_pressed = true + KEY_SHIFT: + inputEvent.shift_pressed = true + KEY_CTRL: + inputEvent.ctrl_pressed = true + _: + inputEvent.keycode = key_code as Key + inputEvent.physical_keycode = key_code as Key + return inputEvent + + +func register_shortcut(p_shortcut: GdUnitShortcut.ShortCut, p_input_event: InputEvent) -> void: + GdUnitTools.prints_verbose("register shortcut: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[p_shortcut], p_input_event.as_text()]) + var shortcut := Shortcut.new() + shortcut.set_events([p_input_event]) + var command_name := get_shortcut_command(p_shortcut) + _shortcuts[p_shortcut] = GdUnitShortcutAction.new(p_shortcut, shortcut, command_name) + + +func get_shortcut(shortcut_type: GdUnitShortcut.ShortCut) -> Shortcut: + return get_shortcut_action(shortcut_type).shortcut + + +func get_shortcut_action(shortcut_type: GdUnitShortcut.ShortCut) -> GdUnitShortcutAction: + return _shortcuts.get(shortcut_type) + + +func get_shortcut_command(p_shortcut: GdUnitShortcut.ShortCut) -> String: + return CommandMapping.get(p_shortcut, "unknown command") + + +func register_command(p_command: GdUnitCommand) -> void: + _commands[p_command.name] = p_command + + +func command(cmd_name: String) -> GdUnitCommand: + return _commands.get(cmd_name) + + +func cmd_run_test_suites(scripts: Array[Script], debug: bool, rerun := false) -> void: + # Update test discovery + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + var tests_to_execute: Array[GdUnitTestCase] = [] + for script in scripts: + GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void: + tests_to_execute.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + + # create new runner runner_config for fresh run otherwise use saved one + if not rerun: + var result := _runner_config.clear()\ + .add_test_cases(tests_to_execute)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run_test_case(script: Script, test_case: String, test_param_index: int, debug: bool, rerun := false) -> void: + # Update test discovery + var tests_to_execute: Array[GdUnitTestCase] = [] + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + # We filter for a single test + if test.test_name == test_case: + # We only add selected parameterized test to the execution list + if test_param_index == -1: + tests_to_execute.append(test) + elif test.attribute_index == test_param_index: + tests_to_execute.append(test) + GdUnitTestDiscoverSink.discover(test) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + + # create new runner config for fresh run otherwise use saved one + if not rerun: + var result := _runner_config.clear()\ + .add_test_cases(tests_to_execute)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run_tests(tests_to_execute: Array[GdUnitTestCase], debug: bool) -> void: + # Save tests to runner config before execute + var result := _runner_config.clear()\ + .add_test_cases(tests_to_execute)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run_overall(debug: bool) -> void: + var tests_to_execute := await GdUnitTestDiscoverer.run() + var result := _runner_config.clear()\ + .add_test_cases(tests_to_execute)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run(debug: bool) -> void: + # don't start is already running + if _is_running: + return + + # save current selected excution config + var server_port: int = Engine.get_meta("gdunit_server_port") + var result := _runner_config.set_server_port(server_port).save_config() + if result.is_error(): + push_error(result.error_message()) + return + # before start we have to save all changes + ScriptEditorControls.save_all_open_script() + gdunit_runner_start.emit() + _current_runner_process_id = -1 + _running_debug_mode = debug + if debug: + run_debug_mode() + else: + run_release_mode() + + +func cmd_stop(client_id: int) -> void: + # don't stop if is already stopped + if not _is_running: + return + _is_running = false + gdunit_runner_stop.emit(client_id) + if _running_debug_mode: + EditorInterface.stop_playing_scene() + elif _current_runner_process_id > 0: + if OS.is_process_running(_current_runner_process_id): + var result := OS.kill(_current_runner_process_id) + if result != OK: + push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result) + _current_runner_process_id = -1 + + +func cmd_editor_run_test(debug: bool) -> void: + if is_active_script_editor(): + var cursor_line := active_base_editor().get_caret_line() + #run test case? + var regex := RegEx.new() + @warning_ignore("return_value_discarded") + regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") + var result := regex.search(active_base_editor().get_line(cursor_line)) + if result: + var func_name := result.get_string(2).strip_edges() + if func_name.begins_with("test_"): + cmd_run_test_case(active_script(), func_name, -1, debug) + return + # otherwise run the full test suite + var selected_test_suites: Array[Script] = [active_script()] + cmd_run_test_suites(selected_test_suites, debug) + + +func cmd_create_test() -> void: + if not is_active_script_editor(): + return + var cursor_line := active_base_editor().get_caret_line() + var result := GdUnitTestSuiteBuilder.create(active_script(), cursor_line) + if result.is_error(): + # show error dialog + push_error("Failed to create test case: %s" % result.error_message()) + return + var info: Dictionary = result.value() + var script_path: String = info.get("path") + var script_line: int = info.get("line") + ScriptEditorControls.edit_script(script_path, script_line) + + +func cmd_discover_tests() -> void: + await GdUnitTestDiscoverer.run() + + +func run_debug_mode() -> void: + EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") + _is_running = true + + +func run_release_mode() -> void: + var arguments := Array() + if OS.is_stdout_verbose(): + arguments.append("--verbose") + arguments.append("--no-window") + arguments.append("--path") + arguments.append(ProjectSettings.globalize_path("res://")) + arguments.append("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") + _current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false); + _is_running = true + + +func is_active_script_editor() -> bool: + return EditorInterface.get_script_editor().get_current_editor() != null + + +func active_base_editor() -> TextEdit: + return EditorInterface.get_script_editor().get_current_editor().get_base_editor() + + +func active_script() -> Script: + return EditorInterface.get_script_editor().get_current_script() + + + +################################################################################ +# signals handles +################################################################################ +func _on_event(event: GdUnitEvent) -> void: + if event.type() == GdUnitEvent.SESSION_CLOSE: + cmd_stop(_client_id) + + +func _on_stop_pressed() -> void: + cmd_stop(_client_id) + + +func _on_run_pressed(debug := false) -> void: + cmd_run(debug) + + +func _on_run_overall_pressed(_debug := false) -> void: + cmd_run_overall(true) + + +func _on_settings_changed(property: GdUnitProperty) -> void: + if SETTINGS_SHORTCUT_MAPPING.has(property.name()): + var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name()) + var value: PackedInt32Array = property.value() + var input_event := create_shortcut_input_even(value) + prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()]) + var action := get_shortcut_action(shortcut) + if action != null: + action.update_shortcut(input_event) + else: + register_shortcut(shortcut, input_event) + if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED: + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3) + @warning_ignore("return_value_discarded") + timer.timeout.connect(cmd_discover_tests) + + +################################################################################ +# Network stuff +################################################################################ +func _on_client_connected(client_id: int) -> void: + _client_id = client_id + + +func _on_client_disconnected(client_id: int) -> void: + # only stops is not in debug mode running and the current client + if not _running_debug_mode and _client_id == client_id: + cmd_stop(client_id) + _client_id = -1 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid index e69de29b..42342b3f 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid @@ -0,0 +1 @@ +uid://dooc00u4rahqp diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd index e69de29b..8fac4829 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd @@ -0,0 +1,52 @@ +class_name GdUnitShortcut +extends RefCounted + + +enum ShortCut { + NONE, + RUN_TESTS_OVERALL, + RUN_TESTCASE, + RUN_TESTCASE_DEBUG, + RUN_TESTSUITE, + RUN_TESTSUITE_DEBUG, + RERUN_TESTS, + RERUN_TESTS_DEBUG, + STOP_TEST_RUN, + CREATE_TEST, +} + +const DEFAULTS_MACOS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6], + ShortCut.CREATE_TEST : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F10], +} + +const DEFAULTS_WINDOWS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6], + ShortCut.CREATE_TEST : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F10], +} + + +static func default_keys(shortcut :ShortCut) -> PackedInt32Array: + match OS.get_name().to_lower(): + 'windows': + return DEFAULTS_WINDOWS[shortcut] + 'macos': + return DEFAULTS_MACOS[shortcut] + _: + return DEFAULTS_WINDOWS[shortcut] diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid index e69de29b..fb7896db 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid @@ -0,0 +1 @@ +uid://bsg0clvy7wf0m diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd index e69de29b..c49e83e5 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd +++ b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd @@ -0,0 +1,40 @@ +class_name GdUnitShortcutAction +extends RefCounted + + +func _init(p_type :GdUnitShortcut.ShortCut, p_shortcut :Shortcut, p_command :String) -> void: + assert(p_type != null, "missing parameter 'type'") + assert(p_shortcut != null, "missing parameter 'shortcut'") + assert(p_command != null, "missing parameter 'command'") + self.type = p_type + self.shortcut = p_shortcut + self.command = p_command + + +var type: GdUnitShortcut.ShortCut: + set(value): + type = value + get: + return type + + +var shortcut: Shortcut: + set(value): + shortcut = value + get: + return shortcut + + +var command: String: + set(value): + command = value + get: + return command + + +func update_shortcut(input_event: InputEventKey) -> void: + shortcut.set_events([input_event]) + + +func _to_string() -> String: + return "GdUnitShortcutAction: %s (%s) -> %s" % [GdUnitShortcut.ShortCut.keys()[type], shortcut.get_as_text(), command] diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid index e69de29b..286b4d5d 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid @@ -0,0 +1 @@ +uid://cmlh3hniafm5s diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd index e69de29b..00332f90 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd @@ -0,0 +1,46 @@ +## A class representing a globally unique identifier for GdUnit test elements. +## Uses random values to generate unique identifiers that can be used +## to track and reference test cases and suites across the test framework. +class_name GdUnitGUID +extends RefCounted + + +## The internal string representation of the GUID. +## Generated using Godot's ResourceUID system when no existing GUID is provided. +var _guid: String + + +## Creates a new GUID instance. +## If no GUID is provided, generates a new one using Godot's ResourceUID system. +func _init(from_guid: String = "") -> void: + if from_guid.is_empty(): + _guid = _generate_guid() + else: + _guid = from_guid + + +## Compares this GUID with another for equality. +## Returns true if both GUIDs represent the same unique identifier. +func equals(other: GdUnitGUID) -> bool: + return other._guid == _guid + + +## Generates a custom GUID using random bytes.[br] +## The format uses 16 random bytes encoded to hex and formatted with hyphens. +static func _generate_guid() -> String: + # Pre-allocate array with exact size needed + var bytes := PackedByteArray() + bytes.resize(16) + + # Fill with random bytes + for i in range(16): + bytes[i] = randi() % 256 + + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return bytes.hex_encode().insert(8, "-").insert(16, "-").insert(24, "-") + + +func _to_string() -> String: + return _guid diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid index e69de29b..08f1f325 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid @@ -0,0 +1 @@ +uid://d4lobvde8tufj diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid index e69de29b..8cfe8d7d 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid @@ -0,0 +1 @@ +uid://i4kgxeu6rjiv diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid index e69de29b..788ede26 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid @@ -0,0 +1 @@ +uid://cojycdwxjbkf3 diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd index e69de29b..5d0e5b68 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd @@ -0,0 +1,13 @@ +## A static utility class that acts as a central sink for test case discovery events in GdUnit4. +## Instead of implementing custom sink classes, test discovery consumers should connect to +## the GdUnitSignals.gdunit_test_discovered signal to receive test case discoveries. +## This design allows for a more flexible and decoupled test discovery system. +class_name GdUnitTestDiscoverSink +extends RefCounted + + +## Emits a discovered test case through the GdUnitSignals system.[br] +## Sends the test case to all listeners connected to the gdunit_test_discovered signal.[br] +## [member test_case] The discovered test case to be broadcast to all connected listeners. +static func discover(test_case: GdUnitTestCase) -> void: + GdUnitSignals.instance().gdunit_test_discover_added.emit(test_case) diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid index e69de29b..648ab17b 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid @@ -0,0 +1 @@ +uid://ct0kk6824vhxf diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd index e69de29b..cb3e4071 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd @@ -0,0 +1,171 @@ +class_name GdUnitTestDiscoverer +extends RefCounted + + +static func run() -> Array[GdUnitTestCase]: + console_log("Running test discovery ..") + await (Engine.get_main_loop() as SceneTree).process_frame + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + + # We run the test discovery in an extra thread so that the main thread is not blocked + var t:= Thread.new() + @warning_ignore("return_value_discarded") + t.start(func () -> Array[GdUnitTestCase]: + # Loading previous test session + var runner_config := GdUnitRunnerConfig.new() + runner_config.load_config() + var recovered_tests := runner_config.test_cases() + var test_suite_directories := scan_all_test_directories(GdUnitSettings.test_root_folder()) + var scanner := GdUnitTestSuiteScanner.new() + + var collected_tests: Array[GdUnitTestCase] = [] + var collected_test_suites: Array[Script] = [] + # collect test suites + for test_suite_dir in test_suite_directories: + collected_test_suites.append_array(scanner.scan_directory(test_suite_dir)) + + # Do sync the main thread before emit the discovered test suites to the inspector + await (Engine.get_main_loop() as SceneTree).process_frame + for test_suites_script in collected_test_suites: + discover_tests(test_suites_script, func(test_case: GdUnitTestCase) -> void: + # Sync test uid from last test session + recover_test_guid(test_case, recovered_tests) + collected_tests.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + + console_log_discover_results(collected_tests) + if !recovered_tests.is_empty(): + console_log("Recovered last test session successfully, %d tests restored." % recovered_tests.size(), true) + return collected_tests + ) + # wait unblocked to the tread is finished + while t.is_alive(): + await (Engine.get_main_loop() as SceneTree).process_frame + # needs finally to wait for finish + var test_to_execute: Array[GdUnitTestCase] = await t.wait_to_finish() + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + return test_to_execute + + +## Restores the last test run session by loading the test run config file and rediscover the tests +static func restore_last_session() -> void: + if GdUnitSettings.is_test_discover_enabled(): + return + + var runner_config := GdUnitRunnerConfig.new() + var result := runner_config.load_config() + # Report possible config loading errors + if result.is_error(): + console_log("Recovery of the last test session failed: %s" % result.error_message(), true) + # If no config file found, skip test recovery + if result.is_warn(): + return + + # If no tests recorded, skip test recovery + var test_cases := runner_config.test_cases() + if test_cases.size() == 0: + return + + # We run the test session restoring in an extra thread so that the main thread is not blocked + var t:= Thread.new() + t.start(func () -> void: + # Do sync the main thread before emit the discovered test suites to the inspector + await (Engine.get_main_loop() as SceneTree).process_frame + console_log("Recovering last test session ..", true) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + for test_case in test_cases: + GdUnitTestDiscoverSink.discover(test_case) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + console_log("Recovered last test session successfully, %d tests restored." % test_cases.size(), true) + ) + t.wait_to_finish() + + +static func recover_test_guid(current: GdUnitTestCase, recovered_tests: Array[GdUnitTestCase]) -> void: + for recovered_test in recovered_tests: + if recovered_test.fully_qualified_name == current.fully_qualified_name: + current.guid = recovered_test.guid + + +static func console_log_discover_results(tests: Array[GdUnitTestCase]) -> void: + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.source_file + ) + for suite_tests: Array in grouped_by_suites.values(): + var test_case: GdUnitTestCase = suite_tests[0] + console_log("Discover: TestSuite %s with %d tests found" % [test_case.source_file, suite_tests.size()]) + console_log("Discover tests done, %d TestSuites and total %d Tests found. " % [grouped_by_suites.size(), tests.size()]) + console_log("") + + +static func console_log(message: String, on_console := false) -> void: + prints(message) + if on_console: + GdUnitSignals.instance().gdunit_message.emit(message) + + +static func filter_tests(method: Dictionary) -> bool: + var method_name: String = method["name"] + return method_name.begins_with("test_") + + +static func default_discover_sink(test_case: GdUnitTestCase) -> void: + GdUnitTestDiscoverSink.discover(test_case) + + +static func discover_tests(source_script: Script, discover_sink := default_discover_sink) -> void: + if source_script is GDScript: + var test_names := source_script.get_script_method_list()\ + .filter(filter_tests)\ + .map(func(method: Dictionary) -> String: return method["name"]) + # no tests discovered? + if test_names.is_empty(): + return + + var parser := GdScriptParser.new() + var fds := parser.get_function_descriptors(source_script as GDScript, test_names) + for fd in fds: + var resolver := GdFunctionParameterSetResolver.new(fd) + for test_case in resolver.resolve_test_cases(source_script as GDScript): + discover_sink.call(test_case) + elif source_script.get_class() == "CSharpScript": + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return + for test_case in GdUnit4CSharpApiLoader.discover_tests(source_script): + discover_sink.call(test_case) + + +static func scan_all_test_directories(root: String) -> PackedStringArray: + var base_directory := "res://" + # If the test root folder is configured as blank, "/", or "res://", use the root folder as described in the settings panel + if root.is_empty() or root == "/" or root == base_directory: + return [base_directory] + return scan_test_directories(base_directory, root, []) + + +static func scan_test_directories(base_directory: String, test_directory: String, test_suite_paths: PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) + for directory in DirAccess.get_directories_at(base_directory): + if directory.begins_with("."): + continue + var current_directory := normalize_path(base_directory + "/" + directory) + if FileAccess.file_exists(current_directory + "/.gdignore"): + continue + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + @warning_ignore("return_value_discarded") + test_suite_paths.append(current_directory) + else: + @warning_ignore("return_value_discarded") + scan_test_directories(current_directory, test_directory, test_suite_paths) + return test_suite_paths + + +static func normalize_path(path: String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory: String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid index e69de29b..acb1b090 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid @@ -0,0 +1 @@ +uid://uakc3vyaaagr diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd index e69de29b..6bec1059 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -0,0 +1,206 @@ +class_name GdUnitEvent +extends Resource + +const WARNINGS = "warnings" +const FAILED = "failed" +const FLAKY = "flaky" +const ERRORS = "errors" +const SKIPPED = "skipped" +const ELAPSED_TIME = "elapsed_time" +const ORPHAN_NODES = "orphan_nodes" +const ERROR_COUNT = "error_count" +const FAILED_COUNT = "failed_count" +const SKIPPED_COUNT = "skipped_count" +const RETRY_COUNT = "retry_count" + +enum { + INIT, + STOP, + TESTSUITE_BEFORE, + TESTSUITE_AFTER, + TESTCASE_BEFORE, + TESTCASE_AFTER, + DISCOVER_START, + DISCOVER_END, + SESSION_START, + SESSION_CLOSE +} + +var _event_type: int +var _guid: GdUnitGUID +var _resource_path: String +var _suite_name: String +var _test_name: String +var _total_count: int = 0 +var _statistics := Dictionary() +var _reports: Array[GdUnitReport] = [] + + +func suite_before(p_resource_path: String, p_suite_name: String, p_total_count: int) -> GdUnitEvent: + _guid = GdUnitGUID.new() + _event_type = TESTSUITE_BEFORE + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "before" + _total_count = p_total_count + return self + + +func suite_after(p_resource_path: String, p_suite_name: String, p_statistics: Dictionary = {}, p_reports: Array[GdUnitReport] = []) -> GdUnitEvent: + _guid = GdUnitGUID.new() + _event_type = TESTSUITE_AFTER + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "after" + _statistics = p_statistics + _reports = p_reports + return self + + +func test_before(p_guid: GdUnitGUID) -> GdUnitEvent: + _event_type = TESTCASE_BEFORE + _guid = p_guid + return self + + +func test_after(p_guid: GdUnitGUID, p_statistics: Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: + _event_type = TESTCASE_AFTER + _guid = p_guid + _statistics = p_statistics + _reports = p_reports + return self + + +func type() -> int: + return _event_type + + +func guid() -> GdUnitGUID: + return _guid + + +func suite_name() -> String: + return _suite_name + + +func test_name() -> String: + return _test_name + + +func elapsed_time() -> int: + return _statistics.get(ELAPSED_TIME, 0) + + +func orphan_nodes() -> int: + return _statistics.get(ORPHAN_NODES, 0) + + +func statistic(p_type :String) -> int: + return _statistics.get(p_type, 0) + + +func total_count() -> int: + return _total_count + + +func success_count() -> int: + return total_count() - error_count() - failed_count() - skipped_count() + + +func error_count() -> int: + return _statistics.get(ERROR_COUNT, 0) + + +func failed_count() -> int: + return _statistics.get(FAILED_COUNT, 0) + + +func skipped_count() -> int: + return _statistics.get(SKIPPED_COUNT, 0) + + +func retry_count() -> int: + return _statistics.get(RETRY_COUNT, 0) + + +func resource_path() -> String: + return _resource_path + + +func is_success() -> bool: + return not is_failed() and not is_error() + + +func is_warning() -> bool: + return _statistics.get(WARNINGS, false) + + +func is_failed() -> bool: + return _statistics.get(FAILED, false) + + +func is_error() -> bool: + return _statistics.get(ERRORS, false) + + +func is_flaky() -> bool: + return _statistics.get(FLAKY, false) + + +func is_skipped() -> bool: + return _statistics.get(SKIPPED, false) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +func _to_string() -> String: + return "Event: %s id:%s %s:%s, %s, %s" % [_event_type, _guid, _suite_name, _test_name, _statistics, _reports] + + +func serialize() -> Dictionary: + var serialized := { + "type" : _event_type, + "resource_path": _resource_path, + "suite_name" : _suite_name, + "test_name" : _test_name, + "total_count" : _total_count, + "statistics" : _statistics + } + if _guid != null: + serialized["guid"] = _guid._guid + serialized["reports"] = _serialize_TestReports() + return serialized + + +func deserialize(serialized: Dictionary) -> GdUnitEvent: + _event_type = serialized.get("type", null) + _guid = GdUnitGUID.new(str(serialized.get("guid", ""))) + _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()) + if serialized.has("reports"): + # needs this workaround to copy typed values in the array + var reports_to_deserializ :Array[Dictionary] = [] + @warning_ignore("unsafe_cast") + reports_to_deserializ.append_array(serialized.get("reports") as Array) + _reports = _deserialize_reports(reports_to_deserializ) + return self + + +func _serialize_TestReports() -> Array[Dictionary]: + var serialized_reports :Array[Dictionary] = [] + for report in _reports: + serialized_reports.append(report.serialize()) + return serialized_reports + + +func _deserialize_reports(p_reports: Array[Dictionary]) -> Array[GdUnitReport]: + var deserialized_reports :Array[GdUnitReport] = [] + for report in p_reports: + var test_report := GdUnitReport.new().deserialize(report) + deserialized_reports.append(test_report) + return deserialized_reports diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid index e69de29b..8a259172 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid @@ -0,0 +1 @@ +uid://c4wkq83n4a4bk diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd index e69de29b..774e7d4f 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd @@ -0,0 +1,6 @@ +class_name GdUnitInit +extends GdUnitEvent + + +func _init() -> void: + _event_type = INIT diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid index e69de29b..a7e1498a 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid @@ -0,0 +1 @@ +uid://c8t36rmkcsvqm diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd index e69de29b..d7a3c11c 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd @@ -0,0 +1,6 @@ +class_name GdUnitStop +extends GdUnitEvent + + +func _init() -> void: + _event_type = STOP diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid index e69de29b..fa3d212a 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid @@ -0,0 +1 @@ +uid://cg768i3qgef2x diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd index e69de29b..c6194ef3 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd @@ -0,0 +1,19 @@ +class_name GdUnitEventTestDiscoverEnd +extends GdUnitEvent + + +var _total_testsuites: int + + +func _init(testsuite_count: int, test_count: int) -> void: + _event_type = DISCOVER_END + _total_testsuites = testsuite_count + _total_count = test_count + + +func total_test_suites() -> int: + return _total_testsuites + + +func total_tests() -> int: + return _total_count diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid index e69de29b..bb01a6b2 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid @@ -0,0 +1 @@ +uid://bt4blgp4lw3p0 diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd index e69de29b..c7dd36f7 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd @@ -0,0 +1,6 @@ +class_name GdUnitEventTestDiscoverStart +extends GdUnitEvent + + +func _init() -> void: + _event_type = DISCOVER_START diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid index e69de29b..06bc4665 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid @@ -0,0 +1 @@ +uid://npuh47e34ud2 diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd index e69de29b..52dab3ff 100644 --- a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionClose +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_CLOSE diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid index e69de29b..c9022705 100644 --- a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid @@ -0,0 +1 @@ +uid://eqiw85rg4fgn diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd index e69de29b..420ad538 100644 --- a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionStart +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_START diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid index e69de29b..bd07390a 100644 --- a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid @@ -0,0 +1 @@ +uid://hpagtimkbhev diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd index e69de29b..457fd678 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -0,0 +1,269 @@ +## The execution context +## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor +class_name GdUnitExecutionContext + +enum GC_ORPHANS_CHECK { + NONE, + SUITE_HOOK_AFTER, + TEST_HOOK_AFTER, + TEST_CASE +} + + +var _parent_context: GdUnitExecutionContext +var _sub_context: Array[GdUnitExecutionContext] = [] +var _orphan_monitor: GdUnitOrphanNodesMonitor +var _memory_observer: GdUnitMemoryObserver +var _report_collector: GdUnitTestReportCollector +var _timer: LocalTime +var _test_case_name: StringName +var _test_case_parameter_set: Array +var _name: String +var _test_execution_iteration: int = 0 +var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled() +var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries() +var _orphans := -1 + + +var error_monitor: GodotGdErrorMonitor = null: + get: + if _parent_context != null: + return _parent_context.error_monitor + if error_monitor == null: + error_monitor = GodotGdErrorMonitor.new() + return error_monitor + + +var test_suite: GdUnitTestSuite = null: + get: + if _parent_context != null: + return _parent_context.test_suite + return test_suite + + +var test_case: _TestCase = null: + get: + if test_case == null and _parent_context != null: + return _parent_context.test_case + return test_case + + +func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void: + _name = name + _parent_context = parent_context + _timer = LocalTime.now() + _orphan_monitor = GdUnitOrphanNodesMonitor.new(name) + _orphan_monitor.start() + _memory_observer = GdUnitMemoryObserver.new() + _report_collector = GdUnitTestReportCollector.new() + if parent_context != null: + parent_context._sub_context.append(self) + + +func dispose() -> void: + _timer = null + _orphan_monitor = null + _report_collector = null + _memory_observer = null + _parent_context = null + test_suite = null + test_case = null + dispose_sub_contexts() + + +func dispose_sub_contexts() -> void: + for context in _sub_context: + context.dispose() + _sub_context.clear() + + +static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context._test_execution_iteration = pe._test_execution_iteration + return context + + +static func of_test_suite(p_test_suite: GdUnitTestSuite) -> GdUnitExecutionContext: + assert(p_test_suite, "test_suite is null") + var context := GdUnitExecutionContext.new(p_test_suite.get_name()) + context.test_suite = p_test_suite + return context + + +static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext: + assert(p_test_case, "test_case is null") + var context := GdUnitExecutionContext.new(p_test_case.get_name(), pe) + context.test_case = p_test_case + return context + + +static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(test_case_name, pe) + context._test_case_name = test_case_name + context._test_case_parameter_set = test_case_parameter_set + return context + + +func get_test_suite_path() -> String: + return test_suite.get_script().resource_path + + +func get_test_suite_name() -> StringName: + return test_suite.get_name() + + +func get_test_case_name() -> StringName: + if _test_case_name.is_empty(): + return test_case._test_case.display_name + return _test_case_name + + +func error_monitor_start() -> void: + error_monitor.start() + + +func error_monitor_stop() -> void: + await error_monitor.scan() + for error_report in error_monitor.to_reports(): + if error_report.is_error(): + _report_collector.push_back(error_report) + + +func orphan_monitor_start() -> void: + _orphan_monitor.start() + + +func orphan_monitor_stop() -> void: + _orphan_monitor.stop() + + +func add_report(report: GdUnitReport) -> GdUnitReport: + _report_collector.push_back(report) + return report + + +func reports() -> Array[GdUnitReport]: + return _report_collector.reports() + + +func collect_reports(recursive: bool) -> Array[GdUnitReport]: + if not recursive: + return reports() + + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + # we strictly need to copy the reports before adding sub context reports to avoid manipulation of the current context + var current_reports := reports().duplicate() + for sub_context in _sub_context: + current_reports.append_array(sub_context.collect_reports(true)) + + return current_reports + + +func calculate_statistics(reports_: Array[GdUnitReport]) -> Dictionary: + var failed_count := GdUnitTestReportCollector.count_failures(reports_) + var error_count := GdUnitTestReportCollector.count_errors(reports_) + var warn_count := GdUnitTestReportCollector.count_warnings(reports_) + var skip_count := GdUnitTestReportCollector.count_skipped(reports_) + var is_failed := !is_success() + var orphan_count := _count_orphans() + var elapsed_time := _timer.elapsed_since_ms() + var retries := 1 if _parent_context == null else _sub_context.size() + # Mark as flaky if it is successful, but errors were counted + var is_flaky := retries > 1 and not is_failed + # In the case of a flakiness test, we do not report an error counter, as an unreliable test is considered successful + # after a certain number of repetitions. + if is_flaky: + failed_count = 0 + + return { + GdUnitEvent.RETRY_COUNT: retries, + GdUnitEvent.ELAPSED_TIME: elapsed_time, + GdUnitEvent.FAILED: is_failed, + GdUnitEvent.ERRORS: error_count > 0, + GdUnitEvent.WARNINGS: warn_count > 0, + GdUnitEvent.FLAKY: is_flaky, + GdUnitEvent.SKIPPED: skip_count > 0, + GdUnitEvent.FAILED_COUNT: failed_count, + GdUnitEvent.ERROR_COUNT: error_count, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.ORPHAN_NODES: orphan_count, + } + + +func is_success() -> bool: + if _sub_context.is_empty(): + return not _report_collector.has_failures() + # we on test suite level? + if _parent_context == null: + return not _report_collector.has_failures() + + return _sub_context[-1].is_success() and not _report_collector.has_failures() + + +func is_skipped() -> bool: + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c.is_skipped()) + or test_case.is_skipped() if test_case != null else false + ) + + +func is_interupted() -> bool: + return false if test_case == null else test_case.is_interupted() + + +func _count_orphans() -> int: + if _orphans != -1: + return _orphans + + var orphans := 0 + for c in _sub_context: + if _orphan_monitor.orphan_nodes() != c._orphan_monitor.orphan_nodes(): + orphans += c._count_orphans() + + _orphans = _orphan_monitor.orphan_nodes() + if _orphan_monitor.orphan_nodes() != orphans: + _orphans -= orphans + + return _orphans + + +func sum(accum: int, number: int) -> int: + return accum + number + + +func retry_execution() -> bool: + var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries + if retry: + _test_execution_iteration += 1 + return retry + + +func register_auto_free(obj: Variant) -> Variant: + return _memory_observer.register_auto_free(obj) + + +## Runs the gdunit garbage collector to free registered object and handle orphan node reporting +func gc(gc_orphan_check: GC_ORPHANS_CHECK = GC_ORPHANS_CHECK.NONE) -> void: + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().clear_assert() + await _memory_observer.gc() + orphan_monitor_stop() + + var orphans := _count_orphans() + match(gc_orphan_check): + GC_ORPHANS_CHECK.SUITE_HOOK_AFTER: + if orphans > 0: + reports().push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) + + GC_ORPHANS_CHECK.TEST_HOOK_AFTER: + if orphans > 0: + reports().push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_test_setup(orphans))) + + GC_ORPHANS_CHECK.TEST_CASE: + if orphans > 0: + reports().push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid index e69de29b..87b63acc 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid @@ -0,0 +1 @@ +uid://dm5otinunwsc1 diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd index e69de29b..dd03a313 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -0,0 +1,135 @@ +## The memory watcher for objects that have been registered and are released when 'gc' is called. +class_name GdUnitMemoryObserver +extends RefCounted + +const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_" +const TAG_AUTO_FREE = "GdUnit4_marked_auto_free" +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _store :Array[Variant] = [] +# enable for debugging purposes +var _is_stdout_verbose := false +const _show_debug := false + + +## Registration of an instance to be released when an execution phase is completed +func register_auto_free(obj :Variant) -> Variant: + if not is_instance_valid(obj): + return obj + # do not register on GDScriptNativeClass + @warning_ignore("unsafe_cast") + if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : + return obj + #if obj is GDScript or obj is ScriptExtension: + # return obj + if obj is MainLoop: + push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj) + return + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():register auto_free(%s)" % obj) + # only register pure objects + if obj is GdUnitSceneRunner: + _store.push_back(obj) + else: + _store.append(obj) + _tag_object(obj) + return obj + + +# to disable instance guard when run into issues. +static func _is_instance_guard_enabled() -> bool: + return false + + +static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: + if not _show_debug: + return + var script :GDScript= obj if obj is GDScript else obj.get_script() + if script: + var base_script :GDScript = script.get_base_script() + @warning_ignore("unsafe_method_access") + prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) + if base_script: + debug_observe("+", base_script, indent+1) + else: + @warning_ignore("unsafe_method_access") + prints(name, obj, obj.get_class(), obj.get_name()) + + +static func guard_instance(obj :Object) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if Engine.has_meta(tag): + return + debug_observe("Gard on instance", obj) + Engine.set_meta(tag, obj) + + +static func unguard_instance(obj :Object, verbose := true) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if verbose: + debug_observe("unguard instance", obj) + if Engine.has_meta(tag): + Engine.remove_meta(tag) + + +static func gc_guarded_instance(name :String, instance :Object) -> void: + if not _is_instance_guard_enabled(): + return + await (Engine.get_main_loop() as SceneTree).process_frame + unguard_instance(instance, false) + if is_instance_valid(instance) and instance is RefCounted: + # finally do this very hacky stuff + # we need to manually unreferece to avoid leaked scripts + # but still leaked GDScriptFunctionState exists + #var script :GDScript = instance.get_script() + #if script: + # var base_script :GDScript = script.get_base_script() + # if base_script: + # base_script.unreference() + debug_observe(name, instance) + (instance as RefCounted).unreference() + await (Engine.get_main_loop() as SceneTree).process_frame + + +static func gc_on_guarded_instances() -> void: + if not _is_instance_guard_enabled(): + return + for tag in Engine.get_meta_list(): + if tag.begins_with(TAG_OBSERVE_INSTANCE): + var instance :Object = Engine.get_meta(tag) + await gc_guarded_instance("Leaked instance detected:", instance) + await GdUnitTools.free_instance(instance, false) + + +# store the object into global store aswell to be verified by 'is_marked_auto_free' +func _tag_object(obj :Variant) -> void: + var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, []) + tagged_object.append(obj) + Engine.set_meta(TAG_AUTO_FREE, tagged_object) + + +## Runs over all registered objects and releases them +func gc() -> void: + if _store.is_empty(): + return + # give engine time to free objects to process objects marked by queue_free() + await (Engine.get_main_loop() as SceneTree).process_frame + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size()) + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) + while not _store.is_empty(): + var value :Variant = _store.pop_front() + tagged_objects.erase(value) + await GdUnitTools.free_instance(value, _is_stdout_verbose) + assert(_store.is_empty(), "The memory observer has still entries in the store!") + + +## Checks whether the specified object is registered for automatic release +static func is_marked_auto_free(obj: Variant) -> bool: + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) + return tagged_objects.has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid index e69de29b..a6692bbb 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid @@ -0,0 +1 @@ +uid://ibpnqu61f7yw diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd index e69de29b..5f42d148 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -0,0 +1,62 @@ +# Collects all reports seperated as warnings, failures and errors +class_name GdUnitTestReportCollector +extends RefCounted + + +var _reports :Array[GdUnitReport] = [] + + +static func __filter_is_error(report :GdUnitReport) -> bool: + return report.is_error() + + +static func __filter_is_failure(report :GdUnitReport) -> bool: + return report.is_failure() + + +static func __filter_is_warning(report :GdUnitReport) -> bool: + return report.is_warning() + + +static func __filter_is_skipped(report :GdUnitReport) -> bool: + return report.is_skipped() + + +static func count_failures(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_failure).size() + + +static func count_errors(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_error).size() + + +static func count_warnings(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_warning).size() + + +static func count_skipped(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_skipped).size() + + +func has_failures() -> bool: + return _reports.any(__filter_is_failure) + + +func has_errors() -> bool: + return _reports.any(__filter_is_error) + + +func has_warnings() -> bool: + return _reports.any(__filter_is_warning) + + +func has_skipped() -> bool: + return _reports.any(__filter_is_skipped) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +func push_back(report :GdUnitReport) -> void: + _reports.push_back(report) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid index e69de29b..771e5b20 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid @@ -0,0 +1 @@ +uid://cl13ejhh26vv7 diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd index e69de29b..e3fd510c 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -0,0 +1,48 @@ +## The executor to run a test-suite +class_name GdUnitTestSuiteExecutor + + +# preload all asserts here +@warning_ignore("unused_private_class_variable") +var _assertions := GdUnitAssertions.new() +var _executeStage := GdUnitTestSuiteExecutionStage.new() +var _debug_mode : bool + +func _init(debug_mode :bool = false) -> void: + _executeStage.set_debug_mode(debug_mode) + _debug_mode = debug_mode + + +func execute(test_suite :GdUnitTestSuite) -> void: + var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans() + if not orphan_detection_enabled: + prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") + + (Engine.get_main_loop() as SceneTree).root.call_deferred("add_child", test_suite) + await (Engine.get_main_loop() as SceneTree).process_frame + await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) + + +func run_and_wait(tests: Array[GdUnitTestCase]) -> void: + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitInit.new()) + # first we group all tests by resource path + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.suite_resource_path + ) + var scanner := GdUnitTestSuiteScanner.new() + for suite_path: String in grouped_by_suites.keys(): + @warning_ignore("unsafe_call_argument") + var suite_tests: Array[GdUnitTestCase] = Array(grouped_by_suites[suite_path], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(suite_path) + if script.get_class() == "GDScript": + var test_suite := scanner.load_suite(script as GDScript, suite_tests) + await execute(test_suite) + else: + await GdUnit4CSharpApiLoader.execute(suite_tests) + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitStop.new()) + + +func fail_fast(enabled :bool) -> void: + _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid index e69de29b..38263f2b 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid @@ -0,0 +1 @@ +uid://hl8otc6pepsh diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd index e69de29b..d3c62455 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -0,0 +1,22 @@ +## The test case shutdown hook implementation.[br] +## It executes the 'test_after()' block from the test-suite. +class_name GdUnitTestCaseAfterStage +extends IGdUnitExecutionStage + + +var _call_stage: bool + + +func _init(call_stage := true) -> void: + _call_stage = call_stage + + +func _execute(context: GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.after_test() + + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + await context.error_monitor_stop() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid index e69de29b..2707e05a 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid @@ -0,0 +1 @@ +uid://ddknkun7aw51d diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd index e69de29b..4e04fad2 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -0,0 +1,19 @@ +## The test case startup hook implementation.[br] +## It executes the 'test_before()' block from the test-suite. +class_name GdUnitTestCaseBeforeStage +extends IGdUnitExecutionStage + +var _call_stage :bool + + +func _init(call_stage := true) -> void: + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.before_test() + context.error_monitor_start() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid index e69de29b..864e7b8a 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid @@ -0,0 +1 @@ +uid://c8gq3sb8q6xih diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd index e69de29b..12cc6fde 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -0,0 +1,37 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseExecutionStage +extends IGdUnitExecutionStage + + +var _stage_single_test: IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test: IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() + + +## Executes the test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_before() [br] +## -> test_case() [br] +## -> test_after() [br] +@warning_ignore("redundant_await") +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + + context.error_monitor_start() + + if test_case.is_fuzzed(): + await _stage_fuzzer_test.execute(context) + else: + await _stage_single_test.execute(context) + + await context.gc() + await context.error_monitor_stop() + + # finally free the test instance + if is_instance_valid(context.test_case): + context.test_case.dispose() + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_single_test.set_debug_mode(debug_mode) + _stage_fuzzer_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid index e69de29b..8079e2fd 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://brfrhige0dbmm diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd index e69de29b..03bbd0f7 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -0,0 +1,29 @@ +## The test suite shutdown hook implementation.[br] +## It executes the 'after()' block from the test-suite. +class_name GdUnitTestSuiteAfterStage +extends IGdUnitExecutionStage + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + @warning_ignore("redundant_await") + await test_suite.after() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + + var reports := context.collect_reports(false) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new()\ + .suite_after(context.get_test_suite_path(),\ + test_suite.get_name(), + statistics, + reports)) + GdUnitFileAccess.clear_tmp() + # Guard that checks if all doubled (spy/mock) objects are released + await GdUnitClassDoubler.check_leaked_instances() + # we hide the scene/main window after runner is finished + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid index e69de29b..0695b809 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid @@ -0,0 +1 @@ +uid://vs73mmj8rsbs diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd index e69de29b..e9fa7186 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -0,0 +1,14 @@ +## The test suite startup hook implementation.[br] +## It executes the 'before()' block from the test-suite. +class_name GdUnitTestSuiteBeforeStage +extends IGdUnitExecutionStage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + fire_event(GdUnitEvent.new()\ + .suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count())) + + @warning_ignore("redundant_await") + await test_suite.before() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid index e69de29b..411d0c76 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid @@ -0,0 +1 @@ +uid://ce78xguk84kwb diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd index e69de29b..ac921e07 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -0,0 +1,147 @@ +## The test suite main execution stage.[br] +class_name GdUnitTestSuiteExecutionStage +extends IGdUnitExecutionStage + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new() +var _fail_fast := false + + +## Executes all tests of an test suite.[br] +## It executes synchronized following stages[br] +## -> before() [br] +## -> run all test cases [br] +## -> after() [br] +func _execute(context :GdUnitExecutionContext) -> void: + if context.test_suite.__is_skipped: + await fire_test_suite_skipped(context) + else: + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) + await _stage_before.execute(context) + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + context.test_suite.set_active_test_case(test_case.test_name()) + await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case)) + # stop on first error or if fail fast is enabled + if _fail_fast and not context.is_success(): + break + if test_case.is_interupted(): + # it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out + # we delete the current test suite where is execute the current test case to kill the function state + # and replace it by a clone without function state + context.test_suite = await clone_test_suite(context.test_suite) + await _stage_after.execute(context) + GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter) + await (Engine.get_main_loop() as SceneTree).process_frame + context.test_suite.free() + context.dispose() + + +# clones a test suite and moves the test cases to new instance +func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: + await (Engine.get_main_loop() as SceneTree).process_frame + dispose_timers(test_suite) + await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) + var parent := test_suite.get_parent() + var _test_suite := GdUnitTestSuite.new() + parent.remove_child(test_suite) + copy_properties(test_suite, _test_suite) + for child in test_suite.get_children(): + test_suite.remove_child(child) + _test_suite.add_child(child) + parent.add_child(_test_suite) + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) + # finally free current test suite instance + test_suite.free() + await (Engine.get_main_loop() as SceneTree).process_frame + return _test_suite + + +func dispose_timers(test_suite :GdUnitTestSuite) -> void: + GdUnitTools.release_timers() + for child in test_suite.get_children(): + if child is Timer: + (child as Timer).stop() + test_suite.remove_child(child) + child.free() + + +func copy_properties(source :Object, target :Object) -> void: + if not source is _TestCase and not source is GdUnitTestSuite: + return + for property in source.get_property_list(): + var property_name :String = property["name"] + if property_name == "__awaiter": + continue + target.set(property_name, source.get(property_name)) + + +func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var skip_count := test_suite.get_child_count() + fire_event(GdUnitEvent.new()\ + .suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count)) + + + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case) + fire_event(GdUnitEvent.new().test_before(test_case.id())) + # use skip count 0 because we counted it over the complete test suite + fire_test_skipped(test_case_context, 0) + + + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.SKIPPED: true + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) + fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report])) + await (Engine.get_main_loop() as SceneTree).process_frame + + +func fire_test_skipped(context: GdUnitExecutionContext, skip_count := 1) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: skip_count, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite")) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fail_fast(enabled :bool) -> void: + _fail_fast = enabled diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid index e69de29b..98878317 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://bfbyfr8ocwivm diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd index e69de29b..39de3809 100644 --- a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -0,0 +1,39 @@ +## The interface of execution stage.[br] +## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br] +## Execution stage are always called synchronously. +class_name IGdUnitExecutionStage +extends RefCounted + +var _debug_mode := false + + +## Executes synchronized the implemented stage in its own execution context.[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await MyExecutionStage.new().execute() +## [/codeblock][br] +func execute(context :GdUnitExecutionContext) -> void: + GdUnitThreadManager.get_current_context().set_execution_context(context) + @warning_ignore("redundant_await") + await _execute(context) + + +## Sends the event to registered listeners +func fire_event(event :GdUnitEvent) -> void: + if _debug_mode: + GdUnitSignals.instance().gdunit_event_debug.emit(event) + else: + GdUnitSignals.instance().gdunit_event.emit(event) + + +## Internal testing stuff.[br] +## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug` +func set_debug_mode(debug_mode :bool) -> void: + _debug_mode = debug_mode + + +## The execution phase to be carried out. +func _execute(_context :GdUnitExecutionContext) -> void: + @warning_ignore("assert_always_false") + assert(false, "The execution stage is not implemented") diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid index e69de29b..0e92e3ee 100644 --- a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://blqjb8vicbune diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd index e69de29b..73a7a66e 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -0,0 +1,52 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseFuzzedExecutionStage +extends IGdUnitExecutionStage + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) + + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports)) + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid index e69de29b..533c600b 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://bur2on601qwvw diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd index e69de29b..62dda37e 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -0,0 +1,55 @@ +## The fuzzed test case execution stage.[br] +class_name GdUnitTestCaseFuzzedTestStage +extends IGdUnitExecutionStage + +var _expression_runner := GdUnitExpressionRunner.new() + + +## Executes a test case with given fuzzers 'test_()' iterative.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case := context.test_case + var fuzzers := create_fuzzers(test_suite, test_case) + + # guard on fuzzers + for fuzzer in fuzzers: + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(fuzzer) + + for iteration in test_case.iterations(): + @warning_ignore("redundant_await") + await test_suite.before_test() + await test_case.execute(fuzzers, iteration) + @warning_ignore("redundant_await") + await test_suite.after_test() + if test_case.is_interupted(): + break + # interrupt at first failure + var reports := context.reports() + if not reports.is_empty(): + var report :GdUnitReport = reports.pop_front() + reports.append(GdUnitReport.new() \ + .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) + break + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + + # unguard on fuzzers + if not test_case.is_interupted(): + for fuzzer in fuzzers: + GdUnitMemoryObserver.unguard_instance(fuzzer) + + +func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: + if not test_case.is_fuzzed(): + return Array() + test_case.generate_seed() + var fuzzers :Array[Fuzzer] = [] + for fuzzer_arg in test_case.fuzzer_arguments(): + @warning_ignore("unsafe_cast") + var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String) + fuzzer._iteration_index = 0 + fuzzer._iteration_limit = test_case.iterations() + fuzzers.append(fuzzer) + return fuzzers diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid index e69de29b..83ca05ec 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid @@ -0,0 +1 @@ +uid://dky6221ssl6re diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd index e69de29b..70d687f4 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -0,0 +1,53 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseSingleExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not test_context.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports)) + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid index e69de29b..547f856a 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://ckbcmvbm3bee8 diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd index e69de29b..9006b368 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -0,0 +1,11 @@ +## The single test case execution stage.[br] +class_name GdUnitTestCaseSingleTestStage +extends IGdUnitExecutionStage + + +## Executes a single test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + await context.test_case.execute() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid index e69de29b..16791711 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid @@ -0,0 +1 @@ +uid://cq4bgmdjjl77j diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd index e69de29b..0f87ad1b 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd @@ -0,0 +1,78 @@ +class_name GdUnitBaseReporterTestSessionHook +extends GdUnitTestSessionHook + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(_on_test_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(_on_test_event) + + +var _report_summary: GdUnitReportSummary +var _reporter: GdUnitTestReporter +var _report_writer: GdUnitReportWriter +var _report_converter: Callable + +func _init(report_writer: GdUnitReportWriter, hook_name: String, hook_description: String, report_converter: Callable) -> void: + super(hook_name, hook_description) + _reporter = GdUnitTestReporter.new() + _report_writer = report_writer + _report_converter = report_converter + + +func startup(session: GdUnitTestSession) -> GdUnitResult: + test_session = session + _report_summary = GdUnitReportSummary.new(_report_converter) + _reporter.init_summary() + + return GdUnitResult.success() + + +func shutdown(session: GdUnitTestSession) -> GdUnitResult: + var report_path := _report_writer.write(session.report_path, _report_summary) + session.send_message("Open {0} Report at: file://{1}".format([_report_writer.output_format(), report_path])) + + return GdUnitResult.success() + + +func _on_test_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + _report_summary.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) + GdUnitEvent.TESTSUITE_AFTER: + var statistics := _reporter.build_test_suite_statisitcs(event) + _report_summary.update_testsuite_counters( + event.resource_path(), + _reporter.error_count(statistics), + _reporter.failed_count(statistics), + _reporter.orphan_nodes(statistics), + _reporter.skipped_count(statistics), + _reporter.flaky_count(statistics), + event.elapsed_time()) + _report_summary.add_testsuite_reports( + event.resource_path(), + event.reports() + ) + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _report_summary.add_testcase(test.source_file, test.suite_name, test.display_name) + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + var test := test_session.find_test_by_id(event.guid()) + _report_summary.set_counters(test.source_file, + test.display_name, + event.error_count(), + event.failed_count(), + event.orphan_nodes(), + event.is_skipped(), + event.is_flaky(), + event.elapsed_time()) + _report_summary.add_reports(test.source_file, test.display_name, event.reports()) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid index e69de29b..8c973a68 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://carpav0doacrx diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd index e69de29b..4b8f390f 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd @@ -0,0 +1,9 @@ +class_name GdUnitHtmlReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _init() -> void: + super(GdUnitHtmlReportWriter.new(), "GdUnitHtmlTestReporter", "The Html test reporting hook.", GdUnitTools.richtext_normalize) + set_meta("SYSTEM_HOOK", true) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid index e69de29b..19a15878 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://1etm8aqdrkqm diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd index e69de29b..23850e4a 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd @@ -0,0 +1,111 @@ +## @since GdUnit4 5.1.0 +## +## Base class for creating custom test session hooks in GdUnit4.[br] +## [br] +## [i]Test session hooks allow users to extend the GdUnit4 test framework by providing +## custom functionality that runs at specific points during the test execution lifecycle. +## This base class defines the interface that all test session hooks must implement.[/i] +## [br] +## [br] +## [b][u]Usage[/u][/b][br] +## 1. Create a new class that extends GdUnitTestSessionHook[br] +## 2. Override the required methods (startup, shutdown)[br] +## 3. Register your hook with the test engine (using the GdUnit4 settings dialog)[br] +## [br] +## [b][u]Example[/u][/b] +## [codeblock] +## class_name MyCustomTestHook +## extends GdUnitTestSessionHook +## +## func _init(): +## super("MyHook", "This is a description") +## +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook initialized") +## # Initialize resources, setup test environment, etc. +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook cleanup completed") +## # Cleanup resources, generate reports, etc. +## return GdUnitResult.success() +## [/codeblock] +## +## [b][u]Hook Lifecycle[/u][/b][br] +## 1. [i][b]Registration[/b][/i]: Hooks are registered with the test engine via settings dialog[br] +## 2. [i][b]Priority Sorting[/b][/i]: Hooks are sorted by priority[br] +## 3. [i][b]Startup[/b][/i]: startup() is called before test execution begins, if it returns an error is shown in the console[br] +## 4. [i][b]Test Execution[/b][/i]: Tests run normally (only if all hooks started successfully)[br] +## 5. [i][b]Shutdown[/b][/i]: shutdown() is called after all tests complete, regardless of startup success[br] +## [br] +## [b][u]Priority System[/u][/b][br] +## The priority system allows controlling the execution order of multiple hooks.[br] +## - The order can be changed in the GdUnit4 settings dialog.[br] +## - The priority of system hooks cannot be changed and they cannot be deleted.[br] +## [br] +## [b][u]Session Access[/u][/b][br] +## +## Both [i]startup()[/i] and [i]shutdown()[/i] methods receive a [GdUnitTestSession] parameter that provides:[br] +## - Access to test cases being executed[br] +## - Event emission capabilities for test progress tracking[br] +## - Message sending functionality for logging and communication[br] +class_name GdUnitTestSessionHook +extends RefCounted + + +## The display name of this hook. +var name: String: + get: + return name + + +## A detailed description of what this hook does. +var description: String: + get: + return description + + +## Initializes a new test session hook. +## +## [param _name] The display name for this hook +## [param _description] A detailed description of the hook's functionality +func _init(_name: String, _description: String) -> void: + self.name = _name + self.description = _description + + +## Called when the test session starts up, before any tests are executed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom initialization logic[/i][/color][br] +## [br] +## such as:[br] +## - Setting up test databases or external services[br] +## - Initializing mock objects or test fixtures[br] +## - Configuring logging or reporting systems[br] +## - Preparing the test environment[br] +## - Subscribing to test events via the session[br] +## [br] +## [param session] The test session instance providing access to test data and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if initialization succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if initialization fails. +func startup(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:startup is not implemented" % get_script().resource_path) + + +## Called when the test session shuts down, after all tests have completed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom cleanup logic[/i][/color][br] +## [br] +## such as:[br] +## - Cleaning up test databases or external services[br] +## - Generating test reports or artifacts[br] +## - Releasing resources allocated during startup[br] +## - Performing final validation or assertions[br] +## - Processing collected test events and data[br] +## [br] +## [param session] The test session instance providing access to test results and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if cleanup succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if cleanup fails. Cleanup errors are typically logged +## but don't prevent the test engine from shutting down. +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:shutdown is not implemented" % get_script().resource_path) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid index e69de29b..e149ee8c 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://bc2ru0ffg38cx diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd index e69de29b..5a9bdcb8 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd @@ -0,0 +1,191 @@ +class_name GdUnitTestSessionHookService +extends Object + + +var enigne_hooks: Array[GdUnitTestSessionHook] = []: + get: + return enigne_hooks + set(value): + enigne_hooks.append(value) + + +var _save_settings: bool = false + + +static func instance() -> GdUnitTestSessionHookService: + return GdUnitSingleton.instance("GdUnitTestSessionHookService", func()->GdUnitTestSessionHookService: + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session system hooks.") + var service := GdUnitTestSessionHookService.new() + # Register default system hooks here + service._save_settings = false + service.register(GdUnitHtmlReporterTestSessionHook.new()) + service.register(GdUnitXMLReporterTestSessionHook.new()) + service.load_hook_settings() + service._save_settings = true + return service + ) + + +static func contains_hook(current: GdUnitTestSessionHook, other: GdUnitTestSessionHook) -> bool: + return current.get_script().resource_path == other.get_script().resource_path + + +func find_custom(hook: GdUnitTestSessionHook) -> int: + for index in enigne_hooks.size(): + if contains_hook.call(enigne_hooks[index], hook): + return index + return -1 + + +func load_hook(hook_resourc_path: String) -> GdUnitResult: + if !FileAccess.file_exists(hook_resourc_path): + return GdUnitResult.error("The hook '%s' not exists." % hook_resourc_path) + var script: GDScript = load(hook_resourc_path) + if script.get_base_script() != GdUnitTestSessionHook: + return GdUnitResult.error("The hook '%s' must inhertit from 'GdUnitTestSessionHook'." % hook_resourc_path) + + return GdUnitResult.success(script.new()) + + +func enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + _enable_hook(hook, enabled) + GdUnitSignals.instance().gdunit_message.emit("Session hook '{name}' {enabled}.".format({ + "name": hook.name, + "enabled": "enabled" if enabled else "disabled"}) + ) + save_hock_setttings() + + +func register(hook: GdUnitTestSessionHook, enabled: bool = true) -> GdUnitResult: + if find_custom(hook) != -1: + return GdUnitResult.error("A hook instance of '%s' is already registered." % hook.get_script().resource_path) + + _enable_hook(hook, enabled) + enigne_hooks.append(hook) + save_hock_setttings() + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' installed." % hook.name) + + return GdUnitResult.success() + + +func unregister(hook: GdUnitTestSessionHook) -> GdUnitResult: + var hook_index := find_custom(hook) + if hook_index == -1: + return GdUnitResult.error("The hook instance of '%s' is NOT registered." % hook.get_script().resource_path) + + enigne_hooks.remove_at(hook_index) + save_hock_setttings() + return GdUnitResult.success() + + +func move_before(hook: GdUnitTestSessionHook, before: GdUnitTestSessionHook) -> void: + var before_index := find_custom(before) + var hook_index := find_custom(hook) + + # Verify the hook to move is behind the hook to be moved + if before_index >= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(before_index, hook) + save_hock_setttings() + + +func move_after(hook: GdUnitTestSessionHook, after: GdUnitTestSessionHook) -> void: + var after_index := find_custom(after) + var hook_index := find_custom(hook) + + # Verify the hook to move is before the hook to be moved + if after_index <= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(after_index, hook) + save_hock_setttings() + + +func execute_startup(session: GdUnitTestSession) -> GdUnitResult: + return await execute("startup", session) + + +func execute_shutdown(session: GdUnitTestSession) -> GdUnitResult: + return await execute("shutdown", session, true) + + +func execute(hook_func: String, session: GdUnitTestSession, reverse := false) -> GdUnitResult: + var failed_hook_calls: Array[GdUnitResult] = [] + + for hook_index in enigne_hooks.size(): + var index := enigne_hooks.size()-hook_index-1 if reverse else hook_index + var hook: = enigne_hooks[index] + if not is_enabled(hook): + continue + if OS.is_stdout_verbose(): + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' > %s()" % [hook.name, hook_func]) + var result: GdUnitResult = await hook.call(hook_func, session) + if result == null: + failed_hook_calls.push_back(GdUnitResult.error("Result is null! Check '%s'" % hook.get_script().resource_path)) + elif result.is_error(): + failed_hook_calls.push_back(result) + + if failed_hook_calls.is_empty(): + return GdUnitResult.success() + + var errors := failed_hook_calls.map(func(result: GdUnitResult) -> String: + return "Hook call '%s' failed with error: '%s'" % [hook_func, result.error_message()] + ) + return GdUnitResult.error( "\n".join(errors)) + + +func save_hock_setttings() -> void: + if not _save_settings: + return + + var hooks_to_save: Dictionary[String, bool] = {} + for hook in enigne_hooks: + var enabled: bool = hook.get_meta("enabled") + hooks_to_save[hook.get_script().resource_path] = enabled + + GdUnitSettings.set_session_hooks(hooks_to_save) + + +func load_hook_settings() -> void: + var hooks_resource_paths := GdUnitSettings.get_session_hooks() + if hooks_resource_paths.is_empty(): + return + + for hock_path: String in hooks_resource_paths.keys(): + var enabled := hooks_resource_paths[hock_path] + + # Do not reinstall already installed hooks + var existing_hook: GdUnitTestSessionHook = enigne_hooks.filter(func(element: GdUnitTestSessionHook) -> bool: + return element.get_script().resource_path == hock_path + ).front() + # Applay enabled settings + if existing_hook != null: + _enable_hook(existing_hook, enabled) + continue + + # Load additional hooks + var result := load_hook(hock_path) + if result.is_error(): + push_error(result.error_message()) + continue + + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session hooks.") + var hook: GdUnitTestSessionHook = result.value() + + result = register(hook, enabled) + if result.is_error(): + push_error(result.error_message()) + continue + + +static func is_enabled(hook: GdUnitTestSessionHook) -> bool: + if hook.has_meta("enabled"): + return hook.get_meta("enabled") + return true + + +func _enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + hook.set_meta("enabled", enabled) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid index e69de29b..ca770f4f 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid @@ -0,0 +1 @@ +uid://b83ijyttsj34w diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd index e69de29b..94caef59 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd @@ -0,0 +1,11 @@ +class_name GdUnitXMLReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + + +func _init() -> void: + super(JUnitXmlReportWriter.new(), "GdUnitXMLTestReporter", "The JUnit XML test reporting hook.", convert_report_message) + set_meta("SYSTEM_HOOK", true) + + +func convert_report_message(value: String) -> String: + return value diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid index e69de29b..049277bc 100644 --- a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://cg7fh7nftc48e diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd index e69de29b..fc83742c 100644 --- a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd @@ -0,0 +1,25 @@ +class_name GdClassDescriptor +extends RefCounted + + +var _name :String +var _is_inner_class :bool +var _functions :Array[GdFunctionDescriptor] + + +func _init(p_name :String, p_is_inner_class :bool, p_functions :Array[GdFunctionDescriptor]) -> void: + _name = p_name + _is_inner_class = p_is_inner_class + _functions = p_functions + + +func name() -> String: + return _name + + +func is_inner_class() -> bool: + return _is_inner_class + + +func functions() -> Array[GdFunctionDescriptor]: + return _functions diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid index e69de29b..c35b2a6d 100644 --- a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid @@ -0,0 +1 @@ +uid://c1ipsxino6xxt diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd index e69de29b..f1b22440 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -0,0 +1,290 @@ +# holds all decodings for default values +class_name GdDefaultValueDecoder +extends GdUnitSingleton + + +@warning_ignore("unused_parameter") +var _decoders := { + TYPE_NIL: func(value :Variant) -> String: return "null", + TYPE_STRING: func(value :Variant) -> String: return '"%s"' % value, + TYPE_STRING_NAME: _on_type_StringName, + TYPE_BOOL: func(value :Variant) -> String: return str(value).to_lower(), + TYPE_FLOAT: func(value :Variant) -> String: return '%f' % value, + TYPE_COLOR: _on_type_Color, + TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY), + TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY), + TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY), + TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY), + TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY), + TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY), + TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY), + TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), + TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), + TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), + TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR4_ARRAY), + TYPE_DICTIONARY: _on_type_Dictionary, + TYPE_RID: _on_type_RID, + TYPE_NODE_PATH: _on_type_NodePath, + TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2), + TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I), + TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3), + TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I), + TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4), + TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I), + TYPE_RECT2: _on_type_Rect2, + TYPE_RECT2I: _on_type_Rect2i, + TYPE_PLANE: _on_type_Plane, + TYPE_QUATERNION: _on_type_Quaternion, + TYPE_AABB: _on_type_AABB, + TYPE_BASIS: _on_type_Basis, + TYPE_CALLABLE: _on_type_Callable, + TYPE_SIGNAL: _on_type_Signal, + TYPE_TRANSFORM2D: _on_type_Transform2D, + TYPE_TRANSFORM3D: _on_type_Transform3D, + TYPE_PROJECTION: _on_type_Projection, + TYPE_OBJECT: _on_type_Object +} + +static func _regex(pattern: String) -> RegEx: + var regex := RegEx.new() + var err := regex.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex + + +func get_decoder(type: int) -> Callable: + return _decoders.get(type, func(value :Variant) -> String: return '%s' % value) + + +func _on_type_StringName(value: StringName) -> String: + if value.is_empty(): + return 'StringName()' + return 'StringName("%s")' % value + + +func _on_type_Object(value: Variant, _type: int) -> String: + return str(value) + + +func _on_type_Color(color: Color) -> String: + if color == Color.BLACK: + return "Color()" + return "Color%s" % color + + +func _on_type_NodePath(path: NodePath) -> String: + if path.is_empty(): + return 'NodePath()' + return 'NodePath("%s")' % path + + +func _on_type_Callable(_cb: Callable) -> String: + return 'Callable()' + + +func _on_type_Signal(_s: Signal) -> String: + return 'Signal()' + + +func _on_type_Dictionary(dict: Dictionary) -> String: + if dict.is_empty(): + return '{}' + return str(dict) + + +func _on_type_Array(value: Variant, type: int) -> String: + match type: + TYPE_ARRAY: + return str(value) + + TYPE_PACKED_COLOR_ARRAY: + var colors := PackedStringArray() + for color: Color in value: + @warning_ignore("return_value_discarded") + colors.append(_on_type_Color(color)) + if colors.is_empty(): + return "PackedColorArray()" + return "PackedColorArray([%s])" % ", ".join(colors) + + TYPE_PACKED_VECTOR2_ARRAY: + var vectors := PackedStringArray() + for vector: Vector2 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) + if vectors.is_empty(): + return "PackedVector2Array()" + return "PackedVector2Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR3_ARRAY: + var vectors := PackedStringArray() + for vector: Vector3 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) + if vectors.is_empty(): + return "PackedVector3Array()" + return "PackedVector3Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR4_ARRAY: + var vectors := PackedStringArray() + for vector: Vector4 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR4)) + if vectors.is_empty(): + return "PackedVector4Array()" + return "PackedVector4Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_STRING_ARRAY: + var values := PackedStringArray() + for v: String in value: + @warning_ignore("return_value_discarded") + values.append('"%s"' % v) + if values.is_empty(): + return "PackedStringArray()" + return "PackedStringArray([%s])" % ", ".join(values) + + TYPE_PACKED_BYTE_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY,\ + TYPE_PACKED_FLOAT64_ARRAY,\ + TYPE_PACKED_INT32_ARRAY,\ + TYPE_PACKED_INT64_ARRAY: + var vectors := PackedStringArray() + for vector: Variant in value: + @warning_ignore("return_value_discarded") + vectors.append(str(vector)) + if vectors.is_empty(): + return GdObjects.type_as_string(type) + "()" + return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)] + return "unknown array type %d" % type + + +func _on_type_Vector(value: Variant, type: int) -> String: + + if typeof(value) != type: + push_error("Internal Error: type missmatch detected for value '%s', expects type %s" % [value, type_string(type)]) + return "" + + match type: + TYPE_VECTOR2: + if value == Vector2(): + return "Vector2()" + return "Vector2%s" % value + TYPE_VECTOR2I: + if value == Vector2i(): + return "Vector2i()" + return "Vector2i%s" % value + TYPE_VECTOR3: + if value == Vector3(): + return "Vector3()" + return "Vector3%s" % value + TYPE_VECTOR3I: + if value == Vector3i(): + return "Vector3i()" + return "Vector3i%s" % value + TYPE_VECTOR4: + if value == Vector4(): + return "Vector4()" + return "Vector4%s" % value + TYPE_VECTOR4I: + if value == Vector4i(): + return "Vector4i()" + return "Vector4i%s" % value + return "unknown vector type %d" % type + + +func _on_type_Transform2D(transform: Transform2D) -> String: + if transform == Transform2D(): + return "Transform2D()" + return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] + + +func _on_type_Transform3D(transform: Transform3D) -> String: + if transform == Transform3D(): + return "Transform3D()" + return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] + + +func _on_type_Projection(projection: Projection) -> String: + return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] + + +@warning_ignore("unused_parameter") +func _on_type_RID(value: RID) -> String: + return "RID()" + + +func _on_type_Rect2(rect: Rect2) -> String: + if rect == Rect2(): + return "Rect2()" + return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] + + +func _on_type_Rect2i(rect: Variant) -> String: + if rect == Rect2i(): + return "Rect2i()" + return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] + + +func _on_type_Plane(plane: Plane) -> String: + if plane == Plane(): + return "Plane()" + return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] + + +func _on_type_Quaternion(quaternion: Quaternion) -> String: + if quaternion == Quaternion(): + return "Quaternion()" + return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + + +func _on_type_AABB(aabb: AABB) -> String: + if aabb == AABB(): + return "AABB()" + return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] + + +func _on_type_Basis(basis: Basis) -> String: + if basis == Basis(): + return "Basis()" + return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] + + +static func decode(value: Variant) -> String: + var type := typeof(value) + @warning_ignore("unsafe_cast") + if GdArrayTools.is_type_array(type) and (value as Array).is_empty(): + return "" + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) + + +static func decode_typed(type: int, value: Variant) -> String: + if value == null: + return "null" + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) + + +static func _get_value_decoder(type: int) -> Callable: + var decoder: GdDefaultValueDecoder = instance( + "GdUnitDefaultValueDecoders", + func() -> GdDefaultValueDecoder: + return GdDefaultValueDecoder.new()) + return decoder.get_decoder(type) diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid index e69de29b..77e9e472 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid @@ -0,0 +1 @@ +uid://lklwx7a3htjd diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid index e69de29b..4d744c31 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid @@ -0,0 +1 @@ +uid://c1fyr61upo4ts diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd index e69de29b..7eae4b2f 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -0,0 +1,286 @@ +class_name GdFunctionDescriptor +extends RefCounted + +var _is_virtual :bool +var _is_static :bool +var _is_engine :bool +var _is_coroutine :bool +var _name :String +var _source_path: String +var _line_number :int +var _return_type :int +var _return_class :String +var _args : Array[GdFunctionArgument] +var _varargs :Array[GdFunctionArgument] + + + +static func create(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, false, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + +static func create_static(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, true, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + + +func _init(p_name :String, + p_line_number :int, + p_is_virtual :bool, + p_is_static :bool, + p_is_engine :bool, + p_return_type :int, + p_return_class :String, + p_args : Array[GdFunctionArgument], + p_varargs :Array[GdFunctionArgument] = []) -> void: + _name = p_name + _line_number = p_line_number + _return_type = p_return_type + _return_class = p_return_class + _is_virtual = p_is_virtual + _is_static = p_is_static + _is_engine = p_is_engine + _is_coroutine = false + _args = p_args + _varargs = p_varargs + + +func with_return_class(clazz_name: String) -> GdFunctionDescriptor: + _return_class = clazz_name + return self + + +func name() -> String: + return _name + + +func source_path() -> String: + return _source_path + + +func line_number() -> int: + return _line_number + + +func is_virtual() -> bool: + return _is_virtual + + +func is_static() -> bool: + return _is_static + + +func is_engine() -> bool: + return _is_engine + + +func is_vararg() -> bool: + return not _varargs.is_empty() + + +func is_coroutine() -> bool: + return _is_coroutine + + +func is_parameterized() -> bool: + for current in _args: + var arg :GdFunctionArgument = current + if arg.name() in GdFunctionArgument.ARG_PARAMETERIZED_TEST: + return true + return false + + +func is_private() -> bool: + return name().begins_with("_") and not is_virtual() + + +func return_type() -> int: + return _return_type + + +func return_type_as_string() -> String: + if return_type() == TYPE_NIL: + return "void" + if (return_type() == TYPE_OBJECT or return_type() == GdObjects.TYPE_ENUM) and not _return_class.is_empty(): + return _return_class + return GdObjects.type_as_string(return_type()) + + +func set_argument_value(arg_name: String, value: String) -> void: + var argument: GdFunctionArgument = _args.filter(func(arg: GdFunctionArgument) -> bool: + return arg.name() == arg_name + ).front() + if argument != null: + argument.set_value(value) + + +func enrich_arguments(arguments: Array[Dictionary]) -> void: + for arg_index: int in arguments.size(): + var arg: Dictionary = arguments[arg_index] + if arg["type"] != GdObjects.TYPE_VARARG: + var arg_name: String = arg["name"] + var arg_value: String = arg["value"] + set_argument_value(arg_name, arg_value) + + +func enrich_file_info(p_source_path: String, p_line_number: int) -> void: + _source_path = p_source_path + _line_number = p_line_number + + +func args() -> Array[GdFunctionArgument]: + return _args + + +func varargs() -> Array[GdFunctionArgument]: + return _varargs + + +func typed_args() -> String: + var collect := PackedStringArray() + for arg in args(): + @warning_ignore("return_value_discarded") + collect.push_back(arg._to_string()) + for arg in varargs(): + @warning_ignore("return_value_discarded") + collect.push_back(arg._to_string()) + return ", ".join(collect) + + +func _to_string() -> String: + var fsignature := "virtual " if is_virtual() else "" + if _return_type == TYPE_NIL: + return fsignature + "[Line:%s] func %s(%s):" % [line_number(), name(), typed_args()] + var func_template := fsignature + "[Line:%s] func %s(%s) -> %s:" + if is_static(): + func_template= "[Line:%s] static func %s(%s) -> %s:" + return func_template % [line_number(), name(), typed_args(), return_type_as_string()] + + +# extract function description given by Object.get_method_list() +static func extract_from(descriptor :Dictionary, is_engine_ := true) -> GdFunctionDescriptor: + var func_name: String = descriptor["name"] + var function_flags: int = descriptor["flags"] + var return_descriptor: Dictionary = descriptor["return"] + var clazz_name: String = return_descriptor["class_name"] + var is_virtual_: bool = function_flags & METHOD_FLAG_VIRTUAL + var is_static_: bool = function_flags & METHOD_FLAG_STATIC + var is_vararg_: bool = function_flags & METHOD_FLAG_VARARG + + return GdFunctionDescriptor.new( + func_name, + -1, + is_virtual_, + is_static_, + is_engine_, + _extract_return_type(return_descriptor), + clazz_name, + _extract_args(descriptor), + _build_varargs(is_vararg_) + ) + +# temporary exclude GlobalScope enums +const enum_fix := [ + "Side", + "Corner", + "Orientation", + "ClockDirection", + "HorizontalAlignment", + "VerticalAlignment", + "InlineAlignment", + "EulerOrder", + "Error", + "Key", + "MIDIMessage", + "MouseButton", + "MouseButtonMask", + "JoyButton", + "JoyAxis", + "PropertyHint", + "PropertyUsageFlags", + "MethodFlags", + "Variant.Type", + "Control.LayoutMode"] + + +static func _extract_return_type(return_info :Dictionary) -> int: + var type :int = return_info["type"] + var usage :int = return_info["usage"] + if type == TYPE_INT and usage & PROPERTY_USAGE_CLASS_IS_ENUM: + return GdObjects.TYPE_ENUM + if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT + if type == TYPE_NIL and usage == 6: + return GdObjects.TYPE_VOID + return type + + +static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]: + var args_ :Array[GdFunctionArgument] = [] + var arguments :Array = descriptor["args"] + var defaults :Array = descriptor["default_args"] + # iterate backwards because the default values are stored from right to left + while not arguments.is_empty(): + var arg :Dictionary = arguments.pop_back() + var arg_name := _argument_name(arg) + var arg_type := _argument_type(arg) + var arg_type_hint := _argument_hint(arg) + #var arg_class: StringName = arg["class_name"] + var default_value: Variant = GdFunctionArgument.UNDEFINED if defaults.is_empty() else defaults.pop_back() + args_.push_front(GdFunctionArgument.new(arg_name, arg_type, default_value, arg_type_hint)) + return args_ + + +static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]: + var varargs_ :Array[GdFunctionArgument] = [] + if not p_is_vararg: + return varargs_ + varargs_.push_back(GdFunctionArgument.new("varargs", GdObjects.TYPE_VARARG, '')) + return varargs_ + + +static func _argument_name(arg :Dictionary) -> String: + return arg["name"] + + +static func _argument_type(arg :Dictionary) -> int: + var type :int = arg["type"] + var usage :int = arg["usage"] + + if type == TYPE_OBJECT: + if arg["class_name"] == "Node": + return GdObjects.TYPE_NODE + if arg["class_name"] == "Fuzzer": + return GdObjects.TYPE_FUZZER + + # if the argument untyped we need to scan the assignef value type + if type == TYPE_NIL and usage == PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT + return type + + +static func _argument_hint(arg :Dictionary) -> int: + var hint :int = arg["hint"] + var hint_string :String = arg["hint_string"] + + match hint: + PROPERTY_HINT_ARRAY_TYPE: + return GdObjects.string_to_type(hint_string) + _: + return 0 + + +static func _argument_type_as_string(arg :Dictionary) -> String: + var type := _argument_type(arg) + match type: + TYPE_NIL: + return "" + TYPE_OBJECT: + var clazz_name :String = arg["class_name"] + if not clazz_name.is_empty(): + return clazz_name + return "" + _: + return GdObjects.type_as_string(type) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid index e69de29b..f11d0ec4 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid @@ -0,0 +1 @@ +uid://bascqhwocxsl4 diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd index e69de29b..9c45e625 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd @@ -0,0 +1,188 @@ +class_name GdFunctionParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func resolve_test_cases(script: GDScript) -> Array[GdUnitTestCase]: + if not is_parameterized(): + return [GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name())] + return extract_test_cases_by_reflection(script) + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(input_value_set: Array) -> String: + var input_arguments := _fd.args() + # check given parameter set with test case arguments + var expected_arg_count := input_arguments.size() - 1 + for input_values :Variant in input_value_set: + var parameter_set_index := input_value_set.find(input_values) + if input_values is Array: + var arr_values: Array = input_values + var current_arg_count := arr_values.size() + if current_arg_count != expected_arg_count: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] + var error := validate_parameter_types(input_arguments, arr_values, parameter_set_index) + if not error.is_empty(): + return error + else: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index + return "" + + +static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value :Variant = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed or is Variant we skip the type test + if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param] + return "" + + +func extract_test_cases_by_reflection(script: GDScript) -> Array[GdUnitTestCase]: + var source: Node = script.new() + source.queue_free() + + var fa := GdFunctionArgument.get_parameter_set(_fd.args()) + var parameter_sets := fa.parameter_sets() + # if no parameter set detected we need to resolve it by using reflection + if parameter_sets.size() == 0: + _is_static = false + return _extract_test_cases_by_reflection(source, script) + else: + var test_cases: Array[GdUnitTestCase] = [] + var property_names := _extract_property_names(source) + for parameter_set_index in parameter_sets.size(): + var parameter_set := parameter_sets[parameter_set_index] + _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), parameter_set_index, parameter_set)) + parameter_set_index += 1 + return test_cases + + +func _extract_property_names(source: Node) -> PackedStringArray: + return source.get_property_list()\ + .map(func(property :Dictionary) -> String: return property["name"])\ + .filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +func _extract_test_cases_by_reflection(source: Node, script: GDScript) -> Array[GdUnitTestCase]: + var parameter_sets := load_parameter_sets(source) + var test_cases: Array[GdUnitTestCase] = [] + for index in parameter_sets.size(): + var parameter_set := str(parameter_sets[index]) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), index, parameter_set)) + return test_cases + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(source: Node) -> Array: + var source_script: GDScript = source.get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code := CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script := GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result := script.reload() + if result != OK: + push_error("Extracting test parameters failed! Script loading error: %s" % result) + return [] + var instance: Node = script.new() + GdFunctionParameterSetResolver.copy_properties(source, instance) + instance.queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") + return fixure_typed_parameters(parameter_sets, _fd.args()) + + +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid index e69de29b..083918b5 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid @@ -0,0 +1 @@ +uid://d0q8x2w5alxsx diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd index e69de29b..a1025082 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -0,0 +1,764 @@ +class_name GdScriptParser +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + + +var TOKEN_NOT_MATCH := Token.new("") +var TOKEN_SPACE := SkippableToken.new(" ") +var TOKEN_TABULATOR := SkippableToken.new("\t") +var TOKEN_NEW_LINE := SkippableToken.new("\n") +var TOKEN_COMMENT := SkippableToken.new("#") +var TOKEN_CLASS_NAME := RegExToken.new("class_name", GdUnitTools.to_regex("(class_name)\\s+([\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class_name)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_INNER_CLASS := TokenInnerClass.new("class", GdUnitTools.to_regex("(class)\\s+(\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_EXTENDS := RegExToken.new("extends", GdUnitTools.to_regex("extends\\s+")) +var TOKEN_ENUM := RegExToken.new("enum", GdUnitTools.to_regex("enum\\s+")) +var TOKEN_FUNCTION_STATIC_DECLARATION := RegExToken.new("static func", GdUnitTools.to_regex("^static\\s+func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) +var TOKEN_FUNCTION_DECLARATION := RegExToken.new("func", GdUnitTools.to_regex("^func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) +var TOKEN_FUNCTION := Token.new(".") +var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->") +var TOKEN_FUNCTION_END := Token.new("):") +var TOKEN_ARGUMENT_ASIGNMENT := Token.new("=") +var TOKEN_ARGUMENT_TYPE_ASIGNMENT := Token.new(":=") +var TOKEN_ARGUMENT_FUZZER := FuzzerToken.new(GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")) +var TOKEN_ARGUMENT_TYPE := Token.new(":") +var TOKEN_ARGUMENT_VARIADIC := Token.new("...") +var TOKEN_ARGUMENT_SEPARATOR := Token.new(",") +var TOKEN_BRACKET_ROUND_OPEN := Token.new("(") +var TOKEN_BRACKET_ROUND_CLOSE := Token.new(")") +var TOKEN_BRACKET_SQUARE_OPEN := Token.new("[") +var TOKEN_BRACKET_SQUARE_CLOSE := Token.new("]") +var TOKEN_BRACKET_CURLY_OPEN := Token.new("{") +var TOKEN_BRACKET_CURLY_CLOSE := Token.new("}") + + +var OPERATOR_ADD := Operator.new("+") +var OPERATOR_SUB := Operator.new("-") +var OPERATOR_MUL := Operator.new("*") +var OPERATOR_DIV := Operator.new("/") +var OPERATOR_REMAINDER := Operator.new("%") + +var TOKENS :Array[Token] = [ + TOKEN_SPACE, + TOKEN_TABULATOR, + TOKEN_NEW_LINE, + TOKEN_COMMENT, + TOKEN_BRACKET_ROUND_OPEN, + TOKEN_BRACKET_ROUND_CLOSE, + TOKEN_BRACKET_SQUARE_OPEN, + TOKEN_BRACKET_SQUARE_CLOSE, + TOKEN_BRACKET_CURLY_OPEN, + TOKEN_BRACKET_CURLY_CLOSE, + TOKEN_CLASS_NAME, + TOKEN_INNER_CLASS, + TOKEN_EXTENDS, + TOKEN_ENUM, + TOKEN_FUNCTION_STATIC_DECLARATION, + TOKEN_FUNCTION_DECLARATION, + TOKEN_ARGUMENT_FUZZER, + TOKEN_ARGUMENT_TYPE_ASIGNMENT, + TOKEN_ARGUMENT_ASIGNMENT, + TOKEN_ARGUMENT_TYPE, + TOKEN_ARGUMENT_VARIADIC, + TOKEN_FUNCTION, + TOKEN_ARGUMENT_SEPARATOR, + TOKEN_FUNCTION_RETURN_TYPE, + OPERATOR_ADD, + OPERATOR_SUB, + OPERATOR_MUL, + OPERATOR_DIV, + OPERATOR_REMAINDER, +] + +var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*") +var _scanned_inner_classes := PackedStringArray() +var _script_constants := {} +var _is_awaiting := GdUnitTools.to_regex("\\bawait\\s+(?![^\"]*\"[^\"]*$)(?!.*#.*await)") + + +static func to_unix_format(input :String) -> String: + return input.replace("\r\n", "\n") + + +class Token extends RefCounted: + var _token: String + var _consumed: int + var _is_operator: bool + + func _init(p_token: String, p_is_operator := false) -> void: + _token = p_token + _is_operator = p_is_operator + _consumed = p_token.length() + + func match(input: String, pos: int) -> bool: + return input.findn(_token, pos) == pos + + func value() -> Variant: + return _token + + func is_operator() -> bool: + return _is_operator + + func is_inner_class() -> bool: + return _token == "class" + + func is_variable() -> bool: + return false + + func is_token(token_name :String) -> bool: + return _token == token_name + + func is_skippable() -> bool: + return false + + func _to_string() -> String: + return "Token{" + _token + "}" + + +class Operator extends Token: + func _init(p_value: String) -> void: + super(p_value, true) + + func _to_string() -> String: + return "OperatorToken{%s}" % [_token] + + +# A skippable token, is just a placeholder like space or tabs +class SkippableToken extends Token: + + func _init(p_token: String) -> void: + super(p_token) + + func is_skippable() -> bool: + return true + + +# Token to parse function arguments +class Variable extends Token: + var _plain_value :String + var _typed_value :Variant + var _type :int = TYPE_NIL + + + func _init(p_value: String) -> void: + super(p_value) + _type = _scan_type(p_value) + _plain_value = p_value + _typed_value = _cast_to_type(p_value, _type) + + + func _scan_type(p_value: String) -> int: + if p_value.begins_with("\"") and p_value.ends_with("\""): + return TYPE_STRING + var type_ := GdObjects.string_to_type(p_value) + if type_ != TYPE_NIL: + return type_ + if p_value.is_valid_int(): + return TYPE_INT + if p_value.is_valid_float(): + return TYPE_FLOAT + if p_value.is_valid_hex_number(): + return TYPE_INT + return TYPE_OBJECT + + + func _cast_to_type(p_value :String, p_type: int) -> Variant: + match p_type: + TYPE_STRING: + return p_value#.substr(1, p_value.length() - 2) + TYPE_INT: + return p_value.to_int() + TYPE_FLOAT: + return p_value.to_float() + return p_value + + + func is_variable() -> bool: + return true + + + func type() -> int: + return _type + + + func value() -> Variant: + return _typed_value + + + func plain_value() -> String: + return _plain_value + + + func _to_string() -> String: + return "Variable{%s: %s : '%s'}" % [_plain_value, GdObjects.type_as_string(_type), _token] + + +class RegExToken extends Token: + var _regex: RegEx + var _extract_group_index: int + var _value := "" + + + func _init(token: String, regex: RegEx, extract_group_index: int = -1) -> void: + super(token, false) + _regex = regex + _extract_group_index = extract_group_index + + + func match(input: String, pos: int) -> bool: + var matching := _regex.search(input, pos) + if matching == null or pos != matching.get_start(): + return false + if _extract_group_index != -1: + _value = matching.get_string(_extract_group_index) + _consumed = matching.get_end() - matching.get_start() + return true + + + func value() -> String: + return _value + + +# Token to parse Fuzzers +class FuzzerToken extends RegExToken: + + + func _init(regex: RegEx) -> void: + super("fuzzer", regex, 1) + + + func name() -> String: + return value() + + + func type() -> int: + return GdObjects.TYPE_FUZZER + + + func _to_string() -> String: + return "FuzzerToken{%s: '%s'}" % [value(), _token] + + +class TokenInnerClass extends RegExToken: + var _content := PackedStringArray() + + + static func _strip_leading_spaces(input: String) -> String: + var characters := input.to_utf8_buffer() + while not characters.is_empty(): + if characters[0] != 0x20: + break + characters.remove_at(0) + return characters.get_string_from_utf8() + + + static func _consumed_bytes(row: String) -> int: + return row.replace(" ", "").replace(" ", "").length() + + + func _init(token: String, p_regex: RegEx, extract_group_index: int = -1) -> void: + super(token, p_regex, extract_group_index) + + + func is_class_name(clazz_name: String) -> bool: + return value() == clazz_name + + + func content() -> PackedStringArray: + return _content + + + @warning_ignore_start("return_value_discarded") + func parse(source_rows: PackedStringArray, offset: int) -> void: + # add class signature + _content.clear() + _content.append(source_rows[offset]) + # parse class content + for row_index in range(offset+1, source_rows.size()): + # scan until next non tab + var source_row := source_rows[row_index] + var row := TokenInnerClass._strip_leading_spaces(source_row) + if row.is_empty() or row.begins_with("\t") or row.begins_with("#"): + # fold all line to left by removing leading tabs and spaces + if source_row.begins_with("\t"): + source_row = source_row.trim_prefix("\t") + # refomat invalid empty lines + if source_row.dedent().is_empty(): + _content.append("") + else: + _content.append(source_row) + continue + break + _consumed += TokenInnerClass._consumed_bytes("".join(_content)) + @warning_ignore_restore("return_value_discarded") + + + func _to_string() -> String: + return "TokenInnerClass{%s}" % [value()] + + + +func get_token(input: String, current_index: int) -> Token: + for t in TOKENS: + if t.match(input, current_index): + return t + return TOKEN_NOT_MATCH + + +func next_token(input: String, current_index: int, ignore_tokens :Array[Token] = []) -> Token: + var token := TOKEN_NOT_MATCH + for t :Token in TOKENS.filter(func(t :Token) -> bool: return not ignore_tokens.has(t)): + + if t.match(input, current_index): + token = t + break + if token == OPERATOR_SUB: + token = tokenize_value(input, current_index, token) + if token == TOKEN_NOT_MATCH: + return tokenize_value(input, current_index, token, ignore_tokens.has(TOKEN_FUNCTION)) + return token + + +func tokenize_value(input: String, current: int, token: Token, ignore_dots := false) -> Token: + var next := 0 + var current_token := "" + # test for '--', '+-', '*-', '/-', '%-', or at least '-x' + var test_for_sign := (token == null or token.is_operator()) and input[current] == "-" + while current + next < len(input): + var character := input[current + next] as String + # if first charater a sign + # or allowend charset + # or is a float value + if (test_for_sign and next==0) \ + or is_allowed_character(character) \ + or (character == "." and (ignore_dots or current_token.is_valid_int())): + current_token += character + next += 1 + continue + break + if current_token != "": + return Variable.new(current_token) + return TOKEN_NOT_MATCH + + +# const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" +func is_allowed_character(input: String) -> bool: + var code_point := input.unicode_at(0) + # Unicode + if code_point > 127: + # This is a Unicode character (Chinese, Japanese, etc.) + return true + # ASCII digit 0-9 + if code_point >= 48 and code_point <= 57: + return true + # ASCII lowercase a-z + if code_point >= 97 and code_point <= 122: + return true + # ASCII uppercase A-Z + if code_point >= 65 and code_point <= 90: + return true + # underscore _ + if code_point == 95: + return true + # quotes '" + if code_point == 34 or code_point == 39: + return true + return false + + +func parse_return_token(input: String) -> Variable: + var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token) + if index == -1: + return TOKEN_NOT_MATCH + index += TOKEN_FUNCTION_RETURN_TYPE._consumed + # We scan for the return value exclusive '.' token because it could be referenced to a + # external or internal class e.g. 'func foo() -> InnerClass.Bar:' + var token := next_token(input, index, [TOKEN_FUNCTION]) + while !token.is_variable() and token != TOKEN_NOT_MATCH: + index += token._consumed + token = next_token(input, index, [TOKEN_FUNCTION]) + return token + + +func get_function_descriptors(script: GDScript, included_functions: PackedStringArray = []) -> Array[GdFunctionDescriptor]: + var fds: Array[GdFunctionDescriptor] = [] + for method_descriptor in script.get_script_method_list(): + var func_name: String = method_descriptor["name"] + if included_functions.is_empty() or func_name in included_functions: + # exclude type set/geters + if is_getter_or_setter(func_name): + continue + if not fds.any(func(fd: GdFunctionDescriptor) -> bool: return fd.name() == func_name): + fds.append(GdFunctionDescriptor.extract_from(method_descriptor, false)) + + # we need to enrich it by default arguments and line number by parsing the script + # the engine core functions has no valid methods to get this info + _prescan_script(script) + _enrich_function_descriptor(script, fds) + return fds + + +func is_getter_or_setter(func_name: String) -> bool: + return func_name.begins_with("@") and (func_name.ends_with("getter") or func_name.ends_with("setter")) + + +func _parse_function_arguments(input: String) -> Array[Dictionary]: + var arguments: Array[Dictionary] = [] + var current_index := 0 + var token: Token = null + var bracket := 0 + var in_function := false + + + while current_index < len(input): + token = next_token(input, current_index) + # fallback to not end in a endless loop + if token == TOKEN_NOT_MATCH: + var error : = """ + Parsing Error: Invalid token at pos %d found. + Please report this error! + source_code: + -------------------------------------------------------------- + %s + -------------------------------------------------------------- + """.dedent() % [current_index, input] + push_error(error) + current_index += 1 + continue + current_index += token._consumed + if token.is_skippable(): + continue + if token == TOKEN_BRACKET_ROUND_OPEN : + in_function = true + bracket += 1 + if token == TOKEN_BRACKET_ROUND_CLOSE: + bracket -= 1 + # if function end? + if in_function and bracket == 0: + return arguments + # is function + if token == TOKEN_FUNCTION_DECLARATION: + continue + + # is value argument + if in_function: + var arg_value := "" + var current_argument := { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : TYPE_VARIANT + } + + # parse type and default value + while current_index < len(input): + token = next_token(input, current_index) + current_index += token._consumed + if token.is_skippable(): + continue + + if token.is_variable() && current_argument["name"] == "": + arguments.append(current_argument) + current_argument["name"] = (token as Variable).plain_value() + continue + + match token: + # is fuzzer argument + TOKEN_ARGUMENT_FUZZER: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["name"] = (token as FuzzerToken).name() + current_argument["value"] = arg_value.lstrip(" ") + current_argument["type"] = TYPE_FUZZER + arguments.append(current_argument) + continue + + TOKEN_ARGUMENT_VARIADIC: + current_argument["type"] = TYPE_VARARG + + TOKEN_ARGUMENT_TYPE: + token = next_token(input, current_index) + if token == TOKEN_SPACE: + current_index += token._consumed + token = next_token(input, current_index) + current_index += token._consumed + if current_argument["type"] != TYPE_VARARG: + current_argument["type"] = GdObjects.string_to_type((token as Variable).plain_value()) + + TOKEN_ARGUMENT_TYPE_ASIGNMENT: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["value"] = arg_value.lstrip(" ") + TOKEN_ARGUMENT_ASIGNMENT: + token = next_token(input, current_index) + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["value"] = arg_value.lstrip(" ") + + TOKEN_BRACKET_SQUARE_OPEN: + bracket += 1 + TOKEN_BRACKET_CURLY_OPEN: + bracket += 1 + TOKEN_BRACKET_ROUND_OPEN : + bracket += 1 + # if value a function? + if bracket > 1: + # complete the argument value + var func_begin := input.substr(current_index-TOKEN_BRACKET_ROUND_OPEN ._consumed) + var func_body := _parse_end_function(func_begin) + arg_value += func_body + # fix parse index to end of value + current_index += func_body.length() - TOKEN_BRACKET_ROUND_OPEN ._consumed - TOKEN_BRACKET_ROUND_CLOSE._consumed + TOKEN_BRACKET_SQUARE_CLOSE: + bracket -= 1 + TOKEN_BRACKET_CURLY_CLOSE: + bracket -= 1 + TOKEN_BRACKET_ROUND_CLOSE: + bracket -= 1 + # end of function + if bracket == 0: + break + TOKEN_ARGUMENT_SEPARATOR: + if bracket <= 1: + # next argument + current_argument = { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : GdObjects.TYPE_VARIANT + } + continue + return arguments + + +func _parse_end_function(input: String, remove_trailing_char := false) -> String: + # find end of function + var current_index := 0 + var bracket_count := 0 + var in_array := 0 + var in_dict := 0 + var end_of_func := false + + while current_index < len(input) and not end_of_func: + var character := input[current_index] + # step over strings + if character == "'" : + current_index = input.find("'", current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + if character == '"' : + # test for string blocks + if input.find('"""', current_index) == current_index: + current_index = input.find('"""', current_index+3) + 3 + else: + current_index = input.find('"', current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + + match character: + # count if inside an array + "[": in_array += 1 + "]": in_array -= 1 + # count if inside an dictionary + "{": in_dict += 1 + "}": in_dict -= 1 + # count if inside a function + "(": bracket_count += 1 + ")": + bracket_count -= 1 + if bracket_count < 0 and in_array <= 0 and in_dict <= 0: + end_of_func = true + ",": + if bracket_count == 0 and in_array == 0 and in_dict <= 0: + end_of_func = true + current_index += 1 + if remove_trailing_char: + # check if the parsed value ends with comma or end of doubled breaked + # `,` or `())` + var trailing_char := input[current_index-1] + if trailing_char == ',' or (bracket_count < 0 and trailing_char == ')'): + return input.substr(0, current_index-1) + return input.substr(0, current_index) + + +func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray: + for row_index in source_rows.size(): + var input := source_rows[row_index] + var token := next_token(input, 0) + if token.is_inner_class(): + @warning_ignore("unsafe_method_access") + if token.is_class_name(clazz_name): + @warning_ignore("unsafe_method_access") + token.parse(source_rows, row_index) + @warning_ignore("unsafe_method_access") + return token.content() + return PackedStringArray() + + +func extract_func_signature(rows: PackedStringArray, index: int) -> String: + var signature := "" + + for rowIndex in range(index, rows.size()): + var row := rows[rowIndex] + row = _regex_strip_comments.sub(row, "").strip_edges(false) + if row.is_empty(): + continue + signature += row + "\n" + if is_func_end(row): + return signature.strip_edges() + push_error("Can't fully extract function signature of '%s'" % rows[index]) + return "" + + +func get_class_name(script :GDScript) -> String: + var source_code := GdScriptParser.to_unix_format(script.source_code) + var source_rows := source_code.split("\n") + + for index :int in min(10, source_rows.size()): + var input := source_rows[index] + var token := next_token(input, 0) + if token == TOKEN_CLASS_NAME: + return token.value() + # if no class_name found extract from file name + return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file()) + + +func parse_func_name(input: String) -> String: + if TOKEN_FUNCTION_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_DECLARATION.value() + if TOKEN_FUNCTION_STATIC_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_STATIC_DECLARATION.value() + push_error("Can't extract function name from '%s'" % input) + return "" + + +## Enriches the function descriptor by line number and argument default values +## - enrich all function descriptors form current script up to all inherited scrips +func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void: + var enriched_functions := {} # Use Dictionary for O(1) lookup instead of PackedStringArray + var script_to_scan := script + while script_to_scan != null: + # do not scan the test suite base class itself + if script_to_scan.resource_path == "res://addons/gdUnit4/src/GdUnitTestSuite.gd": + break + + var rows := script_to_scan.source_code.split("\n") + for rowIndex in rows.size(): + var input := rows[rowIndex] + # step over inner class functions + if input.begins_with("\t"): + continue + # skip comments and empty lines + if input.begins_with("#") or input.length() == 0: + continue + var token := next_token(input, 0) + if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION: + continue + + var function_name: String = token.value() + # Skip if already enriched (from parent class scan) + if enriched_functions.has(function_name): + continue + + # Find matching function descriptor + var fd: GdFunctionDescriptor = null + for candidate in fds: + if candidate.name() == function_name: + fd = candidate + break + if fd == null: + continue + # Mark as enriched + enriched_functions[function_name] = true + var func_signature := extract_func_signature(rows, rowIndex) + var func_arguments := _parse_function_arguments(func_signature) + # enrich missing default values + fd.enrich_arguments(func_arguments) + fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1) + fd._is_coroutine = is_func_coroutine(rows, rowIndex) + # enrich return class name if not set + if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]: + var var_token := parse_return_token(func_signature) + if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT: + fd._return_class = _patch_inner_class_names(var_token.plain_value(), "") + # if the script ihnerits we need to scan this also + script_to_scan = script_to_scan.get_base_script() + + +func is_func_coroutine(rows :PackedStringArray, index :int) -> bool: + var is_coroutine := false + for rowIndex in range(index+1, rows.size()): + var input := rows[rowIndex].strip_edges() + if input.begins_with("#") or input.is_empty(): + continue + var token := next_token(input, 0) + # scan until next function + if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: + break + + if _is_awaiting.search(input): + return true + return is_coroutine + + +func is_inner_class(clazz_path :PackedStringArray) -> bool: + return clazz_path.size() > 1 + + +func is_func_end(row :String) -> bool: + return row.strip_edges(false, true).ends_with(":") + + +func _patch_inner_class_names(clazz :String, clazz_name :String = "") -> String: + var inner_clazz_name := clazz.split(".")[0] + if _scanned_inner_classes.has(inner_clazz_name): + return inner_clazz_name + #var base_clazz := clazz_name.split(".")[0] + #return base_clazz + "." + clazz + if _script_constants.has(clazz): + return clazz_name + "." + clazz + return clazz + + +func _prescan_script(script: GDScript) -> void: + _script_constants = script.get_script_constant_map() + for key :String in _script_constants.keys(): + var value :Variant = _script_constants.get(key) + if value is GDScript: + @warning_ignore("return_value_discarded") + _scanned_inner_classes.append(key) + + +func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: + if clazz_path.is_empty(): + return GdUnitResult.error("Invalid script path '%s'" % clazz_path) + var is_inner_class_ := is_inner_class(clazz_path) + var script :GDScript = load(clazz_path[0]) + _prescan_script(script) + + if is_inner_class_: + var inner_class_name := clazz_path[1] + if _scanned_inner_classes.has(inner_class_name): + # do load only on inner class source code and enrich the stored script instance + var source_code := _load_inner_class(script, inner_class_name) + script = _script_constants.get(inner_class_name) + script.source_code = source_code + var function_descriptors := get_function_descriptors(script) + var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors) + return GdUnitResult.success(gd_class) + + +func _load_inner_class(script: GDScript, inner_clazz: String) -> String: + var source_rows := GdScriptParser.to_unix_format(script.source_code).split("\n") + # extract all inner class names + var inner_class_code := extract_inner_class(source_rows, inner_clazz) + return "\n".join(inner_class_code) diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid index e69de29b..e6acfdab 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid @@ -0,0 +1 @@ +uid://47c36e540hgy diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd index e69de29b..9faf830b 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -0,0 +1,74 @@ +class_name GdUnitExpressionRunner +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ExpressionRunner extends '${clazz_path}' + +func __run_expression() -> Variant: + return $expression + +""" + +var constructor_args_regex := RegEx.create_from_string("new\\((?.*)\\)") + + +func execute(src_script: GDScript, value: Variant) -> Variant: + if typeof(value) != TYPE_STRING: + return value + + var expression: String = value + var parameter_map := src_script.get_script_constant_map() + for key: String in parameter_map.keys(): + var parameter_value: Variant = parameter_map[key] + # check we need to construct from inner class + # we need to use the original class instance from the script_constant_map otherwise we run into a runtime error + if expression.begins_with(key + ".new") and parameter_value is GDScript: + var object: GDScript = parameter_value + var args := build_constructor_arguments(parameter_map, expression.substr(expression.find("new"))) + if args.is_empty(): + return object.new() + return object.callv("new", args) + + var script := GDScript.new() + var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path + script.source_code = CLASS_TEMPLATE.dedent()\ + .replace("${clazz_path}", resource_path)\ + .replace("$expression", expression) + #script.take_over_path(resource_path) + @warning_ignore("return_value_discarded") + script.reload(true) + var runner: Object = script.new() + if runner.has_method("queue_free"): + (runner as Node).queue_free() + @warning_ignore("unsafe_method_access") + return runner.__run_expression() + + +func build_constructor_arguments(parameter_map: Dictionary, expression: String) -> Array[Variant]: + var result := constructor_args_regex.search(expression) + var extracted_arguments := result.get_string("args").strip_edges() + if extracted_arguments.is_empty(): + return [] + var arguments :Array = extracted_arguments.split(",") + return arguments.map(func(argument: String) -> Variant: + var value := argument.strip_edges() + + # is argument an constant value + if parameter_map.has(value): + return parameter_map[value] + # is typed named value like Vector3.ONE + for type:int in GdObjects.TYPE_AS_STRING_MAPPINGS: + var type_as_string:String = GdObjects.TYPE_AS_STRING_MAPPINGS[type] + if value.begins_with(type_as_string): + return type_convert(value, type) + # is value a string + if value.begins_with("'") or value.begins_with('"'): + return value.trim_prefix("'").trim_suffix("'").trim_prefix('"').trim_suffix('"') + # fallback to default value converting + return str_to_var(value) + ) + + +func to_fuzzer(src_script: GDScript, expression: String) -> Fuzzer: + @warning_ignore("unsafe_cast") + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid index e69de29b..143f3083 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid @@ -0,0 +1 @@ +uid://8gc4dp0ot52d diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd index e69de29b..527661da 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd @@ -0,0 +1,163 @@ +## @deprecated see GdFunctionParameterSetResolver +class_name GdUnitTestParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(parameter_sets: Array, parameter_set_index: int) -> GdUnitResult: + if parameter_sets.size() < parameter_set_index: + return GdUnitResult.error("Internal error: the resolved paremeterset has invalid size.") + + var input_values: Array = parameter_sets[parameter_set_index] + if input_values == null: + return GdUnitResult.error("The parameter set '%s' must be an Array!" % parameter_sets[parameter_set_index]) + + # check given parameter set with test case arguments + var input_arguments := _fd.args() + var expected_arg_count := input_arguments.size() - 1 #(-1 we exclude the parameter set itself) + var current_arg_count := input_values.size() + if current_arg_count != expected_arg_count: + var arg_names := input_arguments\ + .filter(func(arg: GdFunctionArgument) -> bool: return not arg.is_parameter_set())\ + .map(func(arg: GdFunctionArgument) -> String: return str(arg)) + + return GdUnitResult.error(""" + The test data set at index (%d) does not match the expected test arguments: + test function: [color=snow]func test...(%s)[/color] + test input values: [color=snow]%s[/color] + """ + .dedent() % [parameter_set_index, ",".join(arg_names), input_values]) + return GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values) + + +static func validate_parameter_types(input_arguments: Array[GdFunctionArgument], input_values: Array) -> GdUnitResult: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value :Variant = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed or is Variant we skip the type test + if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return GdUnitResult.error(""" + The test data value does not match the expected input type! + input value: [color=snow]'%s', <%s>[/color] + expected argument: [color=snow]%s[/color] + """ + .dedent() % [input_value, type_string(input_value_type), str(input_param)]) + return GdUnitResult.success("No errors found.") + + +func _extract_property_names(node :Node) -> PackedStringArray: + return node.get_property_list()\ + .map(func(property :Dictionary) -> String: return property["name"])\ + .filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(test_suite: Node) -> GdUnitResult: + var source_script: Script = test_suite.get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code := CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script := GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result := script.reload() + if result != OK: + return GdUnitResult.error("Extracting test parameters failed! Script loading error: %s" % error_string(result)) + var instance :Object = script.new() + GdUnitTestParameterSetResolver.copy_properties(test_suite, instance) + (instance as Node).queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") + fixure_typed_parameters(parameter_sets, _fd.args()) + return GdUnitResult.success(parameter_sets) + + +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid index e69de29b..5953ad47 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid @@ -0,0 +1 @@ +uid://buij3yet6d2hg diff --git a/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid index e69de29b..f25a1cf7 100644 --- a/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid +++ b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid @@ -0,0 +1 @@ +uid://cq37movmiy3l1 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid index e69de29b..c7d89252 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid @@ -0,0 +1 @@ +uid://c60bwkw5e0bgo diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd index e69de29b..5af6c9b9 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd @@ -0,0 +1,87 @@ +extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd" +## Runner implementation used by the editor UI.[br] +## [br] +## This runner connects to a GdUnit server via TCP to report test results.[br] +## Test results are reported in real-time and displayed in the editor UI.[br] +## [br] +## The runner uses an RPC message protocol to communicate status and events:[br] +## - Messages to report progress[br] +## - Events to report test results[br] + +## The TCP client used to connect to the GdUnit server +@onready var _client: GdUnitTcpClient = $GdUnitTcpClient +@onready var _version_label: Control = %Version + + +func _init() -> void: + super() + # We set the default max report history to 1 + max_report_history = 1 + + +func _ready() -> void: + super() + GdUnit4Version.init_version_label(_version_label) + + var config_result := _runner_config.load_config() + if config_result.is_error(): + push_error(config_result.error_message()) + _state = EXIT + return + @warning_ignore("return_value_discarded") + _client.connect("connection_failed", _on_connection_failed) + GdUnitSignals.instance().gdunit_message.connect(_on_send_message) + var result := _client.start("127.0.0.1", _runner_config.server_port()) + if result.is_error(): + push_error(result.error_message()) + return + + +## Cleanup and quit the runner.[br] +## [br] +## [param code] The exit code to return. +func quit(code: int) -> void: + if code != RETURN_SUCCESS: + _state = EXIT + await GdUnitMemoryObserver.gc_on_guarded_instances() + + +## Called when the TCP connection to the GdUnit server fails.[br] +## Stops the test execution.[br] +## [br] +## [param message] The error message describing the failure. +func _on_connection_failed(message: String) -> void: + prints("_on_connection_failed", message) + _state = STOP + + +## Initializes the test runner.[br] +## Waits for TCP client connection and then scans for test suites.[br] +## Reports the number of found test suites via TCP message. +func init_runner() -> void: + # wait until client is connected to the GdUnitServer + if _client.is_client_connected(): + await gdUnitInit() + _state = RUN + + +## Initializes the GdUnit framework.[br] +## Sends initial message about number of test suites. +func gdUnitInit() -> void: + #enable_manuall_polling() + _test_cases = _runner_config.test_cases() + await get_tree().process_frame + + +## Sends a message via TCP to the GdUnit server.[br] +## [br] +## [param message] The message to send. +func _on_send_message(message: String) -> void: + _client.send(RPCMessage.of(message)) + + +## Handles GdUnit events by sending them via TCP to the server.[br] +## [br] +## [param event] The event to send. +func _on_gdunit_event(event: GdUnitEvent) -> void: + _client.send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid index e69de29b..895829aa 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid @@ -0,0 +1 @@ +uid://rof73we1mvyk diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn index e69de29b..eaa6f1ab 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="GdUnitTcpClient" type="Node" parent="."] +script = ExtResource("2") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +custom_minimum_size = Vector2(0, 24) +layout_direction = 2 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 10 +alignment = 2 + +[node name="Version" type="RichTextLabel" parent="HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(128, 0) +layout_mode = 2 +size_flags_horizontal = 10 +bbcode_enabled = true +scroll_active = false +shortcut_keys_enabled = false +horizontal_alignment = 1 +justification_flags = 0 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd index e69de29b..c789847b 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd @@ -0,0 +1,169 @@ +## +## @since GdUnit4 5.1.0 +## +## Represents a test execution session in GdUnit4.[br] +## [br] +## [i]A test session encapsulates a complete test execution cycle, managing the collection +## of test cases to be executed and providing communication channels for test events +## and messages. This class serves as the central coordination point for test execution +## and allows hooks and other components to interact with the running test session.[/i][br] +## [br] +## [b][u]Key Features[/u][/b][br] +## - [i][b]Test Case Management[/b][/i]: Maintains a collection of test cases to be executed[br] +## - [i][b]Event Broadcasting[/b][/i]: Forwards GdUnit events to session-specific listeners[br] +## - [i][b]Message Communication[/b][/i]: Provides a channel for sending messages during test execution[br] +## - [i][b]Hook Integration[/b][/i]: Passed to test session hooks for startup and shutdown operations[br] +## [br] +## [b][u]Usage in Test Hooks[/u][/b] +## [codeblock] +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## # Access test cases +## print("Running %d test cases" % session.test_cases.size()) +## +## # Send status messages +## session.send_message("Custom hook initialized") +## +## # Listen for test events +## session.test_event.connect(_on_test_event) +## +## return GdUnitResult.success() +## +## func _on_test_event(event: GdUnitEvent) -> void: +## print("Test event received: %s" % event.type) +## [/codeblock] +## [br] +## [b][u]Event Flow[/u][/b][br] +## 1. Session is created with a collection of test cases[br] +## 2. Session connects to the global GdUnit event system[br] +## 3. During test execution, events are automatically forwarded to session listeners[br] +## 4. Hooks and other components can subscribe to session events[br] +## 5. Messages can be sent through the session for logging and communication[br] +class_name GdUnitTestSession +extends RefCounted + + +## Emitted when a test execution event occurs.[br] +## [br] +## [i]This signal forwards events from the global GdUnit event system to session-specific +## listeners. It allows hooks and other session components to react to test events +## without directly connecting to the global event system.[/i][br] +## [br] +## [u]Common event types include:[/u][br] +## - Test suite start/end events[br] +## - Test case start/end events[br] +## - Test assertion events[br] +## - Test failure/error events[br] +## +## [param event] The test event containing details about test execution, timing, and results +@warning_ignore("unused_signal") +signal test_event(event: GdUnitEvent) + + +## [b][color=red]@readonly: Should not be modified directly during test execution![/color][/b][br] +## Collection of test cases to be executed in this session.[br] +## [br] +## This array contains all the test cases that will be run during the session. +## Test hooks can access this collection to: +## - Get the total number of tests to be executed +## - Access individual test case metadata +## - Perform setup/teardown based on test case requirements +## - Generate reports or statistics about the test suite +## +## The collection is typically populated before session startup and remains +## constant during test execution. +var _test_cases : Array[GdUnitTestCase] = [] + + +## [b][color=red]@readonly: The report path should not be modified after session creation![/color][/b][br] +## The file system path where test reports for this session will be generated.[br] +## [br] +## [i]This property provides centralized access to the report output location, +## allowing test hooks, reporters, and other components to reference the same +## report path without coupling to specific reporter implementations.[/i][br] +## [br] +## [b][u]Common use cases include:[/u][/b][br] +## - Test hooks generating additional report files in the same directory[br] +## - Custom reporters creating supplementary output files[br] +## - Post-processing scripts that need to locate generated reports[br] +## - Cleanup operations that need to manage report artifacts[br] +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## var report_dir = session.report_path.get_base_dir() +## var custom_report = report_dir.path_join("custom_metrics.json") +## # Generate additional reports in the same location +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Reports available at: " + session.report_path) +## return GdUnitResult.success() +## [/codeblock] +## [br] +## The path is set during session initialization and remains constant throughout +## the test execution lifecycle. +var report_path: String: + get: + return report_path + + +## Initializes the test session and sets up event forwarding.[br] +## [br] +## [i]This constructor automatically connects to the global GdUnit event system +## and forwards all events to the session's test_event signal. This allows +## session-specific components to listen for test events without managing +## global signal connections.[/i] +func _init(test_cases: Array[GdUnitTestCase], session_report_path: String) -> void: + # We build a copy to prevent a user is modifing the tests + _test_cases = test_cases.duplicate(true) + report_path = session_report_path + GdUnitSignals.instance().gdunit_event.connect(func(event: GdUnitEvent) -> void: + test_event.emit(event) + ) + + +## Finds a test case by its unique identifier.[br] +## [br] +## [i]Searches through all test cases to find a test with the matching GUID.[/i][br] +## [br] +## [param id] The GUID of the test to find[br] +## Returns the matching test case or null if not found. +func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase: + for test in _test_cases: + if test.guid.equals(id): + return test + + return null + + +## Sends a message through the GdUnit messaging system.[br] +## [br] +## [i]This method provides a convenient way for test hooks and other session +## components to send messages that will be handled by the GdUnit framework.[/i] +## [br][br] +## [b][u]Messages are typically used for:[/u][/b][br] +## - Status updates during test execution[br] +## - Progress reporting from test hooks[br] +## - Debug information and logging[br] +## - User notifications and alerts[br] +## [br] +## The message will be processed by the global GdUnit message system and +## may be displayed in the test runner UI, logged to files, or handled +## by other registered message handlers. +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Database connection established") +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Generated test report: report.html") +## return GdUnitResult.success() +## ``` +## [/codeblock] +## [param message] The message text to send through the GdUnit messaging system +func send_message(message: String) -> void: + GdUnitSignals.instance().gdunit_message.emit(message) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid index e69de29b..700e3e66 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid @@ -0,0 +1 @@ +uid://difxtk3nrahpi diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid index e69de29b..1735e2ec 100644 --- a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid @@ -0,0 +1 @@ +uid://bkgcd3ukyv02c diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd index e69de29b..20325ac1 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd @@ -0,0 +1,36 @@ +class_name GdUnitTestSuiteDefaultTemplate +extends RefCounted + + +const DEFAULT_TEMP_TS_GD =""" + # GdUnit generated TestSuite + class_name ${suite_class_name} + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + # TestSuite generated from + const __source: String = '${source_resource_path}' +""" + + +const DEFAULT_TEMP_TS_CS = """ + // GdUnit generated TestSuite + + using Godot; + using GdUnit4; + + namespace ${name_space} + { + using static Assertions; + using static Utils; + + [TestSuite] + public class ${suite_class_name} + { + // TestSuite generated from + private const string sourceClazzPath = "${source_resource_path}"; + + } + } +""" diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid index e69de29b..7ed67fb0 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid @@ -0,0 +1 @@ +uid://ceebaaf33485v diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid index e69de29b..a8627f40 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid @@ -0,0 +1 @@ +uid://bqinvriib2uxv diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd index e69de29b..f2b2672c 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -0,0 +1,66 @@ +class_name GdUnitThreadContext +extends RefCounted + +var _thread :Thread +var _thread_name :String +var _thread_id :int +var _signal_collector :GdUnitSignalCollector +var _execution_context :GdUnitExecutionContext +var _asserts := [] + + +func _init(thread :Thread = null) -> void: + if thread != null: + _thread = thread + _thread_name = thread.get_meta("name") + _thread_id = thread.get_id() as int + else: + _thread_name = "main" + _thread_id = OS.get_main_thread_id() + _signal_collector = GdUnitSignalCollector.new() + + +func dispose() -> void: + clear_assert() + if is_instance_valid(_signal_collector): + _signal_collector.clear() + _signal_collector = null + _execution_context = null + _thread = null + + +func clear_assert() -> void: + _asserts.clear() + + +func set_assert(value :GdUnitAssert) -> void: + if value != null: + _asserts.append(value) + + +func get_assert() -> GdUnitAssert: + return null if _asserts.is_empty() else _asserts[-1] + + +func set_execution_context(context :GdUnitExecutionContext) -> void: + _execution_context = context + + +func get_execution_context() -> GdUnitExecutionContext: + return _execution_context + + +func get_execution_context_id() -> int: + return _execution_context.get_instance_id() + + +func get_signal_collector() -> GdUnitSignalCollector: + return _signal_collector + + +func thread_id() -> int: + return _thread_id + + +func _to_string() -> String: + return "ThreadContext <%s>: %s " % [_thread_name, _thread_id] diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid index e69de29b..747a6bb4 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid @@ -0,0 +1 @@ +uid://bbnovcu4fci0e diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd index e69de29b..31b10782 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -0,0 +1,64 @@ +## A manager to run new thread and crate a ThreadContext shared over the actual test run +class_name GdUnitThreadManager +extends Object + +## { = } +var _thread_context_by_id := {} +## holds the current thread id +var _current_thread_id :int = -1 + +func _init() -> void: + # add initail the main thread + _current_thread_id = OS.get_thread_caller_id() + _thread_context_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() + + +static func instance() -> GdUnitThreadManager: + return GdUnitSingleton.instance("GdUnitThreadManager", func() -> GdUnitThreadManager: return GdUnitThreadManager.new()) + + +## Runs a new thread by given name and Callable.[br] +## A new GdUnitThreadContext is created, which is used for the actual test execution.[br] +## We need this custom implementation while this bug is not solved +## Godot issue https://github.com/godotengine/godot/issues/79637 +static func run(name :String, cb :Callable) -> Variant: + return await instance()._run(name, cb) + + +## Returns the current valid thread context +static func get_current_context() -> GdUnitThreadContext: + return instance()._get_current_context() + + +func _run(name :String, cb :Callable) -> Variant: + # we do this hack because of `OS.get_thread_caller_id()` not returns the current id + # when await process_frame is called inside the fread + var save_current_thread_id := _current_thread_id + var thread := Thread.new() + thread.set_meta("name", name) + @warning_ignore("return_value_discarded") + thread.start(cb) + _current_thread_id = thread.get_id() as int + _register_thread(thread, _current_thread_id) + var result :Variant = await thread.wait_to_finish() + _unregister_thread(_current_thread_id) + # restore original thread id + _current_thread_id = save_current_thread_id + return result + + +func _register_thread(thread :Thread, thread_id :int) -> void: + var context := GdUnitThreadContext.new(thread) + _thread_context_by_id[thread_id] = context + + +func _unregister_thread(thread_id :int) -> void: + var context: GdUnitThreadContext = _thread_context_by_id.get(thread_id) + if context: + @warning_ignore("return_value_discarded") + _thread_context_by_id.erase(thread_id) + context.dispose() + + +func _get_current_context() -> GdUnitThreadContext: + return _thread_context_by_id.get(_current_thread_id) diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid index e69de29b..8b45643c 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid @@ -0,0 +1 @@ +uid://frnnd5anspey diff --git a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid index e69de29b..7f20e9d1 100644 --- a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid +++ b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://bgk5ubygmpous diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd index e69de29b..886c7347 100644 --- a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd @@ -0,0 +1,214 @@ +@tool +class_name GdUnitMessageWriter +extends RefCounted +## Base interface class for writing formatted messages to different outputs.[br] +## [br] +## This class defines the interface and common functionality for writing formatted messages.[br] +## It provides a fluent API for message formatting and supports different output targets.[br] +## [br] +## The class provides formatting options for:[br] +## - Text colors[br] +## - Text styles (bold, italic, underline)[br] +## - Text effects (e.g., wave)[br] +## - Text alignment[br] +## - Indentation[br] +## [br] +## Two concrete implementations are available:[br] +## - [GdUnitRichTextMessageWriter] writing to a [RichTextLabel][br] +## - [GdUnitCSIMessageWriter] writing to console using CSI codes[br] +## [br] +## Example usage:[br] +## [codeblock] +## writer.color(Color.RED).style(BOLD).println_message("Test failed!") +## writer.color(Color.GREEN).align(Align.RIGHT).print_at("Success", 80) +## [/codeblock] + + +## Text style flag for bold formatting +const BOLD = 0x1 +## Text style flag for italic formatting +const ITALIC = 0x2 +## Text style flag for underline formatting +const UNDERLINE = 0x4 + + +## Represents special text effects that can be applied to the output +enum Effect { + ## No special effect applied + NONE, + ## Applies a wave animation to the text + WAVE +} + + +## Controls text alignment at the specified cursor position +enum Align { + ## Aligns text to the left of the cursor position + LEFT, + ## Aligns text to the right of the cursor position, accounting for text length + RIGHT +} + + +## The current text color to be used for the next output operation +var _current_color := Color.WHITE + +## The current indentation level to be used for the next output operation.[br] +## Each level represents two spaces of indentation. +var _current_indent := 0 + +## The current text style flags (BOLD, ITALIC, UNDERLINE) to be used for the next output operation +var _current_flags := 0 + +## The current text alignment to be used for the next output operation +var _current_align := Align.LEFT + +## The current text effect to be used for the next output operation +var _current_effect := Effect.NONE + + +## Sets the text color for the next output operation.[br] +## [br] +## [param value] The color to be used for the text. +## Returns self for method chaining. +func color(value: Color) -> GdUnitMessageWriter: + _current_color = value + return self + + +## Sets the indentation level for the next output operation.[br] +## [br] +## [param value] The number of indentation levels, where each level equals two spaces. +## Returns self for method chaining. +func indent(value: int) -> GdUnitMessageWriter: + _current_indent = value + return self + + +## Sets text style flags for the next output operation.[br] +## [br] +## [param value] A combination of style flags (BOLD, ITALIC, UNDERLINE). +## Returns self for method chaining. +func style(value: int) -> GdUnitMessageWriter: + _current_flags = value + return self + + +## Sets text effect for the next output operation.[br] +## [br] +## [param value] The effect to apply to the text (NONE, WAVE). +## Returns self for method chaining. +func effect(value: Effect) -> GdUnitMessageWriter: + _current_effect = value + return self + + +## Sets text alignment for the next output operation.[br] +## [br] +## [param value] The alignment to use (LEFT, RIGHT). +## Returns self for method chaining. +func align(value: Align) -> GdUnitMessageWriter: + _current_align = value + return self + + +## Resets all formatting options to their default values.[br] +## [br] +## Defaults:[br] +## - color: Color.WHITE[br] +## - indent: 0[br] +## - flags: 0[br] +## - align: LEFT[br] +## - effect: NONE[br] +## Returns self for method chaining. +func reset() -> GdUnitMessageWriter: + _current_color = Color.WHITE + _current_indent = 0 + _current_flags = 0 + _current_align = Align.LEFT + _current_effect = Effect.NONE + return self + + +## Prints a warning message in golden color.[br] +## [br] +## [param message] The warning message to print. +func prints_warning(message: String) -> void: + color(Color.GOLDENROD).println_message(message) + + +## Prints an error message in crimson color.[br] +## [br] +## [param message] The error message to print. +func prints_error(message: String) -> void: + color(Color.CRIMSON).println_message(message) + + +## Prints a message with current formatting settings.[br] +## [br] +## [param message] The text to print. +func print_message(message: String) -> void: + _print_message(message, _current_color, _current_indent, _current_flags) + reset() + + +## Prints a message with current formatting settings followed by a newline.[br] +## [br] +## [param message] The text to print. +func println_message(message: String) -> void: + _println_message(message, _current_color, _current_indent, _current_flags) + reset() + + +## Prints a message at a specific column position with current formatting settings.[br] +## [br] +## [param message] The text to print.[br] +## [param cursor_pos] The column position where the text should start. +func print_at(message: String, cursor_pos: int) -> void: + _print_at(message, cursor_pos, _current_color, _current_effect, _current_align, _current_flags) + reset() + + +## Internal implementation of print_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param color] The color to use.[br] +## [param indent] The indentation level.[br] +## [param flags] The style flags to apply. +func _print_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + pass + + +## Internal implementation of println_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param color] The color to use.[br] +## [param indent] The indentation level.[br] +## [param flags] The style flags to apply. +func _println_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + pass + + +## Internal implementation of print_at.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param cursor_pos] The column position.[br] +## [param color] The color to use.[br] +## [param effect] The effect to apply.[br] +## [param align] The text alignment.[br] +## [param flags] The style flags to apply. +func _print_at(_message: String, _cursor_pos: int, _color: Color, _effect: Effect, _align: Align, _flags: int) -> void: + pass + + +## Clears all output content.[br] +## [br] +## To be overridden by concrete formatters. +func clear() -> void: + pass diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid index e69de29b..63b470db 100644 --- a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://dlf6gid7myugk diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd index e69de29b..37b6e39d 100644 --- a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd @@ -0,0 +1,115 @@ +@tool +class_name GdUnitRichTextMessageWriter +extends GdUnitMessageWriter +## A message writer implementation using [RichTextLabel] for the test report UI.[br] +## [br] +## This writer implementation writes formatted messages to a [RichTextLabel] using BBCode.[br] +## It supports:[br] +## - Text formatting using BBCode (bold, italic, underline)[br] +## - Text coloring using push colors[br] +## - Text indentation using push indent[br] +## - Text effects like wave[br] +## - Basic cursor positioning[br] +## [br] +## Used to format test reports in the editor UI. + + +## The [RichTextLabel] instance to write formatted messages +var _output: RichTextLabel + +## Tracks current position in characters from line start +var _current_pos := 0 + + +## Creates a new message writer for the given [RichTextLabel].[br] +## [br] +## [param output] The [RichTextLabel] used for output. +func _init(output: RichTextLabel) -> void: + _output = output + + +## Applies text style flags by wrapping text in BBCode tags.[br] +## [br] +## Available styles:[br] +## - BOLD: [b]text[/b][br] +## - ITALIC: [i]text[/i][br] +## - UNDERLINE: [u]text[/u][br] +## [br] +## [param message] The text to format.[br] +## [param flags] The text style flags to apply. +func _apply_flags(message: String, flags: int) -> String: + if flags & BOLD: + message = "[b]%s[/b]" % message + if flags & ITALIC: + message = "[i]%s[/i]" % message + if flags & UNDERLINE: + message = "[u]%s[/u]" % message + return message + + +## Writes a message with formatting.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _print_message(message: String, _color: Color, _indent: int, flags: int) -> void: + for i in _indent: + _output.push_indent(1) + _output.push_color(_color) + message = _apply_flags(message, flags) + _output.append_text(message) + _output.pop() + for i in _indent: + _output.pop() + _current_pos += _indent * 2 + message.length() + + +## Writes a message with formatting followed by a line break.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _println_message(message: String, _color: Color, _indent: int, flags: int) -> void: + _print_message(message, _color, _indent, flags) + _output.newline() + _current_pos = 0 + + +## Writes a message at a specific column position.[br] +## [br] +## [param message] The text to write.[br] +## [param cursor_pos] The column position from line start.[br] +## [param _color] The color to use.[br] +## [param _effect] The text effect to apply (e.g. wave).[br] +## [param _align] The text alignment (left or right).[br] +## [param flags] The text style flags to apply. +func _print_at(message: String, cursor_pos: int, _color: Color, _effect: Effect, _align: Align, flags: int) -> void: + if _align == Align.RIGHT: + cursor_pos = cursor_pos - message.length() + + var spaces := cursor_pos - _current_pos + if spaces > 0: + _output.append_text("".lpad(spaces)) + _current_pos += spaces + else: + _output.append_text(" ") + _current_pos += 1 + + _output.push_color(_color) + message = _apply_flags(message, flags) + match _effect: + Effect.NONE: + pass + Effect.WAVE: + message = "[wave]%s[/wave]" % message + _output.append_text(message) + _output.pop() + _current_pos += message.length() + + +## Clears all written content from the [RichTextLabel]. +func clear() -> void: + _output.clear() + _current_pos = 0 diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid index e69de29b..bd375f58 100644 --- a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://dasu47qjmnlks diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid index e69de29b..6e1eeb71 100644 --- a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid @@ -0,0 +1 @@ +uid://cr0wgxnfgottx diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid index e69de29b..bea59385 100644 --- a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid @@ -0,0 +1 @@ +uid://d4du2nsrdreck diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd b/addons/gdUnit4/src/doubler/CallableDoubler.gd index e69de29b..1bc78a1a 100644 --- a/addons/gdUnit4/src/doubler/CallableDoubler.gd +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd @@ -0,0 +1,156 @@ +## The helper class to allow to double Callable +## Is just a wrapper to the original callable with the same function signature. +## +## Due to interface conflicts between 'Callable' and 'Object', +## it is not possible to stub the 'call' and 'call_deferred' methods. +## +## The Callable interface and the Object class have overlapping method signatures, +## which causes conflicts when attempting to stub these methods. +## As a result, you cannot create stubs for 'call' and 'call_deferred' methods. + +class_name CallableDoubler + + +const doubler_script :Script = preload("res://addons/gdUnit4/src/doubler/CallableDoubler.gd") + +var _cb: Callable + + +func _init(cb: Callable) -> void: + assert(cb!=null, "Invalid argument must not be null") + _cb = cb + +## --- helpers ----------------------------------------------------------------------------------------------------------------------------- +static func map_func_name(method_info: Dictionary) -> String: + return method_info["name"] + + +## We do not want to double all functions based on Object for this class +## Is used on SpyBuilder to excluding functions to be doubled for Callable +static func excluded_functions() -> PackedStringArray: + return ClassDB.class_get_method_list("Object")\ + .map(CallableDoubler.map_func_name)\ + .filter(func (name: String) -> bool: + return !CallableDoubler.callable_functions().has(name)) + + +static func non_callable_functions(name: String) -> bool: + return ![ + # we allow "_init", is need to construct it, + "excluded_functions", + "non_callable_functions", + "callable_functions", + "map_func_name" + ].has(name) + + +## Returns the list of supported Callable functions +static func callable_functions() -> PackedStringArray: + var supported_functions :Array = doubler_script.get_script_method_list()\ + .map(CallableDoubler.map_func_name)\ + .filter(CallableDoubler.non_callable_functions) + # We manually add these functions that we cannot/may not overwrite in this class + supported_functions.append_array(["call_deferred", "callv"]) + return supported_functions + + +## ----------------------------------------------------------------------------------------------------------------------------------------- +## Callable functions stubing +## ----------------------------------------------------------------------------------------------------------------------------------------- + +func bind(...varargs: Array) -> Callable: + _cb = _cb.bindv(varargs) + return _cb + + +func bindv(caller_args: Array) -> Callable: + _cb = _cb.bindv(caller_args) + return _cb + + +@warning_ignore("native_method_override") +func call(...varargs: Array) -> Variant: + return _cb.callv(varargs) + + +# Is not supported, see class description +#func call_deferred(...varargs: Array) -> void: +# return _cb.call_deferred(varargs) + + +# Is not supported, see class description +#func callv(arguments: Array) -> Variant: +# return _cb.callv(arguments) + + +func get_bound_arguments() -> Array: + return _cb.get_bound_arguments() + + +func get_bound_arguments_count() -> int: + return _cb.get_bound_arguments_count() + + +func get_method() -> StringName: + return _cb.get_method() + + +func get_object() -> Object: + return _cb.get_object() + + +func get_object_id() -> int: + return _cb.get_object_id() + + +func hash() -> int: + return _cb.hash() + + +func is_custom() -> bool: + return _cb.is_custom() + + +func is_null() -> bool: + return _cb.is_null() + + +func is_standard() -> bool: + return _cb.is_standard() + + +func is_valid() -> bool: + return _cb.is_valid() + + +func rpc(...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc() + 1: _cb.rpc(varargs[0]) + 2: _cb.rpc(varargs[0], varargs[1]) + 3: _cb.rpc(varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) + + +@warning_ignore("untyped_declaration") +func rpc_id(peer_id: int, ...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc_id(peer_id ) + 1: _cb.rpc_id(peer_id, varargs[0]) + 2: _cb.rpc_id(peer_id, varargs[0], varargs[1]) + 3: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) + +func unbind(argcount: int) -> Callable: + _cb = _cb.unbind(argcount) + return _cb diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid index e69de29b..f29cf556 100644 --- a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid @@ -0,0 +1 @@ +uid://dh3830b0e71kh diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd index e69de29b..d9459dc3 100644 --- a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd @@ -0,0 +1,4 @@ +@abstract class_name GdFunctionDoubler +extends RefCounted + +@abstract func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid index e69de29b..9b7e3cb7 100644 --- a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://bdwnfd8fqrube diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd index e69de29b..31aa06bc 100644 --- a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd @@ -0,0 +1,119 @@ +# A class doubler used to mock and spy checked implementations +class_name GdUnitClassDoubler +extends RefCounted + +const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" +const EXCLUDE_VIRTUAL_FUNCTIONS = [ + # we have to exclude notifications because NOTIFICATION_PREDELETE is try + # to delete already freed spy/mock resources and will result in a conflict + "_notification", + "notification", + # https://github.com/godotengine/godot/issues/67461 + "get_name", + "get_path", + "duplicate", + ] +# define functions to be exclude when spy or mock checked a scene +const EXLCUDE_SCENE_FUNCTIONS = [ + # needs to exclude get/set script functions otherwise it endsup in recursive endless loop + "set_script", + "get_script", + # needs to exclude otherwise verify fails checked collection arguments checked calling to string + "_to_string", +] +const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"] + + +static func check_leaked_instances() -> void: + ## we check that all registered spy/mock instances are removed from the engine meta data + for key in Engine.get_meta_list(): + if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): + var instance: Variant = Engine.get_meta(key) + push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + await (Engine.get_main_loop() as SceneTree).process_frame + + +# loads the doubler template +# class_info = { "class_name": <>, "class_path" : <>} +static func load_template(template: String, class_info: Dictionary) -> PackedStringArray: + var clazz_name: String = class_info.get("class_name") + var source_code := template\ + .replace("${source_class}", clazz_name)\ + # Replace template class_name DoubledClass with source class name + .replace("SourceClassName", clazz_name.replace(".", "_")) + var lines := GdScriptParser.to_unix_format(source_code).split("\n") + @warning_ignore("return_value_discarded") + lines.insert(1, extends_clazz(class_info)) + lines.insert(0, "@warning_ignore_start('unsafe_call_argument', 'shadowed_variable', 'untyped_declaration', 'native_method_override', 'int_as_enum_without_cast')") + return lines + + +static func extends_clazz(class_info: Dictionary) -> String: + var clazz_name: String = class_info.get("class_name") + var clazz_path: PackedStringArray = class_info.get("class_path", []) + # is inner class? + if clazz_path.size() > 1: + return "extends %s" % clazz_name + if clazz_path.size() == 1 and clazz_path[0].ends_with(".gd"): + return "extends '%s'" % clazz_path[0] + return "extends %s" % clazz_name + + +# double all functions of given instance +static func double_functions(instance: Object, clazz_name: String, clazz_path: PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions: Array) -> PackedStringArray: + var doubled_source := PackedStringArray() + var parser := GdScriptParser.new() + var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions + var functions := Array() + + # double script functions + if not ClassDB.class_exists(clazz_name): + var result := parser.parse(clazz_name, clazz_path) + if result.is_error(): + push_error(result.error_message()) + return PackedStringArray() + var class_descriptor: GdClassDescriptor = result.value() + for func_descriptor in class_descriptor.functions(): + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + doubled_source += func_doubler.double(func_descriptor) + functions.append(func_descriptor.name()) + + # double regular class functions + var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path) + for method: Dictionary in clazz_functions: + var func_descriptor := GdFunctionDescriptor.extract_from(method) + # exclude private core functions + if func_descriptor.is_private(): + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + # GD-110: Hotfix do not double invalid engine functions + if is_invalid_method_descriptior(method): + #prints("'%s': invalid method descriptor found! %s" % [clazz_name, method]) + continue + # do not double on not implemented virtual functions + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + functions.append(func_descriptor.name()) + doubled_source.append_array(func_doubler.double(func_descriptor)) + return doubled_source + + +# GD-110 +static func is_invalid_method_descriptior(method: Dictionary) -> bool: + var return_info: Dictionary = method["return"] + var type: int = return_info["type"] + var usage: int = return_info["usage"] + var clazz_name: String = return_info["class_name"] + # is method returning a type int with a given 'class_name' we have an enum + # and the PROPERTY_USAGE_CLASS_IS_ENUM must be set + if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM): + return true + if clazz_name == "Variant.Type": + return true + return false diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid index e69de29b..de295008 100644 --- a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid @@ -0,0 +1 @@ +uid://2y4l1xykubtq diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd index e69de29b..a1a75f11 100644 --- a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd @@ -0,0 +1,336 @@ +class_name GdUnitFunctionDoublerBuilder +extends RefCounted + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + +const DEFAULT_TYPED_RETURN_VALUES := { + TYPE_NIL: "null", + TYPE_BOOL: "false", + TYPE_INT: "0", + TYPE_FLOAT: "0.0", + TYPE_STRING: "\"\"", + TYPE_STRING_NAME: "&\"\"", + TYPE_VECTOR2: "Vector2.ZERO", + TYPE_VECTOR2I: "Vector2i.ZERO", + TYPE_RECT2: "Rect2()", + TYPE_RECT2I: "Rect2i()", + TYPE_VECTOR3: "Vector3.ZERO", + TYPE_VECTOR3I: "Vector3i.ZERO", + TYPE_VECTOR4: "Vector4.ZERO", + TYPE_VECTOR4I: "Vector4i.ZERO", + TYPE_TRANSFORM2D: "Transform2D()", + TYPE_PLANE: "Plane()", + TYPE_QUATERNION: "Quaternion()", + TYPE_AABB: "AABB()", + TYPE_BASIS: "Basis()", + TYPE_TRANSFORM3D: "Transform3D()", + TYPE_PROJECTION: "Projection()", + TYPE_COLOR: "Color()", + TYPE_NODE_PATH: "NodePath()", + TYPE_RID: "RID()", + TYPE_OBJECT: "null", + TYPE_CALLABLE: "Callable()", + TYPE_SIGNAL: "Signal()", + TYPE_DICTIONARY: "Dictionary()", + TYPE_ARRAY: "Array()", + TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()", + TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()", + TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()", + TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()", + TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()", + TYPE_PACKED_STRING_ARRAY: "PackedStringArray()", + TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()", + TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()", + TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array()", + TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()", + GdObjects.TYPE_VARIANT: "null", + GdObjects.TYPE_ENUM: "0" +} + + +# @GlobalScript enums +# needs to manually map because of https://github.com/godotengine/godot/issues/73835 +const DEFAULT_ENUM_RETURN_VALUES = { + "Side" : "SIDE_LEFT", + "Corner" : "CORNER_TOP_LEFT", + "Orientation" : "HORIZONTAL", + "ClockDirection" : "CLOCKWISE", + "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT", + "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP", + "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO", + "EulerOrder" : "EULER_ORDER_XYZ", + "Key" : "KEY_NONE", + "KeyModifierMask" : "KEY_CODE_MASK", + "MouseButton" : "MOUSE_BUTTON_NONE", + "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT", + "JoyButton" : "JOY_BUTTON_INVALID", + "JoyAxis" : "JOY_AXIS_INVALID", + "MIDIMessage" : "MIDI_MESSAGE_NONE", + "Error" : "OK", + "PropertyHint" : "PROPERTY_HINT_NONE", + "Variant.Type" : "TYPE_NIL", + "Vector2.Axis" : "Vector2.AXIS_X", + "Vector2i.Axis" : "Vector2i.AXIS_X", + "Vector3.Axis" : "Vector3.AXIS_X", + "Vector3i.Axis" : "Vector3i.AXIS_X", + "Vector4.Axis" : "Vector4.AXIS_X", + "Vector4i.Axis" : "Vector4i.AXIS_X", +} + + +static var def_constructor := """ + func _init({constructor_args}) -> void: + __init_doubler() + super({args}) + """.dedent() + + +static var def_verify_block := """ + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("{func_name}", __args) + {default_return} + else: + __verifier.save_function_interaction("{func_name}", __args) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_prepare_block := """ + if __is_prepare_return_value(): + __save_function_return_value("{func_name}", __args) + {default_return} + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_prepare_block := """ + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return __return_mock_value("{func_name}", __args, {default_return}) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return + """.dedent().indent("\t").trim_suffix("\n") + + +var fd: GdFunctionDescriptor +var func_args: Array +var default_return: String +var verify_block: String = "" +var prepare_block: String = "" +var mock_return: String = "" + + +func _init(descriptor: GdFunctionDescriptor) -> void: + # verify all default types are covered + for type_key in TYPE_MAX: + if not DEFAULT_TYPED_RETURN_VALUES.has(type_key): + push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX]) + prints("missing default definition for type", type_key) + assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!") + + fd = descriptor + func_args = argument_names() + default_return = default_return_value() + + +func build_func_signature() -> String: + var return_type := ":" if fd._return_type == TYPE_VARIANT else " -> %s:" % fd.return_type_as_string() + return "{static}func {func_name}({args}){return_type}".format({ + "static" : "static " if fd.is_static() else "", + "func_name": fd.name(), + "args": arguments_full_quilified(), + "return_type": return_type + }) + + +func arguments_full_quilified() -> String: + var collect := PackedStringArray() + for arg in fd.args(): + var name := argument_name(arg) + if arg.has_default(): + var signature := "{argument_name}{arg_typed}={arg_value}".format({ + "argument_name" : name, + "arg_typed" : ":"+GdObjects.type_as_string(arg.type()) if arg.type() == GdObjects.TYPE_VARIANT else "", + "arg_value" : arg.value_as_string() + }) + collect.push_back(signature) + else: + collect.push_back(name) + if fd.is_vararg(): + var arg_descriptor := fd.varargs()[0] + collect.push_back("...%s_: Array" % arg_descriptor.name()) + return ", ".join(collect) + + +func argument_name(arg: GdFunctionArgument) -> String: + return arg.name() + "_" + + +func argument_names() -> PackedStringArray: + return fd.args().map(argument_name) + + +func argument_default(arg :GdFunctionArgument) -> String: + return (arg.value_as_string() + if arg.has_default() + else DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null")) + + +func build_constructor_arguments() -> String: + var arguments := PackedStringArray() + for arg in fd.args(): + var default_value := argument_default(arg) + var arg_signature := "{name}:{type}={default}".format({ + "name" : argument_name(arg), + "type" : "Variant" if default_value == "null" else "", + "default" : default_value + }) + arguments.append(arg_signature) + if fd.is_vararg(): + arguments.append("...varargs: Array") + return ", ".join(arguments) + + +func build_arguments() -> String: + return "\tvar __args := [{args}]{varargs}".format({ + "args" : ", ".join(func_args), + "varargs" : " + varargs_" if fd.is_vararg() else "" + }) + + +func build_super_calls() -> String: + if !fd.is_vararg(): + return 'super(%s)\n' % ", ".join(func_args) + + var match_block := "match varargs_.size():\n" + for index in range(0, 11): + match_block += '{index}: super({args})\n'.format({ + "index" : index, + "args" : ", ".join(func_args + build_vararg_list(index)) + }).indent("\t") + match_block += '_: push_error("To many varradic arguments.")\n'.indent("\t") + match_block += "return\n" if is_void_func() else "return %s\n" % default_return + return match_block + + +func build_vararg_list(count: int) -> Array: + var arg_list := [] + for index in count: + arg_list.append("varargs_[%d]" % index) + return arg_list + + +func default_return_value() -> String: + var return_type: Variant = fd.return_type() + if return_type == GdObjects.TYPE_ENUM: + var enum_class := fd._return_class + if DEFAULT_ENUM_RETURN_VALUES.has(enum_class): + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + + var enum_path := enum_class.split(".") + if enum_path.size() >= 2: + var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1]) + if not keys.is_empty(): + return "%s.%s" % [enum_path[0], keys[0]] + var enum_value: Variant = get_enum_default(enum_class) + if enum_value != null: + return str(enum_value) + # we need fallback for @GlobalScript enums, + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid") + + +# Determine the enum default by reflection +func get_enum_default(value: String) -> Variant: + var script := GDScript.new() + script.source_code = """ + extends RefCounted + + static func get_enum_default() -> Variant: + return %s.values()[0] + + """.dedent() % value + var err := script.reload() + if err != OK: + push_error("Cant get enum values form '%s', %s" % [value, error_string(err)]) + return 0 + @warning_ignore("unsafe_method_access") + return script.new().call("get_enum_default") + + +func is_void_func() -> bool: + return fd.return_type() == TYPE_NIL or fd.return_type() == TYPE_VOID + + +func with_verify_block() -> GdUnitFunctionDoublerBuilder: + verify_block = def_verify_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_prepare_block() -> GdUnitFunctionDoublerBuilder: + if fd.return_type() == TYPE_NIL or fd.return_type() == GdObjects.TYPE_VOID: + prepare_block = def_void_prepare_block + return self + + prepare_block = def_prepare_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_mocked_return_value() -> GdUnitFunctionDoublerBuilder: + if is_void_func(): + mock_return = def_void_mock_return.format({ + "func_name" : fd.name(), + }) + else: + mock_return = def_mock_return.format({ + "func_name" : fd.name(), + "default_return" : '"no_arg"' if is_void_func() else default_return + }) + return self + + +func build() -> PackedStringArray: + if fd.name() == "_init": + return [def_constructor.format({ + "constructor_args" : build_constructor_arguments(), + "args" : ", ".join(func_args) + })] + + var func_body: PackedStringArray = [] + func_body.append(build_func_signature()) + func_body.append(build_arguments()) + if not prepare_block.is_empty(): + func_body.append(prepare_block) + func_body.append(verify_block) + if not mock_return.is_empty(): + func_body.append(mock_return) + func_body.append("") + var super_calls := build_super_calls() + if not is_void_func(): + super_calls = super_calls.replace("super(", "return super(" ) + if fd.is_coroutine(): + super_calls = super_calls.replace("super(", "await super(" ) + func_body.append(super_calls.indent("\t")) + return func_body diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid index e69de29b..6e05ecf9 100644 --- a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid @@ -0,0 +1 @@ +uid://kt1awdql6r3j diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd index e69de29b..d735bc56 100644 --- a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd @@ -0,0 +1,10 @@ +class_name GdUnitMockFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_prepare_block()\ + .with_verify_block()\ + .with_mocked_return_value()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid index e69de29b..f0b9ae11 100644 --- a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://bvr3yq1djn4af diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd index e69de29b..789eb327 100644 --- a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd @@ -0,0 +1,53 @@ +class_name GdUnitObjectInteractions +extends RefCounted + + +static func verify(interaction_object: Object, interactions_times: int) -> Variant: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).do_verify_interactions(interactions_times) + return interaction_object + + +static func verify_no_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool.report_success() + + var summary := _get_verifier(interaction_object).verify_no_interactions() + if summary.is_empty(): + return assert_tool.report_success() + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func verify_no_more_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool + + var summary := _get_verifier(interaction_object).verify_no_more_interactions() + if summary.is_empty(): + return assert_tool + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func reset(interaction_object: Object) -> Object: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).reset_interactions() + return interaction_object + + +static func _is_mock_or_spy(instance: Object) -> bool: + if instance != null and instance.has_method("__get_verifier"): + return true + + push_error("Error: The given object '%s' is not a mock or spy instance!" % instance) + return false + + +static func _get_verifier(interaction_object: Object) -> GdUnitObjectInteractionsVerifier: + @warning_ignore("unsafe_method_access") + return interaction_object.__get_verifier() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid index e69de29b..2eec03e9 100644 --- a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid @@ -0,0 +1 @@ +uid://b1jlomyn5tf7t diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd index e69de29b..36aa9783 100644 --- a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd @@ -0,0 +1,84 @@ +class_name GdUnitObjectInteractionsVerifier + +var expected_interactions: int = -1 +var saved_interactions := Dictionary() +var verified_interactions := Array() + + +func save_function_interaction(func_name: String, args :Array[Variant]) -> void: + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + saved_interactions[key] += 1 + return + saved_interactions[function_args] = 1 + + +func is_verify_interactions() -> bool: + return expected_interactions != -1 + + +func do_verify_interactions(interactions_times: int = 1) -> void: + expected_interactions = interactions_times + + +func verify_interactions(func_name: String, args: Array[Variant]) -> void: + var summary := Dictionary() + var total_interactions := 0 + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + var interactions: int = saved_interactions.get(key, 0) + total_interactions += interactions + summary[key] = interactions + # add as verified + verified_interactions.append(key) + + var assert_tool := GdUnitAssertImpl.new("") + if total_interactions != expected_interactions: + var __expected_summary := {function_args : expected_interactions} + var error_message: String + # if no interactions macht collect not verified interactions for failure report + if summary.is_empty(): + var __current_summary := verify_no_more_interactions() + error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) + else: + error_message = GdAssertMessages.error_validate_interactions(summary, __expected_summary) + @warning_ignore("return_value_discarded") + assert_tool.report_error(error_message) + else: + @warning_ignore("return_value_discarded") + assert_tool.report_success() + expected_interactions = -1 + + +func verify_no_interactions() -> Dictionary: + var summary := Dictionary() + if not saved_interactions.is_empty(): + for index in saved_interactions.keys().size(): + var func_call: Variant = saved_interactions.keys()[index] + summary[func_call] = saved_interactions[func_call] + return summary + + +func verify_no_more_interactions() -> Dictionary: + var summary := Dictionary() + var called_functions: Array[Variant] = saved_interactions.keys() + if called_functions != verified_interactions: + # collect the not verified functions + var called_but_not_verified := called_functions.duplicate() + for index in verified_interactions.size(): + called_but_not_verified.erase(verified_interactions[index]) + + for index in called_but_not_verified.size(): + var not_verified: Variant = called_but_not_verified[index] + summary[not_verified] = saved_interactions[not_verified] + return summary + + +func reset_interactions() -> void: + saved_interactions.clear() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid index e69de29b..b53e17aa 100644 --- a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid @@ -0,0 +1 @@ +uid://cjwrb8kjh55bh diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd index e69de29b..091241da 100644 --- a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd @@ -0,0 +1,8 @@ +class_name GdUnitSpyFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_verify_block()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid index e69de29b..e41989b0 100644 --- a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://b2uheqy6cfcn2 diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd index e69de29b..124d3d4e 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -0,0 +1,73 @@ +# This class defines a value extractor by given function name and args +class_name GdUnitFuncValueExtractor +extends GdUnitValueExtractor + +var _func_names :PackedStringArray +var _args :Array + +func _init(func_name :String, p_args :Array) -> void: + _func_names = func_name.split(".") + _args = p_args + + +func func_names() -> PackedStringArray: + return _func_names + + +func args() -> Array: + return _args + + +# Extracts a value by given `func_name` and `args`, +# Allows to use a chained list of functions setarated ba a dot. +# e.g. "func_a.func_b.name" +# do calls instance.func_a().func_b().name() and returns finally the name +# If a function returns an array, all elements will by collected in a array +# e.g. "get_children.get_name" checked a node +# do calls node.get_children() for all childs get_name() and returns all names in an array +# +# if the value not a Object or not accesible be `func_name` the value is converted to `"n.a."` +# expecing null values +func extract_value(value: Variant) -> Variant: + if value == null: + return null + for func_name in func_names(): + if GdArrayTools.is_array_type(value): + var values := Array() + @warning_ignore("unsafe_cast") + for element: Variant in (value as Array): + values.append(_call_func(element, func_name)) + value = values + else: + value = _call_func(value, func_name) + var type := typeof(value) + if type == TYPE_STRING_NAME: + return str(value) + if type == TYPE_STRING and value == "n.a.": + return value + return value + + +func _call_func(value :Variant, func_name :String) -> Variant: + # for array types we need to call explicit by function name, using funcref is only supported for Objects + # TODO extend to all array functions + if GdArrayTools.is_array_type(value) and func_name == "empty": + @warning_ignore("unsafe_cast") + return (value as Array).is_empty() + + if is_instance_valid(value): + # extract from function + var obj_value: Object = value + if obj_value.has_method(func_name): + var extract := Callable(obj_value, func_name) + if extract.is_valid(): + return obj_value.call(func_name) if args().is_empty() else obj_value.callv(func_name, args()) + else: + # if no function exists than try to extract form parmeters + var parameter: Variant = obj_value.get(func_name) + if parameter != null: + return parameter + # nothing found than return 'n.a.' + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) + return "n.a." diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid index e69de29b..f576491b 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid @@ -0,0 +1 @@ +uid://dcaqmjqxw1urv diff --git a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd index e69de29b..906ee2c5 100644 --- a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd @@ -0,0 +1,24 @@ +## A fuzzer that generates random boolean values for testing.[br] +## +## This is useful for testing code paths that +## depend on boolean conditions, flags, or toggle states.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## func test_toggle_feature(fuzzer := BoolFuzzer.new(), _fuzzer_iterations = 100): +## var enabled := fuzzer.next_value() +## my_feature.set_enabled(enabled) +## assert_bool(my_feature.is_enabled()),is_equal(enabled) +## [/codeblock] +class_name BoolFuzzer +extends Fuzzer + + +## Generates a random boolean value.[br] +## +## Returns either [code]true[/code] or [code]false[/code] with equal probability. +## This method is called automatically during fuzz testing iterations.[br] +## +## @returns A randomly generated boolean value ([code]true[/code] or [code]false[/code]). +func next_value() -> bool: + return randi() % 2 diff --git a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid index e69de29b..5e31e4ba 100644 --- a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid @@ -0,0 +1 @@ +uid://crq46j53dic2o diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd index e69de29b..5da1a777 100644 --- a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd @@ -0,0 +1,37 @@ +## A fuzzer that generates random floating-point values within a specified range.[br] +## +## This is particularly useful for testing numerical calculations, +## physics simulations, shader parameters, or any code that processes floating-point +## values.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## func test_calculate_damage(fuzzer := FloatFuzzer.new(0.0, 100.0), _fuzzer_iterations := 500): +## var damage := fuzzer.next_value() +## var result = calculate_damage_reduction(damage) +## assert_float(result).is_between(0.0, damage) +## [/codeblock] +## [br] +## [b]Note:[/b] The range is inclusive on both ends, and values are uniformly distributed. +class_name FloatFuzzer +extends Fuzzer + +## Minimum value (inclusive) for generated floats. +var _from: float = 0 +## Maximum value (inclusive) for generated floats. +var _to: float = 0 + +func _init(from: float, to: float) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random float value within the configured range.[br] +## +## Returns a uniformly distributed random float between [member _from] and +## [member _to] (inclusive). Each call produces a new random value.[br] +## +## @returns A random float value within the specified range. +func next_value() -> float: + return randf_range(_from, _to) diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid index e69de29b..c03fe9fe 100644 --- a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid @@ -0,0 +1 @@ +uid://drahq5ep3dw4s diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Fuzzer.gd index e69de29b..e4e597d0 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -0,0 +1,80 @@ +## Base interface for fuzz testing.[br] +## +## Fuzzer is an abstract base class that provides the foundation for creating +## custom fuzzers used in automated testing. Fuzz testing (fuzzing) is a software +## testing technique that involves providing invalid, unexpected, or random data +## as inputs to a program to find bugs and potential security vulnerabilities. +## [br][br] +## To use a fuzzer in your test cases, add optional parameters to your test function: +## [codeblock] +## func test_foo(fuzzer := Fuzzers.randomInt(), _fuzzer_iterations := 10, _fuzzer_seed := 12345): +## var value := fuzzer.next_value() +## # Test logic using the fuzzed value +## [/codeblock] +## [br] +## @tutorial(Fuzzing on Wikipedia): https://en.wikipedia.org/wiki/Fuzzing +@abstract +class_name Fuzzer +extends RefCounted + +## Default number of iterations for fuzz testing when not specified. +const ITERATION_DEFAULT_COUNT := 1000 +## Parameter name for passing the fuzzer instance to test functions. +const ARGUMENT_FUZZER_INSTANCE := "fuzzer" +## Parameter name for specifying the number of iterations in test functions. +const ARGUMENT_ITERATIONS := "fuzzer_iterations" +## Parameter name for specifying the random seed in test functions. +const ARGUMENT_SEED := "fuzzer_seed" + +## Current iteration index during fuzzing execution. +var _iteration_index := 0 +## Maximum number of iterations to run for this fuzzer. +var _iteration_limit := ITERATION_DEFAULT_COUNT + + +## Generates the next fuzz value.[br] +## +## This abstract method must be implemented by derived classes to provide +## the specific fuzzing logic for generating test values.[br] +## +## [b]Example implementation:[/b] +## [codeblock] +## func next_value() -> int: +## return randi_range(0, 100) +## [/codeblock] +## +## @returns The next generated fuzz value. The type depends on the specific fuzzer implementation. +@abstract +func next_value() -> Variant + + +## Returns the current iteration index.[br] +## +## Useful for tracking progress during fuzzing or for debugging purposes +## when a specific iteration causes a failure.[br] +## +## [b]Example:[/b] +## [codeblock] +## if fuzzer.iteration_index() % 100 == 0: +## print("Processed %d iterations" % fuzzer.iteration_index()) +## [/codeblock] +## +## @returns The current iteration index, starting from 0. +func iteration_index() -> int: + return _iteration_index + + +## Returns the maximum number of iterations for this fuzzer.[br] +## +## This value determines how many times the fuzzer will generate values +## during a test run. It can be overridden by the [code]fuzzer_iterations[/code] +## parameter in test functions.[br] +## +## [b]Example:[/b] +## [codeblock] +## print("Running %d fuzzing iterations" % fuzzer.iteration_limit()) +## [/codeblock] +## +## @returns The maximum number of iterations to be executed. +func iteration_limit() -> int: + return _iteration_limit diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid index e69de29b..e1b39247 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://1aff57gewcrx diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd index e69de29b..4100d514 100644 --- a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd @@ -0,0 +1,77 @@ +## A fuzzer that generates random integer values with optional even/odd constraints.[br] +## +## It supports three modes: normal (any integer), even-only, +## and odd-only generation. This is useful for testing array indices, loop counters, +## enumeration values, or any code that processes integer values.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test with any integer in range +## func test_array_access(fuzzer = IntFuzzer.new(0, 99), fuzzer_iterations = 100): +## var index = fuzzer.next_value() +## var array = create_array(100) +## assert(array[index] != null) +## +## # Test with only even numbers +## func test_even_processing(fuzzer := IntFuzzer.new(0, 100, IntFuzzer.EVEN)): +## var even_num := fuzzer.next_value() +## assert_int(even_num % 2).is_equal(0) +## [/codeblock] +class_name IntFuzzer +extends Fuzzer + + +## Generates any integer within the range. +enum { + NORMAL, ## Generate any integer within the specified range. + EVEN, ## Generate only even integers within the specified range. + ODD ## Generate only odd integers within the specified range. +} + + +## Minimum value (inclusive) for generated integers. +var _from: int = 0 +## Maximum value (inclusive) for generated integers. +var _to: int = 0 +## Generation mode: NORMAL, EVEN, or ODD. +var _mode: int = NORMAL + + +func _init(from: int, to: int, mode: int = NORMAL) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + _mode = mode + + +## Generates a random integer value based on the configured mode.[br] +## +## Returns a random integer between [member _from] and [member _to] (inclusive).[br] +## The value will be constrained according to the [member _mode]:[br] +## - [constant NORMAL]: Any integer in the range[br] +## - [constant EVEN]: Only even integers[br] +## - [constant ODD]: Only odd integers[br] +## +## [b]Example:[/b] +## [codeblock] +## var normal_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.NORMAL) +## var even_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.EVEN) +## var odd_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.ODD) +## +## print(normal_fuzzer.next_value()) # Could be any: 1, 2, 3, ..., 10 +## print(even_fuzzer.next_value()) # Only even: 2, 4, 6, 8, 10 +## print(odd_fuzzer.next_value()) # Only odd: 1, 3, 5, 7, 9 +## [/codeblock] +## +## @returns A random integer value within the specified range and mode +func next_value() -> int: + var value := randi_range(_from, _to) + match _mode: + NORMAL: + return value + EVEN: + return int((value / 2.0) * 2) + ODD: + return int((value / 2.0) * 2 + 1) + _: + return value diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid index e69de29b..27a2f4ef 100644 --- a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid @@ -0,0 +1 @@ +uid://ckj3hkhc86176 diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid index e69de29b..ca1b1198 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid @@ -0,0 +1 @@ +uid://cowpncn3c1e3j diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd index e69de29b..70ef31b7 100644 --- a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd @@ -0,0 +1,45 @@ +## A fuzzer that generates random Vector2 values within a specified rectangular range.[br] +## +## This is particularly useful for testing 2D physics, movement +## systems, UI positioning, sprite coordinates, or any code that processes 2D vectors.[br] +## +## The fuzzer generates vectors where each component (x, y) is independently randomized +## within its respective range, creating a uniform distribution over the rectangular area.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test 2D movement within screen bounds +## func test_movement(fuzzer := Vector2Fuzzer.new(Vector2.ZERO, Vector2(1920, 1080)), _fuzzer_iterations := 200) -> void: +## var position := fuzzer.next_value() +## player.set_position(position) +## +## [/codeblock] +class_name Vector2Fuzzer +extends Fuzzer + + +## Minimum bounds for the generated vectors (inclusive for both x and y). +var _from: Vector2 +## Maximum bounds for the generated vectors (inclusive for both x and y). +var _to: Vector2 + + +func _init(from: Vector2, to: Vector2) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random Vector2 within the configured rectangular range.[br] +## +## Returns a Vector2 where each component is independently randomized:[br] +## - x: random float between [code]_from.x[/code] and [code]_to.x[/code][br] +## - y: random float between [code]_from.y[/code] and [code]_to.y[/code][br] +## +## The distribution is uniform over the rectangular area defined by the bounds.[br] +## +## @returns A random Vector2 within the specified range. +func next_value() -> Vector2: + var x := randf_range(_from.x, _to.x) + var y := randf_range(_from.y, _to.y) + return Vector2(x, y) diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid index e69de29b..38157ad5 100644 --- a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://d1rqf80tw6mye diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd index e69de29b..b54870d1 100644 --- a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd @@ -0,0 +1,48 @@ +## A fuzzer that generates random Vector3 values within a specified box range.[br] +## +## This is particularly useful for testing 3D physics, spatial +## positioning, camera systems, particle effects, or any code that processes 3D vectors.[br] +## +## The fuzzer generates vectors where each component (x, y, z) is independently +## randomized within its respective range, creating a uniform distribution over the +## 3D box volume.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test 3D object placement within world bounds +## func test_spawn_position(fuzzer := Vector3Fuzzer.new(Vector3(-100, 0, -100), Vector3(100, 50, 100)), _fuzzer_iterations := 300): +## var position := fuzzer.next_value() +## var object = spawn_object(position) +## +## [/codeblock] +class_name Vector3Fuzzer +extends Fuzzer + + +## Minimum bounds for the generated vectors (inclusive for x, y, and z). +var _from: Vector3 +## Maximum bounds for the generated vectors (inclusive for x, y, and z). +var _to: Vector3 + + +func _init(from: Vector3, to: Vector3) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random Vector3 within the configured box range.[br] +## +## Returns a Vector3 where each component is independently randomized:[br] +## - x: random float between [code]_from.x[/code] and [code]_to.x[/code][br] +## - y: random float between [code]_from.y[/code] and [code]_to.y[/code][br] +## - z: random float between [code]_from.z[/code] and [code]_to.z[/code][br] +## +## The distribution is uniform over the 3D box volume defined by the bounds.[br] +## +## @returns A random Vector3 within the specified range. +func next_value() -> Vector3: + var x := randf_range(_from.x, _to.x) + var y := randf_range(_from.y, _to.y) + var z := randf_range(_from.z, _to.z) + return Vector3(x, y, z) diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid index e69de29b..8a09af7d 100644 --- a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://c14v7yn5r6b8f diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd index e69de29b..bd503130 100644 --- a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd @@ -0,0 +1,11 @@ +class_name AnyArgumentMatcher +extends GdUnitArgumentMatcher + + +@warning_ignore("unused_parameter") +func is_match(value :Variant) -> bool: + return true + + +func _to_string() -> String: + return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid index e69de29b..a131d485 100644 --- a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://dx684y48ctd3n diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd index e69de29b..ba34431c 100644 --- a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd @@ -0,0 +1,50 @@ +class_name AnyBuildInTypeArgumentMatcher +extends GdUnitArgumentMatcher + +var _type : PackedInt32Array = [] + + +func _init(type :PackedInt32Array) -> void: + _type = type + + +func is_match(value :Variant) -> bool: + return _type.has(typeof(value)) + + +func _to_string() -> String: + match _type[0]: + TYPE_BOOL: return "any_bool()" + TYPE_STRING, TYPE_STRING_NAME: return "any_string()" + TYPE_INT: return "any_int()" + TYPE_FLOAT: return "any_float()" + TYPE_COLOR: return "any_color()" + TYPE_VECTOR2: return "any_vector2()" if _type.size() == 1 else "any_vector()" + TYPE_VECTOR2I: return "any_vector2i()" + TYPE_VECTOR3: return "any_vector3()" + TYPE_VECTOR3I: return "any_vector3i()" + TYPE_VECTOR4: return "any_vector4()" + TYPE_VECTOR4I: return "any_vector4i()" + TYPE_RECT2: return "any_rect2()" + TYPE_RECT2I: return "any_rect2i()" + TYPE_PLANE: return "any_plane()" + TYPE_QUATERNION: return "any_quat()" + TYPE_AABB: return "any_aabb()" + TYPE_BASIS: return "any_basis()" + TYPE_TRANSFORM2D: return "any_transform_2d()" + TYPE_TRANSFORM3D: return "any_transform_3d()" + TYPE_NODE_PATH: return "any_node_path()" + TYPE_RID: return "any_rid()" + TYPE_OBJECT: return "any_object()" + TYPE_DICTIONARY: return "any_dictionary()" + TYPE_ARRAY: return "any_array()" + TYPE_PACKED_BYTE_ARRAY: return "any_packed_byte_array()" + TYPE_PACKED_INT32_ARRAY: return "any_packed_int32_array()" + TYPE_PACKED_INT64_ARRAY: return "any_packed_int64_array()" + TYPE_PACKED_FLOAT32_ARRAY: return "any_packed_float32_array()" + TYPE_PACKED_FLOAT64_ARRAY: return "any_packed_float64_array()" + TYPE_PACKED_STRING_ARRAY: return "any_packed_string_array()" + TYPE_PACKED_VECTOR2_ARRAY: return "any_packed_vector2_array()" + TYPE_PACKED_VECTOR3_ARRAY: return "any_packed_vector3_array()" + TYPE_PACKED_COLOR_ARRAY: return "any_packed_color_array()" + _: return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid index e69de29b..f496acb5 100644 --- a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://ba1akty5ih1xe diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd index e69de29b..b5e3de3a 100644 --- a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd @@ -0,0 +1,32 @@ +class_name AnyClazzArgumentMatcher +extends GdUnitArgumentMatcher + +var _clazz :Object + + +func _init(clazz :Object) -> void: + _clazz = clazz + + +func is_match(value :Variant) -> bool: + if typeof(value) != TYPE_OBJECT: + return false + if is_instance_valid(value) and GdObjects.is_script(_clazz): + @warning_ignore("unsafe_cast") + return (value as Object).get_script() == _clazz + return is_instance_of(value, _clazz) + + +func _to_string() -> String: + if (_clazz as Object).is_class("GDScriptNativeClass"): + @warning_ignore("unsafe_method_access") + var instance :Object = _clazz.new() + var clazz_name := instance.get_class() + if not instance is RefCounted: + instance.free() + return "any_class(<"+clazz_name+">)"; + if _clazz is GDScript: + var result := GdObjects.extract_class_name(_clazz) + if result.is_success(): + return "any_class(<"+ result.value() + ">)" + return "any_class()" diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid index e69de29b..3424f142 100644 --- a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://bieqh1kv1i6e4 diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd index e69de29b..f779bd79 100644 --- a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name ChainedArgumentMatcher +extends GdUnitArgumentMatcher + +var _matchers :Array + + +func _init(matchers :Array) -> void: + _matchers = matchers + + +func is_match(arguments :Variant) -> bool: + var arg_array: Array = arguments + if arg_array == null or arg_array.size() != _matchers.size(): + return false + + for index in arg_array.size(): + var arg: Variant = arg_array[index] + var matcher: GdUnitArgumentMatcher = _matchers[index] + + if not matcher.is_match(arg): + return false + return true diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid index e69de29b..7f366826 100644 --- a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://cdqv8eh1coshc diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd index e69de29b..2d387edc 100644 --- a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name EqualsArgumentMatcher +extends GdUnitArgumentMatcher + +var _current :Variant +var _auto_deep_check_mode :bool + + +func _init(current :Variant, auto_deep_check_mode := false) -> void: + _current = current + _auto_deep_check_mode = auto_deep_check_mode + + +func is_match(value :Variant) -> bool: + var case_sensitive_check := true + return GdObjects.equals(_current, value, case_sensitive_check, compare_mode(value)) + + +func compare_mode(value :Variant) -> GdObjects.COMPARE_MODE: + if _auto_deep_check_mode and is_instance_valid(value): + # we do deep check on all InputEvent's + return GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST if value is InputEvent else GdObjects.COMPARE_MODE.OBJECT_REFERENCE + return GdObjects.COMPARE_MODE.OBJECT_REFERENCE diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid index e69de29b..25a33c8f 100644 --- a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://7iihl6fyxqtq diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd index e69de29b..d5adc4bb 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd @@ -0,0 +1,13 @@ +## The base class of all argument matchers +class_name GdUnitArgumentMatcher +extends RefCounted + + +@warning_ignore("unused_parameter") +func is_match(value: Variant) -> bool: + return true + + +func _to_string() -> String: + assert(false, "`_to_string()` Is not implemented!") + return "" diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid index e69de29b..400e1aeb 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://bhwuperdcf2n8 diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd index e69de29b..6a70cc15 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd @@ -0,0 +1,42 @@ +class_name GdUnitArgumentMatchers +extends RefCounted + +const TYPE_ANY = TYPE_MAX + 100 + + +static func to_matcher(arguments: Array[Variant], auto_deep_check_mode := false) -> ChainedArgumentMatcher: + var matchers: Array[Variant] = [] + for arg: Variant in arguments: + # argument is already a matcher + if arg is GdUnitArgumentMatcher: + matchers.append(arg) + else: + # pass argument into equals matcher + matchers.append(EqualsArgumentMatcher.new(arg, auto_deep_check_mode)) + return ChainedArgumentMatcher.new(matchers) + + +static func any() -> GdUnitArgumentMatcher: + return AnyArgumentMatcher.new() + + +static func by_type(type: int) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new([type]) + + +static func by_types(types: PackedInt32Array) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new(types) + + +static func any_class(clazz: Object) -> GdUnitArgumentMatcher: + return AnyClazzArgumentMatcher.new(clazz) + + +static func is_variant_string_matching(value: Variant) -> GdUnitResult: + if value is String or value is StringName: + return GdUnitResult.success() + if value is GdUnitArgumentMatcher: + if str(value) == "any()" or str(value) == "any_string()": + return GdUnitResult.success() + return GdUnitResult.error("Only 'any()' and 'any_string()' argument matchers are allowed!") + return GdUnitResult.error("Only String or StringName types are allowed!") diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid index e69de29b..d651605d 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid @@ -0,0 +1 @@ +uid://dji6lqxoelm5j diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd b/addons/gdUnit4/src/mocking/GdUnitMock.gd index e69de29b..c520d922 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMock.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd @@ -0,0 +1,45 @@ +class_name GdUnitMock +extends RefCounted + +## do call the real implementation +const CALL_REAL_FUNC = "CALL_REAL_FUNC" +## do return a default value for primitive types or null +const RETURN_DEFAULTS = "RETURN_DEFAULTS" +## do return a default value for primitive types and a fully mocked value for Object types +## builds full deep mocked object +const RETURN_DEEP_STUB = "RETURN_DEEP_STUB" + +var _value: Variant + + +func _init(value: Variant) -> void: + _value = value + + +## Selects the mock to work on, used in combination with [method GdUnitTestSuite.do_return][br] +## Example: +## [codeblock] +## do_return(false).on(myMock).is_selected() +## [/codeblock] +func on(obj: Variant) -> Variant: + if not GdUnitMock._is_mock_or_spy(obj, "__do_return"): + return obj + @warning_ignore("unsafe_method_access") + return obj.__do_return(_value) + + +## [color=yellow]`checked` is obsolete, use `on` instead [/color] +func checked(obj :Object) -> Object: + push_warning("Using a deprecated function 'checked' use `on` instead") + return on(obj) + + +static func _is_mock_or_spy(obj: Variant, func_sig: String) -> bool: + if obj is Object and not as_object(obj).has_method(func_sig): + push_error("Error: You try to use a non mock or spy!") + return false + return true + + +static func as_object(value: Variant) -> Object: + return value diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid index e69de29b..35329e78 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid @@ -0,0 +1 @@ +uid://obuiauaajequ diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid index e69de29b..905bb14a 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid @@ -0,0 +1 @@ +uid://dwvulgg5xpwid diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid index e69de29b..b6a81409 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid @@ -0,0 +1 @@ +uid://cw0dphkuu3a63 diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd index e69de29b..5ee14878 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -0,0 +1,72 @@ +extends RefCounted +class_name ErrorLogEntry + + +enum TYPE { + SCRIPT_ERROR, + PUSH_ERROR, + PUSH_WARNING +} + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const PATTERN_SCRIPT_ERROR := "USER SCRIPT ERROR:" +const PATTERN_PUSH_ERROR := "USER ERROR:" +const PATTERN_PUSH_WARNING := "USER WARNING:" +# With Godot 4.4 the pattern has changed +const PATTERN_4x4_SCRIPT_ERROR := "SCRIPT ERROR:" +const PATTERN_4x4_PUSH_ERROR := "ERROR:" +const PATTERN_4x4_PUSH_WARNING := "WARNING:" + +static var _regex_parse_error_line_number: RegEx + +var _type: TYPE +var _line: int +var _message: String +var _details: String + + +func _init(type: TYPE, line: int, message: String, details: String) -> void: + _type = type + _line = line + _message = message + _details = details + + +static func is_godot4x4() -> bool: + return Engine.get_version_info().hex >= 0x40400 + + +static func extract_push_warning(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_PUSH_WARNING if is_godot4x4() else PATTERN_PUSH_WARNING + return _extract(records, index, TYPE.PUSH_WARNING, pattern) + + +static func extract_push_error(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_PUSH_ERROR if is_godot4x4() else PATTERN_PUSH_ERROR + return _extract(records, index, TYPE.PUSH_ERROR, pattern) + + +static func extract_error(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_SCRIPT_ERROR if is_godot4x4() else PATTERN_SCRIPT_ERROR + return _extract(records, index, TYPE.SCRIPT_ERROR, pattern) + + +static func _extract(records: PackedStringArray, index: int, type: TYPE, pattern: String) -> ErrorLogEntry: + var message := records[index] + if message.begins_with(pattern): + var error := message.replace(pattern, "").strip_edges() + var details := records[index+1].strip_edges() + var line := _parse_error_line_number(details) + return ErrorLogEntry.new(type, line, error, details) + return null + + +static func _parse_error_line_number(record: String) -> int: + if _regex_parse_error_line_number == null: + _regex_parse_error_line_number = GdUnitTools.to_regex("at: .*res://.*:(\\d+)") + var matches := _regex_parse_error_line_number.search(record) + if matches != null: + return matches.get_string(1).to_int() + return -1 diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid index e69de29b..cf5d4ca2 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid @@ -0,0 +1 @@ +uid://8kjgr8gyjg5f diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd index e69de29b..b6429cad 100644 --- a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd @@ -0,0 +1,24 @@ +# GdUnit Monitoring Base Class +class_name GdUnitMonitor +extends RefCounted + +var _id :String + +# constructs new Monitor with given id +func _init(p_id :String) -> void: + _id = p_id + + +# Returns the id of the monitor to uniqe identify +func id() -> String: + return _id + + +# starts monitoring +func start() -> void: + pass + + +# stops monitoring +func stop() -> void: + pass diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid index e69de29b..30cef024 100644 --- a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid @@ -0,0 +1 @@ +uid://gq08i83yup2g diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd index e69de29b..725dd1fb 100644 --- a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,27 @@ +class_name GdUnitOrphanNodesMonitor +extends GdUnitMonitor + +var _initial_count := 0 +var _orphan_count := 0 +var _orphan_detection_enabled :bool + + +func _init(name :String = "") -> void: + super("OrphanNodesMonitor:" + name) + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +func start() -> void: + _initial_count = _orphans() + + +func stop() -> void: + _orphan_count = max(0, _orphans() - _initial_count) + + +func _orphans() -> int: + return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) as int + + +func orphan_nodes() -> int: + return _orphan_count if _orphan_detection_enabled else 0 diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid index e69de29b..862b5113 100644 --- a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid @@ -0,0 +1 @@ +uid://cufv71udymwm5 diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd index e69de29b..16db4582 100644 --- a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -0,0 +1,103 @@ +class_name GodotGdErrorMonitor +extends GdUnitMonitor + +var _godot_log_file: String +var _eof: int +var _report_enabled := false +var _entries: Array[ErrorLogEntry] = [] + + +func _init() -> void: + super("GodotGdErrorMonitor") + _godot_log_file = GdUnitSettings.get_log_path() + _report_enabled = _is_reporting_enabled() + + +func start() -> void: + var file := FileAccess.open(_godot_log_file, FileAccess.READ) + if file: + file.seek_end(0) + _eof = file.get_length() + + +func stop() -> void: + pass + + +func to_reports() -> Array[GdUnitReport]: + var reports_: Array[GdUnitReport] = [] + if _report_enabled: + reports_.assign(_entries.map(_to_report)) + _entries.clear() + return reports_ + + +static func _to_report(errorLog: ErrorLogEntry) -> GdUnitReport: + var failure := "%s\n\t%s\n%s %s" % [ + GdAssertMessages._error("Godot Runtime Error !"), + GdAssertMessages._colored_value(errorLog._details), + GdAssertMessages._error("Error:"), + GdAssertMessages._colored_value(errorLog._message)] + return GdUnitReport.new().create(GdUnitReport.ABORT, errorLog._line, failure) + + +func scan(force_collect_reports := false) -> Array[ErrorLogEntry]: + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame + _entries.append_array(_collect_log_entries(force_collect_reports)) + return _entries + + +func erase_log_entry(entry: ErrorLogEntry) -> void: + _entries.erase(entry) + + +func collect_full_logs() -> PackedStringArray: + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame + + var file := FileAccess.open(_godot_log_file, FileAccess.READ) + file.seek(_eof) + var records := PackedStringArray() + while not file.eof_reached(): + @warning_ignore("return_value_discarded") + records.append(file.get_line()) + + return records + + +func _collect_log_entries(force_collect_reports: bool) -> Array[ErrorLogEntry]: + var file := FileAccess.open(_godot_log_file, FileAccess.READ) + if not file: + # Log file might not be available. + return [] + file.seek(_eof) + var records := PackedStringArray() + while not file.eof_reached(): + @warning_ignore("return_value_discarded") + records.append(file.get_line()) + file.seek_end(0) + _eof = file.get_length() + var log_entries: Array[ErrorLogEntry]= [] + var is_report_errors := force_collect_reports or _is_report_push_errors() + var is_report_script_errors := force_collect_reports or _is_report_script_errors() + for index in records.size(): + if force_collect_reports: + log_entries.append(ErrorLogEntry.extract_push_warning(records, index)) + if is_report_errors: + log_entries.append(ErrorLogEntry.extract_push_error(records, index)) + if is_report_script_errors: + log_entries.append(ErrorLogEntry.extract_error(records, index)) + return log_entries.filter(func(value: ErrorLogEntry) -> bool: return value != null ) + + +func _is_reporting_enabled() -> bool: + return _is_report_script_errors() or _is_report_push_errors() + + +func _is_report_push_errors() -> bool: + return GdUnitSettings.is_report_push_errors() + + +func _is_report_script_errors() -> bool: + return GdUnitSettings.is_report_script_errors() diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid index e69de29b..56a51e9a 100644 --- a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid @@ -0,0 +1 @@ +uid://de86ibngfhvf5 diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd b/addons/gdUnit4/src/network/GdUnitServer.gd index e69de29b..6d878a01 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.gd +++ b/addons/gdUnit4/src/network/GdUnitServer.gd @@ -0,0 +1,42 @@ +@tool +extends Node + +@onready var _server :GdUnitTcpServer = $TcpServer + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + var result := _server.start() + if result.is_error(): + push_error(result.error_message()) + return + var server_port :int = result.value() + Engine.set_meta("gdunit_server_port", server_port) + _server.client_connected.connect(_on_client_connected) + _server.client_disconnected.connect(_on_client_disconnected) + _server.rpc_data.connect(_receive_rpc_data) + GdUnitCommandHandler.instance().gdunit_runner_stop.connect(_on_gdunit_runner_stop) + + +func _on_client_connected(client_id: int) -> void: + GdUnitSignals.instance().gdunit_client_connected.emit(client_id) + + +func _on_client_disconnected(client_id: int) -> void: + GdUnitSignals.instance().gdunit_client_disconnected.emit(client_id) + + +func _on_gdunit_runner_stop(client_id: int) -> void: + if _server: + _server.disconnect_client(client_id) + + +func _receive_rpc_data(p_rpc: RPC) -> void: + if p_rpc is RPCMessage: + var rpc_message: RPCMessage = p_rpc + GdUnitSignals.instance().gdunit_message.emit(rpc_message.message()) + return + if p_rpc is RPCGdUnitEvent: + var rpc_event: RPCGdUnitEvent = p_rpc + GdUnitSignals.instance().gdunit_event.emit(rpc_event.event()) + return diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd.uid b/addons/gdUnit4/src/network/GdUnitServer.gd.uid index e69de29b..55005223 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitServer.gd.uid @@ -0,0 +1 @@ +uid://2pr0fvtay8vf diff --git a/addons/gdUnit4/src/network/GdUnitServer.tscn b/addons/gdUnit4/src/network/GdUnitServer.tscn index e69de29b..4dbe8c49 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.tscn +++ b/addons/gdUnit4/src/network/GdUnitServer.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=3 uid="uid://cn5mp3tmi2gb1"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitServer.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpServer.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="TcpServer" type="Node" parent="."] +script = ExtResource("2") diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd b/addons/gdUnit4/src/network/GdUnitServerConstants.gd index e69de29b..d31eee77 100644 --- a/addons/gdUnit4/src/network/GdUnitServerConstants.gd +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd @@ -0,0 +1,6 @@ +class_name GdUnitServerConstants +extends RefCounted + +const DEFAULT_SERVER_START_RETRY_TIMES :int = 5 +const GD_TEST_SERVER_PORT :int = 31002 +const JSON_RESPONSE_DELIMITER :String = "<>" diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid index e69de29b..6c70d8c1 100644 --- a/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid @@ -0,0 +1 @@ +uid://eobp360ofaq5 diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd b/addons/gdUnit4/src/network/GdUnitTask.gd index e69de29b..e0188a04 100644 --- a/addons/gdUnit4/src/network/GdUnitTask.gd +++ b/addons/gdUnit4/src/network/GdUnitTask.gd @@ -0,0 +1,25 @@ +class_name GdUnitTask +extends RefCounted + +const TASK_NAME = "task_name" +const TASK_ARGS = "task_args" + +var _task_name :String +var _fref :Callable + + +func _init(task_name :String,instance :Object,func_name :String) -> void: + _task_name = task_name + if not instance.has_method(func_name): + push_error("Can't create GdUnitTask, Invalid func name '%s' for instance '%s'" % [instance, func_name]) + _fref = Callable(instance, func_name) + + +func name() -> String: + return _task_name + + +func execute(args :Array) -> GdUnitResult: + if args.is_empty(): + return _fref.call() + return _fref.callv(args) diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd.uid b/addons/gdUnit4/src/network/GdUnitTask.gd.uid index e69de29b..1b73e3b0 100644 --- a/addons/gdUnit4/src/network/GdUnitTask.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTask.gd.uid @@ -0,0 +1 @@ +uid://t6wuy7w6nlve diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd index e69de29b..2cf32a37 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -0,0 +1,124 @@ +class_name GdUnitTcpClient +extends GdUnitTcpNode + +signal connection_succeeded(message: String) +signal connection_failed(message: String) + + +var _client_name: String +var _debug := false +var _host: String +var _port: int +var _client_id: int +var _connected: bool +var _stream: StreamPeerTCP + + +func _init(client_name := "GdUnit4 TCP Client", debug := false) -> void: + _client_name = client_name + _debug = debug + + +func _ready() -> void: + _connected = false + _stream = StreamPeerTCP.new() + #_stream.set_big_endian(true) + + +func stop() -> void: + console("Disconnecting from server") + if _stream != null: + rpc_send(_stream, RPCClientDisconnect.new().with_id(_client_id)) + if _stream != null: + _stream.disconnect_from_host() + _connected = false + + +func start(host: String, port: int) -> GdUnitResult: + _host = host + _port = port + if _connected: + return GdUnitResult.warn("Client already connected ... %s:%d" % [_host, _port]) + + # Connect client to server + if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + var err := _stream.connect_to_host(host, port) + #prints("connect_to_host", host, port, err) + if err != OK: + return GdUnitResult.error("GdUnit4: Can't establish client, error code: %s" % err) + return GdUnitResult.success("GdUnit4: Client connected checked port %d" % port) + + +func _process(_delta: float) -> void: + match _stream.get_status(): + StreamPeerTCP.STATUS_NONE: + return + + StreamPeerTCP.STATUS_CONNECTING: + set_process(false) + # wait until client is connected to server + for retry in 10: + @warning_ignore("return_value_discarded") + _stream.poll() + console("Waiting to connect ..") + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTING: + await get_tree().create_timer(0.500).timeout + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: + set_process(true) + return + set_process(true) + _stream.disconnect_from_host() + console("Connection failed") + connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) + + StreamPeerTCP.STATUS_CONNECTED: + if not _connected: + var rpc_data :RPC = null + set_process(false) + while rpc_data == null: + await get_tree().create_timer(0.500).timeout + rpc_data = rpc_receive() + set_process(true) + _client_id = (rpc_data as RPCClientConnect).client_id() + console("Connected to Server: %d" % _client_id) + connection_succeeded.emit("Connect to TCP Server %s:%d success." % [_host, _port]) + _connected = true + process_rpc() + + StreamPeerTCP.STATUS_ERROR: + console("Connection failed") + _stream.disconnect_from_host() + connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) + return + + +func is_client_connected() -> bool: + return _connected + + +func process_rpc() -> void: + if _stream.get_available_bytes() > 0: + var rpc_data := rpc_receive() + if rpc_data is RPCClientDisconnect: + stop() + + +func send(data: RPC) -> void: + rpc_send(_stream, data) + + +func rpc_receive() -> RPC: + return receive_packages(_stream).front() + + +func console(value: Variant) -> void: + if _debug: + print(_client_name, ": ", value) + + +func _on_connection_failed(message: String) -> void: + console("Connection faild by: " + message) + + +func _on_connection_succeeded(message: String) -> void: + console("Connected: " + message) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid index e69de29b..0aa42a04 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid @@ -0,0 +1 @@ +uid://c5dwxfihc8jwp diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd b/addons/gdUnit4/src/network/GdUnitTcpNode.gd index e69de29b..156c764b 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpNode.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd @@ -0,0 +1,73 @@ +class_name GdUnitTcpNode +extends Node + + +func rpc_send(stream: StreamPeerTCP, data: RPC) -> void: + var package_buffer := StreamPeerBuffer.new() + var buffer := data.serialize().to_utf16_buffer() + package_buffer.put_u32(0xDEADBEEF) + package_buffer.put_u32(buffer.size()) + var status_code := package_buffer.put_data(buffer) + if status_code != OK: + push_error("'rpc_send:' Can't put_data(), error: %s" % error_string(status_code)) + return + stream.put_data(package_buffer.data_array) + + +func receive_packages(stream: StreamPeerTCP, rpc_cb: Callable = noop) -> Array[RPC]: + var received_packages: Array[RPC] = [] + var package_buffer := StreamPeerBuffer.new() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + + while stream.get_status() == StreamPeerTCP.STATUS_CONNECTED and stream.get_available_bytes() > 0: + var buffer := stream.get_data(8) + var status_code: int = buffer[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for available_bytes, error: %s" + % [stream.get_available_bytes(), error_string(status_code)]) + return received_packages + + var data_package: PackedByteArray + package_buffer.data_array = buffer[1] + package_buffer.seek(0) + + if package_buffer.get_u32() == 0xDEADBEEF: + var size := package_buffer.get_u32() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + if stream.get_available_bytes() < size: + prints("size check:", + package_buffer.get_size(), ":", + package_buffer.get_position(), + "to read:", + size, + "available size:", + stream.get_available_bytes()) + push_error("'receive_packages:' Can't receive data get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + return received_packages + + buffer = stream.get_data(size) + package_buffer.data_array = buffer[1] + + var rpc_data := package_buffer.get_data(size) + status_code = rpc_data[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + continue + data_package = rpc_data[1] + else: + data_package = buffer[1] + + var json := data_package.get_string_from_utf16() + if json.is_empty(): + push_warning("json is empty, can't process data") + continue + var data := RPC.deserialize(json) + received_packages.append(data) + rpc_cb.call(data) + return received_packages + + +static func noop(_rpc_data: RPC) -> void: + pass diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid index e69de29b..c5f90ee4 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid @@ -0,0 +1 @@ +uid://bcywbsdfcc88k diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index e69de29b..4687ac19 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -0,0 +1,129 @@ +@tool +class_name GdUnitTcpServer +extends Node + +signal client_connected(client_id: int) +signal client_disconnected(client_id: int) +@warning_ignore("unused_signal") +signal rpc_data(rpc_data: RPC) + +var _server: TCPServer +var _server_name: String + +class TcpConnection extends GdUnitTcpNode: + var _id: int + var _stream: StreamPeerTCP + + + func _init(tcp_server: TCPServer) -> void: + _stream = tcp_server.take_connection() + #_stream.set_big_endian(true) + _id = _stream.get_instance_id() + rpc_send(_stream, RPCClientConnect.new().with_id(_id)) + + + func _ready() -> void: + server().client_connected.emit(_id) + + + func close() -> void: + if _stream != null and _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: + _stream.disconnect_from_host() + queue_free() + + + func id() -> int: + return _id + + + func server() -> GdUnitTcpServer: + return get_parent() + + + func _process(_delta: float) -> void: + if _stream == null or _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return + receive_packages(_stream, func(rpc_data: RPC) -> void: + server().rpc_data.emit(rpc_data) + # is client disconnecting we close the server after a timeout of 1 second + if rpc_data is RPCClientDisconnect: + close() + ) + + + func console(_value: Variant) -> void: + #print_debug("TCP Server: ", value) + pass + + +func _init(server_name := "GdUnit4 TCP Server") -> void: + _server_name = server_name + + +func _ready() -> void: + _server = TCPServer.new() + client_connected.connect(_on_client_connected) + client_disconnected.connect(_on_client_disconnected) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + stop() + + +func start(server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT) -> GdUnitResult: + var err := OK + for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: + err = _server.listen(server_port, "127.0.0.1") + if err != OK: + prints("GdUnit4: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) + server_port += 1 + prints("GdUnit4: Retry (%d) ..." % retry) + else: + break + if err != OK: + if err == ERR_ALREADY_IN_USE: + return GdUnitResult.error("GdUnit4: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) + return GdUnitResult.error("GdUnit4: Can't establish server. Error: %s." % error_string(err)) + console("Successfully started checked port: %d" % server_port) + return GdUnitResult.success(server_port) + + +func stop() -> void: + if _server: + _server.stop() + for connection in get_children(): + if connection is TcpConnection: + @warning_ignore("unsafe_method_access") + connection.close() + remove_child(connection) + _server = null + + +func disconnect_client(client_id: int) -> void: + client_disconnected.emit(client_id) + + +func _process(_delta: float) -> void: + if _server != null and not _server.is_listening(): + return + # check if connection is ready to be used + if _server != null and _server.is_connection_available(): + add_child(TcpConnection.new(_server)) + + +func _on_client_connected(client_id: int) -> void: + console("Client connected %d" % client_id) + + +func _on_client_disconnected(client_id: int) -> void: + for connection in get_children(): + @warning_ignore("unsafe_method_access") + if connection is TcpConnection and connection.id() == client_id: + @warning_ignore("unsafe_method_access") + connection.close() + remove_child(connection) + + +func console(value: Variant) -> void: + print(_server_name, ": ", value) diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid index e69de29b..15c8cd19 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid @@ -0,0 +1 @@ +uid://dkxh0mktjia50 diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd b/addons/gdUnit4/src/network/rpc/RPC.gd index e69de29b..6569cb1c 100644 --- a/addons/gdUnit4/src/network/rpc/RPC.gd +++ b/addons/gdUnit4/src/network/rpc/RPC.gd @@ -0,0 +1,37 @@ +class_name RPC +extends RefCounted + + +var _data: Dictionary = {} + + +func _init(obj: Object = null) -> void: + if obj != null: + if obj.has_method("serialize"): + _data = obj.call("serialize") + else: + _data = inst_to_dict(obj) + + +func get_data() -> Object: + return dict_to_inst(_data) + + +func serialize() -> String: + return JSON.stringify(inst_to_dict(self)) + + +# using untyped version see comments below +static func deserialize(json_value: String) -> Object: + var json := JSON.new() + var err := json.parse(json_value) + if err != OK: + push_error("Can't deserialize JSON, error at line %d:\n error: %s \n json: '%s'" + % [json.get_error_line(), json.get_error_message(), json_value]) + return null + var result: Dictionary = json.get_data() + if not typeof(result) == TYPE_DICTIONARY: + push_error("Can't deserialize JSON. Expecting dictionary, error at line %d:\n error: %s \n json: '%s'" + % [result.error_line, result.error_string, json_value]) + return null + return dict_to_inst(result) diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd.uid b/addons/gdUnit4/src/network/rpc/RPC.gd.uid index e69de29b..e69cd9c8 100644 --- a/addons/gdUnit4/src/network/rpc/RPC.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPC.gd.uid @@ -0,0 +1 @@ +uid://cop588f5ov5ul diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid index e69de29b..ce9bbef4 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid @@ -0,0 +1 @@ +uid://c0nqfpo3y6lkk diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd index e69de29b..7445b9de 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd @@ -0,0 +1,13 @@ +class_name RPCClientDisconnect +extends RPC + +var _client_id: int + + +func with_id(id: int) -> RPCClientDisconnect: + _client_id = id + return self + + +func client_id() -> int: + return _client_id diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid index e69de29b..29edb9ae 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid @@ -0,0 +1 @@ +uid://cos6yegplfqxr diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd index e69de29b..dbf55c63 100644 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd @@ -0,0 +1,14 @@ +class_name RPCGdUnitEvent +extends RPC + + +static func of(p_event: GdUnitEvent) -> RPCGdUnitEvent: + return RPCGdUnitEvent.new(p_event) + + +func event() -> GdUnitEvent: + return GdUnitEvent.new().deserialize(_data) + + +func _to_string() -> String: + return "RPCGdUnitEvent: " + str(_data) diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid index e69de29b..03816ea7 100644 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid @@ -0,0 +1 @@ +uid://b1ocmn5c833kt diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd b/addons/gdUnit4/src/network/rpc/RPCMessage.gd index e69de29b..1db0470d 100644 --- a/addons/gdUnit4/src/network/rpc/RPCMessage.gd +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd @@ -0,0 +1,18 @@ +class_name RPCMessage +extends RPC + +var _message: String + + +static func of(msg :String) -> RPCMessage: + var rpc := RPCMessage.new() + rpc._message = msg + return rpc + + +func message() -> String: + return _message + + +func _to_string() -> String: + return "RPCMessage: " + _message diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid index e69de29b..8f9cdee4 100644 --- a/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid @@ -0,0 +1 @@ +uid://0mpkrrxtbgao diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd index e69de29b..b55b964f 100644 --- a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd @@ -0,0 +1,202 @@ +class_name GdUnitReportSummary +extends RefCounted + +var _resource_path: String +var _name: String +var _test_count := 0 +var _failure_count := 0 +var _error_count := 0 +var _orphan_count := 0 +var _skipped_count := 0 +var _flaky_count := 0 +var _duration := 0 +var _reports: Array[GdUnitReportSummary] = [] +var _text_formatter: Callable + + +func _init(text_formatter: Callable) -> void: + _text_formatter = text_formatter + + +func name() -> String: + return _name + + +func path() -> String: + return _resource_path.get_base_dir().replace("res://", "") + + +func get_resource_path() -> String: + return _resource_path + + +func suite_count() -> int: + return _reports.size() + + +func suite_executed_count() -> int: + var executed := _reports.size() + for report in _reports: + if report.test_count() == report.skipped_count(): + executed -= 1 + return executed + + +func test_count() -> int: + var count := _test_count + for report in _reports: + count += report.test_count() + return count + + +func test_executed_count() -> int: + return test_count() - skipped_count() + + +func success_count() -> int: + return test_count() - error_count() - failure_count() - flaky_count() - skipped_count() + + +func error_count() -> int: + return _error_count + + +func failure_count() -> int: + return _failure_count + + +func skipped_count() -> int: + return _skipped_count + + +func flaky_count() -> int: + return _flaky_count + + +func orphan_count() -> int: + return _orphan_count + + +func duration() -> int: + return _duration + + +func get_reports() -> Array: + return _reports + + +func add_report(report: GdUnitReportSummary) -> void: + _reports.append(report) + + +func report_state() -> String: + return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count(), skipped_count()) + + +func succes_rate() -> String: + return calculate_succes_rate(test_count(), error_count(), failure_count()) + + +@warning_ignore("shadowed_variable") +func add_testcase(resource_path: String, suite_name: String, test_name: String) -> void: + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == resource_path: + var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name, _text_formatter) + report.add_or_create_test_report(test_report) + + +func add_reports( + p_resource_path: String, + p_test_name: String, + p_reports: Array[GdUnitReport]) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.add_testcase_reports(p_test_name, p_reports) + + +func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void: + _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count, _text_formatter)) + + +func add_testsuite_reports( + p_resource_path: String, + p_reports: Array = []) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_reports(p_reports) + + +func set_counters( + p_resource_path: String, + p_test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count, + p_is_skipped, p_is_flaky, p_duration) + + +func update_testsuite_counters( + p_resource_path: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report._update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, p_duration) + _update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, 0) + + +func _update_summary_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int, p_skipped_count: int) -> String: + if p_error_count > 0: + return "ERROR" + if p_failure_count > 0: + return "FAILED" + if p_flaky_count > 0: + return "FLAKY" + if p_orphan_count > 0: + return "WARNING" + if p_skipped_count > 0: + return "SKIPPED" + return "PASSED" + + +func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: + if p_failure_count == 0: + return "100%" + var count := p_test_count-p_failure_count-p_error_count + if count < 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +func create_summary(_report_dir :String) -> String: + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid index e69de29b..7e46ff44 100644 --- a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid @@ -0,0 +1 @@ +uid://meit1uha85vb diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd index e69de29b..bf4c96ea 100644 --- a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd @@ -0,0 +1,12 @@ +class_name GdUnitReportWriter +extends RefCounted + + +func write(_report_path: String, _report: GdUnitReportSummary) -> String: + assert(false, "'write' is not implemented!") + return "" + + +func output_format() -> String: + assert(false, "'output_format' is not implemented!") + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid index e69de29b..49724c0b 100644 --- a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid @@ -0,0 +1 @@ +uid://bapou3o42rua2 diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd index e69de29b..2801696c 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd @@ -0,0 +1,47 @@ +class_name GdUnitTestCaseReport +extends GdUnitReportSummary + + +var _suite_name: String +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_suite_name: String, p_test_name: String, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _suite_name = p_suite_name + _name = p_test_name + _text_formatter = text_formatter + + +func suite_name() -> String: + return _suite_name + + +func failure_report() -> String: + var report_message := "" + for report in get_test_reports(): + report_message += _text_formatter.call(str(report)) + "\n" + return report_message + + +func add_testcase_reports(reports: Array[GdUnitReport]) -> void: + _failure_reports.append_array(reports) + + +func set_testcase_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + _error_count = p_error_count + _failure_count = p_failure_count + _orphan_count = p_orphan_count + _skipped_count = p_is_skipped + _flaky_count = p_is_flaky as int + _duration = p_duration + + +func get_test_reports() -> Array[GdUnitReport]: + return _failure_reports diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid index e69de29b..467772e2 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid @@ -0,0 +1 @@ +uid://d34dh6aril014 diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd index e69de29b..46b81931 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd @@ -0,0 +1,112 @@ +class_name GdUnitTestReporter +extends RefCounted + + +var _statistics := {} +var _summary := {} + + +func init_summary() -> void: + _summary["suite_count"] = 0 + _summary["total_count"] = 0 + _summary["error_count"] = 0 + _summary["failed_count"] = 0 + _summary["skipped_count"] = 0 + _summary["flaky_count"] = 0 + _summary["orphan_nodes"] = 0 + _summary["elapsed_time"] = 0 + + +func init_statistics() -> void: + _statistics.clear() + + +func add_test_statistics(event: GdUnitEvent) -> void: + _statistics[event.guid()] = { + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : event.is_flaky() as int, + "orphan_nodes" : event.orphan_nodes() + } + + +func build_test_suite_statisitcs(event: GdUnitEvent) -> Dictionary: + var statistic := { + "total_count" : _statistics.size(), + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : 0, + "orphan_nodes" : event.orphan_nodes() + } + _summary["suite_count"] += 1 + _summary["total_count"] += _statistics.size() + _summary["error_count"] += event.error_count() + _summary["failed_count"] += event.failed_count() + _summary["skipped_count"] += event.skipped_count() + _summary["orphan_nodes"] += event.orphan_nodes() + _summary["elapsed_time"] += event.elapsed_time() + + for key: String in ["error_count", "failed_count", "skipped_count", "flaky_count", "orphan_nodes"]: + var value: int = _statistics.values().reduce(get_value.bind(key), 0 ) + statistic[key] += value + _summary[key] += value + + return statistic + + +func get_value(acc: int, value: Dictionary, key: String) -> int: + return acc + value[key] + + +func processed_suite_count() -> int: + return _summary["suite_count"] + + +func total_test_count() -> int: + return _summary["total_count"] + + +func total_flaky_count() -> int: + return _summary["flaky_count"] + + +func total_error_count() -> int: + return _summary["error_count"] + + +func total_failure_count() -> int: + return _summary["failed_count"] + + +func total_skipped_count() -> int: + return _summary["skipped_count"] + + +func total_orphan_count() -> int: + return _summary["orphan_nodes"] + + +func elapsed_time() -> int: + return _summary["elapsed_time"] + + +func error_count(statistics: Dictionary) -> int: + return statistics["error_count"] + + +func failed_count(statistics: Dictionary) -> int: + return statistics["failed_count"] + + +func orphan_nodes(statistics: Dictionary) -> int: + return statistics["orphan_nodes"] + + +func skipped_count(statistics: Dictionary) -> int: + return statistics["skipped_count"] + + +func flaky_count(statistics: Dictionary) -> int: + return statistics["flaky_count"] diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid index e69de29b..52e61aed 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid @@ -0,0 +1 @@ +uid://cd8snijgldveh diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd index e69de29b..9be06825 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd @@ -0,0 +1,96 @@ +class_name GdUnitTestSuiteReport +extends GdUnitReportSummary + +var _time_stamp: int +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_name: String, p_test_count: int, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _name = p_name + _test_count = p_test_count + _time_stamp = Time.get_unix_time_from_system() as int + _text_formatter = text_formatter + + +func failure_report() -> String: + var report_message := "" + for report in _failure_reports: + report_message += _text_formatter.call(str(report)) + return report_message + + +func set_duration(p_duration :int) -> void: + _duration = p_duration + + +func time_stamp() -> int: + return _time_stamp + + +func duration() -> int: + return _duration + + +func set_skipped(skipped :int) -> void: + _skipped_count += skipped + + +func set_orphans(orphans :int) -> void: + _orphan_count = orphans + + +func set_failed(count :int) -> void: + _failure_count += count + + +func set_reports(failure_reports :Array[GdUnitReport]) -> void: + _failure_reports = failure_reports + + +func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void: + _reports.append(test_report) + + +func _update_testsuite_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func set_testcase_counters( + test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + if _reports.is_empty(): + return + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) + + +func add_testcase_reports(test_name: String, reports: Array[GdUnitReport]) -> void: + if reports.is_empty(): + return + # we lookup to latest matching report because of flaky tests could be retry the tests + # and resultis in multipe report entries with the same name + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.add_testcase_reports(reports) diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid index e69de29b..12986589 100644 --- a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid @@ -0,0 +1 @@ +uid://buoo3f5s15ptt diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd index e69de29b..05d4b1d7 100644 --- a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd @@ -0,0 +1,234 @@ +@tool +class_name GdUnitConsoleTestReporter + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(on_gdunit_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(on_gdunit_event) + + +var _writer: GdUnitMessageWriter +var _reporter: GdUnitTestReporter = GdUnitTestReporter.new() +var _status_indent := 86 +var _detailed: bool +var _text_color: Color = Color.ANTIQUE_WHITE +var _function_color: Color = Color.ANTIQUE_WHITE +var _engine_type_color: Color = Color.ANTIQUE_WHITE + + +func _init(writer: GdUnitMessageWriter, detailed := false) -> void: + _writer = writer + _writer.clear() + _detailed = detailed + if _detailed: + _status_indent = 20 + init_colors() + + +func init_colors() -> void: + if Engine.is_editor_hint(): + var settings := EditorInterface.get_editor_settings() + _text_color = settings.get_setting("text_editor/theme/highlighting/text_color") + _function_color = settings.get_setting("text_editor/theme/highlighting/function_color") + _engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color") + + +func clear() -> void: + _writer.clear() + + +func on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + _reporter.init_summary() + + GdUnitEvent.STOP: + _print_summary() + println_message(build_executed_test_suite_msg(processed_suite_count(), processed_suite_count()), Color.DARK_SALMON) + println_message(build_executed_test_case_msg(total_test_count(), total_skipped_count()), Color.DARK_SALMON) + println_message("Total execution time: %s" % LocalTime.elapsed(elapsed_time()), Color.DARK_SALMON) + # We need finally to set the wave effect to enable the animations + _writer.effect(GdUnitMessageWriter.Effect.WAVE).print_at("", 0) + + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + print_message("Run Test Suite: ", Color.DARK_TURQUOISE) + println_message(event.resource_path(), _engine_type_color) + + GdUnitEvent.TESTSUITE_AFTER: + if not event.reports().is_empty(): + _writer.color(Color.DARK_SALMON) \ + .style(GdUnitMessageWriter.BOLD) \ + .println_message(event.suite_name() + ":finalze") + _print_failure_report(event.reports()) + _print_statistics(_reporter.build_test_suite_statisitcs(event)) + _print_status(event) + println_message("") + if _detailed: + println_message("") + + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + if _detailed: + _writer.color(Color.FOREST_GREEN).print_at("STARTED", _status_indent) + println_message("") + + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + if _detailed: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + _print_status(event) + _print_failure_report(event.reports()) + if _detailed: + println_message("") + + +func _print_test_path(test: GdUnitTestCase, uid: GdUnitGUID) -> void: + if test == null: + prints_warning("Can't print full test info, the test by uid: '%s' was not discovered." % uid) + _writer.indent(1).color(_engine_type_color).print_message("Test ID: %s" % uid) + return + + var suite_name := test.source_file if _detailed else test.suite_name + _writer.indent(1).color(_engine_type_color).print_message(suite_name) + print_message(" > ") + print_message(test.display_name, _function_color) + + +func _print_status(event: GdUnitEvent) -> void: + if event.is_flaky() and event.is_success(): + var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT) + _writer.color(Color.GREEN_YELLOW) \ + .style(GdUnitMessageWriter.ITALIC) \ + .print_at("FLAKY (%d retries)" % retries, _status_indent) + elif event.is_success(): + _writer.color(Color.FOREST_GREEN).print_at("PASSED", _status_indent) + elif event.is_skipped(): + _writer.color(Color.GOLDENROD).style(GdUnitMessageWriter.ITALIC).print_at("SKIPPED", _status_indent) + elif event.is_failed() or event.is_error(): + var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT) + var message := "FAILED (retry %d)" % retries if retries > 1 else "FAILED" + _writer.color(Color.FIREBRICK) \ + .style(GdUnitMessageWriter.BOLD) \ + .effect(GdUnitMessageWriter.Effect.WAVE) \ + .print_at(message, _status_indent) + elif event.is_warning(): + _writer.color(Color.GOLDENROD) \ + .style(GdUnitMessageWriter.UNDERLINE) \ + .print_at("WARNING", _status_indent) + + println_message(" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE) + + +func _print_failure_report(reports: Array[GdUnitReport]) -> void: + for report in reports: + if ( + report.is_failure() + or report.is_error() + or report.is_warning() + or report.is_skipped() + ): + _writer.indent(1) \ + .color(Color.DARK_TURQUOISE) \ + .style(GdUnitMessageWriter.BOLD | GdUnitMessageWriter.UNDERLINE) \ + .println_message("Report:") + var text := str(report) + for line in text.split("\n", false): + _writer.indent(2).color(Color.DARK_TURQUOISE).println_message(line) + + if not reports.is_empty(): + println_message("") + + +func _print_statistics(statistics: Dictionary) -> void: + print_message("Statistics:", Color.DODGER_BLUE) + print_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % \ + [statistics["total_count"], + statistics["error_count"], + statistics["failed_count"], + statistics["flaky_count"], + statistics["skipped_count"], + statistics["orphan_nodes"]]) + + +func _print_summary() -> void: + print_message("Overall Summary:", Color.DODGER_BLUE) + _writer \ + .println_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % [ + total_test_count(), + total_error_count(), + total_failure_count(), + total_flaky_count(), + total_skipped_count(), + total_orphan_count() + ]) + + +func build_executed_test_suite_msg(executed_count: int, total_count: int) -> String: + if executed_count == total_count: + return "Executed test suites: (%d/%d)" % [executed_count, total_count] + return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)] + + +func build_executed_test_case_msg(total_count: int, p_skipped_count: int) -> String: + if p_skipped_count == 0: + return "Executed test cases : (%d/%d)" % [total_count, total_count] + return "Executed test cases : (%d/%d), %d skipped" % [total_count - p_skipped_count, total_count, p_skipped_count] + + +func print_message(message: String, color: Color = _text_color) -> void: + _writer.color(color).print_message(message) + + +func println_message(message: String, color: Color = _text_color) -> void: + _writer.color(color).println_message(message) + + +func prints_warning(message: String) -> void: + _writer.prints_warning(message) + + +func prints_error(message: String) -> void: + _writer.prints_error(message) + + +func total_test_count() -> int: + return _reporter.total_test_count() + + +func total_error_count() -> int: + return _reporter.total_error_count() + + +func total_failure_count() -> int: + return _reporter.total_failure_count() + + +func total_flaky_count() -> int: + return _reporter.total_flaky_count() + + +func total_skipped_count() -> int: + return _reporter.total_skipped_count() + + +func total_orphan_count() -> int: + return _reporter.total_orphan_count() + + +func processed_suite_count() -> int: + return _reporter.processed_suite_count() + + +func elapsed_time() -> int: + return _reporter.elapsed_time() diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid index e69de29b..ca3a3c5a 100644 --- a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid @@ -0,0 +1 @@ +uid://cnc3bdrbur61g diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd index e69de29b..74b961b4 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd @@ -0,0 +1,60 @@ +class_name GdUnitByPathReport +extends GdUnitReportSummary + + +func _init(p_path :String, report_summaries :Array[GdUnitReportSummary]) -> void: + _resource_path = p_path + _reports = report_summaries + + +# -> Dictionary[String, Array[GdUnitReportSummary]] +static func sort_reports_by_path(report_summaries :Array[GdUnitReportSummary]) -> Dictionary: + var by_path := Dictionary() + for report in report_summaries: + var suite_path :String = ProjectSettings.localize_path(report.path()) + var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) + suite_report.append(report) + by_path[suite_path] = suite_report + return by_path + + +func path() -> String: + return _resource_path.replace("res://", "").trim_suffix("/") + + +func create_record(report_link :String) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_PATH, self, report_link) + + +func write(report_dir :String) -> String: + calculate_summary() + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/folder_report.html") + var path_report := GdUnitHtmlPatterns.build(template, self, "") + path_report = apply_testsuite_reports(report_dir, path_report, _reports) + + var output_path := "%s/path/%s.html" % [report_dir, path().replace("/", ".")] + var dir := output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(output_path, FileAccess.WRITE).store_string(path_report) + return output_path + + +func apply_testsuite_reports(report_dir :String, template :String, test_suite_reports :Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + for report:GdUnitTestSuiteReport in test_suite_reports: + var report_link := GdUnitHtmlReportWriter.create_output_path(report_dir, report.path(), report.name()).replace(report_dir, "..") + @warning_ignore("return_value_discarded") + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func calculate_summary() -> void: + for report:GdUnitTestSuiteReport in get_reports(): + _error_count += report.error_count() + _failure_count += report.failure_count() + _orphan_count += report.orphan_count() + _skipped_count += report.skipped_count() + _flaky_count += report.flaky_count() + _duration += report.duration() diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid index e69de29b..495496cc 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid @@ -0,0 +1 @@ +uid://byxm8mid1s1ev diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd index e69de29b..b8be904f 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd @@ -0,0 +1,199 @@ +class_name GdUnitHtmlPatterns +extends RefCounted + +const TABLE_RECORD_TESTSUITE = """ + + ${testsuite_name} + ${report_state_label} + ${test_count} + ${skipped_count} + ${flaky_count} + ${failure_count} + ${orphan_count} + ${duration} + +
+
+
+
+
+
+
+
+ + +""" + +const TABLE_RECORD_PATH = """ + + ${path} + ${report_state_label} + ${test_count} + ${skipped_count} + ${flaky_count} + ${failure_count} + ${orphan_count} + ${duration} + +
+
+
+
+
+
+
+
+ + +""" + + +const TABLE_REPORT_TESTSUITE = """ + + TestSuite hooks + n/a + ${orphan_count} + ${duration} + +
+${failure-report}
+										
+ + +""" + + +const TABLE_RECORD_TESTCASE = """ + + ${testcase_name} + ${report_state_label} + ${skipped_count} + ${orphan_count} + ${duration} + +
+${failure-report}
+										
+ + +""" + +const CHARACTERS_TO_ENCODE := { + '<' : '<', + '>' : '>' +} + +const TABLE_BY_PATHS = "${report_table_paths}" +const TABLE_BY_TESTSUITES = "${report_table_testsuites}" +const TABLE_BY_TESTCASES = "${report_table_tests}" + +# the report state success, error, warning +const REPORT_STATE = "${report_state}" +const REPORT_STATE_LABEL = "${report_state_label}" +const PATH = "${path}" +const RESOURCE_PATH = "${resource_path}" +const TESTSUITE_COUNT = "${suite_count}" +const TESTCASE_COUNT = "${test_count}" +const FAILURE_COUNT = "${failure_count}" +const FLAKY_COUNT = "${flaky_count}" +const SKIPPED_COUNT = "${skipped_count}" +const ORPHAN_COUNT = "${orphan_count}" +const DURATION = "${duration}" +const FAILURE_REPORT = "${failure-report}" +const SUCCESS_PERCENT = "${success_percent}" + + +const QUICK_STATE_SKIPPED = "${skipped-percent}" +const QUICK_STATE_PASSED = "${passed-percent}" +const QUICK_STATE_FLAKY = "${flaky-percent}" +const QUICK_STATE_ERROR = "${error-percent}" +const QUICK_STATE_FAILED = "${failed-percent}" +const QUICK_STATE_WARNING = "${warning-percent}" + +const TESTSUITE_NAME = "${testsuite_name}" +const TESTCASE_NAME = "${testcase_name}" +const REPORT_LINK = "${report_link}" +const BREADCRUMP_PATH_LINK = "${breadcrumb_path_link}" +const BUILD_DATE = "${buid_date}" + + +static func current_date() -> String: + return Time.get_datetime_string_from_system(true, true) + + +static func build(template: String, report: GdUnitReportSummary, report_link: String) -> String: + return template\ + .replace(PATH, get_report_path(report))\ + .replace(BREADCRUMP_PATH_LINK, get_path_as_link(report))\ + .replace(RESOURCE_PATH, report.get_resource_path())\ + .replace(TESTSUITE_NAME, html_encoded(report.name()))\ + .replace(TESTSUITE_COUNT, str(report.suite_count()))\ + .replace(TESTCASE_COUNT, str(report.test_count()))\ + .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\ + .replace(FLAKY_COUNT, str(report.flaky_count()))\ + .replace(SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(DURATION, LocalTime.elapsed(report.duration()))\ + .replace(SUCCESS_PERCENT, report.calculate_succes_rate(report.test_count(), report.error_count(), report.failure_count()))\ + .replace(REPORT_STATE, report.report_state().to_lower())\ + .replace(REPORT_STATE_LABEL, report.report_state())\ + .replace(QUICK_STATE_SKIPPED, calculate_percentage(report.test_count(), report.skipped_count()))\ + .replace(QUICK_STATE_PASSED, calculate_percentage(report.test_count(), report.success_count()))\ + .replace(QUICK_STATE_FLAKY, calculate_percentage(report.test_count(), report.flaky_count()))\ + .replace(QUICK_STATE_ERROR, calculate_percentage(report.test_count(), report.error_count()))\ + .replace(QUICK_STATE_FAILED, calculate_percentage(report.test_count(), report.failure_count()))\ + .replace(QUICK_STATE_WARNING, calculate_percentage(report.test_count(), 0))\ + .replace(REPORT_LINK, report_link)\ + .replace(BUILD_DATE, current_date()) + + +static func load_template(template_name :String) -> String: + return FileAccess.open(template_name, FileAccess.READ).get_as_text() + + +static func get_path_as_link(report: GdUnitReportSummary) -> String: + return "../path/%s.html" % report.path().replace("/", ".") + + +static func get_report_path(report: GdUnitReportSummary) -> String: + var path := report.path() + if path.is_empty(): + return "/" + return path + + +static func calculate_percentage(p_test_count: int, count: int) -> String: + if count <= 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +static func html_encoded(value: String) -> String: + for key: String in CHARACTERS_TO_ENCODE.keys(): + @warning_ignore("unsafe_cast") + value = value.replace(key, CHARACTERS_TO_ENCODE[key] as String) + return value + + +static func create_suite_record(report_link: String, report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, report, report_link) + + +static func create_test_failure_report(_report_dir :String, report: GdUnitTestCaseReport) -> String: + return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.TESTCASE_NAME, report.name())\ + .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report()) + + +static func create_suite_failure_report(report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report()) diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid index e69de29b..74abfe25 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid @@ -0,0 +1 @@ +uid://dndrqodnpdtx0 diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd index e69de29b..a3d2bfff 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd @@ -0,0 +1,72 @@ +class_name GdUnitHtmlReportWriter +extends GdUnitReportWriter + + +func output_format() -> String: + return "HTML" + + +func write(report_path: String, report: GdUnitReportSummary) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/index.html") + var to_write := GdUnitHtmlPatterns.build(template, report, "") + to_write = _apply_path_reports(report_path, to_write, report.get_reports()) + to_write = _apply_testsuite_reports(report_path, to_write, report.get_reports()) + # write report + DirAccess.make_dir_recursive_absolute(report_path) + var html_report_file := "%s/index.html" % report_path + FileAccess.open(html_report_file, FileAccess.WRITE).store_string(to_write) + @warning_ignore("return_value_discarded") + GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/reporters/html/template/css/", report_path + "/css") + return html_report_file + + +func _apply_path_reports(report_dir: String, template: String, report_summaries: Array) -> String: + #Dictionary[String, Array[GdUnitReportSummary]] + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(report_summaries) + var table_records := PackedStringArray() + var paths: Array[String] = [] + paths.append_array(path_report_mapping.keys()) + paths.sort() + for report_at_path in paths: + var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_at_path) + var report := GdUnitByPathReport.new(report_at_path, reports) + var report_link: String = report.write(report_dir).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) + + +func _apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + for report: GdUnitTestSuiteReport in test_suite_reports: + var report_link: String = _write(report_dir, report).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func _write(report_dir :String, report: GdUnitTestSuiteReport) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/suite_report.html") + template = GdUnitHtmlPatterns.build(template, report, "") + + var report_output_path := create_output_path(report_dir, report.path(), report.name()) + var test_report_table := PackedStringArray() + if not report._failure_reports.is_empty(): + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_suite_failure_report(report)) + for test_report: GdUnitTestCaseReport in report._reports: + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_test_failure_report(report_output_path, test_report)) + + template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) + + var dir := report_output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template) + return report_output_path + + +static func create_output_path(report_dir :String, path: String, name: String) -> String: + return "%s/test_suites/%s.%s.html" % [report_dir, path.replace("/", "."), name] diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid index e69de29b..dedbb39f 100644 --- a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://dc1daqsf13p2m diff --git a/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css index e69de29b..17215ff2 100644 --- a/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css +++ b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css @@ -0,0 +1,66 @@ +.breadcrumb { + display: flex; + border-radius: 6px; + overflow: hidden; + height: 45px; + z-index: 1; + background-color: #9d73eb; + margin-top: 0px; + margin-bottom: 10px; + box-shadow: 0 0 3px black; +} + +.breadcrumb a { + position: relative; + display: flex; + -ms-flex-positive: 1; + flex-grow: 1; + text-decoration: none; + margin: auto; + height: 100%; + color: white; +} + +.breadcrumb a:first-child { + padding-left: 5.2px; +} + +.breadcrumb a:last-child { + padding-right: 5.2px; +} + +.breadcrumb a:after { + content: ""; + position: absolute; + display: inline-block; + width: 45px; + height: 45px; + top: 0; + right: -20px; + background-color: #9d73eb; + border-top-right-radius: 5px; + transform: scale(0.707) rotate(45deg); + box-shadow: 2px -2px rgba(0, 0, 0, 0.25); + z-index: 1; +} + +.breadcrumb a:last-child:after { + content: none; +} + +.breadcrumb a.active, +.breadcrumb a:hover { + background: #b899f2; + color: white; + text-decoration: underline; +} + +.breadcrumb a.active:after, +.breadcrumb a:hover:after { + background: #b899f2; +} + +.breadcrumb span { + margin: inherit; + z-index: 2; +} diff --git a/addons/gdUnit4/src/reporters/html/template/css/logo.png b/addons/gdUnit4/src/reporters/html/template/css/logo.png index e69de29b..c1db2242 100644 --- a/addons/gdUnit4/src/reporters/html/template/css/logo.png +++ b/addons/gdUnit4/src/reporters/html/template/css/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed176306a061dff6c2a97c76e572bbbdee50cb7f5a496ba10d6898721f198351 +size 49775 diff --git a/addons/gdUnit4/src/reporters/html/template/css/styles.css b/addons/gdUnit4/src/reporters/html/template/css/styles.css index e69de29b..e92d59b7 100644 --- a/addons/gdUnit4/src/reporters/html/template/css/styles.css +++ b/addons/gdUnit4/src/reporters/html/template/css/styles.css @@ -0,0 +1,475 @@ +html, +body { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + font-family: sans-serif; + background-color: white; + height: 100%; +} + +main { + flex-grow: 1; + overflow: auto; + margin: 0 10em; +} + + +header { + color: white; + padding: 1px; + position: relative; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); +} + +.logo { + position: fixed; + top: 20px; + left: 20px; + display: flex; + align-items: center; + z-index: 1000; + filter: grayscale(1); + mix-blend-mode: plus-lighter; +} + +.logo img { + width: 64px; + height: 64px; +} + +.logo span { + font-size: 1.2em; + color: lightslategray; +} + +.report-container { + margin: 0 15em; + text-align: center; + margin-top: 60px; + flex-grow: 0; +} + +h1 { + margin: 0 0 20px 0; + font-size: 2.5em; + font-weight: normal; +} + +.summary { + display: inline-flex; + justify-content: center; + flex-wrap: nowrap; + margin-bottom: 20px; + align-items: baseline; + max-width: 960px; +} + +.summary-item { + flex: 1; + min-width: 80px; +} + +.label { + font-size: 1em; + flex-wrap: nowrap; +} + +.value { + font-size: 0.9em; + display: block; + padding-top: 10px; + color: lightgray; +} + +.success-rate { + padding-left: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.check-icon { + background-color: #34c538; + color: white; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4em; +} + +.rate-text { + text-align: center; + flex-wrap: nowrap; +} + +.percentage { + font-size: 1.2em; + font-weight: bold; +} + + +nav { + padding: 20px 0px; + font-family: monospace; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + border-bottom: 1px solid lightgray; +} + +nav li { + cursor: pointer; + padding: 5px 20px; + font-size: 1.1em; + color: lightslategray; +} + +nav li.active { + color: darkslategray; + border-bottom: 1px solid darkslategray; + font-weight: bold; +} + +div#content { + height: calc(100vh - 400px); +} + + +table { + width: 100%; + height: 100%; + border-collapse: collapse; + overflow: hidden; +} + +thead th { + position: sticky; + top: 0; + background-color: white; + z-index: 1; + border-bottom: 2px solid #ddd; +} + +tbody { + display: block; + /* Limit the height of the table body */ + max-height: calc(100vh - 400px); + /* Enable scrolling on the table body */ + overflow-y: auto; +} + +thead, +tbody tr { + display: table; + width: 100%; + table-layout: fixed; +} + +tbody td { + overflow: hidden; +} + +/* Ensure scrollbar visibility */ +tbody::-webkit-scrollbar { + height: 4px; + width: 14px; +} + +tbody::-webkit-scrollbar-thumb { + background-color: #aaa6a6; + border-radius: 4px; +} + +tbody::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +th, +td { + font-size: .9em; + padding: 5px 0px; + border-bottom: 1px solid #eee; + color: lightslategrey; + text-align: left; + text-wrap: nowrap; + /* Default max and min width for all columns */ + max-width: 150px; + min-width: 80px; + width: 80px; +} + +th { + font-size: 1em; + font-weight: normal; + padding-top: 20px; + color: gray; + text-wrap: nowrap; +} + +.tab-report { + display: grid; + grid-template-columns: 100%; + margin-bottom: 20px; +} + +.tab-report-grid { + display: grid; + grid-template-columns: 70% 30%; + margin-bottom: 20px; +} + + +/* Specific styling for the first column (Testcase) */ +th:first-child, +td:first-child { + padding-left: 5px; + text-align: left; + /* Max width for the first column */ + min-width: 249px; + width: 250px; + /* Enable scrollbar if content exceeds max-width */ + white-space: nowrap; + overflow: auto; +} + +/* Scrollbar styles for first column */ +td:first-child { + overflow-x: auto; + text-overflow: initial; +} + +/* Scrollbar appearance */ +td:first-child::-webkit-scrollbar { + height: 6px; +} + +td:first-child::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; +} + +td:first-child::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +/* Max width for Result column */ +th:nth-child(2), +td:nth-child(2) { + max-width: 140px; + min-width: 140px; + width: 140px; +} + +/* Max width for Quick Results column */ +th:nth-child(9), +td:nth-child(9) { + max-width: 140px; + min-width: 140px; + width: 140px; + padding-right: 10px; +} + +/* Background color for alternating groups */ +.group-bg-1 { + background-color: #f1f1f1; +} + +.group-bg-2 { + background-color: #e0e0e0; +} + +.grid-item { + overflow: auto; + padding-left: 20px; + color: lightslategrey; + max-height: calc(100vh - 350px); +} + +div.tab td.report-column, +th.report-column { + display: none; +} + +/* Result status styles */ +.status { + padding: 2px 40px; + border-radius: 6px; + color: black; + width: 40px; + display: flex; + align-content: center; + align-items: center; +} + +.status-bar { + display: flex; + border-radius: 8px; + overflow: hidden; + height: 20px; + flex-wrap: nowrap; + justify-content: space-evenly; +} + +.status-bar-column { + margin: -2px; + color: black; + display: flex; + align-content: center; + align-items: center; + transition: width 0.3s ease; +} + +.status-skipped { + background-color: #888888; +} + +.status-passed { + background-color: #63bb38; +} + +.status-error { + background-color: #fd1100; +} + +.status-failed { + background-color: #ed594f; +} + +.status-flaky { + background-color: #1d9a1f; +} + +.status-warning { + background-color: #fdda3f; +} + +div.tab tr:hover { + background-color: #d9e7fa; + box-shadow: 0 0 5px black; +} + +div.tab tr.selected { + background-color: #d9e7fa; +} + +div.report-column { + margin-top: 10px; + width: 100%; + text-align: left; +} + +.logging-container { + width: 100%; + height: 100%; +} + +div.godot-report-frame { + margin: 10px; + font-family: monospace; + height: 100%; + background-color: #eee; +} + +div.include-footer { + position: fixed; + bottom: 0; + width: 100%; + display: flex; +} + +footer { + position: static; + left: 0; + bottom: 0; + width: 100%; + white-space: nowrap; + color: lightgray; + font-size: 12px; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); + display: flex; + justify-content: space-between; + align-items: center; +} + +footer p { + padding-left: 10em; +} + +footer .status-legend { + display: flex; + gap: 15px; + width: 500px; +} + +footer a { + color: lightgray; +} + +footer a:hover { + color: whitesmoke; +} + +footer a:visited { + color: whitesmoke; +} + +.status-legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.status-box { + width: 15px; + height: 15px; + border-radius: 3px; + display: inline-block; +} + +/* Normal link */ +a { + color: lightslategrey; +} + +/* Link when hovered */ +a:hover { + color: #9d73eb; +} + +/* Visited link */ +a:visited { + color: #8058e3; +} + +/* Active link (while being clicked) */ +a:active { + color: #8058e3; + /* Custom color when link is clicked */ +} + + +@media (max-width: 1024px) { + .summary { + flex-direction: column; + } + + nav ul { + flex-wrap: wrap; + } + + nav li { + margin-right: 10px; + margin-bottom: 5px; + } +} diff --git a/addons/gdUnit4/src/reporters/html/template/folder_report.html b/addons/gdUnit4/src/reporters/html/template/folder_report.html index e69de29b..2cdca675 100644 --- a/addons/gdUnit4/src/reporters/html/template/folder_report.html +++ b/addons/gdUnit4/src/reporters/html/template/folder_report.html @@ -0,0 +1,122 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + +
+ +
+

Report by Paths

+
+ ${resource_path} +
+
+
+ TestSuites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
+
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + + ${report_table_testsuites} + +
TestSuitesResultTestsSkippedFlakyFailuresOrphansDurationSuccess rate
+
+
+
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + diff --git a/addons/gdUnit4/src/reporters/html/template/index.html b/addons/gdUnit4/src/reporters/html/template/index.html index e69de29b..342c8f25 100644 --- a/addons/gdUnit4/src/reporters/html/template/index.html +++ b/addons/gdUnit4/src/reporters/html/template/index.html @@ -0,0 +1,164 @@ + + + + + + + GdUnit4 Report + + + + +
+ +
+

Summary Report

+
+
+ Test Suites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
+
+
+
+
+ +
+ +
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/src/reporters/html/template/suite_report.html b/addons/gdUnit4/src/reporters/html/template/suite_report.html index e69de29b..47468410 100644 --- a/addons/gdUnit4/src/reporters/html/template/suite_report.html +++ b/addons/gdUnit4/src/reporters/html/template/suite_report.html @@ -0,0 +1,177 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + + + + +
+ +
+

Testsuite Report

+
+ ${resource_path} +
+
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + ${report_table_tests} + +
TestcaseResultSkippedOrphansDurationReport
+
+
+

Failure Report

+
+
+
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd index e69de29b..0b9674f3 100644 --- a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd @@ -0,0 +1,143 @@ +# This class implements the JUnit XML file format +# based checked https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd +class_name JUnitXmlReportWriter +extends GdUnitReportWriter + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const ATTR_CLASSNAME := "classname" +const ATTR_ERRORS := "errors" +const ATTR_FAILURES := "failures" +const ATTR_HOST := "hostname" +const ATTR_ID := "id" +const ATTR_MESSAGE := "message" +const ATTR_NAME := "name" +const ATTR_PACKAGE := "package" +const ATTR_SKIPPED := "skipped" +const ATTR_FLAKY := "flaky" +const ATTR_TESTS := "tests" +const ATTR_TIME := "time" +const ATTR_TIMESTAMP := "timestamp" +const ATTR_TYPE := "type" + +const HEADER := '\n' + + +func output_format() -> String: + return "XML" + + +func write(report_path: String, report: GdUnitReportSummary) -> String: + var result_file: String = "%s/results.xml" % report_path + DirAccess.make_dir_recursive_absolute(report_path) + var file := FileAccess.open(result_file, FileAccess.WRITE) + if file == null: + push_warning("Can't saving the result to '%s'\n Error: %s" % [result_file, error_string(FileAccess.get_open_error())]) + else: + file.store_string(build_junit_report(report_path, report)) + return result_file + + +func build_junit_report(report_path: String, report: GdUnitReportSummary) -> String: + var iso8601_datetime := Time.get_date_string_from_system() + var test_suites := XmlElement.new("testsuites")\ + .attribute(ATTR_ID, iso8601_datetime)\ + .attribute(ATTR_NAME, report_path.get_file())\ + .attribute(ATTR_TESTS, report.test_count())\ + .attribute(ATTR_FAILURES, report.failure_count())\ + .attribute(ATTR_SKIPPED, report.skipped_count())\ + .attribute(ATTR_FLAKY, report.flaky_count())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ + .add_childs(build_test_suites(report)) + var as_string := test_suites.to_xml() + test_suites.dispose() + return HEADER + as_string + + +func build_test_suites(summary: GdUnitReportSummary) -> Array: + var test_suites: Array[XmlElement] = [] + for index in summary.get_reports().size(): + var suite_report :GdUnitTestSuiteReport = summary.get_reports()[index] + var iso8601_datetime := Time.get_datetime_string_from_unix_time(suite_report.time_stamp()) + test_suites.append(XmlElement.new("testsuite")\ + .attribute(ATTR_ID, index)\ + .attribute(ATTR_NAME, suite_report.name())\ + .attribute(ATTR_PACKAGE, suite_report.path())\ + .attribute(ATTR_TIMESTAMP, iso8601_datetime)\ + .attribute(ATTR_HOST, "localhost")\ + .attribute(ATTR_TESTS, suite_report.test_count())\ + .attribute(ATTR_FAILURES, suite_report.failure_count())\ + .attribute(ATTR_ERRORS, suite_report.error_count())\ + .attribute(ATTR_SKIPPED, suite_report.skipped_count())\ + .attribute(ATTR_FLAKY, suite_report.flaky_count())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(suite_report.duration()))\ + .add_childs(build_test_cases(suite_report))) + return test_suites + + +func build_test_cases(suite_report: GdUnitTestSuiteReport) -> Array: + var test_cases: Array[XmlElement] = [] + for index in suite_report.get_reports().size(): + var report :GdUnitTestCaseReport = suite_report.get_reports()[index] + test_cases.append( XmlElement.new("testcase")\ + .attribute(ATTR_NAME, JUnitXmlReportWriter.encode_xml(report.name()))\ + .attribute(ATTR_CLASSNAME, report.suite_name())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ + .add_childs(build_reports(report))) + return test_cases + + +func build_reports(test_report: GdUnitTestCaseReport) -> Array: + var failure_reports: Array[XmlElement] = [] + + for report: GdUnitReport in test_report.get_test_reports(): + if report.is_failure(): + failure_reports.append(XmlElement.new("failure")\ + .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + elif report.is_error(): + failure_reports.append(XmlElement.new("error")\ + .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + elif report.is_skipped(): + failure_reports.append(XmlElement.new("skipped")\ + .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .text(convert_rtf_to_text(report.message()))) + return failure_reports + + +func convert_rtf_to_text(bbcode: String) -> String: + return GdUnitTools.richtext_normalize(bbcode) + + +static func to_type(type: int) -> String: + match type: + GdUnitReport.SUCCESS: + return "SUCCESS" + GdUnitReport.WARN: + return "WARN" + GdUnitReport.FAILURE: + return "FAILURE" + GdUnitReport.ORPHAN: + return "ORPHAN" + GdUnitReport.TERMINATED: + return "TERMINATED" + GdUnitReport.INTERUPTED: + return "INTERUPTED" + GdUnitReport.ABORT: + return "ABORT" + return "UNKNOWN" + + +static func to_time(duration: int) -> String: + return "%4.03f" % (duration / 1000.0) + + +static func encode_xml(value: String) -> String: + return value.xml_escape(true) + + +#static func to_ISO8601_datetime() -> String: + #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]] diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid index e69de29b..daf0f5a2 100644 --- a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://bxwmuqci2eaj1 diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd b/addons/gdUnit4/src/reporters/xml/XmlElement.gd index e69de29b..86c74212 100644 --- a/addons/gdUnit4/src/reporters/xml/XmlElement.gd +++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd @@ -0,0 +1,69 @@ +class_name XmlElement +extends RefCounted + +var _name :String +# Dictionary[String, String] +var _attributes :Dictionary = {} +var _childs :Array[XmlElement] = [] +var _parent :XmlElement = null +var _text :String = "" + + +func _init(name :String) -> void: + _name = name + + +func dispose() -> void: + for child in _childs: + child.dispose() + _childs.clear() + _attributes.clear() + _parent = null + + +func attribute(name :String, value :Variant) -> XmlElement: + _attributes[name] = str(value) + return self + + +func text(p_text :String) -> XmlElement: + _text = p_text if p_text.ends_with("\n") else p_text + "\n" + return self + + +func add_child(child :XmlElement) -> XmlElement: + _childs.append(child) + child._parent = self + return self + + +func add_childs(childs :Array[XmlElement]) -> XmlElement: + for child in childs: + @warning_ignore("return_value_discarded") + add_child(child) + return self + + +func indentation() -> String: + return "" if _parent == null else _parent.indentation() + " " + + +func to_xml() -> String: + var attributes := "" + for key in _attributes.keys() as Array[String]: + attributes += ' {attr}="{value}"'.format({"attr": key, "value": _attributes.get(key)}) + + var childs := "" + for child in _childs: + childs += child.to_xml() + + return "{_indentation}<{name}{attributes}>\n{childs}{text}{_indentation}\n"\ + .format({"name": _name, + "attributes": attributes, + "childs": childs, + "_indentation": indentation(), + "text": cdata(_text)}) + + +func cdata(p_text :String) -> String: + return "" if p_text.is_empty() else "\n".format({"text" : p_text}) diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid index e69de29b..16bc6f0f 100644 --- a/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid +++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid @@ -0,0 +1 @@ +uid://byogn2u5815e0 diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd index e69de29b..5d8b6318 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -0,0 +1,154 @@ +class_name GdUnitSpyBuilder +extends GdUnitClassDoubler + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") +const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] + + +static func build(to_spy: Variant, debug_write := false) -> Variant: + if GdObjects.is_singleton(to_spy): + @warning_ignore("unsafe_cast") + push_error("Spy on a Singleton is not allowed! '%s'" % (to_spy as Object).get_class()) + return null + + # if resource path load it before + if GdObjects.is_scene_resource_path(to_spy): + var scene_resource_path :String = to_spy + if not FileAccess.file_exists(scene_resource_path): + push_error("Can't build spy on scene '%s'! The given resource not exists!" % scene_resource_path) + return null + var scene_to_spy: PackedScene = load(scene_resource_path) + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) + # spy checked PackedScene + if GdObjects.is_scene(to_spy): + var scene_to_spy: PackedScene = to_spy + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) + # spy checked a scene instance + if GdObjects.is_instance_scene(to_spy): + @warning_ignore("unsafe_cast") + return spy_on_scene(to_spy as Node, debug_write) + + var excluded_functions := [] + if to_spy is Callable: + @warning_ignore("unsafe_cast") + to_spy = CallableDoubler.new(to_spy as Callable) + excluded_functions = CallableDoubler.excluded_functions() + + var spy := spy_on_script(to_spy, excluded_functions, debug_write) + if spy == null: + return null + var spy_instance: Object = spy.new() + @warning_ignore("unsafe_method_access") + # we do not call the original implementation for _ready and all input function, this is actualy done by the engine + spy_instance.__init(["_input", "_gui_input", "_input_event", "_unhandled_input"]) + @warning_ignore("unsafe_cast") + copy_properties(to_spy as Object, spy_instance) + @warning_ignore("return_value_discarded") + GdUnitObjectInteractions.reset(spy_instance) + return register_auto_free(spy_instance) + + +static func get_class_info(clazz :Variant) -> Dictionary: + var clazz_path := GdObjects.extract_class_path(clazz) + var clazz_name :String = GdObjects.extract_class_name(clazz).value() + return { + "class_name" : clazz_name, + "class_path" : clazz_path + } + + +static func spy_on_script(instance: Variant, function_excludes: PackedStringArray, debug_write: bool) -> GDScript: + if GdArrayTools.is_array_type(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % type_string(typeof(instance))) + return null + var class_info := get_class_info(instance) + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + if not GdObjects.is_instance(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) + return null + + @warning_ignore("unsafe_method_access") + var spy_template := SPY_TEMPLATE.source_code.format({ + "instance_id" : abs(instance.get_instance_id()), + "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0] + }) + @warning_ignore("unsafe_cast") + var lines := load_template(spy_template, class_info) + @warning_ignore("unsafe_cast") + lines += double_functions(instance as Object, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) + # We disable warning/errors for inferred_declaration + if Engine.get_version_info().hex >= 0x40400: + lines.insert(0, '@warning_ignore_start("inferred_declaration")') + lines.append('@warning_ignore_restore("inferred_declaration")') + + var spy := GDScript.new() + spy.source_code = "\n".join(lines) + spy.resource_name = "Spy%s.gd" % clazz_name + spy.resource_path = GdUnitFileAccess.create_temp_dir("spy") + "/Spy%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + + if debug_write: + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(spy.resource_path) + @warning_ignore("return_value_discarded") + ResourceSaver.save(spy, spy.resource_path) + var error := spy.reload(true) + if error != OK: + push_error("Unexpected Error!, SpyBuilder error, please contact the developer.") + return null + return spy + + +static func spy_on_scene(scene :Node, debug_write :bool) -> Object: + if scene.get_script() == null: + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) + return null + # buils spy checked original script + @warning_ignore("unsafe_cast") + var scene_script :Object = (scene.get_script() as GDScript).new() + var spy := spy_on_script(scene_script, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) + scene_script.free() + if spy == null: + return null + + # we need to restore the original script properties to apply after script exchange + var original_properties := {} + for p in scene.get_property_list(): + var property_name: String = p["name"] + var usage: int = p["usage"] + if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE) == PROPERTY_USAGE_SCRIPT_VARIABLE: + original_properties[property_name] = scene.get(property_name) + + # exchage with spy + scene.set_script(spy) + # apply original script properties to the spy + for property_name: String in original_properties.keys(): + scene.set(property_name, original_properties[property_name]) + + @warning_ignore("unsafe_method_access") + scene.__init() + return register_auto_free(scene) + + +static func copy_properties(source :Object, dest :Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid index e69de29b..70544a25 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid @@ -0,0 +1 @@ +uid://c02xkecgmjjk5 diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd index e69de29b..e0bcaf2b 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -0,0 +1,46 @@ +class_name DoubledSpyClassSourceClassName + +const __INSTANCE_ID := "gdunit_doubler_instance_id_{instance_id}" + + +class GdUnitSpyDoublerState: + const __SOURCE_CLASS := "{gdunit_source_class}" + + var excluded_methods := PackedStringArray() + + func _init(excluded_methods__ := PackedStringArray()) -> void: + excluded_methods = excluded_methods__ + + +var __spy_state := GdUnitSpyDoublerState.new() +@warning_ignore("unused_private_class_variable") +var __verifier_instance := GdUnitObjectInteractionsVerifier.new() + + +func __init(__excluded_methods := PackedStringArray()) -> void: + __init_doubler() + __spy_state.excluded_methods = __excluded_methods + + +static func __doubler_state() -> GdUnitSpyDoublerState: + if Engine.has_meta(__INSTANCE_ID): + return Engine.get_meta(__INSTANCE_ID).__spy_state + return null + + +func __init_doubler() -> void: + Engine.set_meta(__INSTANCE_ID, self) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +static func __get_verifier() -> GdUnitObjectInteractionsVerifier: + return Engine.get_meta(__INSTANCE_ID).__verifier_instance + + +static func __do_call_real_func(__func_name: String) -> bool: + @warning_ignore("unsafe_method_access") + return not __doubler_state().excluded_methods.has(__func_name) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid index e69de29b..11183e5e 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid @@ -0,0 +1 @@ +uid://n4vkvuk2dwgt diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd index e69de29b..eb1e6254 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -0,0 +1,91 @@ +@tool +extends Control + +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const TITLE = "gdUnit4 ${version} Console" + +@onready var header := $VBoxContainer/Header +@onready var title: RichTextLabel = $VBoxContainer/Header/header_title +@onready var output: RichTextLabel = $VBoxContainer/Console/TextEdit + + +var _test_reporter: GdUnitConsoleTestReporter + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + GdUnitFonts.init_fonts(output) + GdUnit4Version.init_version_label(title) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_message.connect(_on_gdunit_message) + GdUnitSignals.instance().gdunit_client_connected.connect(_on_gdunit_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_gdunit_client_disconnected) + _test_reporter = GdUnitConsoleTestReporter.new(GdUnitRichTextMessageWriter.new(output)) + + +func _notification(what: int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + _test_reporter.init_colors() + if what == NOTIFICATION_PREDELETE: + var instance := GdUnitSignals.instance() + if instance.gdunit_event.is_connected(_on_gdunit_event): + instance.gdunit_event.disconnect(_on_gdunit_event) + if instance.gdunit_message.is_connected(_on_gdunit_event): + instance.gdunit_message.disconnect(_on_gdunit_message) + if instance.gdunit_client_connected.is_connected(_on_gdunit_event): + instance.gdunit_client_connected.disconnect(_on_gdunit_client_connected) + if instance.gdunit_client_disconnected.is_connected(_on_gdunit_event): + instance.gdunit_client_disconnected.disconnect(_on_gdunit_client_disconnected) + + +func setup_update_notification(control: Button) -> void: + if not GdUnitSettings.is_update_notification_enabled(): + _test_reporter.println_message("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) + return + + _test_reporter.print_message("Searching for updates... ", Color.CORNFLOWER_BLUE) + var update_client := GdUnitUpdateClient.new() + add_child(update_client) + var response :GdUnitUpdateClient.HttpResponse = await update_client.request_latest_version() + if response.status() != 200: + _test_reporter.println_message("Information cannot be retrieved from GitHub!", Color.INDIAN_RED) + _test_reporter.println_message("Error: %s" % response.response(), Color.INDIAN_RED) + return + var latest_version := update_client.extract_latest_version(response) + if not latest_version.is_greater(GdUnit4Version.current()): + _test_reporter.println_message("GdUnit4 is up-to-date.", Color.FOREST_GREEN) + return + + _test_reporter.println_message("A new update is available %s" % latest_version, Color.YELLOW) + _test_reporter.println_message("Open the GdUnit4 settings and check the update tab.", Color.YELLOW) + + control.icon = GdUnitUiTools.get_icon("Notification", Color.YELLOW) + var tween := create_tween() + tween.tween_property(control, "self_modulate", Color.VIOLET, .2).set_trans(Tween.TransitionType.TRANS_LINEAR) + tween.tween_property(control, "self_modulate", Color.YELLOW, .2).set_trans(Tween.TransitionType.TRANS_BOUNCE) + tween.parallel() + tween.tween_property(control, "scale", Vector2.ONE*1.05, .4).set_trans(Tween.TransitionType.TRANS_LINEAR) + tween.tween_property(control, "scale", Vector2.ONE, .4).set_trans(Tween.TransitionType.TRANS_BOUNCE) + tween.set_loops(-1) + tween.play() + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.SESSION_START: + _test_reporter.test_session = GdUnitTestSession.new(GdUnitTestDiscoverGuard.instance().get_discovered_tests(), "") + GdUnitEvent.SESSION_CLOSE: + _test_reporter.test_session = null + + +func _on_gdunit_client_connected(client_id: int) -> void: + _test_reporter.clear() + _test_reporter.println_message("GdUnit Test Client connected with id: %d" % client_id, Color.hex(0x9887c4)) + + +func _on_gdunit_client_disconnected(client_id: int) -> void: + _test_reporter.println_message("GdUnit Test Client disconnected with id: %d" % client_id, Color.hex(0x9887c4)) + + +func _on_gdunit_message(message: String) -> void: + _test_reporter.println_message(message, Color.CORNFLOWER_BLUE) diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid index e69de29b..d2090024 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid @@ -0,0 +1 @@ +uid://sohxcxmk1j diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.tscn b/addons/gdUnit4/src/ui/GdUnitConsole.tscn index e69de29b..c3c7e29f 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.tscn +++ b/addons/gdUnit4/src/ui/GdUnitConsole.tscn @@ -0,0 +1,64 @@ +[gd_scene load_steps=2 format=3 uid="uid://dm0wvfyeew7vd"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"] + +[node name="Control" type="Control"] +use_parent_material = true +clip_contents = true +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Header" type="PanelContainer" parent="VBoxContainer"] +auto_translate_mode = 2 +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +localize_numeral_system = false +mouse_filter = 2 + +[node name="header_title" type="RichTextLabel" parent="VBoxContainer/Header"] +auto_translate_mode = 2 +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +localize_numeral_system = false +mouse_filter = 2 +bbcode_enabled = true +scroll_active = false +autowrap_mode = 0 +shortcut_keys_enabled = false + +[node name="Console" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="TextEdit" type="RichTextLabel" parent="VBoxContainer/Console"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +bbcode_enabled = true +scroll_following = true +context_menu_enabled = true +selection_enabled = true diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid index e69de29b..ae4a8839 100644 --- a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid @@ -0,0 +1 @@ +uid://bmclcx5f2s1hx diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index e69de29b..a6d53f9b 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -0,0 +1,31 @@ +@tool +class_name GdUnitInspecor +extends Panel + + +var _command_handler := GdUnitCommandHandler.instance() + + +func _ready() -> void: + @warning_ignore("return_value_discarded") + GdUnitCommandHandler.instance().gdunit_runner_start.connect(func() -> void: + var control :Control = get_parent_control() + # if the tab is floating we dont need to set as current + if control is TabContainer: + var tab_container :TabContainer = control + for tab_index in tab_container.get_tab_count(): + if tab_container.get_tab_title(tab_index) == "GdUnit": + tab_container.set_current_tab(tab_index) + ) + + # propagete the test_counters_changed signal to the progress bar + @warning_ignore("unsafe_property_access", "unsafe_method_access") + %MainPanel.test_counters_changed.connect(%ProgressBar._on_test_counter_changed) + +func _process(_delta: float) -> void: + _command_handler._do_process() + + +@warning_ignore("redundant_await") +func _on_status_bar_request_discover_tests() -> void: + await _command_handler.cmd_discover_tests() diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid index e69de29b..0cecf540 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid @@ -0,0 +1 @@ +uid://bf82ceb5733id diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn index e69de29b..2dcb9505 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.tscn +++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn @@ -0,0 +1,71 @@ +[gd_scene load_steps=8 format=3 uid="uid://mpo5o6d4uybu"] + +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] + +[node name="GdUnit" type="Panel"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 11 +size_flags_vertical = 3 +focus_mode = 2 +script = ExtResource("5") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_vertical = 11 +theme_override_constants/separation = 0 + +[node name="Header" type="VBoxContainer" parent="VBoxContainer"] +use_parent_material = true +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 9 +size_flags_vertical = 0 + +[node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")] +layout_mode = 2 +size_flags_vertical = 1 + +[node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 5 +max_value = 0.0 + +[node name="StatusBar" parent="VBoxContainer/Header" instance=ExtResource("3")] +layout_mode = 2 +size_flags_horizontal = 11 + +[node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")] +layout_mode = 2 + +[node name="event_server" parent="." instance=ExtResource("7_721no")] + +[connection signal="request_discover_tests" from="VBoxContainer/Header/StatusBar" to="." method="_on_status_bar_request_discover_tests"] +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [7]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [7]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_skipped_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [2]] +[connection signal="select_skipped_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [2]] +[connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] +[connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="select_first_orphan"] diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd index e69de29b..8dd0265d 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd @@ -0,0 +1,31 @@ +class_name GdUnitInspectorTreeConstants +extends RefCounted + + +# the inspector panel presantation +enum TREE_VIEW_MODE { + TREE, + FLAT +} + + +# The inspector sort modes +enum SORT_MODE { + UNSORTED, + NAME_ASCENDING, + NAME_DESCENDING, + EXECUTION_TIME +} + + +enum STATE { + INITIAL, + RUNNING, + SKIPPED, + SUCCESS, + WARNING, + FLAKY, + FAILED, + ERROR, + ABORDED, +} diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid index e69de29b..16a06986 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid @@ -0,0 +1 @@ +uid://d11pldsm6rcbm diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd b/addons/gdUnit4/src/ui/GdUnitUiTools.gd index e69de29b..0bcdb1d2 100644 --- a/addons/gdUnit4/src/ui/GdUnitUiTools.gd +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd @@ -0,0 +1,151 @@ +class_name GdUnitUiTools +extends RefCounted + + +static var _spinner: AnimatedTexture + + +enum ImageFlipMode { + HORIZONTAl, + VERITCAL +} + + +## Returns the icon by name, if it exists. +static func get_icon(icon_name: String, color: = Color.BLACK) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons") + if icon == null: + return null + if color != Color.BLACK: + icon = _modulate_texture(icon, color) + return icon + + +## Returns the icon flipped +static func get_flipped_icon(icon_name: String, mode: = ImageFlipMode.HORIZONTAl) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons") + if icon == null: + return null + return ImageTexture.create_from_image(_flip_image(icon, mode)) + + +static func get_spinner() -> AnimatedTexture: + if _spinner != null: + return _spinner + _spinner = AnimatedTexture.new() + _spinner.frames = 8 + _spinner.speed_scale = 2.5 + for frame in _spinner.frames: + _spinner.set_frame_texture(frame, get_icon("Progress%d" % (frame+1))) + _spinner.set_frame_duration(frame, 0.2) + return _spinner + + +static func get_color_animated_icon(icon_name :String, from :Color, to :Color) -> AnimatedTexture: + if not Engine.is_editor_hint(): + return null + var texture := AnimatedTexture.new() + texture.frames = 8 + texture.speed_scale = 2.5 + var color := from + for frame in texture.frames: + color = lerp(color, to, .2) + texture.set_frame_texture(frame, get_icon(icon_name, color)) + texture.set_frame_duration(frame, 0.2) + return texture + + +static func get_run_overall_icon() -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon("Play", "EditorIcons") + var image := _merge_images(icon.get_image(), Vector2i(-2, 0), icon.get_image(), Vector2i(3, 0)) + return ImageTexture.create_from_image(image) + + +static func get_GDScript_icon(status: String, color: Color) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon_a := EditorInterface.get_base_control().get_theme_icon("GDScript", "EditorIcons") + var icon_b := EditorInterface.get_base_control().get_theme_icon(status, "EditorIcons") + var overlay_image := _modulate_image(icon_b.get_image(), color) + var image := _merge_images_scaled(icon_a.get_image(), Vector2i(0, 0), overlay_image, Vector2i(5, 5)) + return ImageTexture.create_from_image(image) + + +static func get_CSharpScript_icon(status: String, color: Color) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon_a := EditorInterface.get_base_control().get_theme_icon("CSharpScript", "EditorIcons") + var icon_b := EditorInterface.get_base_control().get_theme_icon(status, "EditorIcons") + var overlay_image := _modulate_image(icon_b.get_image(), color) + var image := _merge_images_scaled(icon_a.get_image(), Vector2i(0, 0), overlay_image, Vector2i(5, 5)) + return ImageTexture.create_from_image(image) + + +static func _modulate_texture(texture: Texture2D, color: Color) -> Texture2D: + var image := _modulate_image(texture.get_image(), color) + return ImageTexture.create_from_image(image) + + +static func _modulate_image(image: Image, color: Color) -> Image: + var data: PackedByteArray = image.data["data"] + for pixel in range(0, data.size(), 4): + var pixel_a := _to_color(data, pixel) + if pixel_a.a8 != 0: + pixel_a = pixel_a.lerp(color, .9) + data[pixel + 0] = pixel_a.r8 + data[pixel + 1] = pixel_a.g8 + data[pixel + 2] = pixel_a.b8 + data[pixel + 3] = pixel_a.a8 + var output_image := Image.new() + output_image.set_data(image.get_width(), image.get_height(), image.has_mipmaps(), image.get_format(), data) + return output_image + + +static func _merge_images(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image: + ## we need to fix the image to have the same size to avoid merge conflicts + if image1.get_height() < image2.get_height(): + image1.resize(image2.get_width(), image2.get_height()) + # Create a new Image for the merged result + var merged_image := Image.create(image1.get_width(), image1.get_height(), false, Image.FORMAT_RGBA8) + merged_image.blit_rect_mask(image1, image2, Rect2(Vector2.ZERO, image1.get_size()), offset1) + merged_image.blit_rect_mask(image1, image2, Rect2(Vector2.ZERO, image2.get_size()), offset2) + return merged_image + + +@warning_ignore("narrowing_conversion") +static func _merge_images_scaled(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image: + ## we need to fix the image to have the same size to avoid merge conflicts + if image1.get_height() < image2.get_height(): + image1.resize(image2.get_width(), image2.get_height()) + # Create a new Image for the merged result + var merged_image := Image.create(image1.get_width(), image1.get_height(), false, image1.get_format()) + merged_image.blend_rect(image1, Rect2(Vector2.ZERO, image1.get_size()), offset1) + @warning_ignore("narrowing_conversion") + image2.resize(image2.get_width()/1.3, image2.get_height()/1.3) + merged_image.blend_rect(image2, Rect2(Vector2.ZERO, image2.get_size()), offset2) + return merged_image + + +static func _flip_image(texture: Texture2D, mode: ImageFlipMode) -> Image: + var flipped_image := Image.new() + flipped_image.copy_from(texture.get_image()) + if mode == ImageFlipMode.VERITCAL: + flipped_image.flip_x() + else: + flipped_image.flip_y() + return flipped_image + + +static func _to_color(data: PackedByteArray, position: int) -> Color: + var pixel_a := Color() + pixel_a.r8 = data[position + 0] + pixel_a.g8 = data[position + 1] + pixel_a.b8 = data[position + 2] + pixel_a.a8 = data[position + 3] + return pixel_a diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid index e69de29b..5ddad466 100644 --- a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid @@ -0,0 +1 @@ +uid://dtcpngc82r2xb diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd b/addons/gdUnit4/src/ui/ScriptEditorControls.gd index e69de29b..5e07fe15 100644 --- a/addons/gdUnit4/src/ui/ScriptEditorControls.gd +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd @@ -0,0 +1,100 @@ +# A tool to provide extended script editor functionallity +class_name ScriptEditorControls +extends RefCounted + +# https://github.com/godotengine/godot/blob/master/editor/plugins/script_editor_plugin.h +# the Editor menu popup items +enum { + FILE_NEW, + FILE_NEW_TEXTFILE, + FILE_OPEN, + FILE_REOPEN_CLOSED, + FILE_OPEN_RECENT, + FILE_SAVE, + FILE_SAVE_AS, + FILE_SAVE_ALL, + FILE_THEME, + FILE_RUN, + FILE_CLOSE, + CLOSE_DOCS, + CLOSE_ALL, + CLOSE_OTHER_TABS, + TOGGLE_SCRIPTS_PANEL, + SHOW_IN_FILE_SYSTEM, + FILE_COPY_PATH, + FILE_TOOL_RELOAD_SOFT, + SEARCH_IN_FILES, + REPLACE_IN_FILES, + SEARCH_HELP, + SEARCH_WEBSITE, + HELP_SEARCH_FIND, + HELP_SEARCH_FIND_NEXT, + HELP_SEARCH_FIND_PREVIOUS, + WINDOW_MOVE_UP, + WINDOW_MOVE_DOWN, + WINDOW_NEXT, + WINDOW_PREV, + WINDOW_SORT, + WINDOW_SELECT_BASE = 100 +} + + +# Saves the given script and closes if requested by +# The script is saved when is opened in the editor. +# The script is closed when is set to true. +static func save_an_open_script(script_path: String, close:=false) -> bool: + #prints("save_an_open_script", script_path, close) + if !Engine.is_editor_hint(): + return false + var editor := EditorInterface.get_script_editor() + var editor_popup := _menu_popup() + # search for the script in all opened editor scrips + for open_script in editor.get_open_scripts(): + if open_script.resource_path == script_path: + # select the script in the editor + EditorInterface.edit_script(open_script, 0); + # save and close + editor_popup.id_pressed.emit(FILE_SAVE) + if close: + editor_popup.id_pressed.emit(FILE_CLOSE) + return true + return false + + +# Saves all opened script +static func save_all_open_script() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(FILE_SAVE_ALL) + + +static func close_open_editor_scripts() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(CLOSE_ALL) + + +# Edits the given script. +# The script is openend in the current editor and selected in the file system dock. +# The line and column on which to open the script can also be specified. +# The script will be open with the user-configured editor for the script's language which may be an external editor. +static func edit_script(script_path: String, line_number := -1) -> void: + var file_system := EditorInterface.get_resource_filesystem() + file_system.update_file(script_path) + var file_system_dock := EditorInterface.get_file_system_dock() + file_system_dock.navigate_to_path(script_path) + EditorInterface.select_file(script_path) + var script: GDScript = load(script_path) + EditorInterface.edit_script(script, line_number) + + +static func _menu_popup() -> PopupMenu: + @warning_ignore("unsafe_method_access") + return EditorInterface.get_script_editor().get_child(0).get_child(0).get_child(0).get_popup() + + +static func _print_menu(popup: PopupMenu) -> void: + for itemIndex in popup.item_count: + prints("get_item_id", popup.get_item_id(itemIndex)) + prints("get_item_accelerator", popup.get_item_accelerator(itemIndex)) + prints("get_item_shortcut", popup.get_item_shortcut(itemIndex)) + prints("get_item_text", popup.get_item_text(itemIndex)) + prints() diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid index e69de29b..c5864627 100644 --- a/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid @@ -0,0 +1 @@ +uid://gpsfvph81sx8 diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd index e69de29b..044b9efc 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -0,0 +1,79 @@ +@tool +extends Control + +var _context_menus := Dictionary() +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + set_name("EditorFileSystemContextMenuHandler") + + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + if script == null: + return false + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)), + ] + for menu in context_menus: + _context_menus[menu.id] = menu + var popup := _menu_popup() + var file_tree := _file_tree() + @warning_ignore("return_value_discarded") + popup.about_to_popup.connect(on_context_menu_show.bind(popup, file_tree)) + @warning_ignore("return_value_discarded") + popup.id_pressed.connect(on_context_menu_pressed.bind(file_tree)) + + +func on_context_menu_show(context_menu: PopupMenu, file_tree: Tree) -> void: + context_menu.add_separator() + var current_index := context_menu.get_item_count() + + for menu_id: int in _context_menus.keys(): + var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] + + context_menu.add_item(menu_item.name, menu_id) + #context_menu.set_item_icon_modulate(current_index, Color.MEDIUM_PURPLE) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(null)) + context_menu.set_item_icon(current_index, GdUnitUiTools.get_icon(menu_item.icon)) + current_index += 1 + + +func on_context_menu_pressed(id: int, file_tree: Tree) -> void: + if !_context_menus.has(id): + return + var menu_item: GdUnitContextMenuItem = _context_menus[id] + var test_suites := collect_testsuites(menu_item, file_tree) + + menu_item.execute([test_suites]) + + +func collect_testsuites(_menu_item: GdUnitContextMenuItem, file_tree: Tree) -> Array[Script]: + var file_system := EditorInterface.get_resource_filesystem() + var selected_item := file_tree.get_selected() + var selected_test_suites: Array[Script] = [] + var suite_scaner := GdUnitTestSuiteScanner.new() + + while selected_item: + var resource_path: String = selected_item.get_metadata(0) + var file_type := file_system.get_file_type(resource_path) + var is_dir := DirAccess.dir_exists_absolute(resource_path) + if is_dir: + selected_test_suites.append_array(suite_scaner.scan_directory(resource_path)) + elif is_dir or file_type == "GDScript" or file_type == "CSharpScript": + # find a performant way to check if the selected item a testsuite + var resource: Script = ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + if _menu_item.is_visible(resource): + @warning_ignore("return_value_discarded") + selected_test_suites.append(resource) + selected_item = file_tree.get_next_selected(selected_item) + return selected_test_suites + + +func _file_tree() -> Tree: + return GdObjects.find_nodes_by_class(EditorInterface.get_file_system_dock(), "Tree", true)[-1] + + +func _menu_popup() -> PopupMenu: + return GdObjects.find_nodes_by_class(EditorInterface.get_file_system_dock(), "PopupMenu")[-1] diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid index e69de29b..d50bd88d 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid @@ -0,0 +1 @@ +uid://njdp45y5pnap diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx index e69de29b..108450e5 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx @@ -0,0 +1,47 @@ +@tool +extends EditorContextMenuPlugin + +var _context_menus := Dictionary() +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + if script == null: + return false + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + _context_menus[GdUnitContextMenuItem.MENU_ID.TEST_RUN] = GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)) + _context_menus[GdUnitContextMenuItem.MENU_ID.TEST_DEBUG] = GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)) + + # setup shortcuts + for menu_item: GdUnitContextMenuItem in _context_menus.values(): + var cb := func call(files: Array) -> void: + menu_item.execute([files]) + add_menu_shortcut(menu_item.shortcut(), cb) + + +func _popup_menu(paths: PackedStringArray) -> void: + var test_suites: Array[Script] = [] + var suite_scaner := GdUnitTestSuiteScanner.new() + + for resource_path in paths: + # directories and test-suites are valid to enable the menu + if DirAccess.dir_exists_absolute(resource_path): + test_suites.append_array(suite_scaner.scan_directory(resource_path)) + continue + + var file_type := resource_path.get_extension() + if file_type == "gd" or file_type == "cs": + var script: Script = ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + if GdUnitTestSuiteScanner.is_test_suite(script): + test_suites.append(script) + + # no direcory or test-suites selected? + if test_suites.is_empty(): + return + + for menu_item: GdUnitContextMenuItem in _context_menus.values(): + @warning_ignore("unused_parameter") + var cb := func call(files: Array) -> void: + menu_item.execute([test_suites]) + add_context_menu_item(menu_item.name, cb, GdUnitUiTools.get_icon(menu_item.icon)) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd index e69de29b..2f89c61e 100644 --- a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -0,0 +1,69 @@ +class_name GdUnitContextMenuItem + +enum MENU_ID { + UNDEFINED = 0, + TEST_RUN = 1000, + TEST_DEBUG = 1001, + TEST_RERUN = 1002, + CREATE_TEST = 1010, +} + +var id: MENU_ID = MENU_ID.UNDEFINED: + set(value): + id = value + get: + return id + +var name: StringName: + set(value): + name = value + get: + return name + +var command: GdUnitCommand: + set(value): + command = value + get: + return command + +var visible: Callable: + set(value): + visible = value + get: + return visible + +var icon: String: + set(value): + icon = value + get: + return icon + + +func _init(p_id: MENU_ID, p_name: StringName, p_icon :String, p_is_visible: Callable, p_command: GdUnitCommand) -> void: + assert(p_id != null, "(%s) missing parameter 'MENU_ID'" % p_name) + assert(p_is_visible != null, "(%s) missing parameter 'GdUnitCommand'" % p_name) + assert(p_command != null, "(%s) missing parameter 'GdUnitCommand'" % p_name) + self.id = p_id + self.name = p_name + self.icon = p_icon + self.command = p_command + self.visible = p_is_visible + + +func shortcut() -> Shortcut: + return GdUnitCommandHandler.instance().get_shortcut(command.shortcut) + + +func is_enabled(script: Script) -> bool: + return command.is_enabled.call(script) + + +func is_visible(script: Script) -> bool: + return visible.call(script) + + +func execute(arguments:=[]) -> void: + if arguments.is_empty(): + command.runnable.call() + else: + command.runnable.callv(arguments) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid index e69de29b..855f4797 100644 --- a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid @@ -0,0 +1 @@ +uid://bla1g2ce6t53i diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd index e69de29b..550c6d61 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -0,0 +1,81 @@ +@tool +extends Control + +var _context_menus := Dictionary() +var _editor: ScriptEditor +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + set_name("ScriptEditorContextMenuHandler") + + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", "New", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) + ] + for menu in context_menus: + _context_menus[menu.id] = menu + _editor = EditorInterface.get_script_editor() + @warning_ignore("return_value_discarded") + _editor.editor_script_changed.connect(on_script_changed) + on_script_changed(active_script()) + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.is_pressed(): + for action: GdUnitContextMenuItem in _context_menus.values(): + if action.shortcut().matches_event(event) and action.is_visible(active_script()): + #if not has_editor_focus(): + # return + action.execute() + accept_event() + return + + +func has_editor_focus() -> bool: + return (Engine.get_main_loop() as SceneTree).root.gui_get_focus_owner() == active_base_editor() + + +func on_script_changed(script: Script) -> void: + if script is Script: + var popups: Array[Node] = GdObjects.find_nodes_by_class(active_editor(), "PopupMenu", true) + for popup: PopupMenu in popups: + if not popup.about_to_popup.is_connected(on_context_menu_show): + popup.about_to_popup.connect(on_context_menu_show.bind(script, popup)) + if not popup.id_pressed.is_connected(on_context_menu_pressed): + popup.id_pressed.connect(on_context_menu_pressed) + + +func on_context_menu_show(script: Script, context_menu: PopupMenu) -> void: + #prints("on_context_menu_show", _context_menus.keys(), context_menu, self) + context_menu.add_separator() + var current_index := context_menu.get_item_count() + for menu_id: int in _context_menus.keys(): + var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] + if menu_item.is_visible(script): + context_menu.add_item(menu_item.name, menu_id) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(script)) + context_menu.set_item_shortcut(current_index, menu_item.shortcut(), true) + current_index += 1 + + +func on_context_menu_pressed(id: int) -> void: + if !_context_menus.has(id): + return + var menu_item: GdUnitContextMenuItem = _context_menus[id] + menu_item.execute() + + +func active_editor() -> ScriptEditorBase: + return _editor.get_current_editor() + + +func active_base_editor() -> TextEdit: + return active_editor().get_base_editor() + + +func active_script() -> Script: + return _editor.get_current_script() diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid index e69de29b..8f0e9e84 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid @@ -0,0 +1 @@ +uid://cy5cblh8psm0a diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx index e69de29b..a879e378 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx @@ -0,0 +1,33 @@ +@tool +extends EditorContextMenuPlugin + +var _context_menus := Dictionary() +var _editor: ScriptEditor +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", "New", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) + ] + for menu in context_menus: + _context_menus[menu.id] = menu + _editor = EditorInterface.get_script_editor() + @warning_ignore("return_value_discarded") + + +func _popup_menu(paths: PackedStringArray) -> void: + var script_path := paths[0] + var script: Script = ResourceLoader.load(script_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + + for menu_id: int in _context_menus.keys(): + var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] + if menu_item.is_visible(script): + add_context_menu_item(menu_item.name, + func call(files: Array) -> void: + menu_item.execute([script_path]), + GdUnitUiTools.get_icon(menu_item.icon)) diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd index e69de29b..c36b3ecb 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd @@ -0,0 +1,54 @@ +@tool +extends PanelContainer + +signal jump_to_orphan_nodes() + +@onready var ICON_GREEN := GdUnitUiTools.get_icon("Unlinked", Color.WEB_GREEN) +@onready var ICON_RED := GdUnitUiTools.get_color_animated_icon("Unlinked", Color.YELLOW, Color.ORANGE_RED) + +@onready var _button_time: Button = %btn_time +@onready var _time: Label = %time_value +@onready var _orphans: Label = %orphan_value +@onready var _orphan_button: Button = %btn_orphan + +var total_elapsed_time := 0 +var total_orphans := 0 + + +func _ready() -> void: + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + _time.text = "" + _orphans.text = "0" + _button_time.icon = GdUnitUiTools.get_icon("Time") + _orphan_button.icon = ICON_GREEN + + +func status_changed(elapsed_time: int, orphan_nodes: int) -> void: + total_elapsed_time += elapsed_time + total_orphans += orphan_nodes + _time.text = LocalTime.elapsed(total_elapsed_time) + _orphans.text = str(total_orphans) + if total_orphans > 0: + _orphan_button.icon = ICON_RED + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + _orphan_button.icon = ICON_GREEN + total_elapsed_time = 0 + total_orphans = 0 + status_changed(0, 0) + GdUnitEvent.TESTCASE_BEFORE: + pass + GdUnitEvent.TESTCASE_AFTER: + status_changed(0, event.orphan_nodes()) + GdUnitEvent.TESTSUITE_BEFORE: + pass + GdUnitEvent.TESTSUITE_AFTER: + status_changed(event.elapsed_time(), event.orphan_nodes()) + + +func _on_ToolButton_pressed() -> void: + jump_to_orphan_nodes.emit() diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid index e69de29b..93762004 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid @@ -0,0 +1 @@ +uid://bor3tq32bx7ss diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn index e69de29b..0262b8cd 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn @@ -0,0 +1,94 @@ +[gd_scene load_steps=6 format=3 uid="uid://djp8ait0bxpsc"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.gd" id="3"] + +[sub_resource type="Image" id="Image_sx31i"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 227, 227, 227, 36, 227, 227, 227, 36, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 76, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 99, 224, 224, 224, 232, 224, 224, 224, 244, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 244, 224, 224, 224, 233, 224, 224, 224, 97, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 135, 224, 224, 224, 247, 224, 224, 224, 115, 234, 234, 234, 12, 224, 224, 224, 130, 224, 224, 224, 130, 234, 234, 234, 12, 225, 225, 225, 116, 224, 224, 224, 248, 224, 224, 224, 132, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 224, 224, 224, 251, 224, 224, 224, 64, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 66, 224, 224, 224, 252, 225, 225, 225, 75, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 201, 224, 224, 224, 146, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 146, 224, 224, 224, 106, 255, 255, 255, 0, 225, 225, 225, 150, 224, 224, 224, 195, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 255, 226, 226, 226, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 166, 224, 224, 224, 237, 228, 228, 228, 47, 255, 255, 255, 0, 225, 225, 225, 51, 224, 224, 224, 255, 224, 224, 224, 16, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 67, 224, 224, 224, 255, 225, 225, 225, 215, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 223, 223, 223, 239, 224, 224, 224, 253, 224, 224, 224, 49, 255, 255, 255, 0, 230, 230, 230, 30, 224, 224, 224, 230, 224, 224, 224, 255, 224, 224, 224, 49, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 224, 224, 224, 255, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 139, 224, 224, 224, 139, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 117, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 224, 224, 224, 240, 226, 226, 226, 87, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 224, 224, 224, 236, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 143, 224, 224, 224, 211, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 11, 224, 224, 224, 216, 225, 225, 225, 141, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 238, 238, 238, 15, 224, 224, 224, 220, 224, 224, 224, 178, 238, 238, 238, 15, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 227, 227, 227, 18, 224, 224, 224, 184, 224, 224, 224, 218, 238, 238, 238, 15, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 212, 224, 224, 224, 232, 225, 225, 225, 133, 224, 224, 224, 251, 224, 224, 224, 240, 225, 225, 225, 135, 224, 224, 224, 234, 224, 224, 224, 208, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 107, 224, 224, 224, 197, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 196, 224, 224, 224, 104, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ugpqy"] +image = SubResource("Image_sx31i") + +[sub_resource type="Image" id="Image_gkq5u"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 247, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 247, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 22, 138, 22, 236, 255, 255, 255, 0, 22, 138, 22, 255, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 254, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 236, 22, 138, 22, 253, 22, 138, 22, 236, 22, 138, 22, 251, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 248, 22, 138, 22, 233, 23, 138, 23, 233, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 236, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 249, 22, 138, 22, 253, 23, 138, 23, 232, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 22, 138, 22, 255, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 24, 139, 24, 231, 23, 138, 23, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 231, 23, 138, 23, 234, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 247, 22, 138, 22, 249, 24, 139, 24, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 245, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 254, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 241, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 246, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 248, 23, 138, 23, 232, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 245, 23, 138, 23, 241, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 253, 22, 138, 22, 255, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 231, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 253, 22, 138, 22, 255, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 249, 22, 138, 22, 234, 23, 138, 23, 234, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 233, 22, 138, 22, 253, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 246, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 246, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_nj5du"] +image = SubResource("Image_gkq5u") + +[node name="Monitor" type="PanelContainer"] +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -793.0 +offset_bottom = -564.0 +size_flags_horizontal = 9 +size_flags_vertical = 9 +script = ExtResource("3") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 4 + +[node name="timer" type="HBoxContainer" parent="HBoxContainer"] +layout_mode = 2 + +[node name="btn_time" type="Button" parent="HBoxContainer/timer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +auto_translate = false +localize_numeral_system = false +tooltip_text = "Shows the total elapsed time of test execution." +mouse_force_pass_scroll_events = false +button_mask = 0 +shortcut_feedback = false +shortcut_in_tooltip = false +text = "Time" +icon = SubResource("ImageTexture_ugpqy") +flat = true + +[node name="time_value" type="Label" parent="HBoxContainer/timer"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +auto_translate = false +localize_numeral_system = false +max_lines_visible = 1 + +[node name="orphan" type="HBoxContainer" parent="HBoxContainer/timer"] +layout_mode = 2 + +[node name="btn_orphan" type="Button" parent="HBoxContainer/timer/orphan"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +auto_translate = false +localize_numeral_system = false +tooltip_text = "Shows the total orphan nodes detected." +text = "Orphans" +icon = SubResource("ImageTexture_nj5du") + +[node name="orphan_value" type="Label" parent="HBoxContainer/timer/orphan"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +auto_translate = false +localize_numeral_system = false +text = "0" +max_lines_visible = 1 diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd index e69de29b..d368ed3f 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd @@ -0,0 +1,49 @@ +@tool +extends ProgressBar + + +@onready var status: Label = $Label +@onready var style: StyleBoxFlat = get("theme_override_styles/fill") + +var _state: GdUnitInspectorTreeConstants.STATE + +func _ready() -> void: + style.bg_color = Color.DARK_GREEN + value = 0 + max_value = 0 + update_text() + + +func update_text() -> void: + status.text = "%d:%d" % [value, max_value] + + +func _on_test_counter_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) -> void: + value = index + max_value = total + update_text() + + # inital state + if index == 0: + style.bg_color = Color.DARK_GREEN + + # do only update the state is higher prio than current state + if state <= _state: + return + _state = state + + if is_flaky(state): + style.bg_color = Color.WEB_GREEN + if is_failed(state): + style.bg_color = Color.DARK_RED + + +func is_failed(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state in [ + GdUnitInspectorTreeConstants.STATE.FAILED, + GdUnitInspectorTreeConstants.STATE.ERROR, + GdUnitInspectorTreeConstants.STATE.ABORDED] + + +func is_flaky(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state == GdUnitInspectorTreeConstants.STATE.FLAKY diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid index e69de29b..32e2c281 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid @@ -0,0 +1 @@ +uid://cs8fqgtcum3s5 diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn index e69de29b..1824230a 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=3 format=3 uid="uid://dva3tonxsxrlk"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ayfir"] +bg_color = Color(0, 0.392157, 0, 1) + +[node name="ProgressBar" type="ProgressBar"] +custom_minimum_size = Vector2(0, 20) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 9 +theme_override_styles/fill = SubResource("StyleBoxFlat_ayfir") +rounded = true +allow_greater = true +show_percentage = false +script = ExtResource("1") + +[node name="Label" type="Label" parent="."] +use_parent_material = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +horizontal_alignment = 1 +vertical_alignment = 1 +max_lines_visible = 1 diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd index e69de29b..dfed5204 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -0,0 +1,216 @@ +@tool +extends PanelContainer + +signal select_failure_next() +signal select_failure_prevous() +signal select_error_next() +signal select_error_prevous() +signal select_flaky_next() +signal select_flaky_prevous() +signal select_skipped_next() +signal select_skipped_prevous() +signal request_discover_tests() + +@warning_ignore("unused_signal") +signal tree_view_mode_changed(flat :bool) + +@onready var _errors: Label = %error_value +@onready var _failures: Label = %failure_value +@onready var _flaky_value: Label = %flaky_value +@onready var _skipped_value: Label = %skipped_value +#@onready var _button_failure_up: Button = %btn_failure_up +#@onready var _button_failure_down: Button = %btn_failure_down +@onready var _button_sync: Button = %btn_tree_sync +@onready var _button_view_mode: MenuButton = %btn_tree_mode +@onready var _button_sort_mode: MenuButton = %btn_tree_sort + +@onready var _icon_errors: TextureRect = %icon_errors +@onready var _icon_failures: TextureRect = %icon_failures +@onready var _icon_flaky: TextureRect = %icon_flaky +@onready var _icon_skipped: TextureRect = %icon_skipped + +var total_failed := 0 +var total_errors := 0 +var total_flaky := 0 +var total_skipped := 0 + + +var icon_mappings := { + # tree sort modes + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED : GdUnitUiTools.get_icon("TripleBar"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING : GdUnitUiTools.get_icon("Sort"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING : GdUnitUiTools.get_flipped_icon("Sort"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME : GdUnitUiTools.get_icon("History"), + # tree view modes + 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE : GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE), + 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT : GdUnitUiTools.get_icon("AnimationTrackGroup", Color.GHOST_WHITE) +} + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + _failures.text = "0" + _errors.text = "0" + _flaky_value.text = "0" + _skipped_value.text = "0" + _icon_failures.texture = GdUnitUiTools.get_icon("StatusError", Color.SKY_BLUE) + _icon_errors.texture = GdUnitUiTools.get_icon("StatusError", Color.DARK_RED) + _icon_flaky.texture = GdUnitUiTools.get_icon("CheckBox", Color.GREEN_YELLOW) + _icon_skipped.texture = GdUnitUiTools.get_icon("CheckBox", Color.WEB_GRAY) + + #_button_failure_up.icon = GdUnitUiTools.get_icon("ArrowUp") + #_button_failure_down.icon = GdUnitUiTools.get_icon("ArrowDown") + _button_sync.icon = GdUnitUiTools.get_icon("Loop") + _set_sort_mode_menu_options() + _set_view_mode_menu_options() + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + var command_handler := GdUnitCommandHandler.instance() + command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) + command_handler.gdunit_runner_stop.connect(_on_gdunit_runner_stop) + + + +func _set_sort_mode_menu_options() -> void: + _button_sort_mode.icon = GdUnitUiTools.get_icon("Sort") + # construct context sort menu according to the available modes + var context_menu :PopupMenu = _button_sort_mode.get_popup() + context_menu.clear() + + if not context_menu.index_pressed.is_connected(_on_sort_mode_changed): + @warning_ignore("return_value_discarded") + context_menu.index_pressed.connect(_on_sort_mode_changed) + + var configured_sort_mode := GdUnitSettings.get_inspector_tree_sort_mode() + for sort_mode: String in GdUnitInspectorTreeConstants.SORT_MODE.keys(): + var enum_value :int = GdUnitInspectorTreeConstants.SORT_MODE.get(sort_mode) + var icon :Texture2D = icon_mappings[0x100 + enum_value] + context_menu.add_icon_check_item(icon, normalise(sort_mode), enum_value) + context_menu.set_item_checked(enum_value, configured_sort_mode == enum_value) + + +func _set_view_mode_menu_options() -> void: + _button_view_mode.icon = GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE) + # construct context tree view menu according to the available modes + var context_menu :PopupMenu = _button_view_mode.get_popup() + context_menu.clear() + + if not context_menu.index_pressed.is_connected(_on_tree_view_mode_changed): + @warning_ignore("return_value_discarded") + context_menu.index_pressed.connect(_on_tree_view_mode_changed) + + var configured_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode() + for tree_view_mode: String in GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys(): + var enum_value :int = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.get(tree_view_mode) + var icon :Texture2D = icon_mappings[0x200 + enum_value] + context_menu.add_icon_check_item(icon, normalise(tree_view_mode), enum_value) + context_menu.set_item_checked(enum_value, configured_tree_view_mode == enum_value) + + +func normalise(value: String) -> String: + var parts := value.to_lower().split("_") + parts[0] = parts[0].capitalize() + return " ".join(parts) + + +func status_changed(errors: int, failed: int, flaky: int, skipped: int) -> void: + total_failed += failed + total_errors += errors + total_flaky += flaky + total_skipped += skipped + _failures.text = str(total_failed) + _errors.text = str(total_errors) + _flaky_value.text = str(total_flaky) + _skipped_value.text = str(total_skipped) + + +func disable_buttons(value :bool) -> void: + _button_sync.set_disabled(value) + _button_sort_mode.set_disabled(value) + _button_view_mode.set_disabled(value) + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.DISCOVER_START: + disable_buttons(true) + + GdUnitEvent.DISCOVER_END: + disable_buttons(false) + + GdUnitEvent.INIT: + total_errors = 0 + total_failed = 0 + total_flaky = 0 + total_skipped = 0 + status_changed(total_errors, total_failed, total_flaky, total_skipped) + + GdUnitEvent.TESTCASE_AFTER: + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), event.is_skipped()) + + GdUnitEvent.TESTSUITE_AFTER: + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), 0) + + +func _on_btn_error_up_pressed() -> void: + select_error_prevous.emit() + + +func _on_btn_error_down_pressed() -> void: + select_error_next.emit() + + +func _on_failure_up_pressed() -> void: + select_failure_prevous.emit() + + +func _on_failure_down_pressed() -> void: + select_failure_next.emit() + + +func _on_btn_flaky_up_pressed() -> void: + select_flaky_prevous.emit() + + +func _on_btn_flaky_down_pressed() -> void: + select_flaky_next.emit() + + +func _on_btn_skipped_up_pressed() -> void: + select_skipped_prevous.emit() + + +func _on_btn_skipped_down_pressed() -> void: + select_skipped_next.emit() + + +func _on_tree_sync_pressed() -> void: + request_discover_tests.emit() + + +func _on_sort_mode_changed(index: int) -> void: + var selected_sort_mode :GdUnitInspectorTreeConstants.SORT_MODE = GdUnitInspectorTreeConstants.SORT_MODE.values()[index] + GdUnitSettings.set_inspector_tree_sort_mode(selected_sort_mode) + + +func _on_tree_view_mode_changed(index: int) ->void: + var selected_tree_mode :GdUnitInspectorTreeConstants.TREE_VIEW_MODE = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.values()[index] + GdUnitSettings.set_inspector_tree_view_mode(selected_tree_mode) + + +################################################################################ +# external signal receiver +################################################################################ +func _on_gdunit_runner_start() -> void: + disable_buttons(true) + + +func _on_gdunit_runner_stop(_client_id: int) -> void: + disable_buttons(false) + + +func _on_settings_changed(property :GdUnitProperty) -> void: + if property.name() == GdUnitSettings.INSPECTOR_TREE_SORT_MODE: + _set_sort_mode_menu_options() + if property.name() == GdUnitSettings.INSPECTOR_TREE_VIEW_MODE: + _set_view_mode_menu_options() diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid index e69de29b..b0245bed 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid @@ -0,0 +1 @@ +uid://bhkahqvjp7rto diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn index e69de29b..baf6a9fb 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn @@ -0,0 +1,477 @@ +[gd_scene load_steps=30 format=3 uid="uid://c22l4odk7qesc"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" id="3"] + +[sub_resource type="Image" id="Image_mb3ih"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 160, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 75, 224, 224, 224, 188, 224, 224, 224, 238, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 224, 224, 224, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 133, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 226, 226, 226, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 77, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 117, 224, 224, 224, 32, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 212, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 129, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 159, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 255, 224, 224, 224, 185, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 242, 224, 224, 224, 255, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 25, 224, 224, 224, 255, 224, 224, 224, 238, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 243, 224, 224, 224, 254, 233, 233, 233, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 229, 229, 229, 29, 224, 224, 224, 255, 224, 224, 224, 236, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 189, 224, 224, 224, 255, 225, 225, 225, 68, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 224, 224, 224, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 121, 224, 224, 224, 255, 224, 224, 224, 181, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 72, 224, 224, 224, 121, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 43, 224, 224, 224, 213, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 227, 227, 227, 36, 225, 225, 225, 124, 224, 224, 224, 254, 224, 224, 224, 255, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 96, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 95, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 224, 224, 224, 185, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 42, 224, 224, 224, 213, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 225, 225, 225, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_wo03e"] +image = SubResource("Image_mb3ih") + +[sub_resource type="Image" id="Image_ixycx"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_c80wp"] +image = SubResource("Image_ixycx") + +[sub_resource type="Image" id="Image_eis20"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_t2qd7"] +image = SubResource("Image_eis20") + +[sub_resource type="Image" id="Image_jh28t"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_1mh1t"] +image = SubResource("Image_jh28t") + +[sub_resource type="Image" id="Image_lpjla"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 3, 224, 224, 224, 105, 224, 224, 224, 192, 224, 224, 224, 244, 224, 224, 224, 238, 224, 224, 224, 197, 224, 224, 224, 105, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 225, 225, 225, 207, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 226, 226, 226, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 6, 224, 224, 224, 205, 224, 224, 224, 255, 224, 224, 224, 218, 225, 225, 225, 83, 237, 237, 237, 14, 237, 237, 237, 14, 224, 224, 224, 82, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 102, 224, 224, 224, 255, 224, 224, 224, 218, 227, 227, 227, 18, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 224, 224, 224, 16, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 198, 224, 224, 224, 255, 225, 225, 225, 84, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 86, 224, 224, 224, 255, 224, 224, 224, 194, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 255, 255, 255, 4, 224, 224, 224, 238, 224, 224, 224, 255, 227, 227, 227, 18, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 229, 229, 229, 19, 224, 224, 224, 255, 224, 224, 224, 233, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 160, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 159, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 237, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 90, 224, 224, 224, 255, 224, 224, 224, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 42, 224, 224, 224, 245, 224, 224, 224, 245, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 232, 232, 22, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 96, 226, 226, 226, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 20, 224, 224, 224, 88, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 227, 227, 227, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 236, 224, 224, 224, 195, 224, 224, 224, 96, 255, 255, 255, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_bq8kn"] +image = SubResource("Image_lpjla") + +[sub_resource type="Image" id="Image_bwbka"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_8lbfl"] +image = SubResource("Image_bwbka") + +[sub_resource type="Image" id="Image_ki3oo"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ivm1h"] +image = SubResource("Image_ki3oo") + +[sub_resource type="Image" id="Image_uqb0l"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 249, 249, 255, 230, 246, 246, 252, 230, 249, 249, 255, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 237, 246, 246, 252, 255, 246, 246, 252, 248, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 236, 246, 246, 252, 254, 246, 246, 252, 247, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 253, 231, 246, 246, 253, 232, 246, 246, 252, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 243, 246, 246, 252, 255, 246, 246, 252, 242, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 242, 246, 246, 252, 253, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_j00vj"] +image = SubResource("Image_uqb0l") + +[sub_resource type="Image" id="Image_0oden"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 254, 151, 12, 11, 254, 151, 12, 11, 250, 151, 12, 11, 242, 151, 12, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 238, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 10, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 240, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 10, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 241, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 250, 151, 12, 11, 241, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_suo5c"] +image = SubResource("Image_0oden") + +[sub_resource type="Image" id="Image_ipq44"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_1oriu"] +image = SubResource("Image_ipq44") + +[sub_resource type="Image" id="Image_d5kq4"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ikyhk"] +image = SubResource("Image_d5kq4") + +[sub_resource type="Image" id="Image_8d0da"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 223, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 254, 147, 197, 222, 254, 147, 197, 222, 250, 147, 197, 222, 242, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 238, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 240, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 241, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 221, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 237, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 250, 147, 197, 222, 241, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_qagbu"] +image = SubResource("Image_8d0da") + +[sub_resource type="Image" id="Image_oy0ff"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 247, 171, 255, 57, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 58, 232, 170, 254, 57, 232, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 58, 232, 170, 253, 57, 251, 170, 253, 57, 251, 170, 254, 58, 236, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 244, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 170, 254, 57, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_a4rkr"] +image = SubResource("Image_oy0ff") + +[sub_resource type="Image" id="Image_iahim"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 247, 130, 141, 130, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 131, 232, 129, 140, 130, 232, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 130, 140, 131, 232, 129, 139, 130, 251, 129, 139, 130, 251, 129, 139, 130, 236, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 129, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 244, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 129, 139, 130, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_idt7c"] +image = SubResource("Image_iahim") + +[node name="StatusBar" type="PanelContainer"] +clip_contents = true +anchors_preset = 10 +anchor_right = 1.0 +offset_right = -807.0 +offset_bottom = 31.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +size_flags_vertical = 0 +script = ExtResource("3") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 0 + +[node name="tree_tools" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 0 + +[node name="Label" type="Label" parent="VBoxContainer/tree_tools"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Statistics" + +[node name="tree_buttons" type="HBoxContainer" parent="VBoxContainer/tree_tools"] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +alignment = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/tree_tools/tree_buttons"] +layout_mode = 2 + +[node name="btn_tree_sync" type="Button" parent="VBoxContainer/tree_tools/tree_buttons"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Run discover tests." +icon = SubResource("ImageTexture_wo03e") + +[node name="btn_tree_sort" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree sorting mode." +icon = SubResource("ImageTexture_c80wp") +flat = false +item_count = 4 +popup/item_0/text = "Unsorted" +popup/item_0/icon = SubResource("ImageTexture_t2qd7") +popup/item_0/checkable = 1 +popup/item_0/id = 0 +popup/item_1/text = "Name ascending" +popup/item_1/icon = SubResource("ImageTexture_c80wp") +popup/item_1/checkable = 1 +popup/item_1/checked = true +popup/item_1/id = 1 +popup/item_2/text = "Name descending" +popup/item_2/icon = SubResource("ImageTexture_1mh1t") +popup/item_2/checkable = 1 +popup/item_2/id = 2 +popup/item_3/text = "Execution time" +popup/item_3/icon = SubResource("ImageTexture_bq8kn") +popup/item_3/checkable = 1 +popup/item_3/id = 3 + +[node name="btn_tree_mode" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree presentation mode." +icon = SubResource("ImageTexture_8lbfl") +flat = false +item_count = 2 +popup/item_0/text = "Tree" +popup/item_0/icon = SubResource("ImageTexture_ivm1h") +popup/item_0/checkable = 1 +popup/item_0/checked = true +popup/item_0/id = 0 +popup/item_1/text = "Flat" +popup/item_1/icon = SubResource("ImageTexture_j00vj") +popup/item_1/checkable = 1 +popup/item_1/id = 1 + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="status_bar" type="HFlowContainer" parent="VBoxContainer"] +layout_direction = 2 +layout_mode = 2 +size_flags_vertical = 2 + +[node name="error" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +size_flags_vertical = 0 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/error"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="icon_errors" type="TextureRect" parent="VBoxContainer/status_bar/error/icon"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Error Tests" +texture = SubResource("ImageTexture_suo5c") +stretch_mode = 3 + +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/error/icon"] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous error test" +icon = SubResource("ImageTexture_1oriu") + +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/error"] +auto_translate_mode = 2 +layout_mode = 2 +localize_numeral_system = false + +[node name="error_value" type="Label" parent="VBoxContainer/status_bar/error/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) +layout_mode = 2 +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 + +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/error/counter"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next error test" +icon = SubResource("ImageTexture_ikyhk") + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/status_bar"] +layout_mode = 2 + +[node name="failure" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="icon_failures" type="TextureRect" parent="VBoxContainer/status_bar/failure/icon"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Failed Tests" +texture = SubResource("ImageTexture_qagbu") +stretch_mode = 3 + +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/failure/icon"] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous failed test" +icon = SubResource("ImageTexture_1oriu") + +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"] +auto_translate_mode = 2 +layout_mode = 2 +localize_numeral_system = false + +[node name="failure_value" type="Label" parent="VBoxContainer/status_bar/failure/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) +layout_mode = 2 +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 + +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/failure/counter"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next failed test" +icon = SubResource("ImageTexture_ikyhk") + +[node name="VSeparator2" type="VSeparator" parent="VBoxContainer/status_bar"] +layout_mode = 2 + +[node name="flaky" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="icon_flaky" type="TextureRect" parent="VBoxContainer/status_bar/flaky/icon"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Flaky Tests" +texture = SubResource("ImageTexture_a4rkr") +stretch_mode = 3 + +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/flaky/icon"] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous flaky test" +icon = SubResource("ImageTexture_1oriu") + +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +auto_translate_mode = 2 +layout_mode = 2 +localize_numeral_system = false + +[node name="flaky_value" type="Label" parent="VBoxContainer/status_bar/flaky/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) +layout_mode = 2 +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 + +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/flaky/counter"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next flaky test" +icon = SubResource("ImageTexture_ikyhk") + +[node name="VSeparator3" type="VSeparator" parent="VBoxContainer/status_bar"] +layout_mode = 2 + +[node name="skipped" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="icon_skipped" type="TextureRect" parent="VBoxContainer/status_bar/skipped/icon"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Skipped Tests" +texture = SubResource("ImageTexture_idt7c") +stretch_mode = 3 + +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/skipped/icon"] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous skipped test" +icon = SubResource("ImageTexture_1oriu") + +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"] +auto_translate_mode = 2 +layout_mode = 2 +localize_numeral_system = false + +[node name="skipped_value" type="Label" parent="VBoxContainer/status_bar/skipped/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) +layout_mode = 2 +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 + +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/skipped/counter"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next skipped test" +icon = SubResource("ImageTexture_ikyhk") + +[connection signal="pressed" from="VBoxContainer/tree_tools/tree_buttons/btn_tree_sync" to="." method="_on_tree_sync_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/error/icon/btn_up" to="." method="_on_btn_error_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/error/counter/btn_down" to="." method="_on_btn_error_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failure/icon/btn_up" to="." method="_on_failure_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failure/counter/btn_down" to="." method="_on_failure_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/icon/btn_up" to="." method="_on_btn_flaky_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/counter/btn_down" to="." method="_on_btn_flaky_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/skipped/icon/btn_up" to="." method="_on_btn_skipped_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/skipped/counter/btn_down" to="." method="_on_btn_skipped_down_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd index e69de29b..0cca901a 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -0,0 +1,130 @@ +@tool +extends PanelContainer + +signal run_overall_pressed(debug: bool) +signal run_pressed(debug: bool) +signal stop_pressed() + +const InspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd") + +@onready var _version_label: Control = %version +@onready var _button_wiki: Button = %help +@onready var _tool_button: Button = %tool +@onready var _button_run_overall: Button = %run_overall +@onready var _button_run: Button = %run +@onready var _button_run_debug: Button = %debug +@onready var _button_stop: Button = %stop + + +const SETTINGS_SHORTCUT_MAPPING := { + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST: GdUnitShortcut.ShortCut.RERUN_TESTS, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG: GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL: GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP: GdUnitShortcut.ShortCut.STOP_TEST_RUN, +} + + +func _ready() -> void: + var inspector :InspectorTreeMainPanel = get_parent().get_parent().find_child("MainPanel", false, false) + if inspector == null: + push_error("Internal error, can't connect to the test inspector!") + else: + inspector.tree_item_selected.connect(_on_inspector_selected) + run_pressed.connect(inspector._on_run_pressed) + + GdUnit4Version.init_version_label(_version_label) + var command_handler := GdUnitCommandHandler.instance() + run_overall_pressed.connect(command_handler._on_run_overall_pressed) + stop_pressed.connect(command_handler._on_stop_pressed) + command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) + command_handler.gdunit_runner_stop.connect(_on_gdunit_runner_stop) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_gdunit_settings_changed) + init_buttons() + init_shortcuts(command_handler) + + +func init_buttons() -> void: + _button_run_overall.icon = GdUnitUiTools.get_run_overall_icon() + _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() + _button_run.icon = GdUnitUiTools.get_icon("Play") + _button_run_debug.icon = GdUnitUiTools.get_icon("PlayStart") + _button_stop.icon = GdUnitUiTools.get_icon("Stop") + _tool_button.icon = GdUnitUiTools.get_icon("Tools") + _button_wiki.icon = GdUnitUiTools.get_icon("HelpSearch") + # Set run buttons initial disabled + _button_run.disabled = true + _button_run_debug.disabled = true + + +func init_shortcuts(command_handler: GdUnitCommandHandler) -> void: + _button_run.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RERUN_TESTS) + _button_run_overall.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL) + _button_run_debug.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG) + _button_stop.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.STOP_TEST_RUN) + # register for shortcut changes + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed.bind(command_handler)) + + +func _on_inspector_selected(item: TreeItem) -> void: + var button_disabled := item == null + _button_run.disabled = button_disabled + _button_run_debug.disabled = button_disabled + + +func _on_runoverall_pressed(debug:=false) -> void: + run_overall_pressed.emit(debug) + + +func _on_run_pressed(debug := false) -> void: + run_pressed.emit(debug) + + +func _on_stop_pressed() -> void: + stop_pressed.emit() + + +func _on_gdunit_runner_start() -> void: + _button_run_overall.disabled = true + _button_run.disabled = true + _button_run_debug.disabled = true + _button_stop.disabled = false + + +func _on_gdunit_runner_stop(_client_id: int) -> void: + _button_run_overall.disabled = false + _button_stop.disabled = true + + +func _on_gdunit_settings_changed(_property: GdUnitProperty) -> void: + _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() + + +func _on_wiki_pressed() -> void: + var status := OS.shell_open("https://mikeschulze.github.io/gdUnit4/%s" % GdUnit4Version.current().documentation_version()) + if status != OK: + push_error("Can't open GdUnit4 documentaion page: %s" % error_string(status)) + + +func _on_btn_tool_pressed() -> void: + var settings_dlg: Window = EditorInterface.get_base_control().find_child("GdUnitSettingsDialog", false, false) + if settings_dlg == null: + settings_dlg = preload("res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn").instantiate() + EditorInterface.get_base_control().add_child(settings_dlg, true) + settings_dlg.popup_centered_ratio(.60) + + +func _on_settings_changed(property: GdUnitProperty, command_handler: GdUnitCommandHandler) -> void: + # needs to wait a frame to be command handler notified first for settings changes + await get_tree().process_frame + if SETTINGS_SHORTCUT_MAPPING.has(property.name()): + var shortcut: GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name(), GdUnitShortcut.ShortCut.NONE) + match shortcut: + GdUnitShortcut.ShortCut.RERUN_TESTS: + _button_run.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL: + _button_run_overall.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG: + _button_run_debug.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.STOP_TEST_RUN: + _button_stop.shortcut = command_handler.get_shortcut(shortcut) diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid index e69de29b..696553f4 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid @@ -0,0 +1 @@ +uid://dy1ierqsfcw8q diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn index e69de29b..5d75da02 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn @@ -0,0 +1,212 @@ +[gd_scene load_steps=22 format=3 uid="uid://dx7xy4dgi3wwb"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.gd" id="3"] + +[sub_resource type="Image" id="Image_c7rhl"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 168, 224, 224, 224, 233, 224, 224, 224, 236, 224, 224, 224, 170, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 234, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 239, 230, 230, 230, 30, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 168, 224, 224, 224, 255, 224, 224, 224, 186, 224, 224, 224, 32, 224, 224, 224, 33, 224, 224, 224, 187, 224, 224, 224, 255, 225, 225, 225, 167, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 237, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 255, 224, 224, 224, 234, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 237, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 38, 224, 224, 224, 255, 224, 224, 224, 229, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 164, 224, 224, 224, 255, 224, 224, 224, 187, 225, 225, 225, 34, 227, 227, 227, 36, 224, 224, 224, 192, 224, 224, 224, 255, 224, 224, 224, 162, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 225, 225, 225, 215, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 229, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 161, 224, 224, 224, 232, 224, 224, 224, 231, 225, 225, 225, 159, 230, 230, 230, 30, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 107, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 105, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 221, 224, 224, 224, 130, 255, 255, 255, 1, 255, 255, 255, 1, 225, 225, 225, 134, 224, 224, 224, 224, 225, 225, 225, 223, 224, 224, 224, 132, 255, 255, 255, 1, 255, 255, 255, 6, 224, 224, 224, 137, 224, 224, 224, 231, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 130, 225, 225, 225, 133, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 129, 224, 224, 224, 137, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 65, 224, 224, 224, 255, 224, 224, 224, 220, 225, 225, 225, 223, 224, 224, 224, 255, 226, 226, 226, 61, 224, 224, 224, 65, 224, 224, 224, 255, 224, 224, 224, 222, 224, 224, 224, 231, 224, 224, 224, 255, 227, 227, 227, 62, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 67, 224, 224, 224, 255, 224, 224, 224, 219, 224, 224, 224, 222, 224, 224, 224, 255, 227, 227, 227, 63, 225, 225, 225, 67, 224, 224, 224, 255, 224, 224, 224, 219, 224, 224, 224, 230, 224, 224, 224, 255, 227, 227, 227, 63, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 224, 224, 224, 129, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 126, 225, 225, 225, 135, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 221, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 1, 224, 224, 224, 128, 224, 224, 224, 220, 224, 224, 224, 219, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 134, 224, 224, 224, 229, 224, 224, 224, 255, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_t52y3"] +image = SubResource("Image_c7rhl") + +[sub_resource type="Image" id="Image_3erui"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 64, 224, 224, 224, 255, 227, 227, 227, 63, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 142, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 138, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 192, 224, 224, 224, 255, 225, 225, 225, 191, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 142, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 255, 224, 224, 224, 137, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 192, 224, 224, 224, 255, 225, 225, 225, 191, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 236, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 64, 224, 224, 224, 255, 227, 227, 227, 63, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 235, 224, 224, 224, 255, 224, 224, 224, 65, 225, 225, 225, 67, 224, 224, 224, 255, 224, 224, 224, 230, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 140, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 134, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 137, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 135, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 247, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 183, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 179, 224, 224, 224, 237, 224, 224, 224, 179, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 190, 224, 224, 224, 188, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_wip2b"] +image = SubResource("Image_3erui") + +[sub_resource type="InputEventKey" id="InputEventKey_6jdrj"] +ctrl_pressed = true +pressed = true +keycode = 4194338 +physical_keycode = 4194338 + +[sub_resource type="Shortcut" id="Shortcut_t0ytp"] +events = [SubResource("InputEventKey_6jdrj")] + +[sub_resource type="Image" id="Image_p22nw"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 183, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 182, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_mggmt"] +image = SubResource("Image_p22nw") + +[sub_resource type="InputEventKey" id="InputEventKey_pl3pi"] +ctrl_pressed = true +pressed = true +keycode = 4194336 +physical_keycode = 4194336 + +[sub_resource type="Shortcut" id="Shortcut_77xhn"] +events = [SubResource("InputEventKey_pl3pi")] + +[sub_resource type="Image" id="Image_3lcek"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 56, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 183, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 182, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 226, 226, 226, 52, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_q6qbp"] +image = SubResource("Image_3lcek") + +[sub_resource type="InputEventKey" id="InputEventKey_qk8q5"] +ctrl_pressed = true +pressed = true +keycode = 4194337 +physical_keycode = 4194337 + +[sub_resource type="Shortcut" id="Shortcut_ae6em"] +events = [SubResource("InputEventKey_qk8q5")] + +[sub_resource type="Image" id="Image_ndw0i"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 202, 228, 228, 228, 37, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 239, 224, 224, 224, 74, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 123, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 234, 234, 234, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 188, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 168, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 118, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 188, 224, 224, 224, 201, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_q0wt0"] +image = SubResource("Image_ndw0i") + +[sub_resource type="InputEventKey" id="InputEventKey_l8obn"] +ctrl_pressed = true +pressed = true +keycode = 4194339 +physical_keycode = 4194339 + +[sub_resource type="Shortcut" id="Shortcut_2mb87"] +events = [SubResource("InputEventKey_l8obn")] + +[sub_resource type="Image" id="Image_eoihf"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 177, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 177, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 176, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_1wiyx"] +image = SubResource("Image_eoihf") + +[node name="ToolBar" type="PanelContainer"] +anchors_preset = 10 +anchor_right = 1.0 +offset_right = -894.0 +offset_bottom = 24.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +script = ExtResource("3") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="tools" type="HBoxContainer" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 + +[node name="help" type="Button" parent="HBoxContainer/tools"] +unique_name_in_owner = true +layout_mode = 2 +icon = SubResource("ImageTexture_t52y3") + +[node name="tool" type="Button" parent="HBoxContainer/tools"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "GdUnit Settings" +icon = SubResource("ImageTexture_wip2b") + +[node name="controls" type="HBoxContainer" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 6 +size_flags_vertical = 4 +alignment = 1 + +[node name="VSeparator3" type="VSeparator" parent="HBoxContainer/controls"] +layout_mode = 2 + +[node name="run_overall" type="Button" parent="HBoxContainer/controls"] +unique_name_in_owner = true +visible = false +use_parent_material = true +layout_mode = 2 +tooltip_text = "Run overall tests" +shortcut = SubResource("Shortcut_t0ytp") +icon = SubResource("ImageTexture_mggmt") + +[node name="run" type="Button" parent="HBoxContainer/controls"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests" +shortcut = SubResource("Shortcut_77xhn") +icon = SubResource("ImageTexture_q6qbp") + +[node name="debug" type="Button" parent="HBoxContainer/controls"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests (Debug)" +shortcut = SubResource("Shortcut_ae6em") +icon = SubResource("ImageTexture_q0wt0") + +[node name="stop" type="Button" parent="HBoxContainer/controls"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Stops runing unit tests" +disabled = true +shortcut = SubResource("Shortcut_2mb87") +icon = SubResource("ImageTexture_1wiyx") + +[node name="VSeparator4" type="VSeparator" parent="HBoxContainer/controls"] +layout_mode = 2 + +[node name="CenterContainer" type="HBoxContainer" parent="HBoxContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +alignment = 2 + +[node name="version" type="Label" parent="HBoxContainer/CenterContainer"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 13 +auto_translate = false +localize_numeral_system = false +text = "gdUnit4 4.3.0" +horizontal_alignment = 1 +justification_flags = 160 + +[connection signal="pressed" from="HBoxContainer/tools/help" to="." method="_on_wiki_pressed"] +[connection signal="pressed" from="HBoxContainer/tools/tool" to="." method="_on_btn_tool_pressed"] +[connection signal="pressed" from="HBoxContainer/controls/run_overall" to="." method="_on_runoverall_pressed"] +[connection signal="pressed" from="HBoxContainer/controls/run" to="." method="_on_run_pressed"] +[connection signal="pressed" from="HBoxContainer/controls/debug" to="." method="_on_run_pressed" binds= [true]] +[connection signal="pressed" from="HBoxContainer/controls/stop" to="." method="_on_stop_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid index e69de29b..ef9608b1 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid @@ -0,0 +1 @@ +uid://cw8355bfvukx diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn index e69de29b..a4247706 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn @@ -0,0 +1,273 @@ +[gd_scene load_steps=27 format=3 uid="uid://bqfpidewtpeg0"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd" id="1"] + +[sub_resource type="Image" id="Image_466oo"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 223, 224, 224, 224, 148, 228, 228, 228, 28, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 221, 229, 229, 229, 29, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 65, 229, 229, 229, 29, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 1, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 229, 229, 229, 39, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 26, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 1, 229, 229, 229, 39, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_nwpuj"] +image = SubResource("Image_466oo") + +[sub_resource type="Image" id="Image_o6s0p"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 224, 224, 224, 48, 224, 224, 224, 217, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 228, 228, 228, 47, 224, 224, 224, 236, 224, 224, 224, 255, 225, 225, 225, 125, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 86, 224, 224, 224, 252, 224, 224, 224, 252, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 230, 230, 230, 40, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 225, 225, 225, 25, 230, 230, 230, 40, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 40, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_pdcj5"] +image = SubResource("Image_o6s0p") + +[sub_resource type="Image" id="Image_miuuy"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 89, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 233, 224, 224, 224, 255, 225, 225, 225, 124, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 211, 238, 238, 238, 15, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 26, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 1, 229, 229, 229, 39, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_o41n3"] +image = SubResource("Image_miuuy") + +[sub_resource type="Image" id="Image_ern2r"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 101, 224, 224, 224, 49, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 238, 224, 224, 224, 49, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 209, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 26, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 188, 224, 224, 224, 112, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_6oiqe"] +image = SubResource("Image_ern2r") + +[sub_resource type="Image" id="Image_qdci2"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 226, 226, 226, 52, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 53, 224, 224, 224, 239, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 7, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 11, 224, 224, 224, 113, 224, 224, 224, 188, 255, 255, 255, 0, 255, 255, 255, 1, 229, 229, 229, 39, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_l0amb"] +image = SubResource("Image_qdci2") + +[sub_resource type="Image" id="Image_hed0i"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 1, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 202, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 85, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 128, 224, 224, 224, 255, 224, 224, 224, 231, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 224, 224, 224, 212, 229, 229, 229, 39, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 225, 225, 225, 25, 230, 230, 230, 40, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 40, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_nonnc"] +image = SubResource("Image_hed0i") + +[sub_resource type="Image" id="Image_8v04w"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 26, 224, 224, 224, 41, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 19, 224, 224, 224, 218, 227, 227, 227, 45, 255, 255, 255, 0, 227, 227, 227, 9, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 234, 227, 227, 227, 45, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 202, 224, 224, 224, 252, 224, 224, 224, 252, 224, 224, 224, 82, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 26, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 40, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_d2btj"] +image = SubResource("Image_8v04w") + +[sub_resource type="Image" id="Image_arwmg"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 30, 225, 225, 225, 149, 224, 224, 224, 221, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 226, 226, 226, 26, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 7, 224, 224, 224, 214, 224, 224, 224, 255, 224, 224, 224, 253, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 231, 231, 231, 31, 224, 224, 224, 224, 224, 224, 224, 252, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 51, 223, 223, 223, 47, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 226, 226, 226, 43, 227, 227, 227, 9, 255, 255, 255, 0, 224, 224, 224, 32, 227, 227, 227, 63, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 17, 255, 255, 255, 8, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 192, 192, 192, 4, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 225, 225, 225, 51, 225, 225, 225, 51, 225, 225, 225, 17, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 225, 225, 225, 51, 225, 225, 225, 51, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 26, 225, 225, 225, 51, 223, 223, 223, 47, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 9, 223, 223, 223, 47, 225, 225, 225, 51, 225, 225, 225, 25, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 43, 224, 224, 224, 8, 255, 255, 255, 0, 227, 227, 227, 9, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 18, 227, 227, 227, 9, 255, 255, 255, 0, 227, 227, 227, 9, 226, 226, 226, 43, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 227, 227, 227, 9, 228, 228, 228, 47, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 223, 223, 223, 47, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 226, 226, 226, 43, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 255, 255, 255, 1, 225, 225, 225, 51, 225, 225, 225, 51, 226, 226, 226, 43, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 192, 192, 192, 4, 226, 226, 226, 26, 224, 224, 224, 40, 255, 255, 255, 0, 255, 255, 255, 1, 229, 229, 229, 39, 225, 225, 225, 25, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_1bxo7"] +image = SubResource("Image_arwmg") + +[sub_resource type="AnimatedTexture" id="AnimatedTexture_eylo1"] +frames = 8 +speed_scale = 2.5 +frame_0/texture = SubResource("ImageTexture_nwpuj") +frame_0/duration = 0.2 +frame_1/texture = SubResource("ImageTexture_pdcj5") +frame_1/duration = 0.2 +frame_2/texture = SubResource("ImageTexture_o41n3") +frame_2/duration = 0.2 +frame_3/texture = SubResource("ImageTexture_6oiqe") +frame_3/duration = 0.2 +frame_4/texture = SubResource("ImageTexture_l0amb") +frame_4/duration = 0.2 +frame_5/texture = SubResource("ImageTexture_nonnc") +frame_5/duration = 0.2 +frame_6/texture = SubResource("ImageTexture_d2btj") +frame_6/duration = 0.2 +frame_7/texture = SubResource("ImageTexture_1bxo7") +frame_7/duration = 0.2 + +[sub_resource type="Image" id="Image_rqglq"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 56, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 183, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 182, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 226, 226, 226, 52, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_dr7yj"] +image = SubResource("Image_rqglq") + +[sub_resource type="Image" id="Image_ltb1l"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 202, 228, 228, 228, 37, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 239, 224, 224, 224, 74, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 123, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 234, 234, 234, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 188, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 168, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 118, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 188, 224, 224, 224, 201, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_oh8cr"] +image = SubResource("Image_ltb1l") + +[sub_resource type="Image" id="Image_2lq8w"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 196, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 226, 226, 226, 60, 226, 226, 226, 60, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 60, 226, 226, 226, 60, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 134, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 254, 225, 225, 225, 133, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 67, 224, 224, 224, 231, 224, 224, 224, 230, 224, 224, 224, 66, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 71, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 224, 224, 224, 236, 225, 225, 225, 67, 226, 226, 226, 69, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 232, 224, 224, 224, 66, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 65, 224, 224, 224, 229, 224, 224, 224, 229, 224, 224, 224, 64, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 224, 224, 224, 132, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 130, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 64, 224, 224, 224, 64, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 64, 224, 224, 224, 64, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 200, 224, 224, 224, 200, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_x1ivs"] +image = SubResource("Image_2lq8w") + +[sub_resource type="Image" id="Image_kwwmp"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 237, 237, 14, 224, 224, 224, 165, 224, 224, 224, 165, 237, 237, 237, 14, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 58, 225, 225, 225, 223, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 223, 225, 225, 225, 58, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 124, 224, 224, 224, 128, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 128, 225, 225, 225, 124, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 125, 224, 224, 224, 128, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 128, 225, 225, 225, 125, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 59, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 223, 225, 225, 225, 59, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 238, 238, 238, 15, 224, 224, 224, 165, 224, 224, 224, 165, 238, 238, 238, 15, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_i13wr"] +image = SubResource("Image_kwwmp") + +[node name="MainPanel" type="VSplitContainer"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -924.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +split_offset = 200 +script = ExtResource("1") + +[node name="Panel" type="PanelContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Tree" type="Tree" parent="Panel"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/icon_max_width = 16 +columns = 2 +allow_rmb_select = true +hide_root = true +select_mode = 1 + +[node name="discover_hint" type="HBoxContainer" parent="Panel"] +unique_name_in_owner = true +visible = false +use_parent_material = true +layout_mode = 2 +alignment = 1 + +[node name="spinner" type="Button" parent="Panel/discover_hint"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(64, 64) +layout_mode = 2 +size_flags_stretch_ratio = 1.94 +disabled = true +button_mask = 0 +text = "Discover Tests" +icon = SubResource("AnimatedTexture_eylo1") +flat = true +alignment = 2 + +[node name="report" type="PanelContainer" parent="."] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 11 +size_flags_vertical = 11 + +[node name="report_template" type="RichTextLabel" parent="report"] +use_parent_material = true +clip_contents = false +layout_mode = 2 +size_flags_horizontal = 3 +auto_translate = false +localize_numeral_system = false +focus_mode = 2 +bbcode_enabled = true +fit_content = true +selection_enabled = true + +[node name="ScrollContainer" type="ScrollContainer" parent="report"] +use_parent_material = true +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 11 + +[node name="list" type="VBoxContainer" parent="report/ScrollContainer"] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="contextMenu" type="PopupMenu" parent="."] +size = Vector2i(133, 120) +auto_translate = false +item_count = 5 +item_0/text = "Run" +item_0/icon = SubResource("ImageTexture_dr7yj") +item_0/id = 0 +item_1/text = "Debug" +item_1/icon = SubResource("ImageTexture_oh8cr") +item_1/id = 1 +item_2/text = "" +item_2/id = 2 +item_2/separator = true +item_3/text = "Collapse All" +item_3/icon = SubResource("ImageTexture_x1ivs") +item_3/id = 3 +item_4/text = "Expand All" +item_4/icon = SubResource("ImageTexture_i13wr") +item_4/id = 4 + +[connection signal="item_activated" from="Panel/Tree" to="." method="_on_Tree_item_activated"] +[connection signal="item_mouse_selected" from="Panel/Tree" to="." method="_on_tree_item_mouse_selected"] +[connection signal="item_selected" from="Panel/Tree" to="." method="_on_Tree_item_selected"] +[connection signal="index_pressed" from="contextMenu" to="." method="_on_context_m_index_pressed"] diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd index e69de29b..b5cc8370 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd @@ -0,0 +1,56 @@ +@tool +class_name GdUnitInputCapture +extends Control + +signal input_completed(input_event: InputEventKey) + + +var _tween: Tween +var _input_event: InputEventKey + + +func _ready() -> void: + reset() + self_modulate = Color.WHITE + _tween = create_tween() + @warning_ignore("return_value_discarded") + _tween.set_loops() + @warning_ignore("return_value_discarded") + _tween.tween_property(%Label, "self_modulate", Color(1, 1, 1, .8), 1.0).from_current().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT) + + +func reset() -> void: + _input_event = InputEventKey.new() + + +func _input(event: InputEvent) -> void: + if not is_visible_in_tree(): + return + if event is InputEventKey and event.is_pressed() and not event.is_echo(): + var _event := event as InputEventKey + match _event.keycode: + KEY_CTRL: + _input_event.ctrl_pressed = true + KEY_SHIFT: + _input_event.shift_pressed = true + KEY_ALT: + _input_event.alt_pressed = true + KEY_META: + _input_event.meta_pressed = true + _: + _input_event.keycode = _event.keycode + _apply_input_modifiers(_event) + accept_event() + + if event is InputEventKey and not event.is_pressed(): + input_completed.emit(_input_event) + hide() + + +func _apply_input_modifiers(event: InputEvent) -> void: + if event is InputEventWithModifiers: + var _event := event as InputEventWithModifiers + _input_event.meta_pressed = _event.meta_pressed or _input_event.meta_pressed + _input_event.alt_pressed = _event.alt_pressed or _input_event.alt_pressed + _input_event.shift_pressed = _event.shift_pressed or _input_event.shift_pressed + _input_event.ctrl_pressed = _event.ctrl_pressed or _input_event.ctrl_pressed diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid index e69de29b..397d20f6 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid @@ -0,0 +1 @@ +uid://diwikaq2bawk diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn index e69de29b..7e62c438 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=2 format=3 uid="uid://pmnkxrhglak5"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"] + +[node name="GdUnitInputMapper" type="Control"] +modulate = Color(0.929099, 0.929099, 0.929099, 0.936189) +top_level = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_gki1u") + +[node name="Label" type="Label" parent="."] +unique_name_in_owner = true +self_modulate = Color(0.401913, 0.401913, 0.401913, 0.461723) +top_level = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -60.5 +offset_top = -19.5 +offset_right = 60.5 +offset_bottom = 19.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 26 +text = "Press keys for shortcut" diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid index e69de29b..589dfad0 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid @@ -0,0 +1 @@ +uid://bk7c6m1wkjohm diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn index e69de29b..08afee43 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -0,0 +1,349 @@ +[gd_scene load_steps=9 format=3] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] +[ext_resource type="Texture2D" path="res://addons/gdUnit4/src/ui/settings/logo.png" id="3_isfyl"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn" id="4"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn" id="4_nf72w"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn" id="5_n1jtv"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.172549, 0.113725, 0.141176, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.87451, 0.0705882, 0.160784, 1) +border_blend = true +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +shadow_color = Color(0, 0, 0, 0.756863) +shadow_size = 10 +shadow_offset = Vector2(10, 10) + +[node name="GdUnitSettingsDialog" type="Window"] +disable_3d = true +gui_embed_subwindows = true +title = "GdUnit4 Settings" +initial_position = 1 +size = Vector2i(1400, 600) +visible = false +wrap_controls = true +exclusive = true +extend_to_title = true +script = ExtResource("2") + +[node name="property_template" type="Control" parent="."] +visible = false +layout_mode = 3 +anchors_preset = 0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = 4.0 +offset_bottom = 4.0 +size_flags_horizontal = 0 + +[node name="Label" type="Label" parent="property_template"] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -11.5 +offset_right = 1.0 +offset_bottom = 11.5 +grow_vertical = 2 + +[node name="btn_reset" type="Button" parent="property_template"] +layout_mode = 0 +offset_right = 12.0 +offset_bottom = 40.0 +tooltip_text = "Reset to default value" +clip_text = true + +[node name="info" type="Label" parent="property_template"] +clip_contents = true +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -11.5 +offset_right = 316.0 +offset_bottom = 11.5 +grow_vertical = 2 +size_flags_horizontal = 3 +max_lines_visible = 1 + +[node name="sub_category" type="Panel" parent="property_template"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_right = -220.0 +grow_horizontal = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="property_template/sub_category"] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -11.5 +offset_right = 5.0 +offset_bottom = 11.5 +grow_vertical = 2 +theme_override_colors/font_color = Color(0.439216, 0.45098, 1, 1) + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("8_2ggr0") + +[node name="Panel" type="Panel" parent="."] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="PanelContainer" type="PanelContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="v" type="VBoxContainer" parent="Panel/PanelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/PanelContainer/v"] +use_parent_material = true +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 + +[node name="GridContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Panel" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="logo" type="TextureRect" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer"] +custom_minimum_size = Vector2(120, 120) +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +texture = ExtResource("3_isfyl") +expand_mode = 1 +stretch_mode = 5 + +[node name="CenterContainer2" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="version" type="RichTextLabel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer2"] +unique_name_in_owner = true +auto_translate_mode = 2 +use_parent_material = true +clip_contents = false +layout_mode = 2 +size_flags_horizontal = 3 +localize_numeral_system = false +bbcode_enabled = true +scroll_active = false +meta_underlined = false + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="btn_report_bug" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a bug report" +text = "Report Bug" + +[node name="btn_request_feature" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a feature request" +text = "Request Feature" + +[node name="btn_install_examples" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to install the advanced test examples" +disabled = true +text = "Install Examples" + +[node name="Properties" type="TabContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +current_tab = 0 + +[node name="Common" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="common-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Common"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(1026, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Hooks" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4_nf72w")] +visible = false +layout_mode = 2 + +[node name="UI" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 2 + +[node name="ui-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/UI"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(741, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Shortcuts" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 3 + +[node name="shortcut-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Shortcuts"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(683, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Report" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 4 + +[node name="report-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Report"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(667, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Templates" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4")] +visible = false +layout_mode = 2 +metadata/_tab_index = 5 + +[node name="Update" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_n1jtv")] +unique_name_in_owner = true +visible = false +layout_mode = 2 +metadata/_tab_index = 6 + +[node name="GdUnitInputCapture" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_xu3j8")] +unique_name_in_owner = true +visible = false +modulate = Color(1.54884e-09, 1.54884e-09, 1.54884e-09, 0.1) +z_index = 1 +z_as_relative = false +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 1 + +[node name="propertyError" type="PopupPanel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +unique_name_in_owner = true +initial_position = 1 +size = Vector2i(400, 100) +theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5") + +[node name="Label" type="Label" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/propertyError"] +offset_left = 10.0 +offset_top = 4.0 +offset_right = 390.0 +offset_bottom = 96.0 +theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1) +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="MarginContainer2" type="MarginContainer" parent="Panel/PanelContainer/v"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 2 + +[node name="ProgressBar" type="ProgressBar" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="progress_lbl" type="Label" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/ProgressBar"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +clip_text = true + +[node name="btn_close" type="Button" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Close" + +[connection signal="close_requested" from="." to="." method="_on_btn_close_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_report_bug" to="." method="_on_btn_report_bug_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_request_feature" to="." method="_on_btn_request_feature_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_install_examples" to="." method="_on_btn_install_examples_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/btn_close" to="." method="_on_btn_close_pressed"] diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd index e69de29b..3b01f340 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd @@ -0,0 +1,255 @@ +@tool +extends ScrollContainer + + +@onready var _hooks_tree: Tree = %hooks_tree +@onready var _hook_description: RichTextLabel = %hook_description +@onready var _btn_move_up: Button = %hook_actions/btn_move_up +@onready var _btn_move_down: Button = %hook_actions/btn_move_down +@onready var _btn_delete: Button = %hook_actions/btn_delete_hook +@onready var _select_hook_dlg: FileDialog = %select_hook_dlg +@onready var _error_msg_popup :AcceptDialog = %error_msg_popup + +var _selected_hook_item: TreeItem = null +var _root: TreeItem +var _system_box_style: StyleBoxFlat +var _priority_box_style: StyleBoxFlat + +func _ready() -> void: + _setup_styles() + _setup_buttons() + _setup_tree() + _load_registered_hooks() + + +func _setup_styles() -> void: + _system_box_style = StyleBoxFlat.new() + _system_box_style.bg_color = Color(1.0, 0.76, 0.03, 1) + _system_box_style.corner_radius_top_left = 6 + _system_box_style.corner_radius_top_right = 6 + _system_box_style.corner_radius_bottom_left = 6 + _system_box_style.corner_radius_bottom_right = 6 + _priority_box_style = _system_box_style.duplicate() + _priority_box_style.bg_color = Color(0.26, 0.54, 0.89, 1) + + +func _setup_buttons() -> void: + #if Engine.is_editor_hint(): + # _btn_move_up.icon = GdUnitUiTools.get_icon("MoveUp") + # _btn_move_down.icon = GdUnitUiTools.get_icon("MoveDown") + # _btn_add.icon = GdUnitUiTools.get_icon("Add") + # _btn_delete.icon = GdUnitUiTools.get_icon("Remove") + pass + + +func _setup_tree() -> void: + _hooks_tree.clear() + _root = _hooks_tree.create_item() + _hooks_tree.set_columns(2) + _hooks_tree.set_column_custom_minimum_width(1, 32) + _hooks_tree.set_column_expand(1, false) + _hooks_tree.set_hide_root(true) + _hooks_tree.set_hide_folding(true) + _hooks_tree.set_select_mode(Tree.SELECT_SINGLE) + _hooks_tree.item_selected.connect(_on_hook_selected) + _hooks_tree.item_edited.connect(_on_item_edited) + + +func _load_registered_hooks() -> void: + var hook_service := GdUnitTestSessionHookService.instance() + for hook: GdUnitTestSessionHook in hook_service.enigne_hooks: + _create_hook_tree_item(hook) + + # Select first item if any + if _root.get_child_count() > 0: + var first_item: TreeItem = _root.get_first_child() + first_item.select(0) + _on_hook_selected() + + +func _create_hook_tree_item(hook: GdUnitTestSessionHook) -> TreeItem: + var item: TreeItem = _hooks_tree.create_item(_root) + item.set_custom_minimum_height(26) + # Column 0: Hook info with custom drawing + item.set_cell_mode(0, TreeItem.CELL_MODE_CUSTOM) + item.set_custom_draw_callback(0, _draw_hook_item) + item.set_editable(0, false) + item.set_metadata(0, hook) + # Column 1: Checkbox for enable/disable + item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) + item.set_checked(1, GdUnitTestSessionHookService.is_enabled(hook)) + item.set_editable(1, true) + item.set_custom_bg_color(1, _hook_bg_color(hook)) + item.set_tooltip_text(1, "Enable/Disable the Hook") + item.propagate_check(1) + + if _is_system_hook(hook): + item.set_tooltip_text(0, "System hook - (Read-only)") + else: + item.set_tooltip_text(0, "User hook") + return item + + +func _hook_bg_color(hook: GdUnitTestSessionHook) -> Color: + if _is_system_hook(hook): + return Color(0.133, 0.118, 0.090, 1) # Brownish background for system hooks + return Color(0.176, 0.196, 0.235, 1) # Dark background #2d3142 + + +func _draw_hook_item(item: TreeItem, rect: Rect2) -> void: + var hook := _get_hook(item) + var is_system := _is_system_hook(hook) + var is_selected := item == _selected_hook_item + + # Draw background + var bg_color := _hook_bg_color(hook) # Dark background #2d3142 + if is_selected: + bg_color = bg_color.lerp(Color(0.2, 0.4, 0.6, 0.3), 0.5) # Blue tint for selection + _hooks_tree.draw_rect(rect, bg_color) + + # Draw left border for system hooks + if is_system: + var border_rect := Rect2(rect.position.x, rect.position.y, 3, rect.size.y) + _hooks_tree.draw_rect(border_rect, Color(1.0, 0.76, 0.03, 1)) # Yellow border + + var font := _hooks_tree.get_theme_default_font() + + # Draw hook name + var hook_name := hook.name + var text_pos := Vector2(rect.position.x + ( 15 if is_system else 12), rect.position.y + 18) + var text_color := Color(0.95, 0.95, 0.95, 1) + _hooks_tree.draw_string(font, text_pos, hook_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, text_color) + + # Draw system badge if needed + if is_system: + var badge_x := rect.position.x + rect.end.x - 100 + var badge_y := rect.position.y + 14 + var system_badge_rect := Rect2(badge_x, badge_y-8, 48, 16) + _hooks_tree.draw_style_box(_system_box_style, system_badge_rect) + + var system_text_pos := Vector2(badge_x + 4, badge_y + 4) + var system_font_size := 10 + _hooks_tree.draw_string(font, system_text_pos, "SYSTEM", HORIZONTAL_ALIGNMENT_CENTER, -1, system_font_size, Color(0.1, 0.1, 0.1, 1)) + + +func _create_hook_display_text(hook_name: String, priority: int, is_system: bool) -> String: + var text := hook_name + "\n" + text += "Priority: [color=#4299e1][bgcolor=#4299e1] " + str(priority) + " [/bgcolor][/color]" + + if is_system: + text += " [color=#1a202c][bgcolor=#ffc107] SYSTEM [/bgcolor][/color]" + + return text + + +func _update_hook_description() -> void: + if _selected_hook_item == null: + _hook_description.text = "[i]Select a hook to view its description[/i]" + return + _hook_description.text = _get_hook(_selected_hook_item).description + + +func _update_hook_buttons() -> void: + # Is nothing selected disable the move and delete buttons + if _selected_hook_item == null: + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var hook := _get_hook(_selected_hook_item) + var is_system := _is_system_hook(hook) + + # Disable the move and delete buttons for system hooks by default + if is_system: + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var prev_item: TreeItem = _selected_hook_item.get_prev() + var next_item: TreeItem = _selected_hook_item.get_next() + + if prev_item != null: + var prev_hook := _get_hook(prev_item) + _btn_move_up.disabled = _is_system_hook(prev_hook) + + _btn_move_down.disabled = next_item == null + _btn_delete.disabled = false + + +static func _get_hook(item: TreeItem) -> GdUnitTestSessionHook: + return item.get_metadata(0) + + +static func _is_system_hook(hook: GdUnitTestSessionHook) -> bool: + if hook == null: + return false + return hook.get_meta("SYSTEM_HOOK") + + +func _on_hook_selected() -> void: + _selected_hook_item = _hooks_tree.get_selected() + _update_hook_buttons() + _update_hook_description() + + +func _on_item_edited() -> void: + var selected_hook_item := _hooks_tree.get_selected() + if selected_hook_item != null: + var hook := _get_hook(selected_hook_item) + var is_enabled := selected_hook_item.is_checked(1) + GdUnitTestSessionHookService.instance().enable_hook(hook, is_enabled) + + +func _on_btn_add_hook_pressed() -> void: + _select_hook_dlg.show() + + +func _on_select_hook_dlg_file_selected(path: String) -> void: + _select_hook_dlg.set_current_path(path) + _on_select_hook_dlg_confirmed() + + +func _on_select_hook_dlg_confirmed() -> void: + _select_hook_dlg.hide() + var result := GdUnitTestSessionHookService.instance().load_hook(_select_hook_dlg.get_current_path()) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var hook: GdUnitTestSessionHook = result.value() + result = GdUnitTestSessionHookService.instance().register(hook) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var hook_added := _create_hook_tree_item(hook) + _hooks_tree.set_selected(hook_added, 0) + + +func _on_btn_delete_hook_pressed() -> void: + if _selected_hook_item != null: + _root.remove_child(_selected_hook_item) + GdUnitTestSessionHookService.instance()\ + .unregister(_get_hook(_selected_hook_item)) + _selected_hook_item = null + _update_hook_buttons() + + +func _on_btn_move_up_pressed() -> void: + var prev := _selected_hook_item.get_prev() + _selected_hook_item.move_before(prev) + GdUnitTestSessionHookService.instance()\ + .move_before(_get_hook(_selected_hook_item), _get_hook(prev)) + _update_hook_buttons() + + +func _on_btn_move_down_pressed() -> void: + var next := _selected_hook_item.get_next() + _selected_hook_item.move_after(next) + GdUnitTestSessionHookService.instance()\ + .move_after(_get_hook(_selected_hook_item), _get_hook(next)) + _update_hook_buttons() diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid index e69de29b..38508631 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid @@ -0,0 +1 @@ +uid://b10j5xjkq7vfc diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn index e69de29b..a9a850c0 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn @@ -0,0 +1,148 @@ +[gd_scene load_steps=10 format=3 uid="uid://41l7a46fol5m"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd" id="1_8yffn"] + +[sub_resource type="Image" id="Image_h5sr5"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 240, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 228, 228, 228, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_77fm0"] +image = SubResource("Image_h5sr5") + +[sub_resource type="Image" id="Image_77fm0"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 239, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 227, 227, 227, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_rewru"] +image = SubResource("Image_77fm0") + +[sub_resource type="Image" id="Image_kppp6"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_manhx"] +image = SubResource("Image_kppp6") + +[sub_resource type="Image" id="Image_rewru"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_4h4u1"] +image = SubResource("Image_rewru") + +[node name="Hooks" type="ScrollContainer"] +custom_minimum_size = Vector2(400, 300) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_8yffn") +metadata/_tab_index = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hooks_content" type="VBoxContainer" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hooks_tree" type="Tree" parent="HBoxContainer/hooks_content"] +unique_name_in_owner = true +layout_direction = 2 +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +hide_folding = true +hide_root = true + +[node name="hook_description" type="RichTextLabel" parent="HBoxContainer/hooks_content"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 +size_flags_vertical = 2 +bbcode_enabled = true +text = "The test result Html reporting hook." +scroll_active = false + +[node name="hook_actions" type="VBoxContainer" parent="HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/separation = 5 + +[node name="btn_move_up" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Move hook up in priority" +disabled = true +icon = SubResource("ImageTexture_77fm0") +icon_alignment = 1 + +[node name="btn_move_down" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Move hook down in priority" +disabled = true +icon = SubResource("ImageTexture_rewru") +icon_alignment = 1 + +[node name="btn_add_hook" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Add new hook" +icon = SubResource("ImageTexture_manhx") +icon_alignment = 1 + +[node name="btn_delete_hook" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Delete selected hook" +disabled = true +icon = SubResource("ImageTexture_4h4u1") +icon_alignment = 1 + +[node name="select_hook_dlg" type="FileDialog" parent="."] +unique_name_in_owner = true +disable_3d = true +title = "Open a File" +initial_position = 3 +current_screen = 0 +ok_button_text = "Open" +file_mode = 0 +filters = PackedStringArray("*.gd") + +[node name="error_msg_popup" type="AcceptDialog" parent="."] +unique_name_in_owner = true +initial_position = 3 +current_screen = 0 + +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_up" to="." method="_on_btn_move_up_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_down" to="." method="_on_btn_move_down_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_add_hook" to="." method="_on_btn_add_hook_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_delete_hook" to="." method="_on_btn_delete_hook_pressed"] +[connection signal="confirmed" from="select_hook_dlg" to="." method="_on_select_hook_dlg_confirmed"] +[connection signal="file_selected" from="select_hook_dlg" to="." method="_on_select_hook_dlg_file_selected"] diff --git a/addons/gdUnit4/src/ui/settings/logo.png b/addons/gdUnit4/src/ui/settings/logo.png index e69de29b..c1db2242 100644 --- a/addons/gdUnit4/src/ui/settings/logo.png +++ b/addons/gdUnit4/src/ui/settings/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed176306a061dff6c2a97c76e572bbbdee50cb7f5a496ba10d6898721f198351 +size 49775 diff --git a/addons/gdUnit4/src/ui/settings/logo.png.import b/addons/gdUnit4/src/ui/settings/logo.png.import index b2824077..ebcd5b1c 100644 --- a/addons/gdUnit4/src/ui/settings/logo.png.import +++ b/addons/gdUnit4/src/ui/settings/logo.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bwyco6vpmt5g8" -valid=false +uid="uid://cpsbyk6hskqhk" +path="res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/ui/settings/logo.png" +dest_files=["res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.ctex"] [params] diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid index e69de29b..dfd742e8 100644 --- a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid @@ -0,0 +1 @@ +uid://cas22f80cg72g diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn index e69de29b..5ccc4430 100644 --- a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn @@ -0,0 +1,127 @@ +[gd_scene load_steps=2 format=3 uid="uid://dte0m2endcgtu"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd" id="1"] + +[node name="TestSuiteTemplate" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="sub_category" type="Panel" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/sub_category"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_right = 4.0 +offset_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "Test Suite Template +" + +[node name="EdiorLayout" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Editor" type="CodeEdit" parent="VBoxContainer/EdiorLayout"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/EdiorLayout/Editor"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -31.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer"] +layout_mode = 2 +size_flags_vertical = 8 +alignment = 2 + +[node name="Tags" type="Button" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Shows supported tags." +text = "Supported Tags" + +[node name="SelectType" type="OptionButton" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Select the script type specific template." +item_count = 2 +selected = 0 +popup/item_0/text = "GD - GDScript" +popup/item_0/id = 1000 +popup/item_1/text = "C# - CSharpScript" +popup/item_1/id = 2000 + +[node name="Panel" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Panel"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="Restore" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +text = "Restore" + +[node name="Save" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +disabled = true +text = "Save" + +[node name="Tags" type="PopupPanel" parent="."] +size = Vector2i(300, 100) +unresizable = false +content_scale_aspect = 4 + +[node name="MarginContainer" type="MarginContainer" parent="Tags"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = -856.0 +offset_bottom = -552.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TextEdit" type="CodeEdit" parent="Tags/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +editable = false +context_menu_enabled = false +shortcut_keys_enabled = false +virtual_keyboard_enabled = false + +[connection signal="text_changed" from="VBoxContainer/EdiorLayout/Editor" to="." method="_on_Editor_text_changed"] +[connection signal="pressed" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/Tags" to="." method="_on_Tags_pressed"] +[connection signal="item_selected" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType" to="." method="_on_SelectType_item_selected"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Restore" to="." method="_on_Restore_pressed"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Save" to="." method="_on_Save_pressed"] diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd index e69de29b..dab19e40 100644 --- a/addons/gdUnit4/src/update/GdMarkDownReader.gd +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -0,0 +1,405 @@ +@tool +extends RefCounted + +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +const FONT_H1 := 22 +const FONT_H2 := 20 +const FONT_H3 := 18 +const FONT_H4 := 16 +const FONT_H5 := 14 +const FONT_H6 := 12 + +const HORIZONTAL_RULE := "[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img]" +const HEADER_RULE := "[font_size=%d]$1[/font_size]" +const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]" + +const image_download_folder := "res://addons/gdUnit4/tmp-update/" + +const exclude_font_size := "\b(?!(?:(font_size))\b)" + +var md_replace_patterns := [ + # comments + [regex("(?m)^\\n?\\s*\\s*\\n?"), ""], + + # horizontal rules + [regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE], + + # headers + [regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6], + [regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5], + [regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4], + [regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3], + [regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1], + [regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2], + # html headers + [regex("

((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3], + [regex("((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3], + [regex("

((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4], + [regex("((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4], + [regex("
((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5], + [regex("((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5], + [regex("
((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6], + [regex("((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6], + + # asterics + #[regex("(\\*)"), "xxx$1xxx"], + + # extract/compile image references + [regex("!\\[(.*?)\\]\\[(.*?)\\]"), process_image_references], + # extract images with path and optional tool tip + [regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), process_image], + + # links + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"), "[url={\"url\":\"$3\"}]$2[/url]"], + # links with tool tip + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"), "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"], + # links to github, as shorted link + [regex("(https://github.*/?/(\\S+))"), '[url={"url":"$1", "tool_tip":"$1"}]#$2[/url]'], + + # embeded text + [regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i]$1[/i]"], + + # italic + bold font + [regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"], + [regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"], + # bold font + [regex("(.*?)<\\/b>"), "[b]$1[/b]"], + [regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"], + [regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"], + # italic font + [regex("(.*?)<\\/i>"), "[i]$1[/i]"], + [regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"], + [regex("\\*(.*?)\\*"), "[i]$1[/i]"], + + # strikethrough font + [regex("(.*?)"), "[s]$1[/s]"], + [regex("~~(.*?)~~"), "[s]$1[/s]"], + [regex("~(.*?)~"), "[s]$1[/s]"], + + # handling lists + # using an image for dots + [regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)], + [regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)], + [regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)], + [regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)], + [regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)], + + # code + [regex("``([\\s\\S]*?)``"), code_block("$1")], + [regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")], +] + +var code_block_patterns := [ + # code blocks, code blocks looks not like code blocks in richtext + [regex("```(javascript|python|shell|gdscript|gd)([\\s\\S]*?\n)```"), code_block("$2", true)], +] + +var _img_replace_regex := RegEx.new() +var _image_urls := PackedStringArray() +var _on_table_tag := false +var _client: GdUnitUpdateClient + + +static func regex(pattern: String) -> RegEx: + var regex_ := RegEx.new() + var err := regex_.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex_ + + +func _init() -> void: + @warning_ignore("return_value_discarded") + _img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]") + + +func set_http_client(client: GdUnitUpdateClient) -> void: + _client = client + + +@warning_ignore("return_value_discarded") +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + # finally remove_at the downloaded images + for image in _image_urls: + DirAccess.remove_absolute(image) + DirAccess.remove_absolute(image + ".import") + + +func list_replace(indent: int) -> String: + var replace_pattern := "[img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img]" if indent %2 else "[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img]" + replace_pattern += " $1" + + for index in indent: + replace_pattern = replace_pattern.insert(0, " ") + return replace_pattern + + +func code_block(replace: String, border: bool = false) -> String: + if border: + return """ + [img=1400x14]res://addons/gdUnit4/src/update/assets/border_top.png[/img] + [indent][color=GRAY][font_size=16]%s[/font_size][/color][/indent] + [img=1400x14]res://addons/gdUnit4/src/update/assets/border_bottom.png[/img] + """.dedent() % replace + return "[code][bgcolor=DARK_SLATE_GRAY][color=GRAY][font_size=16]%s[/font_size][/color][/bgcolor][/code]" % replace + + +func convert_text(input: String) -> String: + input = process_tables(input) + + for pattern: Array in md_replace_patterns: + var regex_: RegEx = pattern[0] + var bb_replace: Variant = pattern[1] + if bb_replace is Callable: + @warning_ignore("unsafe_method_access") + input = await bb_replace.call(regex_, input) + else: + @warning_ignore("unsafe_cast") + input = regex_.sub(input, bb_replace as String, true) + return input + + +func convert_code_block(input: String) -> String: + for pattern: Array in code_block_patterns: + var regex_: RegEx = pattern[0] + var bb_replace: Variant = pattern[1] + if bb_replace is Callable: + @warning_ignore("unsafe_method_access") + input = await bb_replace.call(regex_, input) + else: + @warning_ignore("unsafe_cast") + input = regex_.sub(input, bb_replace as String, true) + return input + + +func to_bbcode(input: String) -> String: + var re := regex("(?m)```[\\s\\S]*?```") + var current_pos := 0 + var as_bbcode := "" + + # we split by code blocks to handle this blocks customized + for result in re.search_all(input): + # Add text before code block + if result.get_start() > current_pos: + as_bbcode += await convert_text(input.substr(current_pos, result.get_start() - current_pos)) + # Add code block + as_bbcode += await convert_code_block(result.get_string()) + current_pos = result.get_end() + + # Add remaining text after last code block + if current_pos < input.length(): + as_bbcode += await convert_text(input.substr(current_pos)) + return as_bbcode + + +func process_tables(input: String) -> String: + var bbcode := PackedStringArray() + var lines: Array[String] = Array(input.split("\n") as Array, TYPE_STRING, "", null) + while not lines.is_empty(): + if is_table(lines[0]): + bbcode.append_array(parse_table(lines)) + continue + @warning_ignore("return_value_discarded", "unsafe_cast") + bbcode.append(lines.pop_front() as String) + return "\n".join(bbcode) + + +class GdUnitMDReaderTable: + var _columns: int + var _rows: Array[Row] = [] + + class Row: + var _cells := PackedStringArray() + + + func _init(cells: PackedStringArray, columns: int) -> void: + _cells = cells + for i in range(_cells.size(), columns): + @warning_ignore("return_value_discarded") + _cells.append("") + + + func to_bbcode(cell_sizes: PackedInt32Array, bold: bool) -> String: + var cells := PackedStringArray() + for cell_index in _cells.size(): + var cell: String = _cells[cell_index] + if cell.strip_edges() == "--": + cell = create_line(cell_sizes[cell_index]) + if bold: + cell = "[b]%s[/b]" % cell + @warning_ignore("return_value_discarded") + cells.append("[cell]%s[/cell]" % cell) + return "|".join(cells) + + + func create_line(length: int) -> String: + var line := "" + for i in length: + line += "-" + return line + + + func _init(columns: int) -> void: + _columns = columns + + + func parse_row(line :String) -> bool: + # is line containing cells? + if line.find("|") == -1: + return false + _rows.append(Row.new(line.split("|"), _columns)) + return true + + + func calculate_max_cell_sizes() -> PackedInt32Array: + var cells_size := PackedInt32Array() + for column in _columns: + @warning_ignore("return_value_discarded") + cells_size.append(0) + + for row_index in _rows.size(): + var row: Row = _rows[row_index] + for cell_index in row._cells.size(): + var cell_size: int = cells_size[cell_index] + var size := row._cells[cell_index].length() + if size > cell_size: + cells_size[cell_index] = size + return cells_size + + + @warning_ignore("return_value_discarded") + func to_bbcode() -> PackedStringArray: + var cell_sizes := calculate_max_cell_sizes() + var bb_code := PackedStringArray() + + bb_code.append("[table=%d]" % _columns) + for row_index in _rows.size(): + bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0)) + bb_code.append("[/table]\n") + return bb_code + + +func parse_table(lines: Array) -> PackedStringArray: + var line: String = lines[0] + var table := GdUnitMDReaderTable.new(line.count("|") + 1) + while not lines.is_empty(): + line = lines.pop_front() + if not table.parse_row(line): + break + return table.to_bbcode() + + +func is_table(line: String) -> bool: + return line.find("|") != -1 + + +func open_table(line: String) -> String: + _on_table_tag = true + return "[table=%d]" % (line.count("|") + 1) + + +func close_table() -> String: + _on_table_tag = false + return "[/table]" + + +func extract_cells(line: String, bold := false) -> String: + var cells := "" + for cell in line.split("|"): + if bold: + cell = "[b]%s[/b]" % cell + cells += "[cell]%s[/cell]" % cell + return cells + + +func process_image_references(p_regex: RegEx, p_input: String) -> String: + #return p_input + + # exists references? + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + # collect image references and remove_at it + var references := Dictionary() + var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?") + # create copy of original source to replace checked it + var input := p_input.replace("\r", "") + var extracted_references := p_input.replace("\r", "") + for reg_match in link_regex.search_all(input): + var line := reg_match.get_string(0) + "\n" + var ref := reg_match.get_string(1) + #var topl_tip = reg_match.get_string(4) + # collect reference and url + references[ref] = reg_match.get_string(2) + extracted_references = extracted_references.replace(line, "") + + # replace image references by collected url's + for reference_key: String in references.keys(): + var regex_key := regex("\\](\\[%s\\])" % reference_key) + for reg_match in regex_key.search_all(extracted_references): + var ref: String = reg_match.get_string(0) + var image_url: String = "](%s)" % references.get(reference_key) + extracted_references = extracted_references.replace(ref, image_url) + return extracted_references + + +@warning_ignore("return_value_discarded") +func process_image(p_regex: RegEx, p_input: String) -> String: + #return p_input + var to_replace := PackedStringArray() + var tool_tips := PackedStringArray() + # find all matches + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + for reg_match in matches: + # grap the parts to replace and store temporay because a direct replace will distort the offsets + to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0))) + # grap optional tool tips + tool_tips.append(reg_match.get_string(5)) + # finally replace all findings + for replace in to_replace: + var re := p_regex.sub(replace, "[img]$2[/img]") + p_input = p_input.replace(replace, re) + return await _process_external_image_resources(p_input) + + +func _process_external_image_resources(input: String) -> String: + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(image_download_folder) + # scan all img for external resources and download it + for value in _img_replace_regex.search_all(input): + if value.get_group_count() >= 1: + var image_url: String = value.get_string(1) + # if not a local resource we need to download it + if image_url.begins_with("http"): + if OS.is_stdout_verbose(): + prints("download image:", image_url) + var response := await _client.request_image(image_url) + if response.status() == 200: + var image := Image.new() + var error := image.load_png_from_buffer(response.get_body()) + if error != OK: + prints("Error creating image from response", error) + # replace characters where format characters + var new_url := image_download_folder + image_url.get_file().replace("_", "-") + if new_url.get_extension() != 'png': + new_url = new_url + '.png' + var err := image.save_png(new_url) + if err: + push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)]) + @warning_ignore("return_value_discarded") + _image_urls.append(new_url) + input = input.replace(image_url, new_url) + return input diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid index e69de29b..5f7b4164 100644 --- a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid @@ -0,0 +1 @@ +uid://c2ii27n08li2k diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd b/addons/gdUnit4/src/update/GdUnitPatch.gd index e69de29b..daa20f7b 100644 --- a/addons/gdUnit4/src/update/GdUnitPatch.gd +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd @@ -0,0 +1,20 @@ +class_name GdUnitPatch +extends RefCounted + +const PATCH_VERSION = "patch_version" + +var _version :GdUnit4Version + + +func _init(version_ :GdUnit4Version) -> void: + _version = version_ + + +func version() -> GdUnit4Version: + return _version + + +# this function needs to be implement +func execute() -> bool: + push_error("The function 'execute()' is not implemented at %s" % self) + return false diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid index e69de29b..d75401b1 100644 --- a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid @@ -0,0 +1 @@ +uid://drpr6gj1mlhxl diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd index e69de29b..73d25c92 100644 --- a/addons/gdUnit4/src/update/GdUnitPatcher.gd +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -0,0 +1,75 @@ +class_name GdUnitPatcher +extends RefCounted + + +const _base_dir := "res://addons/gdUnit4/src/update/patches/" + +var _patches := Dictionary() + + +func scan(current :GdUnit4Version) -> void: + _scan(_base_dir, current) + + +func _scan(scan_path :String, current :GdUnit4Version) -> void: + _patches = Dictionary() + var patch_paths := _collect_patch_versions(scan_path, current) + for path in patch_paths: + prints("scan for patches checked '%s'" % path) + _patches[path] = _scan_patches(path) + + +func patch_count() -> int: + var count := 0 + for key :String in _patches.keys(): + @warning_ignore("unsafe_method_access") + count += _patches[key].size() + return count + + +func execute() -> void: + for key :String in _patches.keys(): + for path :String in _patches[key]: + var patch :GdUnitPatch = (load(key + "/" + path) as GDScript).new() + if patch: + prints("execute patch", patch.version(), patch.get_script().resource_path) + if not patch.execute(): + prints("error checked execution patch %s" % key + "/" + path) + + +func _collect_patch_versions(scan_path :String, current :GdUnit4Version) -> PackedStringArray: + if not DirAccess.dir_exists_absolute(scan_path): + return PackedStringArray() + var patches := Array() + var dir := DirAccess.open(scan_path) + if dir != null: + @warning_ignore("return_value_discarded") + dir.list_dir_begin() # TODO GODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + var version := GdUnit4Version.parse(next) + if version.is_greater(current): + patches.append(scan_path + next) + patches.sort() + return PackedStringArray(patches) + + +func _scan_patches(path :String) -> PackedStringArray: + var patches := Array() + var dir := DirAccess.open(path) + if dir != null: + @warning_ignore("return_value_discarded") + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + # step over directory links and .uid files + if next.is_empty() or next == "." or next == ".." or next.ends_with(".uid"): + continue + patches.append(next) + # make sorted from lowest to high version + patches.sort() + return PackedStringArray(patches) diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid index e69de29b..80127b6b 100644 --- a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid @@ -0,0 +1 @@ +uid://cxv57lcra5lsd diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd index e69de29b..97b7fdd1 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -0,0 +1,305 @@ +@tool +extends Container + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GDUNIT_TEMP := "user://tmp" + +@onready var _progress_content: RichTextLabel = %message +@onready var _progress_bar: TextureProgressBar = %progress +@onready var _cancel_btn: Button = %cancel +@onready var _update_btn: Button = %update +@onready var _spinner_img := GdUnitUiTools.get_spinner() + + +var _debug_mode := false +var _update_client :GdUnitUpdateClient +var _download_url :String + + +func _ready() -> void: + init_progress(6) + + +func _process(_delta :float) -> void: + if _progress_content != null and _progress_content.is_visible_in_tree(): + _progress_content.queue_redraw() + + +func init_progress(max_value: int) -> void: + _cancel_btn.disabled = false + _update_btn.disabled = false + _progress_bar.max_value = max_value + _progress_bar.value = 1 + message_h4("Press [Update] to start.", Color.GREEN, false) + + +func setup(update_client: GdUnitUpdateClient, download_url: String) -> void: + _update_client = update_client + _download_url = download_url + + +func update_progress(message: String, color := Color.GREEN) -> void: + message_h4(message, color) + _progress_bar.value += 1 + if _debug_mode: + await get_tree().create_timer(3).timeout + await get_tree().create_timer(.2).timeout + + +func _colored(message: String, color: Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message] + + +func message_h4(message: String, color: Color, show_spinner := true) -> void: + _progress_content.clear() + if show_spinner: + _progress_content.add_image(_spinner_img) + _progress_content.append_text(" [font_size=16]%s[/font_size]" % _colored(message, color)) + if _debug_mode: + prints(message) + + +@warning_ignore("return_value_discarded") +func run_update() -> void: + _cancel_btn.disabled = true + _update_btn.disabled = true + + await update_progress("Downloading the update.") + await download_release() + await update_progress("Extracting") + var zip_file := temp_dir() + "/update.zip" + var tmp_path := create_temp_dir("update") + var result :Variant = extract_zip(zip_file, tmp_path) + if result == null: + await update_progress("Update failed! .. Rollback.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + _cancel_btn.disabled = false + _update_btn.disabled = false + init_progress(5) + hide() + return + + await update_progress("Uninstall GdUnit4.") + disable_gdUnit() + if not _debug_mode: + GdUnitFileAccess.delete_directory("res://addons/gdUnit4/") + # give editor time to react on deleted files + await get_tree().create_timer(1).timeout + + await update_progress("Install new GdUnit4 version.") + if _debug_mode: + copy_directory(tmp_path, "res://debug") + else: + copy_directory(tmp_path, "res://") + + await update_progress("Patch invalid UID's") + await patch_uids() + + await rebuild_project() + + await update_progress("New GdUnit version successfully installed, Restarting Godot please wait.") + await get_tree().create_timer(3).timeout + enable_gdUnit() + hide() + GdUnitFileAccess.delete_directory("res://addons/.gdunit_update") + restart_godot() + + +func patch_uids(path := "res://addons/gdUnit4/src/") -> void: + var to_reimport: PackedStringArray + for file in DirAccess.get_files_at(path): + var file_path := path.path_join(file) + var ext := file.get_extension() + + if ext == "tscn" or ext == "scn" or ext == "tres" or ext == "res": + message_h4("Patch GdUnit4 scene: '%s'" % file, Color.WEB_GREEN) + remove_uids_from_file(file_path) + elif FileAccess.file_exists(file_path + ".import"): + to_reimport.append(file_path) + + if not to_reimport.is_empty(): + message_h4("Reimport resources '%s'" % ", ".join(to_reimport), Color.WEB_GREEN) + if Engine.is_editor_hint(): + EditorInterface.get_resource_filesystem().reimport_files(to_reimport) + + for dir in DirAccess.get_directories_at(path): + if not dir.begins_with("."): + patch_uids(path.path_join(dir)) + await get_tree().process_frame + + +func remove_uids_from_file(file_path: String) -> bool: + var file := FileAccess.open(file_path, FileAccess.READ) + if file == null: + print("Failed to open file: ", file_path) + return false + + var original_content := file.get_as_text() + file.close() + + # Remove UIDs using regex + var regex := RegEx.new() + regex.compile("(\\[ext_resource[^\\]]*?)\\s+uid=\"uid://[^\"]*\"") + + var modified_content := regex.sub(original_content, "$1", true) + + # Check if any changes were made + if original_content != modified_content: + prints("Patched invalid uid's out in '%s'" % file_path) + # Write the modified content back + file = FileAccess.open(file_path, FileAccess.WRITE) + if file == null: + print("Failed to write to file: ", file_path) + return false + + file.store_string(modified_content) + file.close() + return true + + return false + + +func restart_godot() -> void: + prints("Force restart Godot") + EditorInterface.restart_editor(true) + + +@warning_ignore("return_value_discarded") +func enable_gdUnit() -> void: + var enabled_plugins := PackedStringArray() + if ProjectSettings.has_setting("editor_plugins/enabled"): + enabled_plugins = ProjectSettings.get_setting("editor_plugins/enabled") + if not enabled_plugins.has("res://addons/gdUnit4/plugin.cfg"): + enabled_plugins.append("res://addons/gdUnit4/plugin.cfg") + ProjectSettings.set_setting("editor_plugins/enabled", enabled_plugins) + ProjectSettings.save() + + +func disable_gdUnit() -> void: + EditorInterface.set_plugin_enabled("gdUnit4", false) + + +func temp_dir() -> String: + if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) + return GDUNIT_TEMP + + +func create_temp_dir(folder_name :String) -> String: + var new_folder := temp_dir() + "/" + folder_name + GdUnitFileAccess.delete_directory(new_folder) + if not DirAccess.dir_exists_absolute(new_folder): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(new_folder) + return new_folder + + +func copy_directory(from_dir: String, to_dir: String) -> bool: + if not DirAccess.dir_exists_absolute(from_dir): + printerr("Source directory not found '%s'" % from_dir) + return false + # check if destination exists + if not DirAccess.dir_exists_absolute(to_dir): + # create it + var err := DirAccess.make_dir_recursive_absolute(to_dir) + if err != OK: + printerr("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)]) + return false + var source_dir := DirAccess.open(from_dir) + var dest_dir := DirAccess.open(to_dir) + if source_dir != null: + @warning_ignore("return_value_discarded") + source_dir.list_dir_begin() + var next := "." + + while next != "": + next = source_dir.get_next() + if next == "" or next == "." or next == "..": + continue + var source := source_dir.get_current_dir() + "/" + next + var dest := dest_dir.get_current_dir() + "/" + next + if source_dir.current_is_dir(): + @warning_ignore("return_value_discarded") + copy_directory(source + "/", dest) + continue + var err := source_dir.copy(source, dest) + if err != OK: + printerr("Error checked copy file '%s' to '%s'" % [source, dest]) + return false + return true + else: + printerr("Directory not found: " + from_dir) + return false + + +func extract_zip(zip_package: String, dest_path: String) -> Variant: + var zip: ZIPReader = ZIPReader.new() + var err := zip.open(zip_package) + if err != OK: + printerr("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + return null + var zip_entries: PackedStringArray = zip.get_files() + # Get base path and step over archive folder + var archive_path := zip_entries[0] + zip_entries.remove_at(0) + + for zip_entry in zip_entries: + var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") + if zip_entry.ends_with("/"): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(new_file_path) + continue + var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) + file.store_buffer(zip.read_file(zip_entry)) + @warning_ignore("return_value_discarded") + zip.close() + return dest_path + + +func download_release() -> void: + var zip_file := GdUnitFileAccess.temp_dir() + "/update.zip" + var response :GdUnitUpdateClient.HttpResponse + if _debug_mode: + response = GdUnitUpdateClient.HttpResponse.new(200, PackedByteArray()) + zip_file = "res://update.zip" + return + + response = await _update_client.request_zip_package(_download_url, zip_file) + if response.status() != 200: + push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()]) + message_h4("Download the update failed! Try it later again.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + + +func rebuild_project() -> void: + # Check if this is a Godot .NET runtime instance + if not ClassDB.class_exists("CSharpScript"): + return + + update_progress("Rebuild the project ...") + await get_tree().process_frame + + var output := [] + var exit_code := OS.execute("dotnet", ["build"], output) + if exit_code == -1: + message_h4("Rebuild the project failed, check your project dependencies.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + return + + for out: String in output: + print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) + await get_tree().process_frame + + +func _on_confirmed() -> void: + await run_update() + + +func _on_cancel_pressed() -> void: + hide() + + +func _on_update_pressed() -> void: + await run_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid index e69de29b..99bd956a 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid @@ -0,0 +1 @@ +uid://2cvldn16wv2b diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.tscn b/addons/gdUnit4/src/update/GdUnitUpdate.tscn index e69de29b..20d60b76 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.tscn +++ b/addons/gdUnit4/src/update/GdUnitUpdate.tscn @@ -0,0 +1,100 @@ +[gd_scene load_steps=6 format=3 uid="uid://2eahgaw88y6q"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"] + +[sub_resource type="Gradient" id="Gradient_wilsr"] +colors = PackedColorArray(0.151276, 0.151276, 0.151276, 1, 1, 1, 1, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_45cww"] +gradient = SubResource("Gradient_wilsr") +fill_to = Vector2(0.75641, 0) + +[sub_resource type="Gradient" id="Gradient_i0qp8"] +colors = PackedColorArray(1, 1, 1, 1, 0.20871, 0.20871, 0.20871, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_wilsr"] +gradient = SubResource("Gradient_i0qp8") +fill_from = Vector2(0.794872, 0) +fill_to = Vector2(0, 0) + +[node name="GdUnitUpdate" type="MarginContainer"] +clip_contents = true +custom_minimum_size = Vector2(0, 80) +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 80.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_right = 10 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="message" type="RichTextLabel" parent="VBoxContainer/Panel"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +bbcode_enabled = true +text = "aaaaa" +fit_content = true +scroll_active = false +shortcut_keys_enabled = false + +[node name="Panel2" type="Panel" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="progress" type="TextureProgressBar" parent="VBoxContainer/Panel2"] +unique_name_in_owner = true +auto_translate_mode = 2 +clip_contents = true +custom_minimum_size = Vector2(0, 20) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +localize_numeral_system = false +min_value = 1.0 +max_value = 5.0 +value = 1.0 +rounded = true +allow_greater = true +nine_patch_stretch = true +texture_under = SubResource("GradientTexture2D_45cww") +texture_progress = SubResource("GradientTexture2D_wilsr") +tint_under = Color(0.0235294, 0.145098, 0.168627, 1) +tint_progress = Color(0.288912, 0.233442, 0.533772, 1) + +[node name="PanelContainer" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 +alignment = 2 + +[node name="update" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Update" + +[node name="cancel" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Cancel" + +[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/update" to="." method="_on_update_pressed"] +[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"] diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd index e69de29b..b4f54b74 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd @@ -0,0 +1,98 @@ +@tool +extends Node + +signal request_completed(response: HttpResponse) + +class HttpResponse: + var _http_status: int + var _body: PackedByteArray + + + func _init(http_status: int, body: PackedByteArray) -> void: + _http_status = http_status + _body = body + + + func status() -> int: + return _http_status + + + func response() -> Variant: + if _http_status != 200: + return _body.get_string_from_utf8() + + var test_json_conv := JSON.new() + @warning_ignore("return_value_discarded") + var error := test_json_conv.parse(_body.get_string_from_utf8()) + if error != OK: + return "HttpResponse: %s Error: %s" % [error_string(error), _body.get_string_from_utf8()] + return test_json_conv.get_data() + + func get_body() -> PackedByteArray: + return _body + + +var _http_request := HTTPRequest.new() + + +func _ready() -> void: + add_child(_http_request) + @warning_ignore("return_value_discarded") + _http_request.request_completed.connect(_on_request_completed) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + if is_instance_valid(_http_request): + _http_request.queue_free() + + +#func list_tags() -> void: +# _http_request.connect("request_completed",Callable(self,"_response_request_tags")) +# var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") +# if error != OK: +# push_error("An error occurred in the HTTP request.") + + +func request_latest_version() -> HttpResponse: + var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") + if error != OK: + var message := "Request latest version failed, %s" % error_string(error) + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_releases() -> HttpResponse: + var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/releases") + if error != OK: + var message := "request_releases failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_image(url: String) -> HttpResponse: + var error := _http_request.request(url) + if error != OK: + var message := "request_image failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_zip_package(url: String, file: String) -> HttpResponse: + _http_request.set_download_file(file) + var error := _http_request.request(url) + if error != OK: + var message := "request_zip_package failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func extract_latest_version(response: HttpResponse) -> GdUnit4Version: + var body: Array = response.response() + return GdUnit4Version.parse(str(body[0]["name"])) + + +func _on_request_completed(_result: int, response_http_status: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + if _http_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: + _http_request.set_download_file("") + request_completed.emit(HttpResponse.new(response_http_status, body)) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid index e69de29b..bdad4a9d 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid @@ -0,0 +1 @@ +uid://cybga3asuikuv diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd index e69de29b..f4ae9aca 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -0,0 +1,206 @@ +@tool +extends MarginContainer + +#signal request_completed(response) + +const GdMarkDownReader = preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd") +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GdUnitUpdateProgress = preload("res://addons/gdUnit4/src/update/GdUnitUpdate.gd") + +@onready var _md_reader: GdMarkDownReader = GdMarkDownReader.new() +@onready var _update_client: GdUnitUpdateClient = $GdUnitUpdateClient +@onready var _header: Label = $Panel/GridContainer/PanelContainer/header +@onready var _update_button: Button = $Panel/GridContainer/Panel/HBoxContainer/update +@onready var _content: RichTextLabel = $Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content +@onready var _update_progress :GdUnitUpdateProgress = %update_banner + +var _debug_mode := false +var _patcher := GdUnitPatcher.new() +var _current_version := GdUnit4Version.current() + + +func _ready() -> void: + _update_button.set_disabled(false) + _md_reader.set_http_client(_update_client) + @warning_ignore("return_value_discarded") + #GdUnitFonts.init_fonts(_content) + _update_progress.set_visible(false) + _update_progress.hidden.connect(func() -> void: + _update_button.set_disabled(false) + ) + + +func request_releases() -> bool: + if _debug_mode: + _update_progress._debug_mode = _debug_mode + _header.text = "A new version 'v4.4.4' is available" + _update_button.set_disabled(false) + return true + + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version() + if response.status() != 200: + _header.text = "Update information cannot be retrieved from GitHub!" + message_h4("\n\nError: %s" % response.response(), Color.INDIAN_RED) + return false + var latest_version := _update_client.extract_latest_version(response) + # if same version exit here no update need + if latest_version.is_greater(_current_version): + _patcher.scan(_current_version) + _header.text = "A new version '%s' is available" % latest_version + var download_zip_url := extract_zip_url(response) + _update_progress.setup(_update_client, download_zip_url) + _update_button.set_disabled(false) + return true + else: + _header.text = "No update is available." + _update_button.set_disabled(true) + return false + + +func _colored(message_: String, color: Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message_] + + +func message_h4(message_: String, color: Color, clear := true) -> void: + if clear: + _content.clear() + _content.append_text("[font_size=16]%s[/font_size]" % _colored(message_, color)) + + +func message(message_: String, color: Color) -> void: + _content.clear() + _content.append_text(_colored(message_, color)) + + +func _process(_delta: float) -> void: + if _content != null and _content.is_visible_in_tree(): + _content.queue_redraw() + + +func show_update() -> void: + if not GdUnitSettings.is_update_notification_enabled(): + _header.text = "No update is available." + message_h4("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) + _update_button.set_disabled(true) + return + + if not await request_releases(): + return + _update_button.set_disabled(true) + + prints("Scan for GdUnit4 Update ...") + message_h4("\n\n\nRequest release infos ... ", Color.SNOW) + _content.add_image(GdUnitUiTools.get_spinner(), 32, 32) + + var content: String + if _debug_mode: + await get_tree().create_timer(.2).timeout + var template := FileAccess.open("res://addons/gdUnit4/test/update/resources/http_response_releases.txt", FileAccess.READ).get_as_text() + content = await _md_reader.to_bbcode(template) + else: + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_releases() + if response.status() == 200: + content = await extract_releases(response, _current_version) + else: + message_h4("\n\n\nError checked request available releases!", Color.INDIAN_RED) + return + + # finally force rescan to import images as textures + if Engine.is_editor_hint(): + await rescan() + message(content, Color.CADET_BLUE) + _update_button.set_disabled(false) + + + +func extract_zip_url(response: GdUnitUpdateClient.HttpResponse) -> String: + var body :Array = response.response() + return body[0]["zipball_url"] + + +func extract_releases(response: GdUnitUpdateClient.HttpResponse, current_version: GdUnit4Version) -> String: + await get_tree().process_frame + var result := "" + for release :Dictionary in response.response(): + var release_version := str(release["tag_name"]) + if GdUnit4Version.parse(release_version).equals(current_version): + break + var release_description := _colored("

GdUnit Release %s

" % release_version, Color.CORNFLOWER_BLUE) + release_description += "\n" + release_description += release["body"] + release_description += "\n\n" + result += await _md_reader.to_bbcode(release_description) + return result + + +func rescan() -> void: + if Engine.is_editor_hint(): + if OS.is_stdout_verbose(): + prints(".. reimport release resources") + var fs := EditorInterface.get_resource_filesystem() + fs.scan() + while fs.is_scanning(): + if OS.is_stdout_verbose(): + progressBar(fs.get_scanning_progress() * 100 as int) + await get_tree().process_frame + await get_tree().process_frame + await get_tree().create_timer(1).timeout + + +func progressBar(p_progress: int) -> void: + if p_progress < 0: + p_progress = 0 + if p_progress > 100: + p_progress = 100 + printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress]) + + +@warning_ignore("return_value_discarded") +func _on_update_pressed() -> void: + _update_button.set_disabled(true) + # close all opend scripts before start the update + if not _debug_mode: + ScriptEditorControls.close_open_editor_scripts() + # copy update source to a temp because the update is deleting the whole gdUnit folder + DirAccess.make_dir_absolute("res://addons/.gdunit_update") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", "res://addons/.gdunit_update/GdUnitUpdate.tscn") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var source := FileAccess.open("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", FileAccess.READ) + var content := source.get_as_text().replace("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var dest := FileAccess.open("res://addons/.gdunit_update/GdUnitUpdate.tscn", FileAccess.WRITE) + dest.store_string(content) + _update_progress.set_visible(true) + + +func _on_show_next_toggled(enabled: bool) -> void: + GdUnitSettings.set_update_notification(enabled) + + +func _on_cancel_pressed() -> void: + hide() + + +func _on_content_meta_clicked(meta: String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("url"): + @warning_ignore("return_value_discarded") + OS.shell_open(str(properties.get("url"))) + + +func _on_content_meta_hover_started(meta: String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("tool_tip"): + _content.set_tooltip_text(str(properties.get("tool_tip"))) + + +@warning_ignore("unused_parameter") +func _on_content_meta_hover_ended(meta: String) -> void: + _content.set_tooltip_text("") + + +func _on_visibility_changed() -> void: + if not is_visible_in_tree(): + return + if _update_progress != null: + _update_progress.set_visible(false) + await show_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid index e69de29b..64610aef 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid @@ -0,0 +1 @@ +uid://crycbwg5hjrkl diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn index e69de29b..46119f14 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=4 format=3 uid="uid://0xyeci1tqebj"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdate.tscn" id="3_x87h6"] + +[node name="Control" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_112wo") + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("2_18asx") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 2 + +[node name="GridContainer" type="VBoxContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 1 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="header" type="Label" parent="Panel/GridContainer/PanelContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="PanelContainer2" type="PanelContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="Panel/GridContainer/PanelContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/GridContainer/PanelContainer2/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="content" type="RichTextLabel" parent="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true + +[node name="update_banner" parent="Panel/GridContainer" instance=ExtResource("3_x87h6")] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 8 + +[node name="Panel" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 8 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/GridContainer/Panel"] +use_parent_material = true +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="update" type="Button" parent="Panel/GridContainer/Panel/HBoxContainer"] +custom_minimum_size = Vector2(100, 40) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +text = "Update" + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="meta_clicked" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_clicked"] +[connection signal="meta_hover_ended" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_ended"] +[connection signal="meta_hover_started" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_started"] +[connection signal="pressed" from="Panel/GridContainer/Panel/HBoxContainer/update" to="." method="_on_update_pressed"] diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png b/addons/gdUnit4/src/update/assets/border_bottom.png index e69de29b..2eb4a6f9 100644 --- a/addons/gdUnit4/src/update/assets/border_bottom.png +++ b/addons/gdUnit4/src/update/assets/border_bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37a40709349e82f04b542de8d17c4118d68531f6515964b0e03f28f6ee529772 +size 1757 diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png.import b/addons/gdUnit4/src/update/assets/border_bottom.png.import index db3ec480..8b5da955 100644 --- a/addons/gdUnit4/src/update/assets/border_bottom.png.import +++ b/addons/gdUnit4/src/update/assets/border_bottom.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://cb4qnsd0po83o" -valid=false +uid="uid://cse3iwnghcmkn" +path="res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/border_bottom.png" +dest_files=["res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex"] [params] diff --git a/addons/gdUnit4/src/update/assets/border_top.png b/addons/gdUnit4/src/update/assets/border_top.png index e69de29b..fcfe17dc 100644 --- a/addons/gdUnit4/src/update/assets/border_top.png +++ b/addons/gdUnit4/src/update/assets/border_top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca6f5d7f59cf272dc87fc4762bd7ec82502bb1987c7803ebaea594408401ac47 +size 1749 diff --git a/addons/gdUnit4/src/update/assets/border_top.png.import b/addons/gdUnit4/src/update/assets/border_top.png.import index 22169248..23bce284 100644 --- a/addons/gdUnit4/src/update/assets/border_top.png.import +++ b/addons/gdUnit4/src/update/assets/border_top.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://p47gfqh53x1q" -valid=false +uid="uid://peoblq5q6qhm" +path="res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/border_top.png" +dest_files=["res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex"] [params] diff --git a/addons/gdUnit4/src/update/assets/dot1.png b/addons/gdUnit4/src/update/assets/dot1.png index e69de29b..0ae482f2 100644 --- a/addons/gdUnit4/src/update/assets/dot1.png +++ b/addons/gdUnit4/src/update/assets/dot1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f37474a8d5dc0c9574cef6fad5308922841e60deb31213e2723e3dd2755bd7e5 +size 730 diff --git a/addons/gdUnit4/src/update/assets/dot1.png.import b/addons/gdUnit4/src/update/assets/dot1.png.import index 753fb959..a8bc5b04 100644 --- a/addons/gdUnit4/src/update/assets/dot1.png.import +++ b/addons/gdUnit4/src/update/assets/dot1.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://cdoj1yp8q7x5p" -valid=false +uid="uid://bhfi0qh1a2hwx" +path="res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/dot1.png" +dest_files=["res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex"] [params] diff --git a/addons/gdUnit4/src/update/assets/dot2.png b/addons/gdUnit4/src/update/assets/dot2.png index e69de29b..02b0af76 100644 --- a/addons/gdUnit4/src/update/assets/dot2.png +++ b/addons/gdUnit4/src/update/assets/dot2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c12758bd1b0b0ef88811b50affd3957bbf9c9e6c0922c0de199a4a3649643103 +size 883 diff --git a/addons/gdUnit4/src/update/assets/dot2.png.import b/addons/gdUnit4/src/update/assets/dot2.png.import index a80ec801..23930c7c 100644 --- a/addons/gdUnit4/src/update/assets/dot2.png.import +++ b/addons/gdUnit4/src/update/assets/dot2.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bbpyk3jcipja7" -valid=false +uid="uid://dg86wkmvp1qyn" +path="res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/dot2.png" +dest_files=["res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex"] [params] diff --git a/addons/gdUnit4/src/update/assets/embedded.png b/addons/gdUnit4/src/update/assets/embedded.png index e69de29b..b09c232f 100644 --- a/addons/gdUnit4/src/update/assets/embedded.png +++ b/addons/gdUnit4/src/update/assets/embedded.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08cead78bbc3d7aefda91f376b1280195333d94a32222b1490876fabf0823f6b +size 287 diff --git a/addons/gdUnit4/src/update/assets/embedded.png.import b/addons/gdUnit4/src/update/assets/embedded.png.import index 8e7d1a96..56cb94f4 100644 --- a/addons/gdUnit4/src/update/assets/embedded.png.import +++ b/addons/gdUnit4/src/update/assets/embedded.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://u74w6eoyl2mx" -valid=false +uid="uid://d160etflupwba" +path="res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/embedded.png" +dest_files=["res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex"] [params] diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png b/addons/gdUnit4/src/update/assets/horizontal-line2.png index e69de29b..91ccc243 100644 --- a/addons/gdUnit4/src/update/assets/horizontal-line2.png +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb01fb89e6b3ff826f05e772336f0cd855e1e838ef920cac9fd6a3e3504dacd9 +size 332 diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png.import b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import index 463c087a..3931900d 100644 --- a/addons/gdUnit4/src/update/assets/horizontal-line2.png.import +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import @@ -2,12 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://b2p4jgab5ehs3" -valid=false +uid="uid://bloqm443lywyi" +path="res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex" +metadata={ +"vram_texture": false +} [deps] source_file="res://addons/gdUnit4/src/update/assets/horizontal-line2.png" +dest_files=["res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex"] [params] diff --git a/project.godot b/project.godot index 6020d088..8d6eefaa 100644 --- a/project.godot +++ b/project.godot @@ -43,7 +43,7 @@ movie_writer/movie_file="D:/Godot/Projects/movement-tests/communication/movie.av [editor_plugins] -enabled=PackedStringArray("res://addons/godot_state_charts/plugin.cfg", "res://addons/guide/plugin.cfg", "res://addons/maaacks_game_template/plugin.cfg", "res://addons/shaker/plugin.cfg") +enabled=PackedStringArray("res://addons/godot_state_charts/plugin.cfg", "res://addons/guide/plugin.cfg", "res://addons/maaacks_game_template/plugin.cfg", "res://addons/shaker/plugin.cfg", "res://addons/gdUnit4/plugin.cfg") [gui]