using Microsoft.CodeAnalysis; using AtomUI.Build.Tasks.LocalizationBuild; using Shouldly; using Xunit; using static AtomUI.Generator.Tests.Localization.LocalizationGeneratorTestHost; namespace AtomUI.Generator.Tests.Localization; public class ApplicationLanguageBootstrapGeneratorTests { [Fact] public void Generates_An_Explicit_Bootstrap_For_A_Partial_Application() { var execution = RunApplication( "public partial class App : Avalonia.Application { }", SourceFile()); execution.Result.Diagnostics.ShouldBeEmpty(); var source = GetBootstrapSource(execution.Result); source.ShouldContain("public partial class App"); source.ShouldContain( "void global::AtomUI.Localization.IGeneratedApplicationLanguageBootstrap.RegisterApplicationLanguages("); CountOccurrences( source, "global::AtomUI.Generated.TestApp.GeneratedLanguageModuleRegistration.Register(builder);") .ShouldBe(1); source.ShouldNotContain("Assembly.GetTypes"); source.ShouldNotContain("GetCustomAttributes"); source.ShouldNotContain("Activator.CreateInstance"); AssertCompiles(execution); } [Fact] public void Ignores_An_Abstract_Application_Base_When_Selecting_The_Concrete_Host() { var execution = RunApplication( """ public abstract partial class ApplicationBase : Avalonia.Application { } public partial class App : ApplicationBase { } """, SourceFile()); execution.Result.Diagnostics.ShouldBeEmpty(); GetBootstrapSource(execution.Result).ShouldContain("public partial class App"); AssertCompiles(execution); } [Fact] public void Compiles_Static_Language_Pack_Bundles_Directly_Into_The_Application_Bootstrap() { var reference = CreateExternalCatalogReference(); var execution = RunWithOutputCompilation( RuntimeSource + "\nnamespace TestApp { public partial class App : Avalonia.Application { } }", [reference], ReferencedSourceFile(), StaticPackFile()); execution.Result.Diagnostics.ShouldBeEmpty(); execution.Result.GeneratedSources.ShouldHaveSingleItem(); var source = GetBootstrapSource(execution.Result); source.ShouldNotContain("GeneratedLanguageModuleRegistration.Register(builder)"); source.ShouldContain("TranslationSourceKind.StaticLanguagePack"); source.ShouldContain("External.Package:External.Localization.ExternalLangResourceKind"); AssertCompiles(execution); } [Fact] public void Uses_A_Referenced_Module_Source_To_Validate_A_Static_Language_Pack() { var reference = CreateExternalCatalogReference(); var execution = RunWithOutputCompilation( RuntimeSource + "\nnamespace TestApp { public partial class App : Avalonia.Application { } }", [reference], ReferencedSourceFile(), StaticPackFile()); execution.Result.Diagnostics.ShouldBeEmpty(); var source = GetBootstrapSource(execution.Result); source.ShouldContain("TranslationSourceKind.StaticLanguagePack"); source.ShouldNotContain("TranslationSourceKind.ModuleBuiltIn"); AssertCompiles(execution); } [Fact] public void Registers_Application_Overrides_Outside_The_Module_Registration() { var execution = RunApplication( "public partial class App : Avalonia.Application { }", SourceFile(), OverrideFile()); execution.Result.Diagnostics.ShouldBeEmpty(); var bootstrap = GetBootstrapSource(execution.Result); bootstrap.ShouldContain("TranslationSourceKind.ApplicationOverride"); bootstrap.ShouldContain("null,"); GetGeneratedSource(execution.Result, "GeneratedLanguageModuleRegistration.g.cs") .ShouldNotContain("TranslationSourceKind.ApplicationOverride"); AssertCompiles(execution); } [Fact] public void Does_Not_Generate_A_Bootstrap_Without_Localization_Input() { var execution = RunWithOutputCompilation( RuntimeSource + "\nnamespace TestApp { public class App : Avalonia.Application { } }"); execution.Result.Diagnostics.ShouldBeEmpty(); execution.Result.GeneratedSources.ShouldBeEmpty(); } [Fact] public void Generates_Only_Module_Registration_For_A_Class_Library() { var execution = RunWithOutputCompilation( RuntimeSource + CatalogSource, SourceFile()); execution.Result.Diagnostics.ShouldBeEmpty(); execution.Result.GeneratedSources.Select(static source => source.HintName) .ShouldContain("GeneratedLanguageModuleRegistration.g.cs"); execution.Result.GeneratedSources.Select(static source => source.HintName) .ShouldNotContain("GeneratedApplicationLanguageBootstrap.g.cs"); } [Fact] public void Reports_A_Non_Partial_Application_At_Its_Identifier() { var execution = RunApplication( "public class App : Avalonia.Application { }", SourceFile()); AssertApplicationDiagnostic(execution.Result, "App", "partial"); execution.Result.GeneratedSources.Select(static source => source.HintName) .ShouldNotContain("GeneratedApplicationLanguageBootstrap.g.cs"); } [Fact] public void Reports_Each_Concrete_Application_When_The_Host_Is_Ambiguous() { var execution = RunApplication( """ public partial class AppA : Avalonia.Application { } public partial class AppB : Avalonia.Application { } """, SourceFile()); var diagnostics = execution.Result.Diagnostics .Where(static diagnostic => diagnostic.Id == "ATOMUILOC008") .ToArray(); diagnostics.Length.ShouldBe(2); diagnostics.ShouldAllBe(static diagnostic => diagnostic.GetMessage().Contains("more than one", StringComparison.Ordinal)); diagnostics.Select(GetLocationText).OrderBy(static text => text) .ShouldBe(["AppA", "AppB"]); } [Theory] [InlineData( "public partial class Host { public partial class App : Avalonia.Application { } }", "top-level")] [InlineData( "public partial class App : Avalonia.Application { }", "non-generic")] public void Reports_Unsupported_Application_Shapes(string declaration, string messageFragment) { var execution = RunApplication(declaration, SourceFile()); AssertApplicationDiagnostic(execution.Result, "App", messageFragment); } private static TestGeneratorExecution RunApplication( string applicationDeclaration, params TestAdditionalText[] files) { return RunWithOutputCompilation( RuntimeSource + CatalogSource + "\nnamespace TestApp { " + applicationDeclaration + " }", files); } private static void AssertApplicationDiagnostic( GeneratorRunResult result, string locationText, string messageFragment) { var diagnostic = result.Diagnostics.Single(diagnostic => diagnostic.Id == "ATOMUILOC008"); diagnostic.Severity.ShouldBe(DiagnosticSeverity.Error); diagnostic.GetMessage().ShouldContain(messageFragment); GetLocationText(diagnostic).ShouldBe(locationText); } private static string GetLocationText(Diagnostic diagnostic) { return diagnostic.Location.SourceTree!.GetText(TestContext.Current.CancellationToken) .ToString(diagnostic.Location.SourceSpan); } private static void AssertCompiles(TestGeneratorExecution execution) { execution.DriverDiagnostics.ShouldBeEmpty(); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } private static string GetBootstrapSource(GeneratorRunResult result) { return GetGeneratedSource(result, "GeneratedApplicationLanguageBootstrap.g.cs"); } private static string GetGeneratedSource(GeneratorRunResult result, string hintName) { return result.GeneratedSources.Single(source => source.HintName == hintName) .SourceText.ToString(); } private static int CountOccurrences(string source, string value) { return source.Split([value], StringSplitOptions.None).Length - 1; } private static TestAdditionalText SourceFile() { return LanguageFile( "Localization/en-US.xlf", SourceXliff, "ModuleBuiltIn", "Test.Package", "Test.Package"); } private static TestAdditionalText OverrideFile() { return LanguageFile( "Localization/Overrides/zh-CN.xlf", TargetXliff( "TestApp.Localization.AppLangResourceKind", "zh-CN", "Application title", "应用标题"), "ApplicationOverride", "TestApp", "Test.Package"); } private static TestAdditionalText StaticPackFile() { return LanguageFile( "packages/External.Package.I18n.ZhCN/zh-CN.xlf", TargetXliff( "External.Localization.ExternalLangResourceKind", "zh-CN", "External title", "外部标题"), "StaticLanguagePack", "External.Package.I18n.ZhCN", "External.Package"); } private static TestAdditionalText ReferencedSourceFile() { return LanguageFile( "packages/External.Package/Localization/en-US.xlf", """ External title """, "ModuleBuiltIn", "External.Package", "External.Package"); } private static TestAdditionalText LanguageFile( string path, string content, string sourceKind, string sourceIdentity, string moduleId) { var metadata = new Dictionary(StringComparer.Ordinal) { ["build_metadata.AdditionalFiles.AtomUILanguage"] = "true", ["build_metadata.AdditionalFiles.AtomUILanguageSourceKind"] = sourceKind, ["build_metadata.AdditionalFiles.AtomUILanguageSourceIdentity"] = sourceIdentity, ["build_metadata.AdditionalFiles.AtomUILanguageModuleId"] = moduleId }; if (sourceKind == "StaticLanguagePack") { metadata["build_metadata.AdditionalFiles.AtomUILanguageContractValidation"] = "Verified"; metadata["build_metadata.AdditionalFiles.AtomUILanguageSourceFingerprint"] = LanguageSourceFingerprint.Compute(Xliff21Parser.Parse(content).Document!); } return new TestAdditionalText(path, content, metadata); } private static MetadataReference CreateExternalCatalogReference() { return CreateMetadataReference( "External.Package", """ namespace AtomUI.Localization { [System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false)] public sealed class LanguageCatalogAttribute : System.Attribute; } namespace External.Localization { [AtomUI.Localization.LanguageCatalog] public enum ExternalLangResourceKind { Title } } """); } private static string TargetXliff( string catalogMetadataName, string language, string source, string target) { return $$""" {{source}}{{target}} """; } private const string RuntimeSource = """ #nullable enable namespace Avalonia { public abstract class Application { } } namespace AtomUI.Localization { [System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false)] public sealed class LanguageCatalogAttribute : System.Attribute; public interface IGeneratedApplicationLanguageBootstrap { void RegisterApplicationLanguages(ILocalizationBuilder builder); } public abstract class LanguageResourceExtension where TResourceKind : struct, System.Enum { protected LanguageResourceExtension() { } protected LanguageResourceExtension(TResourceKind kind) { } } public interface ILocalizationBuilder { void AddCatalog(LanguageCatalogDescriptor descriptor); void AddTranslationBundle(TranslationBundleDescriptor descriptor); } public abstract class LanguageCatalogDescriptor { } public sealed class LanguageCatalogDescriptor : LanguageCatalogDescriptor where TResourceKind : struct, System.Enum { public LanguageCatalogDescriptor( string catalogId, System.Collections.Generic.IReadOnlyList units, System.Func unitSlotResolver) { } } public sealed class LanguageCatalogUnitDescriptor { public LanguageCatalogUnitDescriptor(string key, bool isFormatted = false) { } } public readonly struct LanguageTag { public static LanguageTag Parse(string value) => default; } public enum TranslationSourceKind : byte { ModuleBuiltIn, StaticLanguagePack, ApplicationOverride } public sealed class TranslationBundleDescriptor { public TranslationBundleDescriptor( string catalogId, LanguageTag language, TranslationSourceKind sourceKind, string sourceIdentity, System.Collections.Generic.IReadOnlyList values) { } } } """; private const string CatalogSource = """ namespace TestApp.Localization { [AtomUI.Localization.LanguageCatalog] public enum AppLangResourceKind { Title, Description } } """; private const string SourceXliff = """ Application title Application description """; }