mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-31 18:39:00 +08:00
- 实现 BindNodeSignalGenerator 源代码生成器,用于自动化节点信号绑定 - 添加完整的诊断系统,包含 11 种不同的错误和警告场景检测 - 生成对称的绑定和解绑方法,确保资源正确释放 - 支持一个处理方法通过多个特性绑定到多个节点事件 - 实现生命周期钩子调用检查,确保在 _Ready 和 _ExitTree 中正确调用生成的方法 - 提供详细的单元测试覆盖各种使用场景和边界条件 - 生成器与现有的 GetNode 声明完全兼容并可共存 - 包含命名冲突检测和构造参数验证等安全检查机制
629 lines
26 KiB
C#
629 lines
26 KiB
C#
using GFramework.Godot.SourceGenerators.Tests.Core;
|
|
|
|
namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
|
|
|
|
/// <summary>
|
|
/// 验证 <see cref="BindNodeSignalGenerator" /> 的生成与诊断行为。
|
|
/// </summary>
|
|
[TestFixture]
|
|
public class BindNodeSignalGeneratorTests
|
|
{
|
|
/// <summary>
|
|
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
|
|
/// </summary>
|
|
[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 = """
|
|
// <auto-generated />
|
|
#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<BindNodeSignalGenerator>.RunAsync(
|
|
source,
|
|
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证一个处理方法可以通过多个特性绑定到多个节点事件,且能与 GetNode 声明共存。
|
|
/// </summary>
|
|
[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 = """
|
|
// <auto-generated />
|
|
#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<BindNodeSignalGenerator>.RunAsync(
|
|
source,
|
|
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证引用不存在的事件时会报告错误。
|
|
/// </summary>
|
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证方法签名与事件委托不匹配时会报告错误。
|
|
/// </summary>
|
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
|
|
/// </summary>
|
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
|
|
/// </summary>
|
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
|
|
/// </summary>
|
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
|
{
|
|
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();
|
|
}
|
|
} |