GFramework/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs
GeWuYou 6898866b97 feat(generator): 改进自动注册模块生成器的跨文件顺序稳定性
当partial类分布在多个文件中时,确保生成器使用稳定的跨文件顺序来生成注册代码。
添加了对语法树排序的支持,使相同声明上的注册特性能够按照源码中的书写顺序生成安装代码。
同时修复了测试快照换行符问题,确保跨平台兼容性。
2026-04-13 15:47:06 +08:00

374 lines
19 KiB
C#

using GFramework.SourceGenerators.Architectures;
using GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Architectures;
[TestFixture]
public class AutoRegisterModuleGeneratorTests
{
/// <summary>
/// 验证同一声明上的注册特性会按照源码中的书写顺序生成安装代码。
/// </summary>
[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>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
T RegisterUtility<T>(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 = """
// <auto-generated />
#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<AutoRegisterModuleGenerator>.RunAsync(
source,
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
}
/// <summary>
/// 验证 partial 声明分布在多个文件时,生成器仍然会使用稳定的跨文件顺序生成注册代码。
/// </summary>
[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>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
T RegisterUtility<T>(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 = """
// <auto-generated />
#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<AutoRegisterModuleGenerator, DefaultVerifier>
{
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();
}
/// <summary>
/// 验证生成器会保留可空引用、notnull 与 unmanaged 约束。
/// </summary>
[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>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
T RegisterUtility<T>(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<TNullableRef, TNotNull, TUnmanaged>
where TNullableRef : class?
where TNotNull : notnull
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
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<AutoRegisterModuleGenerator>.RunAsync(
source,
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
}
/// <summary>
/// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。
/// </summary>
/// <param name="content">原始快照内容。</param>
/// <returns>使用当前平台换行符的快照内容。</returns>
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);
}
}