GFramework/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
gewuyou 2187f179c3 fix(pr-review): 修复设置快照比较器契约
- 修复 UnifiedSettingsFile 与 UnifiedSettingsDataRepository 的 comparer 契约,在无法恢复原比较器时显式回退到 StringComparer.Ordinal
- 统一 AutoRegisterExportedCollectionsGeneratorTests 中剩余的 RunAsync 异步等待写法,并补齐 ConfigureAwait(false)
- 更新 analyzer-warning-reduction 跟踪文档,记录 PR follow-up 的验证结果与恢复点
2026-04-24 16:39:25 +08:00

724 lines
31 KiB
C#

using GFramework.Godot.SourceGenerators.Registration;
using GFramework.Godot.SourceGenerators.Tests.Core;
namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests
{
private const string StandardAttributeDeclarations = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
""";
private const string MultiDeclarationAttributeDeclarations = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
""";
[Test]
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Collection_Element_Type_Cannot_Be_Inferred()
{
const string source = """
using System;
using System.Collections;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public IEnumerable {|#0:Values|} { get; } = new ArrayList();
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_005", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Values"));
await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{
string source = CreateSource(
"""
public sealed class ArrayRegistry
{
public void Register(int[] value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ArrayRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new();
}
""",
nullableEnabled: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{
string source = CreateSource(
"""
public interface IKeyValue<TKey, TValue>
{
}
public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
{
}
public sealed class IntConfig : IKeyValue<string, int>
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
""",
nullableEnabled: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Registry(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Register_Method_Is_Only_Explicitly_Implemented_Interface_Member()
{
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public interface IRegistry
{
void Register(int value);
}
public sealed class ExplicitRegistry : IRegistry
{
void IRegistry.Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ExplicitRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), "Register")]
public List<int> {|#0:Values|} { get; } = new();
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public class BaseRegistry
{
public void Register(int value) { }
}
public sealed class DerivedRegistry : BaseRegistry
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly DerivedRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
public abstract class BootstrapperBase
{
protected readonly IntRegistry? _registry = new();
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper : BootstrapperBase
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#0:StaticValues|} = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } }
}
""");
await VerifyDiagnosticsAsync(
source,
skipGeneratedSourcesCheck: true,
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("StaticValues"),
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("StaticPropertyValues"),
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(2)
.WithArguments("WriteOnlyValues")).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Registry_Member_Is_Not_Instance_Readable()
{
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private static readonly IntRegistry {|#0:_registry|} = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> Values { get; } = new();
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Register_Method_Is_Not_Accessible_From_Owner_Type()
{
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
private void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), "Register")]
public List<int> {|#0:Values|} { get; } = new();
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_RegisterExportedCollection_Attribute_Arguments_Are_Invalid()
{
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
[{|#0:RegisterExportedCollection(nameof(_registry))|}]
public List<int> Values { get; } = new();
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_008", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Values"));
await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry? _registry = new();
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true,
allowMultipleDeclarations: true);
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}
""";
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateSource(
string applicationSource,
bool nullableEnabled = false,
bool allowMultipleDeclarations = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string attributeDeclarations = allowMultipleDeclarations
? MultiDeclarationAttributeDeclarations
: StandardAttributeDeclarations;
return $$"""
{{nullableDirective}}using System;
using System.Collections;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclarations}}
}
namespace TestApp
{
{{applicationSource}}
}
""";
}
private static async Task VerifyDiagnosticsAsync(
string source,
bool skipGeneratedSourcesCheck = false,
params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
if (skipGeneratedSourcesCheck)
{
test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
}
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
await test.RunAsync().ConfigureAwait(false);
}
}