using GFramework.Core.SourceGenerators.Architectures; using GFramework.SourceGenerators.Tests.Core; namespace GFramework.SourceGenerators.Tests.Architectures; [TestFixture] public class AutoRegisterModuleGeneratorTests { /// /// 验证同一声明上的注册特性会按照源码中的书写顺序生成安装代码。 /// [Test] public async Task Generates_Module_Install_Method_In_Attribute_Order() { const string source = """ using System; using GFramework.SourceGenerators.Abstractions.Architectures; namespace GFramework.SourceGenerators.Abstractions.Architectures { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterModuleAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterModelAttribute : Attribute { public RegisterModelAttribute(Type modelType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterSystemAttribute : Attribute { public RegisterSystemAttribute(Type systemType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterUtilityAttribute : Attribute { public RegisterUtilityAttribute(Type utilityType) { } } } namespace GFramework.Core.Abstractions.Architectures { public interface IArchitecture { T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel; T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem; T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility; } } namespace GFramework.Core.Abstractions.Model { public interface IModel { } } namespace GFramework.Core.Abstractions.Systems { public interface ISystem { } } namespace GFramework.Core.Abstractions.Utility { public interface IUtility { } } namespace TestApp { using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.SourceGenerators.Abstractions.Architectures; public sealed class PlayerModel : IModel { } public sealed class CombatSystem : ISystem { } public sealed class AudioUtility : IUtility { } [AutoRegisterModule] [RegisterSystem(typeof(CombatSystem))] [RegisterModel(typeof(PlayerModel))] [RegisterUtility(typeof(AudioUtility))] public partial class GameplayModule { } } """; const string expected = """ // #nullable enable namespace TestApp; partial class GameplayModule { public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture) { architecture.RegisterSystem(new global::TestApp.CombatSystem()); architecture.RegisterModel(new global::TestApp.PlayerModel()); architecture.RegisterUtility(new global::TestApp.AudioUtility()); } } """; await GeneratorTest.RunAsync( source, ("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected)); } /// /// 验证 partial 声明分布在多个文件时,生成器仍然会使用稳定的跨文件顺序生成注册代码。 /// [Test] public async Task Generates_Module_Install_Method_In_Deterministic_Order_Across_Partial_Declarations() { const string commonSource = """ using System; namespace GFramework.SourceGenerators.Abstractions.Architectures { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterModuleAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterModelAttribute : Attribute { public RegisterModelAttribute(Type modelType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterSystemAttribute : Attribute { public RegisterSystemAttribute(Type systemType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterUtilityAttribute : Attribute { public RegisterUtilityAttribute(Type utilityType) { } } } namespace GFramework.Core.Abstractions.Architectures { public interface IArchitecture { T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel; T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem; T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility; } } namespace GFramework.Core.Abstractions.Model { public interface IModel { } } namespace GFramework.Core.Abstractions.Systems { public interface ISystem { } } namespace GFramework.Core.Abstractions.Utility { public interface IUtility { } } namespace TestApp { using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; public sealed class PlayerModel : IModel { } public sealed class CombatSystem : ISystem { } public sealed class AudioUtility : IUtility { } } """; const string partASource = """ namespace TestApp { using GFramework.SourceGenerators.Abstractions.Architectures; // Padding ensures this attribute lives later in the file than the attributes in PartB. // The generator should still place it first because PartA sorts before PartB. // padding 01 // padding 02 // padding 03 // padding 04 // padding 05 // padding 06 // padding 07 // padding 08 // padding 09 // padding 10 [AutoRegisterModule] [RegisterUtility(typeof(AudioUtility))] public partial class GameplayModule { } } """; const string partBSource = """ namespace TestApp { using GFramework.SourceGenerators.Abstractions.Architectures; [RegisterSystem(typeof(CombatSystem))] [RegisterModel(typeof(PlayerModel))] public partial class GameplayModule { } } """; const string expected = """ // #nullable enable namespace TestApp; partial class GameplayModule { public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture) { architecture.RegisterUtility(new global::TestApp.AudioUtility()); architecture.RegisterSystem(new global::TestApp.CombatSystem()); architecture.RegisterModel(new global::TestApp.PlayerModel()); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { ("Common.cs", commonSource), ("GameplayModule.PartA.cs", partASource), ("GameplayModule.PartB.cs", partBSource) }, GeneratedSources = { (typeof(AutoRegisterModuleGenerator), "TestApp_GameplayModule.AutoRegisterModule.g.cs", NormalizeLineEndings(expected)) } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; await test.RunAsync(); } /// /// 验证生成器会保留可空引用、notnull 与 unmanaged 约束。 /// [Test] public async Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged() { const string source = """ #nullable enable using System; using GFramework.SourceGenerators.Abstractions.Architectures; namespace GFramework.SourceGenerators.Abstractions.Architectures { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterModuleAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterModelAttribute : Attribute { public RegisterModelAttribute(Type modelType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterSystemAttribute : Attribute { public RegisterSystemAttribute(Type systemType) { } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class RegisterUtilityAttribute : Attribute { public RegisterUtilityAttribute(Type utilityType) { } } } namespace GFramework.Core.Abstractions.Architectures { public interface IArchitecture { T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel; T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem; T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility; } } namespace GFramework.Core.Abstractions.Model { public interface IModel { } } namespace GFramework.Core.Abstractions.Systems { public interface ISystem { } } namespace GFramework.Core.Abstractions.Utility { public interface IUtility { } } namespace TestApp { using GFramework.Core.Abstractions.Model; using GFramework.SourceGenerators.Abstractions.Architectures; public sealed class PlayerModel : IModel { } [AutoRegisterModule] [RegisterModel(typeof(PlayerModel))] public partial class GameplayModule where TNullableRef : class? where TNotNull : notnull where TUnmanaged : unmanaged { } } """; const string expected = """ // #nullable enable namespace TestApp; partial class GameplayModule where TNullableRef : class? where TNotNull : notnull where TUnmanaged : unmanaged { public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture) { architecture.RegisterModel(new global::TestApp.PlayerModel()); } } """; await GeneratorTest.RunAsync( source, ("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected)); } /// /// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。 /// /// 原始快照内容。 /// 使用当前平台换行符的快照内容。 private static string NormalizeLineEndings(string content) { return content .Replace("\r\n", "\n", StringComparison.Ordinal) .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } }