using Microsoft.CodeAnalysis; using Shouldly; using Xunit; using static AtomUI.Generator.Tests.Localization.LocalizationGeneratorTestHost; namespace AtomUI.Generator.Tests.Localization; public class LocalizationGeneratorDeterminismTests { [Fact] public void Reordered_Catalogs_Files_And_Units_Produce_Byte_Identical_Sources() { var first = Run( RuntimeSource + CatalogAThenBSource, LanguageFile("Localization/A/en-US.xlf", CatalogAXliff), LanguageFile("Localization/B/en-US.xlf", CatalogBXliff)); var second = Run( RuntimeSource + CatalogBThenASource, LanguageFile("Localization/B/en-US.xlf", ReverseUnits(CatalogBXliff)), LanguageFile("Localization/A/en-US.xlf", ReverseUnits(CatalogAXliff))); first.Diagnostics.ShouldBeEmpty(); second.Diagnostics.ShouldBeEmpty(); GetSources(second).ShouldBe(GetSources(first)); } [Fact] public void Updating_One_Xliff_Changes_Only_Its_Catalog_Registration_Source() { var original = LanguageFile("Localization/A/en-US.xlf", CatalogAXliff); var replacement = LanguageFile( "Localization/A/en-US.xlf", CatalogAXliff.Replace("Alpha", "Changed alpha")); var execution = RunWithUpdatedAdditionalText( RuntimeSource + CatalogAThenBSource, original, replacement, LanguageFile("Localization/B/en-US.xlf", CatalogBXliff)); var first = execution.FirstRun.Results.ShouldHaveSingleItem(); var second = execution.SecondRun.Results.ShouldHaveSingleItem(); first.Diagnostics.ShouldBeEmpty(); second.Diagnostics.ShouldBeEmpty(); var before = GetSources(first); var after = GetSources(second); var changedHints = before.Keys.Where(hint => before[hint] != after[hint]).ToArray(); changedHints.ShouldBe([ "TestApp.Localization.CatalogALangResourceKind.LanguageCatalogRegistration.g.cs" ]); after["GeneratedLanguageModuleRegistration.g.cs"] .ShouldBe(before["GeneratedLanguageModuleRegistration.g.cs"]); after["GeneratedApplicationLanguageBootstrap.g.cs"] .ShouldBe(before["GeneratedApplicationLanguageBootstrap.g.cs"]); var languageFileReasons = second.TrackedSteps["LocalizationLanguageFiles"] .SelectMany(static step => step.Outputs) .Select(static output => output.Reason) .ToArray(); languageFileReasons.Count(static reason => reason == IncrementalStepRunReason.Modified).ShouldBe(1); languageFileReasons.ShouldContain(IncrementalStepRunReason.Cached); second.TrackedSteps["LocalizationCompilation"] .SelectMany(static step => step.Outputs) .Select(static output => output.Reason) .ShouldContain(IncrementalStepRunReason.Modified); } private static SortedDictionary GetSources(GeneratorRunResult result) { return new SortedDictionary( result.GeneratedSources.ToDictionary( static source => source.HintName, static source => source.SourceText.ToString(), StringComparer.Ordinal), StringComparer.Ordinal); } private static TestAdditionalText LanguageFile(string path, string content) { return new TestAdditionalText( path, content, new Dictionary(StringComparer.Ordinal) { ["build_metadata.AdditionalFiles.AtomUILanguage"] = "true", ["build_metadata.AdditionalFiles.AtomUILanguageSourceKind"] = "ModuleBuiltIn", ["build_metadata.AdditionalFiles.AtomUILanguageSourceIdentity"] = "Test.Package" }); } private static string ReverseUnits(string source) { var firstUnitStart = source.IndexOf(" ", firstUnitStart, StringComparison.Ordinal) + "".Length; var secondUnitStart = source.IndexOf(" ", secondUnitStart, StringComparison.Ordinal) + "".Length; var firstUnit = source.Substring(firstUnitStart, firstUnitEnd - firstUnitStart); var secondUnit = source.Substring(secondUnitStart, secondUnitEnd - secondUnitStart); return source.Substring(0, firstUnitStart) + secondUnit + "\n" + firstUnit + source.Substring(secondUnitEnd); } private const string RuntimeSource = """ namespace Avalonia { public abstract class Application { } } namespace AtomUI.Localization { [System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false)] public sealed class LanguageCatalogAttribute : System.Attribute; } """; private const string CatalogAThenBSource = """ namespace TestApp.Localization { [AtomUI.Localization.LanguageCatalog] public enum CatalogALangResourceKind { Count, Title } [AtomUI.Localization.LanguageCatalog] public enum CatalogBLangResourceKind { Confirm, Cancel } } namespace TestApp { public partial class App : Avalonia.Application { } } """; private const string CatalogBThenASource = """ namespace TestApp.Localization { [AtomUI.Localization.LanguageCatalog] public enum CatalogBLangResourceKind { Cancel, Confirm } [AtomUI.Localization.LanguageCatalog] public enum CatalogALangResourceKind { Title, Count } } namespace TestApp { public partial class App : Avalonia.Application { } } """; private const string CatalogAXliff = """ Alpha {0} Alpha """; private const string CatalogBXliff = """ Confirm Cancel """; }