From a42fa570861d0a02234ea5a725987cd3a675423e Mon Sep 17 00:00:00 2001 From: Nullpointer Date: Tue, 30 Jul 2024 16:41:10 +0200 Subject: [PATCH] GD-135: Allow to use `AssertSignal` on `RefCounted` Objects (#136) # Why see https://github.com/MikeSchulze/gdUnit4Net/issues/135 # What - Added Node instance check before register the `UnregisterEmitter` on `TreeExiting` signals - Added custom cleanup method to the GodotSignalCollector to enshure all registered emitters are unregistered after the test execution - Covered by new test `EmitSignalOnNoneNodeObjects` - Some code cleanup/formattings - Update Example Project to latest API and Test Adapter - Increase API version to 4.3.1 --- PackageVersions.props | 2 +- api/ReleaseNotes.txt | 7 + api/src/asserts/AssertFailures.cs | 479 +++++++++--------- .../core/execution/AfterTestExecutionStage.cs | 19 +- api/src/core/signals/GodotSignalCollector.cs | 84 +-- example/exampleProject.csproj | 8 +- test/src/asserts/SignalAssertTest.cs | 145 ++++-- 7 files changed, 415 insertions(+), 329 deletions(-) diff --git a/PackageVersions.props b/PackageVersions.props index 782376a9..9b28c7e4 100644 --- a/PackageVersions.props +++ b/PackageVersions.props @@ -5,7 +5,7 @@ 17.9.0 4.9.2 4.18.4 - 4.3.0 + 4.3.1 2.0.0 diff --git a/api/ReleaseNotes.txt b/api/ReleaseNotes.txt index 4563bd6f..c3bf8e58 100644 --- a/api/ReleaseNotes.txt +++ b/api/ReleaseNotes.txt @@ -1,3 +1,10 @@ +v4.3.1 + +Bug Fixes: +* GD-135: Cannot test signals on non-node classes + +------------------------------------------------------------------------------------------------------- + v4.3.0 Improvements: diff --git a/api/src/asserts/AssertFailures.cs b/api/src/asserts/AssertFailures.cs index 086417f9..98975865 100644 --- a/api/src/asserts/AssertFailures.cs +++ b/api/src/asserts/AssertFailures.cs @@ -6,6 +6,9 @@ namespace GdUnit4.Asserts; using System.Linq; using System.Runtime.CompilerServices; +using Godot; +using Godot.Collections; + internal sealed class AssertFailures { public const string WARN_COLOR = "#EFF883"; @@ -23,18 +26,18 @@ internal static string AsObjectId(object? value) if (value == null) return ""; var type = value.GetType(); - if (value is Godot.Variant gv) + if (value is Variant gv) { value = gv.UnboxVariant()!; if (value != null) type = value.GetType(); else - return $" (Null)"; + return " (Null)"; } var instanceId = ""; var name = $"<{type.FullName?.Replace("[", "")?.Replace("]", "")!}>"; - if (value is Godot.GodotObject go) + if (value is GodotObject go && GodotObject.IsInstanceValid(go)) instanceId = $"objId: {go.GetInstanceId()}"; else { @@ -42,6 +45,7 @@ internal static string AsObjectId(object? value) if (HasOverriddenToString(value)) name = value.ToString(); } + return $"{name} ({instanceId})"; //if (!type.IsGenericType) @@ -50,7 +54,6 @@ internal static string AsObjectId(object? value) } - private static string FormatDictionary(IDictionary dict, string color) { if (dict.Keys.Count == 0) @@ -62,7 +65,7 @@ private static string FormatDictionary(IDictionary dict, string color) return $"[color={color}]{pairs}[/color]"; } - private static string FormatDictionary(IDictionary dict, string color) + private static string FormatDictionary(IDictionary dict, string color) { if (dict.Keys.Count == 0) return $"[color={color}][/color]"; @@ -74,6 +77,7 @@ private static string FormatDictionary(IDictionary object? value = entry.Value.UnboxVariant(); keyValues.Add($"{{{key.Formatted()}, {value.Formatted()}}}"); } + var pairs = string.Join("; ", keyValues.ToArray()); return $"[color={color}]{pairs}[/color]"; } @@ -86,9 +90,8 @@ private static string FormatEnumerable(IEnumerable enumerable, string color) var keyValues = new ArrayList(); do - { keyValues.Add(enumerator.Current); - } while (enumerator.MoveNext()); + while (enumerator.MoveNext()); return $"[color={color}]{keyValues.Formatted()}[/color]"; } @@ -103,14 +106,14 @@ public static string FormatValue(object? value, string color, bool quoted) if (value is Type) return $"[color={color}]<{value}>[/color]"; - if (value is Godot.Variant gv) + if (value is Variant gv) value = value.UnboxVariant(); var type = value!.GetType(); if (value is IDictionary dict) return FormatDictionary(dict, color); - if (value is Godot.Collections.Dictionary gDict) + if (value is Dictionary gDict) return FormatDictionary(gDict, color); if (type.IsGenericGodotDictionary()) @@ -135,223 +138,226 @@ public static string IsFalse() => $"{FormatFailure("Expecting:")} {FormatExpected(false)} but is {FormatCurrent(true)}"; public static string IsEqual(object? current, object? expected) => - current is IEnumerable || expected is IEnumerable ? $""" - {FormatFailure("Expecting be equal:")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """ + current is IEnumerable || expected is IEnumerable + ? $""" + {FormatFailure("Expecting be equal:")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """ : $""" - {FormatFailure("Expecting be equal:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be equal:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsEqual(IEnumerable current, IEnumerable expected) => $""" - {FormatFailure("Expecting be equal:")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be equal:")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsEqualIgnoringCase(object? current, object expected) => $""" - {FormatFailure("Expecting be equal (ignoring case):")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be equal (ignoring case):")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsNotEqual(object? current, object? expected) => - current is IEnumerable || expected is IEnumerable ? $""" - {FormatFailure("Expecting be NOT equal:")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """ + current is IEnumerable || expected is IEnumerable + ? $""" + {FormatFailure("Expecting be NOT equal:")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """ : $""" - {FormatFailure("Expecting be NOT equal:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be NOT equal:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsNotEqual(IEnumerable current, IEnumerable expected) => $""" - {FormatFailure("Expecting be NOT equal:")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be NOT equal:")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsNotEqualIgnoringCase(object? current, object expected) => $""" - {FormatFailure("Expecting be NOT equal (ignoring case):")} - {FormatExpected(expected).Indentation(1)} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be NOT equal (ignoring case):")} + {FormatExpected(expected).Indentation(1)} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsNull(object current) => $""" - {FormatFailure("Expecting be :")} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be :")} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsNotNull() => "Expecting be NOT :"; public static string IsEmpty(int size, bool isNull) => - isNull ? $""" - {FormatFailure("Expecting be empty:")} - but is - """ + isNull + ? $""" + {FormatFailure("Expecting be empty:")} + but is + """ : $""" - {FormatFailure("Expecting be empty:")} - but has size {FormatCurrent(size)} - """; + {FormatFailure("Expecting be empty:")} + but has size {FormatCurrent(size)} + """; public static string IsEmpty(string? current) => $""" - {FormatFailure("Expecting be empty:")} - but is - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be empty:")} + but is + {FormatCurrent(current).Indentation(1)} + """; public static string IsNotEmpty() => $""" - {FormatFailure("Expecting being NOT empty:")} - but is empty - """; + {FormatFailure("Expecting being NOT empty:")} + but is empty + """; public static string NotInstanceOf(Type? expected) => $""" - {FormatFailure("Expecting be NOT a instance of:")} - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting be NOT a instance of:")} + {FormatExpected(expected).Indentation(1)} + """; public static string IsInstanceOf(Type? current, Type expected) => $""" - {FormatFailure("Expected be instance of:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expected be instance of:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsSame(TValue? current, TValue expected) => $""" - {FormatFailure("Expecting be same:")} - {FormatExpected(expected).Indentation(1)} - to refer to the same object - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting be same:")} + {FormatExpected(expected).Indentation(1)} + to refer to the same object + {FormatCurrent(current).Indentation(1)} + """; public static string IsNotSame(TValue? expected) => $"{FormatFailure("Expecting be NOT same:")} {FormatExpected(expected)}"; public static string IsBetween(TValue? current, TValue from, TValue to) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - in range between - {FormatExpected(from).Indentation(1)} <> {FormatExpected(to)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + in range between + {FormatExpected(from).Indentation(1)} <> {FormatExpected(to)} + """; public static string IsNotBetween(object? current, object from, object to) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - be NOT in range between - {FormatExpected(from).Indentation(1)} <> {FormatExpected(to)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + be NOT in range between + {FormatExpected(from).Indentation(1)} <> {FormatExpected(to)} + """; public static string IsEven(object? current) => $""" - {FormatFailure("Expecting be even:")} - but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be even:")} + but is {FormatCurrent(current)} + """; public static string IsOdd(object? current) => $""" - {FormatFailure("Expecting be odd:")} - but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be odd:")} + but is {FormatCurrent(current)} + """; public static string HasSize(object? current, object expected) => $""" - {FormatFailure("Expecting size:")} - {FormatExpected(expected).Indentation(1)} but is {(current == null ? "unknown" : FormatCurrent(current))} - """; + {FormatFailure("Expecting size:")} + {FormatExpected(expected).Indentation(1)} but is {(current == null ? "unknown" : FormatCurrent(current))} + """; public static string IsGreater(object current, object expected) => $""" - {FormatFailure("Expecting to be greater than:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting to be greater than:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsGreaterEqual(object current, object expected) => $""" - {FormatFailure("Expecting to be greater than or equal:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting to be greater than or equal:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsLess(object current, object expected) => $""" - {FormatFailure("Expecting to be less than:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting to be less than:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsLessEqual(object current, object expected) => $""" - {FormatFailure("Expecting to be less than or equal:")} - {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting to be less than or equal:")} + {FormatExpected(expected).Indentation(1)} but is {FormatCurrent(current)} + """; public static string IsNegative(object current) => $""" - {FormatFailure("Expecting be negative:")} - but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be negative:")} + but is {FormatCurrent(current)} + """; public static string IsNotNegative(object current) => $""" - {FormatFailure("Expecting be NOT negative:")} - but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be NOT negative:")} + but is {FormatCurrent(current)} + """; public static string IsNotZero() => $""" - {FormatFailure("Expecting be NOT zero:")} - but is '0' - """; + {FormatFailure("Expecting be NOT zero:")} + but is '0' + """; public static string IsZero(object? current) => $""" - {FormatFailure("Expecting be zero:")} - but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting be zero:")} + but is {FormatCurrent(current)} + """; public static string IsIn(object? current, object expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - is in - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + is in + {FormatExpected(expected).Indentation(1)} + """; public static string IsNotIn(object? current, object expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - is not in - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + is not in + {FormatExpected(expected).Indentation(1)} + """; public static string Contains(IEnumerable? current, IEnumerable expected, IEnumerable notFound) => $""" - {FormatFailure("Expecting contains elements:")} - {FormatCurrent(current).Indentation(1)} - do contains (in any order) - {FormatExpected(expected).Indentation(1)} - but could not find elements: - {FormatExpected(notFound).Indentation(1)} - """; + {FormatFailure("Expecting contains elements:")} + {FormatCurrent(current).Indentation(1)} + do contains (in any order) + {FormatExpected(expected).Indentation(1)} + but could not find elements: + {FormatExpected(notFound).Indentation(1)} + """; public static string ContainsExactly(IEnumerable? current, IEnumerable expected, List notFound, List notExpected) { @@ -359,139 +365,138 @@ public static string ContainsExactly(IEnumerable? current, IEnum { var diff = notExpected.FindAll(e => !notFound.Any(e2 => Equals(e.UnboxVariant(), e2.UnboxVariant()))); if (diff?.Count == 0) - { return $""" - {FormatFailure("Expecting contains exactly elements:")} - {FormatCurrent(current).Indentation(1)} - do contains (in same order) - {FormatExpected(expected).Indentation(1)} - but there has differences in order: - {ListDifferences(notFound, notExpected)} - """; - } + {FormatFailure("Expecting contains exactly elements:")} + {FormatCurrent(current).Indentation(1)} + do contains (in same order) + {FormatExpected(expected).Indentation(1)} + but there has differences in order: + {ListDifferences(notFound, notExpected)} + """; } + var message = $""" - {FormatFailure("Expecting contains exactly elements:")} - {FormatCurrent(current).Indentation(1)} - do contains (in same order) - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting contains exactly elements:")} + {FormatCurrent(current).Indentation(1)} + do contains (in same order) + {FormatExpected(expected).Indentation(1)} + """; if (notExpected.Count > 0) message += $""" - but others where not expected: - {FormatExpected(notExpected).Indentation(1)} - """; + but others where not expected: + {FormatExpected(notExpected).Indentation(1)} + """; if (notFound.Count > 0) message += $""" - {(notExpected.Count == 0 ? "but" : "and")} some elements not found: - {FormatExpected(notFound).Indentation(1)} - """; + {(notExpected.Count == 0 ? "but" : "and")} some elements not found: + {FormatExpected(notFound).Indentation(1)} + """; return message.UnixFormat(); } public static string ContainsExactlyInAnyOrder(IEnumerable? current, IEnumerable expected, List notFound, List notExpected) { var message = $""" - {FormatFailure("Expecting contains exactly elements:")} - {FormatCurrent(current).Indentation(1)} - do contains (in any order) - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting contains exactly elements:")} + {FormatCurrent(current).Indentation(1)} + do contains (in any order) + {FormatExpected(expected).Indentation(1)} + """; if (notExpected.Count > 0) message += $""" - but some elements where not expected: - {FormatExpected(notExpected).Indentation(1)} - """; + but some elements where not expected: + {FormatExpected(notExpected).Indentation(1)} + """; if (notFound.Count > 0) message += $""" - {(notExpected.Count == 0 ? "but" : "and")} could not find elements: - {FormatExpected(notFound).Indentation(1)} - """; + {(notExpected.Count == 0 ? "but" : "and")} could not find elements: + {FormatExpected(notFound).Indentation(1)} + """; return message; } public static string ContainsKeyValue(IDictionary expected) => $""" - {FormatFailure("Expecting do contain entry:")} - {FormatCurrent(expected).Indentation(1)} - """; + {FormatFailure("Expecting do contain entry:")} + {FormatCurrent(expected).Indentation(1)} + """; public static string ContainsKeyValue(IDictionary expected, object? currentValue) => $""" - {FormatFailure("Expecting do contain entry:")} - {FormatCurrent(expected).Indentation(1)} - found key but value is - {FormatCurrent(currentValue).Indentation(1)} - """; + {FormatFailure("Expecting do contain entry:")} + {FormatCurrent(expected).Indentation(1)} + found key but value is + {FormatCurrent(currentValue).Indentation(1)} + """; public static string NotContains(string current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - do not contain - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + do not contain + {FormatExpected(expected).Indentation(1)} + """; public static string NotContains(IEnumerable? current, IEnumerable expected, List found) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - do NOT contains (in any order) - {FormatExpected(expected).Indentation(1)} - but found elements: - {FormatExpected(found).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + do NOT contains (in any order) + {FormatExpected(expected).Indentation(1)} + but found elements: + {FormatExpected(found).Indentation(1)} + """; public static string NotContainsIgnoringCase(string current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - do not contain (ignoring case) - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + do not contain (ignoring case) + {FormatExpected(expected).Indentation(1)} + """; public static string HasValue(string methodName, object? current, object expected) => $""" - {FormatFailure("Expecting Property:")} - {FormatCurrent(methodName).Indentation(1)} to be {FormatCurrent(expected)} but is {FormatCurrent(current)} - """; + {FormatFailure("Expecting Property:")} + {FormatCurrent(methodName).Indentation(1)} to be {FormatCurrent(expected)} but is {FormatCurrent(current)} + """; public static string Contains(string? current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - do contains - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + do contains + {FormatExpected(expected).Indentation(1)} + """; public static string ContainsIgnoringCase(string? current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - do contains (ignoring case) - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + do contains (ignoring case) + {FormatExpected(expected).Indentation(1)} + """; public static string EndsWith(string? current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - to end with - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + to end with + {FormatExpected(expected).Indentation(1)} + """; public static string StartsWith(string? current, string expected) => $""" - {FormatFailure("Expecting:")} - {FormatCurrent(current).Indentation(1)} - to start with - {FormatExpected(expected).Indentation(1)} - """; + {FormatFailure("Expecting:")} + {FormatCurrent(current).Indentation(1)} + to start with + {FormatExpected(expected).Indentation(1)} + """; public static string HasLength(int currentLength, int expectedLength, IStringAssert.Compare comparator) { @@ -502,7 +507,7 @@ public static string HasLength(int currentLength, int expectedLength, IStringAss IStringAssert.Compare.LESS_EQUAL => "Expecting length to be less than or equal:", IStringAssert.Compare.GREATER_THAN => "Expecting length to be greater than:", IStringAssert.Compare.GREATER_EQUAL => "Expecting length to be greater than or equal:", - _ => "Invalid comparator", + _ => "Invalid comparator" }; return $""" {FormatFailure(errorMessage)} @@ -510,38 +515,44 @@ public static string HasLength(int currentLength, int expectedLength, IStringAss """; } - internal static string IsEmitted(object? current, string signal, Godot.Variant[] args) => + internal static string IsEmitted(object? current, string signal, Variant[] args) => $""" - {FormatFailure("Expecting do emitting signal:")} - {FormatExpected($"{signal}({args.Formatted()})").Indentation(1)} - by - {FormatCurrent(current).Indentation(1)} - """; - internal static string IsNotEmitted(object? current, string signal, Godot.Variant[] args) => + {FormatFailure("Expecting do emitting signal:")} + {FormatExpected($"{signal}({args.Formatted()})").Indentation(1)} + by + {FormatCurrent(current).Indentation(1)} + """; + + internal static string IsNotEmitted(object? current, string signal, Variant[] args) => $""" - {FormatFailure("Expecting do NOT emitting signal:")} - {FormatExpected($"{signal}({args.Formatted()})").Indentation(1)} - by - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting do NOT emitting signal:")} + {FormatExpected($"{signal}({args.Formatted()})").Indentation(1)} + by + {FormatCurrent(current).Indentation(1)} + """; internal static string IsSignalExists(object current, string signal) => $""" - {FormatFailure("Expecting signal exists:")} - {FormatExpected($"{signal}()").Indentation(1)} - on - {FormatCurrent(current).Indentation(1)} - """; + {FormatFailure("Expecting signal exists:")} + {FormatExpected($"{signal}()").Indentation(1)} + on + {FormatCurrent(current).Indentation(1)} + """; private static string? ListDifferences(IEnumerable left, IEnumerable right) { var output = new List(); - foreach (var it in left.Select((value, i) => new { Value = value, Index = i })) + foreach (var it in left.Select((value, i) => new + { + Value = value, + Index = i + })) { var l = it.Value; var r = right.ElementAt(it.Index); output.Add($"- element at index {it.Index} expect {FormatCurrent(l.UnboxVariant())} but is {FormatExpected(r.UnboxVariant())}"); } + return string.Join("\n", output).Indentation(1); } } diff --git a/api/src/core/execution/AfterTestExecutionStage.cs b/api/src/core/execution/AfterTestExecutionStage.cs index c4883665..edd8f812 100644 --- a/api/src/core/execution/AfterTestExecutionStage.cs +++ b/api/src/core/execution/AfterTestExecutionStage.cs @@ -4,17 +4,21 @@ namespace GdUnit4.Executions; using System.Reflection; using System.Threading.Tasks; -using GdUnit4.Asserts; +using Asserts; + +using Core.Signals; internal class AfterTestExecutionStage : ExecutionStage { public AfterTestExecutionStage(TestSuite testSuite) : base("AfterTest", testSuite.Instance.GetType()) - { } + { + } public override async Task Execute(ExecutionContext context) { if (!context.IsSkipped) { + GodotSignalCollector.Instance.Clean(); context.MemoryPool.SetActive(StageName); context.OrphanMonitor.Start(); await base.Execute(context); @@ -23,6 +27,7 @@ public override async Task Execute(ExecutionContext context) if (context.OrphanMonitor.OrphanCount > 0) context.ReportCollector.PushFront(new TestReport(TestReport.ReportType.WARN, 0, ReportOrphans(context))); } + context.FireAfterTestEvent(); } @@ -44,14 +49,14 @@ private static string ReportOrphans(ExecutionContext context) var afterAttributes = AfterTestAttribute(context); if (beforeAttribute != null && afterAttributes != null) return $""" - {AssertFailures.FormatValue("WARNING:", AssertFailures.WARN_COLOR, false)} - Detected <{context.OrphanMonitor.OrphanCount}> orphan nodes during test setup stage! - Check [b]{beforeAttribute.Name + ":" + beforeAttribute.Line}[/b] and [b]{afterAttributes.Name + ":" + afterAttributes.Line}[/b] for unfreed instances! - """; + {AssertFailures.FormatValue("WARNING:", AssertFailures.WARN_COLOR, false)} + Detected <{context.OrphanMonitor.OrphanCount}> orphan nodes during test setup stage! + Check [b]{beforeAttribute.Name + ":" + beforeAttribute.Line}[/b] and [b]{afterAttributes.Name + ":" + afterAttributes.Line}[/b] for unfreed instances! + """; return $""" {AssertFailures.FormatValue("WARNING:", AssertFailures.WARN_COLOR, false)} Detected <{context.OrphanMonitor.OrphanCount}> orphan nodes during test setup stage! - Check [b]{(beforeAttribute != null ? (beforeAttribute.Name + ":" + beforeAttribute.Line) : (afterAttributes?.Name + ":" + afterAttributes?.Line))}[/b] for unfreed instances! + Check [b]{(beforeAttribute != null ? beforeAttribute.Name + ":" + beforeAttribute.Line : afterAttributes?.Name + ":" + afterAttributes?.Line)}[/b] for unfreed instances! """; } } diff --git a/api/src/core/signals/GodotSignalCollector.cs b/api/src/core/signals/GodotSignalCollector.cs index a1b9b152..cdae29c1 100644 --- a/api/src/core/signals/GodotSignalCollector.cs +++ b/api/src/core/signals/GodotSignalCollector.cs @@ -1,89 +1,99 @@ namespace GdUnit4.Core.Signals; -using static System.Console; - using System; -using System.Threading; using System.Collections.Generic; using System.Linq; +using System.Threading; + +using Godot; + +using static System.Console; -internal sealed partial class GodotSignalCollector : Godot.RefCounted +using Array = Godot.Collections.Array; +using Error = Godot.Error; + +internal sealed partial class GodotSignalCollector : RefCounted { - private readonly Dictionary>> collectedSignals = new(); + // ReSharper disable once InconsistentNaming + internal readonly Dictionary>> collectedSignals = new(); public static GodotSignalCollector Instance { get; } = new(); - public void RegisterEmitter(Godot.GodotObject emitter) + public void RegisterEmitter(GodotObject emitter) { // do not register the same emitter at twice if (collectedSignals.ContainsKey(emitter)) return; - collectedSignals[emitter] = new Dictionary>(); + collectedSignals[emitter] = new Dictionary>(); // connect to 'TreeExiting' of the emitter to finally release all acquired resources/connections. var action = UnregisterEmitter; - if (!emitter.IsConnected(Godot.Node.SignalName.TreeExiting, Godot.Callable.From(action))) - ((Godot.Node)emitter).TreeExiting += () => UnregisterEmitter(this, emitter); + if (emitter is Node node && !node.IsConnected(Node.SignalName.TreeExiting, Callable.From(action))) + node.TreeExiting += () => UnregisterEmitter(this, emitter); ConnectAllSignals(emitter); } - private void ConnectAllSignals(Godot.GodotObject emitter) + private void ConnectAllSignals(GodotObject emitter) { foreach (var signalDef in emitter.GetSignalList()) { var signalName = (string)signalDef["name"]; - var args = (Godot.Collections.Array)signalDef["args"]; + var args = (Array)signalDef["args"]; var error = emitter.Connect(signalName, BuildCallable(emitter, signalName, args.Count)); - if (error != Godot.Error.Ok) + if (error != Error.Ok) WriteLine($"Error on connecting signal {signalName}, Error: {error}"); - collectedSignals[emitter][signalName] = new List(); + collectedSignals[emitter][signalName] = new List(); } } - private Godot.Callable BuildCallable(Godot.GodotObject emitter, string signalName, int argumentCount) + private Callable BuildCallable(GodotObject emitter, string signalName, int argumentCount) => argumentCount switch { - 0 => Godot.Callable.From(() => CollectSignal(emitter, signalName)), - 1 => Godot.Callable.From((arg0) => CollectSignal(emitter, signalName, arg0)), - 2 => Godot.Callable.From((arg0, arg1) => CollectSignal(emitter, signalName, arg0, arg1)), - 3 => Godot.Callable.From((arg0, arg1, arg2) => CollectSignal(emitter, signalName, arg0, arg1, arg2)), - 4 => Godot.Callable.From((arg0, arg1, arg2, arg3) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3)), - 5 => Godot.Callable.From((arg0, arg1, arg2, arg3, arg4) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4)), - 6 => Godot.Callable.From((arg0, arg1, arg2, arg3, arg4, arg5) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5)), - 7 => Godot.Callable.From((arg0, arg1, arg2, arg3, arg4, arg5, arg6) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5, arg6)), - 8 => Godot.Callable.From((arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)), - _ => throw new NotImplementedException(), + 0 => Callable.From(() => CollectSignal(emitter, signalName)), + 1 => Callable.From(arg0 => CollectSignal(emitter, signalName, arg0)), + 2 => Callable.From((arg0, arg1) => CollectSignal(emitter, signalName, arg0, arg1)), + 3 => Callable.From((arg0, arg1, arg2) => CollectSignal(emitter, signalName, arg0, arg1, arg2)), + 4 => Callable.From((arg0, arg1, arg2, arg3) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3)), + 5 => Callable.From((arg0, arg1, arg2, arg3, arg4) => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4)), + 6 => Callable.From((arg0, arg1, arg2, arg3, arg4, arg5) + => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5)), + 7 => Callable.From((arg0, arg1, arg2, arg3, arg4, arg5, arg6) + => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5, arg6)), + 8 => Callable.From((arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) + => CollectSignal(emitter, signalName, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)), + _ => throw new NotImplementedException() }; - private void CollectSignal(Godot.GodotObject emitter, string signalName, params Godot.Variant[] signalArgs) + private void CollectSignal(GodotObject emitter, string signalName, params Variant[] signalArgs) { if (IsSignalCollecting(emitter, signalName)) collectedSignals[emitter][signalName].Add(signalArgs); } - internal bool IsSignalCollecting(Godot.GodotObject emitter, string signalName) + internal bool IsSignalCollecting(GodotObject emitter, string signalName) => collectedSignals.ContainsKey(emitter) && collectedSignals[emitter].ContainsKey(signalName); // unregister all acquired resources/connections, otherwise it ends up in orphans // is called when the emitter is removed from the parent - private void UnregisterEmitter(GodotSignalCollector collector, Godot.GodotObject emitter) + private void UnregisterEmitter(GodotSignalCollector collector, GodotObject emitter) { if (IsInstanceValid(collector)) //WriteLine($"disconnect_signals: {emitter}"); foreach (var connection in collector.GetIncomingConnections()) { - var source = (Godot.GodotObject)connection["source"]; + var source = (GodotObject)connection["source"]; var signalName = (string)connection["signal_name"]; var methodName = (string)connection["method_name"]; //WriteLine($"disconnect: {signalName} from {source} target {collector} -> {methodName}"); - source.Disconnect(signalName, new Godot.Callable(collector, methodName)); + source.Disconnect(signalName, new Callable(collector, methodName)); } + if (IsInstanceValid(emitter)) collectedSignals.Remove(emitter); //DebugSignalList("UnregisterEmitter"); } - private void ResetCollectedSignals(Godot.GodotObject emitter) + private void ResetCollectedSignals(GodotObject emitter) { //DebugSignalList("before clear"); if (!collectedSignals.TryGetValue(emitter, out var value)) return; @@ -92,7 +102,7 @@ private void ResetCollectedSignals(Godot.GodotObject emitter) //DebugSignalList("after clear"); } - private bool Match(Godot.GodotObject emitter, string signalName, params Godot.Variant[] args) + private bool Match(GodotObject emitter, string signalName, params Variant[] args) //DebugSignalList("--match--"); => collectedSignals[emitter][signalName].Any(receivedArgs => receivedArgs.VariantEquals(args)); @@ -109,10 +119,11 @@ private void DebugSignalList(string message) WriteLine($"\t\t{signalName} {args.Formatted()}"); } } + WriteLine("}"); } - internal bool IsEmitted(CancellationTokenSource token, Godot.GodotObject emitter, string signal, Godot.Variant[] args) + internal bool IsEmitted(CancellationTokenSource token, GodotObject emitter, string signal, Variant[] args) { try { @@ -129,6 +140,7 @@ internal bool IsEmitted(CancellationTokenSource token, Godot.GodotObject emitter if (token.IsCancellationRequested) return false; } + return true; } catch (Exception e) @@ -142,8 +154,14 @@ internal bool IsEmitted(CancellationTokenSource token, Godot.GodotObject emitter } } - internal int Count(Godot.GodotObject emitter, string signalName, Godot.Variant[] args) + internal int Count(GodotObject emitter, string signalName, Variant[] args) => IsSignalCollecting(emitter, signalName) ? collectedSignals[emitter][signalName].FindAll(signalArgs => signalArgs.VariantEquals(args)).Count : 0; + + internal void Clean() + { + collectedSignals.Keys.ToList().ForEach(emitter => UnregisterEmitter(this, emitter)); + collectedSignals.Clear(); + } } diff --git a/example/exampleProject.csproj b/example/exampleProject.csproj index 5dd3a9eb..22707b20 100644 --- a/example/exampleProject.csproj +++ b/example/exampleProject.csproj @@ -1,5 +1,5 @@ - + net8.0 11.0 @@ -14,8 +14,8 @@ NU1605 - - - + + + diff --git a/test/src/asserts/SignalAssertTest.cs b/test/src/asserts/SignalAssertTest.cs index 5e690853..1794a65f 100644 --- a/test/src/asserts/SignalAssertTest.cs +++ b/test/src/asserts/SignalAssertTest.cs @@ -3,45 +3,28 @@ namespace GdUnit4.Tests.Asserts; using System.Threading.Tasks; +using Exceptions; + using GdUnit4.Asserts; using GdUnit4.Core.Signals; +using Godot; + using static Assertions; [TestSuite] public partial class SignalAssertTest { - private sealed partial class TestEmitter : Godot.Node + [AfterTest] + public void TearDown() { - [Godot.Signal] - public delegate void SignalAEventHandler(); - - [Godot.Signal] - public delegate void SignalBEventHandler(string value); - - [Godot.Signal] - public delegate void SignalCEventHandler(string value, int count); - - private int frame; - - public override void _Process(double delta) - { - switch (frame) - { - case 5: - EmitSignal(SignalName.SignalA); - break; - case 10: - EmitSignal(SignalName.SignalB, "abc"); - break; - case 15: - EmitSignal(SignalName.SignalC, "abc", 100); - break; - } - frame++; - } + var signalCollector = GodotSignalCollector.Instance; + AssertThat(signalCollector.collectedSignals.Keys) + .OverrideFailureMessage($"Found keys: {signalCollector.collectedSignals.Keys.Formatted()}") + .IsEmpty(); } + [TestCase] public async Task IsEmitted() { @@ -52,7 +35,7 @@ public async Task IsEmitted() await AssertThrown(AssertSignal(node).IsEmitted("SignalC", "abc", 101).WithTimeout(200)) .ContinueWith(result => result.Result? - .IsInstanceOf() + .IsInstanceOf() .HasMessage(""" Expecting do emitting signal: "SignalC(["abc", 101])" @@ -65,7 +48,7 @@ await AssertThrown(AssertSignal(node).IsEmitted("SignalC", "abc", 101).WithTimeo [TestCase] public async Task IsNoEmitted() { - var node = AddNode(new Godot.Node2D()); + var node = AddNode(new Node2D()); await AssertSignal(node).IsNotEmitted("visibility_changed", 10).WithTimeout(100); await AssertSignal(node).IsNotEmitted("visibility_changed", 20).WithTimeout(100); await AssertSignal(node).IsNotEmitted("script_changed").WithTimeout(100); @@ -73,7 +56,7 @@ public async Task IsNoEmitted() node.Visible = false; await AssertThrown(AssertSignal(node).IsNotEmitted("visibility_changed").WithTimeout(200)) .ContinueWith(result => result.Result? - .IsInstanceOf() + .IsInstanceOf() .HasMessage(""" Expecting do NOT emitting signal: "visibility_changed()" @@ -86,7 +69,7 @@ await AssertThrown(AssertSignal(node).IsNotEmitted("visibility_changed").WithTim [TestCase] public async Task NodeChangedEmittingSignals() { - var node = AddNode(new Godot.Node2D()); + var node = AddNode(new Node2D()); await AssertSignal(node).IsEmitted("draw").WithTimeout(200); @@ -97,7 +80,7 @@ public async Task NodeChangedEmittingSignals() //node.Visible = false; await AssertThrown(AssertSignal(node).IsEmitted("visibility_changed").WithTimeout(200)) .ContinueWith(result => result.Result? - .IsInstanceOf() + .IsInstanceOf() .HasMessage(""" Expecting do emitting signal: "visibility_changed()" @@ -113,7 +96,7 @@ await AssertThrown(AssertSignal(node).IsEmitted("visibility_changed").WithTimeou [TestCase] public void IsSignalExists() { - var node = AutoFree(new Godot.Node2D())!; + var node = AutoFree(new Node2D())!; AssertSignal(node).IsSignalExists("visibility_changed") .IsSignalExists("draw") @@ -123,7 +106,7 @@ public void IsSignalExists() .IsSignalExists("tree_exited"); AssertThrown(() => AssertSignal(node).IsSignalExists("not_existing_signal")) - .IsInstanceOf() + .IsInstanceOf() .HasMessage(""" Expecting signal exists: "not_existing_signal()" @@ -133,21 +116,6 @@ public void IsSignalExists() .Replace("$obj", AssertFailures.AsObjectId(node))); } - public sealed partial class MyEmitter : Godot.Node { - - [Godot.Signal] - public delegate void SignalAEventHandler(); - - [Godot.Signal] - public delegate void SignalBEventHandler(string value); - - - public void DoEmitSignalA() => EmitSignal(SignalName.SignalA); - - - public void DoEmitSignalB() => EmitSignal(SignalName.SignalB, "foo"); - } - [TestCase(Timeout = 1000)] public async Task MonitorOnSignal() { @@ -190,4 +158,81 @@ public async Task MonitorOnSignal() emitterB.DoEmitSignalA(); await AssertSignal(emitterB).IsEmitted(MyEmitter.SignalName.SignalA).WithTimeout(50); } + + [TestCase(Timeout = 1000, Description = "See https://github.com/MikeSchulze/gdUnit4Net/issues/135")] + public async Task EmitSignalOnNoneNodeObjects() + { + var emitter = AutoFree(new NonNodeEmitter())!; + + // verify initial the emitters are not monitored + AssertThat(GodotSignalCollector.Instance.IsSignalCollecting(emitter, NonNodeEmitter.SignalName.SignalA)).IsFalse(); + + + // start monitoring on the emitter + AssertSignal(emitter).StartMonitoring(); + // verify the emitters are now monitored + AssertThat(GodotSignalCollector.Instance.IsSignalCollecting(emitter, NonNodeEmitter.SignalName.SignalA)).IsTrue(); + + // verify the signals are not emitted initial + await AssertSignal(emitter).IsNotEmitted(NonNodeEmitter.SignalName.SignalA).WithTimeout(50); + + // emit signal `signal_a` on emitter + emitter.DoEmitSignalA(); + await AssertSignal(emitter).IsEmitted(NonNodeEmitter.SignalName.SignalA).WithTimeout(50); + } + + private sealed partial class TestEmitter : Node + { + [Signal] + public delegate void SignalAEventHandler(); + + [Signal] + public delegate void SignalBEventHandler(string value); + + [Signal] + public delegate void SignalCEventHandler(string value, int count); + + private int frame; + + public override void _Process(double delta) + { + switch (frame) + { + case 5: + EmitSignal(SignalName.SignalA); + break; + case 10: + EmitSignal(SignalName.SignalB, "abc"); + break; + case 15: + EmitSignal(SignalName.SignalC, "abc", 100); + break; + } + + frame++; + } + } + + public sealed partial class MyEmitter : Node + { + [Signal] + public delegate void SignalAEventHandler(); + + [Signal] + public delegate void SignalBEventHandler(string value); + + + public void DoEmitSignalA() => EmitSignal(SignalName.SignalA); + + + public void DoEmitSignalB() => EmitSignal(SignalName.SignalB, "foo"); + } + + public sealed partial class NonNodeEmitter : RefCounted + { + [Signal] + public delegate void SignalAEventHandler(); + + public void DoEmitSignalA() => EmitSignal(SignalName.SignalA); + } }