// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.SourceGenerators.Rule; 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 Task Generates_Bindings_For_ContextAwareAttribute_Class() { return VerifyGeneratedSourceAsync( ContextAwareAttributeClassSource, InventoryPanelGeneratedFileName, ContextAwareAttributeClassExpected); } /// /// 验证字段使用 fully-qualified 特性名时仍能生成绑定。 /// [Test] public Task Generates_Bindings_For_Fully_Qualified_Field_Attributes() { return VerifyGeneratedSourceAsync( FullyQualifiedFieldAttributesSource, InventoryPanelGeneratedFileName, FullyQualifiedFieldAttributesExpected); } /// /// 验证 GetAll 会仅为可推断的 model、models、system 与 utility 字段生成绑定。 /// [Test] public Task Generates_Inferred_Bindings_For_GetAll_Class() { return VerifyGeneratedSourceAsync( InferredGetAllClassSource, BattlePanelGeneratedFileName, InferredGetAllClassExpected); } /// /// 验证 GetAll 与显式 [GetService] 可以组合生成绑定。 /// [Test] public Task Generates_Explicit_Service_Binding_For_GetAll_Class() { return VerifyGeneratedSourceAsync( ExplicitServiceGetAllClassSource, BattlePanelGeneratedFileName, ExplicitServiceGetAllClassExpected); } /// /// 验证目标类已声明注入方法时会报告冲突诊断。 /// [Test] public Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() { var source = MarkupTestSource.Parse(GeneratedInjectionMethodAlreadyExistsMarkupSource); return VerifyDiagnosticAsync( source, CreateSingleSpanDiagnostic( source, "GF_Common_Class_002", DiagnosticSeverity.Error, "InventoryPanel", "__InjectContextBindings_Generated")); } /// /// 验证 GetAll 会忽略不可推断的常量字段且不报告诊断。 /// [Test] public Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic() { return VerifyGeneratedSourceAsync( IgnoreConstFieldGetAllSource, BattlePanelGeneratedFileName, IgnoreConstFieldGetAllExpected); } /// /// 验证只读推断字段会被跳过并报告 warning,同时其他字段保持生成。 /// [Test] public Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class() { var source = MarkupTestSource.Parse(ReadonlyInferredFieldGetAllMarkupSource); return VerifyDiagnosticAndGeneratedSourceAsync( source, BattlePanelGeneratedFileName, SkipInvalidGetAllFieldExpected, CreateSingleSpanDiagnostic( source, "GF_ContextGet_008", DiagnosticSeverity.Warning, "_model")); } /// /// 验证静态推断字段会被跳过并报告 warning,同时其他字段保持生成。 /// [Test] public Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class() { var source = MarkupTestSource.Parse(StaticInferredFieldGetAllMarkupSource); return VerifyDiagnosticAndGeneratedSourceAsync( source, BattlePanelGeneratedFileName, SkipInvalidGetAllFieldExpected, CreateSingleSpanDiagnostic( source, "GF_ContextGet_007", DiagnosticSeverity.Warning, "_model")); } /// /// 验证 nullable 的服务样字段不会被 GetAll 误判为可注入字段。 /// [Test] public Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class() { return VerifyGeneratedSourceAsync( SkipNullableServiceLikeFieldSource, GameplayHudGeneratedFileName, SkipNullableServiceLikeFieldExpected); } /// /// 验证实现 IContextAware 的类型无需 [ContextAware] 也能生成显式绑定。 /// [Test] public Task Generates_Bindings_For_IContextAware_Class() { return VerifyGeneratedSourceAsync( IContextAwareClassSource, StrategyHostGeneratedFileName, IContextAwareClassExpected); } /// /// 验证缺少上下文感知契约的类型会报告错误诊断。 /// [Test] public Task Reports_Diagnostic_When_Class_Is_Not_ContextAware() { var source = MarkupTestSource.Parse(NonContextAwareClassMarkupSource); return VerifyDiagnosticAsync( source, CreateSingleSpanDiagnostic( source, "GF_ContextGet_005", DiagnosticSeverity.Error, "InventoryPanel")); } /// /// 验证 [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" } }; } /// /// 将手工声明的期望生成源码归一化到当前平台换行符,避免不同宿主上的伪差异。 /// /// 原始期望源码。 /// 已按当前平台换行符归一化的源码文本。 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); } }