// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using GFramework.Godot.SourceGenerators.Behavior; using GFramework.Godot.SourceGenerators.Tests.Core; namespace GFramework.Godot.SourceGenerators.Tests.Behavior; [TestFixture] public class AutoSceneGeneratorTests { private const string AutoSceneAttributeWithKeyDeclaration = """ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute { public AutoSceneAttribute(string key) { } } """; private const string AutoSceneAttributeWithoutKeyDeclaration = """ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute { public AutoSceneAttribute() { } } """; private const string NodeTypes = """ public class Node { } public class Node2D : Node { } """; private const string SceneBehaviorInfrastructure = """ namespace GFramework.Game.Abstractions.Scene { public interface ISceneBehavior { } } namespace GFramework.Godot.Scene { using GFramework.Game.Abstractions.Scene; using Godot; public static class SceneBehaviorFactory { public static ISceneBehavior Create(T owner, string key) where T : Node { return null!; } } } """; [Test] public async Task Generates_Scene_Behavior_Boilerplate() { string source = CreateAutoSceneSource( AutoSceneAttributeWithKeyDeclaration, """ [AutoScene("Gameplay")] public partial class GameplayRoot : Node2D { } """, includeBehaviorInfrastructure: true); const string expected = """ // #nullable enable namespace TestApp; partial class GameplayRoot { private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated; public static string SceneKeyStr => "Gameplay"; public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene() { return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr); } } """; await GeneratorTest.RunAsync( source, ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() { string source = CreateAutoSceneSource( AutoSceneAttributeWithoutKeyDeclaration, """ [{|#0:AutoScene|}] public partial class GameplayRoot : Node2D { } """); var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters() { string source = CreateAutoSceneSource( AutoSceneAttributeWithKeyDeclaration, """ [AutoScene("Gameplay")] public partial class GameplayRoot : Node2D where TReference : class? where TNotNull : notnull where TValue : struct where TUnmanaged : unmanaged { } """, includeBehaviorInfrastructure: true, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class GameplayRoot where TReference : class? where TNotNull : notnull where TValue : struct where TUnmanaged : unmanaged { private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated; public static string SceneKeyStr => "Gameplay"; public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene() { return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr); } } """; await GeneratorTest.RunAsync( source, ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } /// /// 验证宿主类型声明同名 SceneKeyStr 属性时,生成器会报告保留成员冲突并停止生成。 /// [Test] public async Task Reports_Diagnostic_When_SceneKeyStr_Property_Name_Conflicts() { const string source = """ using System; using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute { public AutoSceneAttribute(string key) { } } } namespace Godot { public class Node { } public class Node2D : Node { } } namespace TestApp { [AutoScene("Gameplay")] public partial class GameplayRoot : Node2D { public static string {|#0:SceneKeyStr|} => "Conflict"; } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("GameplayRoot", "SceneKeyStr")); await test.RunAsync().ConfigureAwait(false); } /// /// 验证宿主类型声明同名缓存字段时,生成器会报告保留成员冲突并停止生成。 /// [Test] public async Task Reports_Diagnostic_When_Generated_Behavior_Field_Name_Conflicts() { const string source = """ using System; using GFramework.Game.Abstractions.Scene; using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute { public AutoSceneAttribute(string key) { } } } namespace Godot { public class Node { } public class Node2D : Node { } } namespace GFramework.Game.Abstractions.Scene { public interface ISceneBehavior { } } namespace TestApp { [AutoScene("Gameplay")] public partial class GameplayRoot : Node2D { private ISceneBehavior? {|#0:__autoSceneBehavior_Generated|}; } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("GameplayRoot", "__autoSceneBehavior_Generated")); await test.RunAsync().ConfigureAwait(false); } private static string CreateAutoSceneSource( string attributeDeclaration, string testAppSource, bool includeBehaviorInfrastructure = false, bool nullableEnabled = false) { string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; string infrastructure = includeBehaviorInfrastructure ? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}" : string.Empty; return $$""" {{nullableDirective}}using System; using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { {{attributeDeclaration}} } namespace Godot { {{NodeTypes}} }{{infrastructure}} namespace TestApp { {{testAppSource}} } """; } }