mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
428 lines
16 KiB
C#
428 lines
16 KiB
C#
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<T> : 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",
|
|
"""
|
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US">
|
|
<file id="External.Localization.ExternalLangResourceKind">
|
|
<unit id="Title"><segment><source>External title</source></segment></unit>
|
|
</file>
|
|
</xliff>
|
|
""",
|
|
"ModuleBuiltIn",
|
|
"External.Package",
|
|
"External.Package");
|
|
}
|
|
|
|
private static TestAdditionalText LanguageFile(
|
|
string path,
|
|
string content,
|
|
string sourceKind,
|
|
string sourceIdentity,
|
|
string moduleId)
|
|
{
|
|
var metadata = new Dictionary<string, string>(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 $$"""
|
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US" trgLang="{{language}}">
|
|
<file id="{{catalogMetadataName}}">
|
|
<unit id="Title"><segment><source>{{source}}</source><target state="translated">{{target}}</target></segment></unit>
|
|
</file>
|
|
</xliff>
|
|
""";
|
|
}
|
|
|
|
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<TResourceKind>
|
|
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<TResourceKind> : LanguageCatalogDescriptor
|
|
where TResourceKind : struct, System.Enum
|
|
{
|
|
public LanguageCatalogDescriptor(
|
|
string catalogId,
|
|
System.Collections.Generic.IReadOnlyList<LanguageCatalogUnitDescriptor> units,
|
|
System.Func<TResourceKind, int> 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<string?> values) { }
|
|
}
|
|
}
|
|
""";
|
|
|
|
private const string CatalogSource = """
|
|
|
|
namespace TestApp.Localization
|
|
{
|
|
[AtomUI.Localization.LanguageCatalog]
|
|
public enum AppLangResourceKind
|
|
{
|
|
Title,
|
|
Description
|
|
}
|
|
}
|
|
""";
|
|
|
|
private const string SourceXliff = """
|
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US">
|
|
<file id="TestApp.Localization.AppLangResourceKind">
|
|
<unit id="Title"><segment><source>Application title</source></segment></unit>
|
|
<unit id="Description"><segment><source>Application description</source></segment></unit>
|
|
</file>
|
|
</xliff>
|
|
""";
|
|
}
|