From 8cd492506d8bd1c004a10d491e3e854c3a37392d Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:17:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor(source-generators-tests):=20=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=20ContextGetGeneratorTests=20=E7=9A=84=20MA0051?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 ContextGetGeneratorTests 的长测试方法为场景常量与验证 helper,保持生成输入和断言语义不变 - 补充测试类与 helper 的 XML 文档,并统一生成源码与诊断断言的复用路径 - 验证 GFramework.SourceGenerators.Tests Release build 与 ContextGetGeneratorTests 过滤测试通过 --- .../Rule/ContextGetGeneratorTests.cs | 2527 +++++++++-------- 1 file changed, 1304 insertions(+), 1223 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs index bae2d00c..1d015e12 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -3,743 +3,1351 @@ using GFramework.SourceGenerators.Tests.Core; namespace GFramework.SourceGenerators.Tests.Rule; +/// +/// 验证 在显式特性、GetAll 推断与诊断场景下的生成契约。 +/// [TestFixture] public class ContextGetGeneratorTests { + private const string InventoryPanelGeneratedFileName = "TestApp_InventoryPanel.ContextGet.g.cs"; + private const string BattlePanelGeneratedFileName = "TestApp_BattlePanel.ContextGet.g.cs"; + private const string GameplayHudGeneratedFileName = "TestApp_GameplayHud.ContextGet.g.cs"; + private const string StrategyHostGeneratedFileName = "TestApp_StrategyHost.ContextGet.g.cs"; + + private const string ContextAwareAttributeClassSource = """ + using System; + using System.Collections.Generic; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServicesAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static IReadOnlyList GetServices(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IInventoryStrategy { } + + [ContextAware] + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel _model = null!; + + [GetServices] + private IReadOnlyList _strategies = null!; + } + } + """; + + private const string ContextAwareAttributeClassExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + _strategies = this.GetServices(); + } + } + + """; + + private const string FullyQualifiedFieldAttributesSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + [ContextAware] + public partial class InventoryPanel + { + [global::GFramework.Core.SourceGenerators.Abstractions.Rule.GetModel] + private IInventoryModel _model = null!; + } + } + """; + + private const string FullyQualifiedFieldAttributesExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + } + } + + """; + + private const string InferredGetAllClassSource = """ + using System; + using System.Collections.Generic; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Abstractions.Architectures + { + public interface IArchitectureContext { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static IReadOnlyList GetModels(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + public static T GetUtility(this object contextAware) => default!; + } + } + + namespace Godot + { + public class Node { } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + public interface IUiUtility : GFramework.Core.Abstractions.Utility.IUtility { } + public interface IStrategy { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private IInventoryModel _model = null!; + private IReadOnlyList _models = null!; + private ICombatSystem _system = null!; + private IUiUtility _utility = null!; + private IStrategy _service = null!; + private IReadOnlyList _services = null!; + private Godot.Node _node = null!; + } + } + """; + + private const string InferredGetAllClassExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + _models = this.GetModels(); + _system = this.GetSystem(); + _utility = this.GetUtility(); + } + } + + """; + + private const string ExplicitServiceGetAllClassSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServiceAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetService(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IStrategy { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private IInventoryModel _model = null!; + + [GetService] + private IStrategy _service = null!; + } + } + """; + + private const string ExplicitServiceGetAllClassExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + _service = this.GetService(); + } + } + + """; + + private const string GeneratedInjectionMethodAlreadyExistsMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + [ContextAware] + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel _model = null!; + + private void {|#0:__InjectContextBindings_Generated|}() + { + } + } + } + """; + + private const string IgnoreConstFieldGetAllSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private const double LogicStep = 0.2; + private IInventoryModel _model = null!; + } + } + """; + + private const string IgnoreConstFieldGetAllExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + } + } + + """; + + private const string ReadonlyInferredFieldGetAllMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private readonly IInventoryModel {|#0:_model|} = null!; + private ICombatSystem _system = null!; + } + } + """; + + private const string SkipInvalidGetAllFieldExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _system = this.GetSystem(); + } + } + + """; + + private const string StaticInferredFieldGetAllMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private static IInventoryModel {|#0:_model|} = null!; + private ICombatSystem _system = null!; + } + } + """; + + private const string SkipNullableServiceLikeFieldSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + } + } + + namespace Godot + { + public class Control { } + } + + namespace TestApp + { + public interface IGridModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IRunLoopSystem : GFramework.Core.Abstractions.Systems.ISystem { } + public interface IUiPageBehavior { } + + [ContextAware] + [GetAll] + public partial class GameplayHud : Godot.Control + { + private IGridModel _gridModel = null!; + private IUiPageBehavior? _page; + private IRunLoopSystem _runLoopSystem = null!; + } + } + """; + + private const string SkipNullableServiceLikeFieldExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class GameplayHud + { + private void __InjectContextBindings_Generated() + { + _gridModel = this.GetModel(); + _runLoopSystem = this.GetSystem(); + } + } + + """; + + private const string IContextAwareClassSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServiceAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetService(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IStrategy { } + + public partial class StrategyHost : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetService] + private IStrategy _strategy = null!; + } + } + """; + + private const string IContextAwareClassExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class StrategyHost + { + private void __InjectContextBindings_Generated() + { + _strategy = this.GetService(); + } + } + + """; + + private const string NonContextAwareClassMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel {|#0:_model|} = null!; + } + } + """; + + private const string GetModelsFieldNotIReadOnlyListMarkupSource = """ + using System; + using System.Collections.Generic; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelsAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static IReadOnlyList GetModels(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModels] + private List {|#0:_models|} = new(); + } + } + """; + + private const string ReadonlyExplicitGetModelFieldMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModel] + private readonly IInventoryModel {|#0:_model|} = null!; + } + } + """; + + private const string StaticExplicitGetModelFieldMarkupSource = """ + using System; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModel] + private static IInventoryModel {|#0:_model|} = null!; + } + } + """; + + private const string GetModelsAssignableSource = """ + using System; + using System.Collections.Generic; + using GFramework.Core.SourceGenerators.Abstractions.Rule; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelsAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static IReadOnlyList GetModels(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModels] + private IEnumerable _models = null!; + } + } + """; + + private const string GetModelsAssignableExpected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _models = this.GetModels(); + } + } + + """; + + /// + /// 验证 [ContextAware] 类上的显式字段特性会生成模型与服务绑定。 + /// [Test] - public async Task Generates_Bindings_For_ContextAwareAttribute_Class() + public Task Generates_Bindings_For_ContextAwareAttribute_Class() { - var source = """ - using System; - using System.Collections.Generic; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class ContextAwareAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetServicesAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static IReadOnlyList GetServices(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface IInventoryStrategy { } - - [ContextAware] - public partial class InventoryPanel - { - [GetModel] - private IInventoryModel _model = null!; - - [GetServices] - private IReadOnlyList _strategies = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class InventoryPanel - { - private void __InjectContextBindings_Generated() - { - _model = this.GetModel(); - _strategies = this.GetServices(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); - Assert.Pass(); + return VerifyGeneratedSourceAsync( + ContextAwareAttributeClassSource, + InventoryPanelGeneratedFileName, + ContextAwareAttributeClassExpected); } + /// + /// 验证字段使用 fully-qualified 特性名时仍能生成绑定。 + /// [Test] - public async Task Generates_Bindings_For_Fully_Qualified_Field_Attributes() + public Task Generates_Bindings_For_Fully_Qualified_Field_Attributes() { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class ContextAwareAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - [ContextAware] - public partial class InventoryPanel - { - [global::GFramework.Core.SourceGenerators.Abstractions.Rule.GetModel] - private IInventoryModel _model = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class InventoryPanel - { - private void __InjectContextBindings_Generated() - { - _model = this.GetModel(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); - Assert.Pass(); + return VerifyGeneratedSourceAsync( + FullyQualifiedFieldAttributesSource, + InventoryPanelGeneratedFileName, + FullyQualifiedFieldAttributesExpected); } + /// + /// 验证 GetAll 会仅为可推断的 model、models、system 与 utility 字段生成绑定。 + /// [Test] - public async Task Generates_Inferred_Bindings_For_GetAll_Class() + public Task Generates_Inferred_Bindings_For_GetAll_Class() { - var source = """ - using System; - using System.Collections.Generic; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - namespace GFramework.Core.Abstractions.Architectures - { - public interface IArchitectureContext { } - } - - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static IReadOnlyList GetModels(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - public static T GetUtility(this object contextAware) => default!; - } - } - - namespace Godot - { - public class Node { } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } - public interface IUiUtility : GFramework.Core.Abstractions.Utility.IUtility { } - public interface IStrategy { } - - [GetAll] - public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase - { - private IInventoryModel _model = null!; - private IReadOnlyList _models = null!; - private ICombatSystem _system = null!; - private IUiUtility _utility = null!; - private IStrategy _service = null!; - private IReadOnlyList _services = null!; - private Godot.Node _node = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class BattlePanel - { - private void __InjectContextBindings_Generated() - { - _model = this.GetModel(); - _models = this.GetModels(); - _system = this.GetSystem(); - _utility = this.GetUtility(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_BattlePanel.ContextGet.g.cs", expected)); - Assert.Pass(); + return VerifyGeneratedSourceAsync( + InferredGetAllClassSource, + BattlePanelGeneratedFileName, + InferredGetAllClassExpected); } + /// + /// 验证 GetAll 与显式 [GetService] 可以组合生成绑定。 + /// [Test] - public async Task Generates_Explicit_Service_Binding_For_GetAll_Class() + public Task Generates_Explicit_Service_Binding_For_GetAll_Class() { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetServiceAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static T GetService(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface IStrategy { } - - [GetAll] - public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase - { - private IInventoryModel _model = null!; - - [GetService] - private IStrategy _service = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class BattlePanel - { - private void __InjectContextBindings_Generated() - { - _model = this.GetModel(); - _service = this.GetService(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_BattlePanel.ContextGet.g.cs", expected)); - Assert.Pass(); + return VerifyGeneratedSourceAsync( + ExplicitServiceGetAllClassSource, + BattlePanelGeneratedFileName, + ExplicitServiceGetAllClassExpected); } + /// + /// 验证目标类已声明注入方法时会报告冲突诊断。 + /// [Test] - public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() + public Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; + var source = MarkupTestSource.Parse(GeneratedInjectionMethodAlreadyExistsMarkupSource); - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class ContextAwareAttribute : Attribute { } + return VerifyDiagnosticAsync( + source, + CreateSingleSpanDiagnostic( + source, + "GF_Common_Class_002", + DiagnosticSeverity.Error, + "InventoryPanel", + "__InjectContextBindings_Generated")); + } - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - } + /// + /// 验证 GetAll 会忽略不可推断的常量字段且不报告诊断。 + /// + [Test] + public Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic() + { + return VerifyGeneratedSourceAsync( + IgnoreConstFieldGetAllSource, + BattlePanelGeneratedFileName, + IgnoreConstFieldGetAllExpected); + } - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } + /// + /// 验证只读推断字段会被跳过并报告 warning,同时其他字段保持生成。 + /// + [Test] + public Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class() + { + var source = MarkupTestSource.Parse(ReadonlyInferredFieldGetAllMarkupSource); - namespace GFramework.Core.Abstractions.Model - { - public interface IModel { } - } + return VerifyDiagnosticAndGeneratedSourceAsync( + source, + BattlePanelGeneratedFileName, + SkipInvalidGetAllFieldExpected, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_008", + DiagnosticSeverity.Warning, + "_model")); + } - namespace GFramework.Core.Abstractions.Systems - { - public interface ISystem { } - } + /// + /// 验证静态推断字段会被跳过并报告 warning,同时其他字段保持生成。 + /// + [Test] + public Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class() + { + var source = MarkupTestSource.Parse(StaticInferredFieldGetAllMarkupSource); - namespace GFramework.Core.Abstractions.Utility - { - public interface IUtility { } - } + return VerifyDiagnosticAndGeneratedSourceAsync( + source, + BattlePanelGeneratedFileName, + SkipInvalidGetAllFieldExpected, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_007", + DiagnosticSeverity.Warning, + "_model")); + } - namespace GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } + /// + /// 验证 nullable 的服务样字段不会被 GetAll 误判为可注入字段。 + /// + [Test] + public Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class() + { + return VerifyGeneratedSourceAsync( + SkipNullableServiceLikeFieldSource, + GameplayHudGeneratedFileName, + SkipNullableServiceLikeFieldExpected); + } - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + /// + /// 验证实现 IContextAware 的类型无需 [ContextAware] 也能生成显式绑定。 + /// + [Test] + public Task Generates_Bindings_For_IContextAware_Class() + { + return VerifyGeneratedSourceAsync( + IContextAwareClassSource, + StrategyHostGeneratedFileName, + IContextAwareClassExpected); + } - [ContextAware] - public partial class InventoryPanel - { - [GetModel] - private IInventoryModel _model = null!; + /// + /// 验证缺少上下文感知契约的类型会报告错误诊断。 + /// + [Test] + public Task Reports_Diagnostic_When_Class_Is_Not_ContextAware() + { + var source = MarkupTestSource.Parse(NonContextAwareClassMarkupSource); - private void {|#0:__InjectContextBindings_Generated|}() - { - } - } - } - """; + return VerifyDiagnosticAsync( + source, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_005", + DiagnosticSeverity.Error, + "InventoryPanel")); + } - var test = new CSharpSourceGeneratorTest + /// + /// 验证 [GetModels] 字段若不是可赋值自 IReadOnlyList<T> 会报告错误。 + /// + [Test] + public Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList() + { + var source = MarkupTestSource.Parse(GetModelsFieldNotIReadOnlyListMarkupSource); + + return VerifyDiagnosticAsync( + source, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_004", + DiagnosticSeverity.Error, + "_models", + "System.Collections.Generic.List", + "GetModels")); + } + + /// + /// 验证显式 [GetModel] 作用于只读字段时会报告错误。 + /// + [Test] + public Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field() + { + var source = MarkupTestSource.Parse(ReadonlyExplicitGetModelFieldMarkupSource); + + return VerifyDiagnosticAsync( + source, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_003", + DiagnosticSeverity.Error, + "_model")); + } + + /// + /// 验证显式 [GetModel] 作用于静态字段时会报告错误。 + /// + [Test] + public Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field() + { + var source = MarkupTestSource.Parse(StaticExplicitGetModelFieldMarkupSource); + + return VerifyDiagnosticAsync( + source, + CreateSingleSpanDiagnostic( + source, + "GF_ContextGet_002", + DiagnosticSeverity.Error, + "_model")); + } + + /// + /// 验证 [GetModels] 字段可以赋值到更宽的可枚举接口上。 + /// + [Test] + public Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList() + { + return VerifyGeneratedSourceAsync( + GetModelsAssignableSource, + InventoryPanelGeneratedFileName, + GetModelsAssignableExpected); + } + + /// + /// 运行单个生成源码断言,保持文件名与文本快照语义不变。 + /// + /// 测试输入源码。 + /// 期望生成文件名。 + /// 期望生成源码。 + /// 表示测试执行的异步任务。 + private static Task VerifyGeneratedSourceAsync( + string source, + string generatedFileName, + string expectedGeneratedSource) + { + return GeneratorTest.RunAsync( + source, + (generatedFileName, expectedGeneratedSource)); + } + + /// + /// 运行仅关注诊断输出的生成器测试。 + /// + /// 包含 markup span 的测试源码。 + /// 期望诊断。 + /// 表示测试执行的异步任务。 + private static Task VerifyDiagnosticAsync( + MarkupTestSource source, + DiagnosticResult expectedDiagnostic) + { + var test = CreateGeneratorTest(source.Source); + test.ExpectedDiagnostics.Add(expectedDiagnostic); + return test.RunAsync(); + } + + /// + /// 运行同时断言诊断与部分生成输出的生成器测试。 + /// + /// 包含 markup span 的测试源码。 + /// 期望生成文件名。 + /// 期望生成源码。 + /// 期望诊断。 + /// 表示测试执行的异步任务。 + private static Task VerifyDiagnosticAndGeneratedSourceAsync( + MarkupTestSource source, + string generatedFileName, + string expectedGeneratedSource, + DiagnosticResult expectedDiagnostic) + { + var test = CreateGeneratorTest(source.Source); + test.TestState.GeneratedSources.Add( + (typeof(ContextGetGenerator), generatedFileName, NormalizeLineEndings(expectedGeneratedSource))); + test.ExpectedDiagnostics.Add(expectedDiagnostic); + return test.RunAsync(); + } + + /// + /// 为单一 markup span 场景构造诊断结果,统一保持定位键与参数组装方式。 + /// + /// 包含 markup span 的测试源码。 + /// 诊断 ID。 + /// 诊断严重级别。 + /// 诊断参数。 + /// 绑定到 markup key 0 的期望诊断。 + private static DiagnosticResult CreateSingleSpanDiagnostic( + MarkupTestSource source, + string diagnosticId, + DiagnosticSeverity severity, + params string[] arguments) + { + return source.WithSpan( + new DiagnosticResult(diagnosticId, severity), + "0") + .WithArguments(arguments); + } + + /// + /// 创建禁用 trace 诊断的通用源生成器测试实例,避免各场景重复样板配置。 + /// + /// 测试输入源码。 + /// 配置完成的生成器测试对象。 + private static CSharpSourceGeneratorTest CreateGeneratorTest(string source) + { + return 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("InventoryPanel", "__InjectContextBindings_Generated")); - - await test.RunAsync(); - } - - [Test] - public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic() - { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - [GetAll] - public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase - { - private const double LogicStep = 0.2; - private IInventoryModel _model = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class BattlePanel - { - private void __InjectContextBindings_Generated() - { - _model = this.GetModel(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_BattlePanel.ContextGet.g.cs", expected)); - Assert.Pass(); - } - - [Test] - public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class() - { - var source = MarkupTestSource.Parse(""" - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } - - [GetAll] - public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase - { - private readonly IInventoryModel {|#0:_model|} = null!; - private ICombatSystem _system = null!; - } - } - """); - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class BattlePanel - { - private void __InjectContextBindings_Generated() - { - _system = this.GetSystem(); - } - } - - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source }, - GeneratedSources = - { - (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected)) - } - }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning), - "0") - .WithArguments("_model")); - - await test.RunAsync(); - Assert.Pass(); - } - - [Test] - public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class() - { - var source = MarkupTestSource.Parse(""" - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } - - [GetAll] - public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase - { - private static IInventoryModel {|#0:_model|} = null!; - private ICombatSystem _system = null!; - } - } - """); - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class BattlePanel - { - private void __InjectContextBindings_Generated() - { - _system = this.GetSystem(); - } - } - - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source }, - GeneratedSources = - { - (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected)) - } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning), - "0") - .WithArguments("_model")); - - await test.RunAsync(); - Assert.Pass(); } + /// + /// 将手工声明的期望生成源码归一化到当前平台换行符,避免不同宿主上的伪差异。 + /// + /// 原始期望源码。 + /// 已按当前平台换行符归一化的源码文本。 private static string NormalizeLineEndings(string content) { return content @@ -747,531 +1355,4 @@ public class ContextGetGeneratorTests .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } - - [Test] - public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class() - { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class ContextAwareAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - } - } - - namespace Godot - { - public class Control { } - } - - namespace TestApp - { - public interface IGridModel : GFramework.Core.Abstractions.Model.IModel { } - public interface IRunLoopSystem : GFramework.Core.Abstractions.Systems.ISystem { } - public interface IUiPageBehavior { } - - [ContextAware] - [GetAll] - public partial class GameplayHud : Godot.Control - { - private IGridModel _gridModel = null!; - private IUiPageBehavior? _page; - private IRunLoopSystem _runLoopSystem = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class GameplayHud - { - private void __InjectContextBindings_Generated() - { - _gridModel = this.GetModel(); - _runLoopSystem = this.GetSystem(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_GameplayHud.ContextGet.g.cs", expected)); - Assert.Pass(); - } - - [Test] - public async Task Generates_Bindings_For_IContextAware_Class() - { - var source = """ - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetServiceAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetService(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IStrategy { } - - public partial class StrategyHost : GFramework.Core.Abstractions.Rule.IContextAware - { - [GetService] - private IStrategy _strategy = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class StrategyHost - { - private void __InjectContextBindings_Generated() - { - _strategy = this.GetService(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_StrategyHost.ContextGet.g.cs", expected)); - Assert.Pass(); - } - - [Test] - public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware() - { - var source = MarkupTestSource.Parse(""" - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - public partial class InventoryPanel - { - [GetModel] - private IInventoryModel {|#0:_model|} = null!; - } - } - """); - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error), - "0") - .WithArguments("InventoryPanel")); - - await test.RunAsync(); - Assert.Pass(); - } - - [Test] - public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList() - { - var source = MarkupTestSource.Parse(""" - using System; - using System.Collections.Generic; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelsAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static IReadOnlyList GetModels(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware - { - [GetModels] - private List {|#0:_models|} = new(); - } - } - """); - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error), - "0") - .WithArguments("_models", "System.Collections.Generic.List", "GetModels")); - - await test.RunAsync(); - Assert.Pass(); - } - - [Test] - public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field() - { - var source = MarkupTestSource.Parse(""" - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware - { - [GetModel] - private readonly IInventoryModel {|#0:_model|} = null!; - } - } - """); - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error), - "0") - .WithArguments("_model")); - - await test.RunAsync(); - Assert.Pass(); - } - - [Test] - public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field() - { - var source = MarkupTestSource.Parse(""" - using System; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static T GetModel(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware - { - [GetModel] - private static IInventoryModel {|#0:_model|} = null!; - } - } - """); - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source.Source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - test.ExpectedDiagnostics.Add(source.WithSpan( - new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error), - "0") - .WithArguments("_model")); - - await test.RunAsync(); - Assert.Pass(); - } - - [Test] - public async Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList() - { - var source = """ - using System; - using System.Collections.Generic; - using GFramework.Core.SourceGenerators.Abstractions.Rule; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Field, Inherited = false)] - public sealed class GetModelsAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware { } - } - - 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 GFramework.Core.Extensions - { - public static class ContextAwareServiceExtensions - { - public static IReadOnlyList GetModels(this object contextAware) => default!; - } - } - - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - - public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware - { - [GetModels] - private IEnumerable _models = null!; - } - } - """; - - const string expected = """ - // - #nullable enable - - using GFramework.Core.Extensions; - - namespace TestApp; - - partial class InventoryPanel - { - private void __InjectContextBindings_Generated() - { - _models = this.GetModels(); - } - } - - """; - - await GeneratorTest.RunAsync( - source, - ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); - Assert.Pass(); - } }