using GFramework.Godot.SourceGenerators.Tests.Core; namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal; /// /// 验证 的生成与诊断行为。 /// [TestFixture] public class BindNodeSignalGeneratorTests { /// /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。 /// [Test] public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { public virtual void _Ready() {} public virtual void _ExitTree() {} } public class Button : Node { public event Action? Pressed { add {} remove {} } } public class SpinBox : Node { public delegate void ValueChangedEventHandler(double value); public event ValueChangedEventHandler? ValueChanged { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private Button _startButton = null!; private SpinBox _startOreSpinBox = null!; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] private void OnStartButtonPressed() { } [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] private void OnStartOreValueChanged(double value) { } public override void _Ready() { __BindNodeSignals_Generated(); } public override void _ExitTree() { __UnbindNodeSignals_Generated(); } } } """; const string expected = """ // #nullable enable namespace TestApp; partial class Hud { private void __BindNodeSignals_Generated() { _startButton.Pressed += OnStartButtonPressed; _startOreSpinBox.ValueChanged += OnStartOreValueChanged; } private void __UnbindNodeSignals_Generated() { _startButton.Pressed -= OnStartButtonPressed; _startOreSpinBox.ValueChanged -= OnStartOreValueChanged; } } """; await GeneratorTest.RunAsync( source, ("TestApp_Hud.BindNodeSignal.g.cs", expected)); } /// /// 验证一个处理方法可以通过多个特性绑定到多个节点事件,且能与 GetNode 声明共存。 /// [Test] public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public sealed class GetNodeAttribute : Attribute { } } namespace Godot { public class Node { public virtual void _Ready() {} public virtual void _ExitTree() {} } public class Button : Node { public event Action? Pressed { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { [GetNode] private Button _startButton = null!; [GetNode] private Button _cancelButton = null!; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] private void OnAnyButtonPressed() { } } } """; const string expected = """ // #nullable enable namespace TestApp; partial class Hud { private void __BindNodeSignals_Generated() { _startButton.Pressed += OnAnyButtonPressed; _cancelButton.Pressed += OnAnyButtonPressed; } private void __UnbindNodeSignals_Generated() { _startButton.Pressed -= OnAnyButtonPressed; _cancelButton.Pressed -= OnAnyButtonPressed; } } """; await GeneratorTest.RunAsync( source, ("TestApp_Hud.BindNodeSignal.g.cs", expected)); } /// /// 验证引用不存在的事件时会报告错误。 /// [Test] public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { } public class Button : Node { public event Action? Pressed { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private Button _startButton = null!; [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] private void OnStartButtonPressed() { } } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("_startButton", "Released")); await test.RunAsync(); } /// /// 验证方法签名与事件委托不匹配时会报告错误。 /// [Test] public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { } public class SpinBox : Node { public delegate void ValueChangedEventHandler(double value); public event ValueChangedEventHandler? ValueChanged { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private SpinBox _startOreSpinBox = null!; [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] private void OnStartOreValueChanged() { } } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")); await test.RunAsync(); } /// /// 验证特性构造参数为空时会报告明确的参数无效诊断。 /// [Test] public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { } public class Button : Node { public event Action? Pressed { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private Button _startButton = null!; [{|#0:BindNodeSignal(nameof(_startButton), "")|}] private void OnStartButtonPressed() { } } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("OnStartButtonPressed", "signalName")); await test.RunAsync(); } /// /// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。 /// [Test] public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { } public class Button : Node { public event Action? Pressed { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private Button _startButton = null!; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] private void OnStartButtonPressed() { } private void {|#0:__BindNodeSignals_Generated|}() { } private void {|#1:__UnbindNodeSignals_Generated|}() { } } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Hud", "__BindNodeSignals_Generated")); test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) .WithLocation(1) .WithArguments("Hud", "__UnbindNodeSignals_Generated")); await test.RunAsync(); } /// /// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。 /// [Test] public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions { [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class BindNodeSignalAttribute : Attribute { public BindNodeSignalAttribute(string nodeFieldName, string signalName) { NodeFieldName = nodeFieldName; SignalName = signalName; } public string NodeFieldName { get; } public string SignalName { get; } } } namespace Godot { public class Node { public virtual void _Ready() {} public virtual void _ExitTree() {} } public class Button : Node { public event Action? Pressed { add {} remove {} } } } namespace TestApp { public partial class Hud : Node { private Button _startButton = null!; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] private void OnStartButtonPressed() { } public override void {|#0:_Ready|}() { } public override void {|#1:_ExitTree|}() { } } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) .WithLocation(0) .WithArguments("Hud")); test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) .WithLocation(1) .WithArguments("Hud")); await test.RunAsync(); } }