using System.Diagnostics; using System.Xml.Linq; using Shouldly; using Xunit; namespace AtomUI.Generator.Tests.BuildInfrastructure; public sealed class BuildLayoutTests { private static readonly string[] s_expectedBuildFiles = [ "AtomUI.Generator.props", "AtomUI.Generator.targets", "AtomUI.GeneratorConsumer.targets", "AtomUI.LinkedRegistration.props", "AtomUI.LinkedRegistration.targets", "AtomUI.LinkedRegistration.SidecarConsumer.targets", "AtomUI.Localization.props", "AtomUI.Localization.targets", "AtomUI.Repository.props", "AtomUI.Repository.targets", "AtomUI.ThemeAssets.targets", "MacOSHomebrewNativeAot.targets", "OutputPaths.props", "PackageMetadata.props", "ProjectDefaults.props", "Versions.props" ]; private static readonly string[] s_expectedNuGetBuildAssets = [ "AtomUI.Generator.props", "AtomUI.Generator.targets", "AtomUI.LinkedRegistration.props", "AtomUI.LinkedRegistration.targets", "AtomUI.LinkedRegistration.SidecarConsumer.targets", "AtomUI.Localization.props", "AtomUI.Localization.targets", "AtomUI.ThemeAssets.targets" ]; [Fact] public void Directory_Build_Files_Import_Only_Repository_Entry_Points() { GetImports("Directory.Build.props") .ShouldBe(["$(MSBuildThisFileDirectory)build/AtomUI.Repository.props"]); GetImports("Directory.Build.targets") .ShouldBe(["$(MSBuildThisFileDirectory)build/AtomUI.Repository.targets"]); } [Fact] public void Repository_Props_Import_Configuration_And_Generator_Entry_Point() { const string repositoryPropsPath = "build/AtomUI.Repository.props"; GetImports(repositoryPropsPath).ShouldBe([ "$(MSBuildThisFileDirectory)Versions.props", "$(MSBuildThisFileDirectory)ProjectDefaults.props", "$(MSBuildThisFileDirectory)PackageMetadata.props", "$(MSBuildThisFileDirectory)OutputPaths.props", "$(OutputPathWithoutFramework)/netstandard2.0/AtomUI.BuildTasks.ShadowKey.props", "$(MSBuildThisFileDirectory)AtomUI.Generator.props" ]); } [Fact] public void Repository_Targets_Reuse_The_Generator_Entry_Point() { var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); GetImports("build/AtomUI.Repository.targets") .ShouldBe(["$(MSBuildThisFileDirectory)AtomUI.Generator.targets"]); GetTargetNames(repositoryTargets) .ShouldContain("AtomUIPrepareGeneratorConsumerPackageAssets"); } [Fact] public void Build_Task_Shadow_Copies_Are_Not_Removed_During_Consumer_Builds() { var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); var stagingTarget = repositoryTargets.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == "_AtomUIStageBuildTasksToolset"); stagingTarget.Descendants() .Where(element => element.Name.LocalName == "RemoveDir") .ShouldBeEmpty(); } [Fact] public void Build_Task_Shadow_Copies_Are_Staged_For_Cross_Targeting_Pack_Targets() { var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); var stagingTarget = repositoryTargets.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == "_AtomUIStageBuildTasksToolset"); ((string?)stagingTarget.Attribute("BeforeTargets")).ShouldNotBeNull() .ShouldContain("AtomUIPrepareLanguageModuleAssets"); ((string?)stagingTarget.Attribute("Condition")).ShouldNotBeNull() .ShouldNotContain("IsCrossTargetingBuild"); } [Fact] public void Generator_Build_Assets_Have_One_Explicit_Manifest() { var repositoryProps = XDocument.Load(GetRepoFile("build/AtomUI.Repository.props")); var buildAssets = repositoryProps.Descendants("AtomUINuGetBuildAsset").ShouldHaveSingleItem(); var includes = ((string?)buildAssets.Attribute("Include")) .ShouldNotBeNull() .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(include => include.Replace("$(MSBuildThisFileDirectory)", string.Empty, StringComparison.Ordinal)) .ToArray(); includes.ShouldBe(s_expectedNuGetBuildAssets); buildAssets.Attribute("Exclude").ShouldBeNull(); buildAssets.Elements("PackagePath") .ShouldHaveSingleItem() .Value.ShouldBe("buildTransitive/%(Filename)%(Extension)"); var toolAssets = repositoryProps.Descendants("AtomUIGeneratorToolAsset").ShouldHaveSingleItem(); var toolIncludes = ((string?)toolAssets.Attribute("Include")).ShouldNotBeNull(); toolIncludes.ShouldContain("AtomUI.Generator.dll"); toolIncludes.ShouldContain("AtomUI.Build.Tasks.dll"); toolAssets.Elements("PackagePath") .ShouldHaveSingleItem() .Value.ShouldBe("tools/netstandard2.0/%(Filename)%(Extension)"); var generatorProject = XDocument.Load(GetRepoFile("src/AtomUI.Generator/AtomUI.Generator.csproj")); generatorProject.Descendants("ProjectReference") .ShouldNotContain(element => ((string?)element.Attribute("Include") ?? string.Empty) .Contains("AtomUI.Generator.LinkedPublish", StringComparison.Ordinal)); generatorProject.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUINuGetBuildAsset)"); generatorProject.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUIGeneratorToolAsset)"); var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); repositoryTargets.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUINuGetBuildAsset)"); repositoryTargets.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUIGeneratorToolAsset)"); var linkedPackTarget = generatorProject.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == "AtomUIPrepareLinkedPublishGeneratorForPack"); ((string?)linkedPackTarget.Attribute("BeforeTargets")).ShouldBe("_GetPackageFiles"); linkedPackTarget.Descendants("MSBuild") .Single() .Attribute("Projects")! .Value.ShouldContain("AtomUI.Generator.LinkedPublish.csproj"); ((string?)linkedPackTarget.Descendants("MSBuild").Single().Attribute("Condition")) .ShouldBe("'$(NoBuild)' != 'true'"); linkedPackTarget.Descendants("Error") .Single() .Attribute("Condition")! .Value.ShouldContain("AtomUILinkedPublishGeneratorAssembly"); } [Fact] public void NuGet_Features_Use_The_Shared_Build_Tasks_Assembly() { var repositoryProps = XDocument.Load(GetRepoFile("build/AtomUI.Repository.props")); repositoryProps.Descendants("AtomUIBuildTasksAssembly") .ShouldHaveSingleItem() .Value.ShouldContain("AtomUI.Build.Tasks.dll"); var generatorProps = XDocument.Load(GetRepoFile("build/AtomUI.Generator.props")); var fallback = generatorProps.Descendants("AtomUIBuildTasksAssembly").ShouldHaveSingleItem(); ((string?)fallback.Attribute("Condition")) .ShouldBe("'$(AtomUIBuildTasksAssembly)' == ''"); fallback.Value.Trim().ShouldBe( "$(MSBuildThisFileDirectory)../tools/netstandard2.0/AtomUI.Build.Tasks.dll"); var buildRoot = Path.Combine(GetRepositoryRoot(), "build"); var featureFiles = s_expectedNuGetBuildAssets .Where(file => Path.GetExtension(file) == ".targets") .Select(file => Path.Combine(buildRoot, file)); var usingTasks = featureFiles.SelectMany(file => XDocument.Load(file).Descendants("UsingTask")) .ToArray(); usingTasks.ShouldNotBeEmpty(); usingTasks.ShouldAllBe(element => (string?)element.Attribute("AssemblyFile") == "$(AtomUIBuildTasksAssembly)"); File.Exists(Path.Combine(buildRoot, "BuildTasks.props")).ShouldBeFalse(); var buildText = string.Join('\n', Directory.EnumerateFiles(buildRoot).Select(File.ReadAllText)); buildText.ShouldNotContain("AtomUILocalizationBuildTasksAssembly"); buildText.ShouldNotContain("AtomUILinkedRegistrationBuildTasksAssembly"); } [Fact] public void NuGet_Build_Tasks_Load_From_Shadow_Copy_In_Process() { var buildRoot = Path.Combine(GetRepositoryRoot(), "build"); var usingTasks = s_expectedNuGetBuildAssets .Where(file => Path.GetExtension(file) == ".targets") .Select(file => Path.Combine(buildRoot, file)) .SelectMany(file => XDocument.Load(file) .Descendants() .Where(element => element.Name.LocalName == "UsingTask")) .Where(element => (string?)element.Attribute("AssemblyFile") == "$(AtomUIBuildTasksAssembly)") .ToArray(); usingTasks.ShouldNotBeEmpty(); usingTasks.ShouldAllBe(element => element.Attribute("Runtime") == null); usingTasks.ShouldAllBe(element => element.Attribute("TaskFactory") == null); } [Fact] public void Repository_Test_Project_Flag_Has_A_Canonical_Boolean_Value() { var defaults = XDocument.Load(GetRepoFile("build/ProjectDefaults.props")); var isTestProject = defaults.Descendants("IsTestProject").ShouldHaveSingleItem(); isTestProject.Value.ShouldBe("true"); } [Fact] public void Repository_Defaults_Exclude_Stale_Project_Local_Obj_Files() { var defaults = XDocument.Load(GetRepoFile("build/ProjectDefaults.props")); var excludes = defaults.Descendants("DefaultItemExcludes").ShouldHaveSingleItem(); excludes.Value.ShouldContain("$(MSBuildProjectDirectory)/obj/**"); } [Fact] public void Repository_Output_Paths_Centralize_Binaries_And_Intermediate_Files() { var outputPaths = XDocument.Load(GetRepoFile("build/OutputPaths.props")); outputPaths.Descendants("PackageOutputPath") .ShouldHaveSingleItem() .Value.ShouldBe("$(MSBuildThisFileDirectory)../.artifacts/Nuget/$(Configuration)"); outputPaths.Descendants("OutputPathWithoutFramework") .ShouldHaveSingleItem() .Value.ShouldBe("$(MSBuildThisFileDirectory)../.artifacts/bin/$(Configuration)"); outputPaths.Descendants("OutputPath") .ShouldHaveSingleItem() .Value.ShouldBe("$(OutputPathWithoutFramework)"); outputPaths.Descendants("BaseIntermediateOutputPath") .ShouldHaveSingleItem() .Value.ShouldBe( "$(MSBuildThisFileDirectory)../.artifacts/$(MSBuildProjectName)/obj"); } [Fact] public void Repository_Test_Runs_Clean_VSTest_Temporary_Results() { var outputPaths = XDocument.Load(GetRepoFile("build/OutputPaths.props")); outputPaths.Descendants("AtomUITestResultsRoot") .ShouldHaveSingleItem() .Value.ShouldBe("$(MSBuildThisFileDirectory)../.artifacts/TestResults"); outputPaths.Descendants("AtomUITestResultsDirectory") .ShouldHaveSingleItem() .Value.ShouldBe("$(AtomUITestResultsRoot)/$(MSBuildProjectName)"); outputPaths.Descendants("VSTestResultsDirectory") .ShouldHaveSingleItem() .Value.ShouldBe("$(AtomUITestResultsDirectory)"); var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); AssertTestResultsCleanupTarget( repositoryTargets, "AtomUICleanTestResultsBeforeRun", "BeforeTargets", "_AtomUITestResultsDirectoryToCleanBeforeRun"); AssertTestResultsCleanupTarget( repositoryTargets, "AtomUICleanTestResultsAfterRun", "AfterTargets", "_AtomUITestResultsDirectoryToCleanAfterRun"); } [Fact] public void Repository_Test_Runs_Forbid_Persistent_VSTest_Dumps() { var outputPaths = XDocument.Load(GetRepoFile("build/OutputPaths.props")); var hangDumpType = outputPaths.Descendants("VSTestBlameHangDumpType") .ShouldHaveSingleItem(); hangDumpType.Value.ShouldBe("none"); ((string?)hangDumpType.Attribute("Condition")).ShouldBe( "'$(IsTestProject)' == 'true' and '$(VSTestBlameHang)' == 'true' and '$(VSTestBlameHangDumpType)' == ''"); var crashDumpType = outputPaths.Descendants("VSTestBlameCrashDumpType") .ShouldHaveSingleItem(); crashDumpType.Value.ShouldBe("none"); ((string?)crashDumpType.Attribute("Condition")).ShouldBe( "'$(IsTestProject)' == 'true' and '$(VSTestBlameCrash)' == 'true' and '$(VSTestBlameCrashDumpType)' == ''"); var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); var guardTarget = repositoryTargets.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == "AtomUIRejectPersistentVSTestDumps"); ((string?)guardTarget.Attribute("BeforeTargets")).ShouldBe("VSTest"); ((string?)guardTarget.Attribute("Condition")).ShouldBe("'$(IsTestProject)' == 'true'"); var errors = guardTarget.Descendants("Error").ToArray(); errors.Length.ShouldBe(2); errors.Select(element => (string?)element.Attribute("Condition")) .ShouldBe([ "'$(VSTestBlameHangDumpType)' == 'full' or '$(VSTestBlameHangDumpType)' == 'mini'", "'$(VSTestBlameCrashDumpType)' == 'full' or '$(VSTestBlameCrashDumpType)' == 'mini'" ]); errors.Select(element => (string?)element.Attribute("Text")) .ShouldAllBe(text => text != null && text.Contains("must not collect persistent", StringComparison.Ordinal)); } private static void AssertTestResultsCleanupTarget( XDocument repositoryTargets, string targetName, string timingAttribute, string itemName) { var cleanupTarget = repositoryTargets.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == targetName); ((string?)cleanupTarget.Attribute(timingAttribute)).ShouldBe("VSTest"); ((string?)cleanupTarget.Attribute("Condition")).ShouldBe("'$(IsTestProject)' == 'true'"); var directoriesToClean = cleanupTarget.Descendants(itemName) .Select(element => (string?)element.Attribute("Include")) .ToArray(); directoriesToClean.ShouldBe([ "$(MSBuildProjectDirectory)/TestResults", "$(AtomUITestResultsDirectory)" ]); var removeDir = cleanupTarget.Descendants("RemoveDir").ShouldHaveSingleItem(); ((string?)removeDir.Attribute("Directories")).ShouldBe($"@({itemName})"); removeDir.Attribute("ContinueOnError").ShouldBeNull(); } [Fact] public void Tool_Source_Files_Stay_Trackable_While_Tool_Build_Outputs_Are_Ignored() { IsIgnoredByGit("tools/AtomUI.Docs.LLMsGenerator/Program.cs").ShouldBeFalse(); IsIgnoredByGit("tools/performances/AtomUI.Performance/Program.cs").ShouldBeFalse(); IsIgnoredByGit("tools/AtomUI.Docs.LLMsGenerator/bin/Debug/tool.dll").ShouldBeTrue(); IsIgnoredByGit("tools/performances/AtomUI.Performance/obj/project.assets.json").ShouldBeTrue(); } [Fact] public void Repository_Targets_Exclude_Compiler_Generated_Files_Once() { const string generatedFilesPattern = "$(CompilerGeneratedFilesOutputPath)/**/*.cs"; var repositoryTargets = XDocument.Load(GetRepoFile("build/AtomUI.Repository.targets")); var removal = repositoryTargets.Descendants("Compile") .Where(element => (string?)element.Attribute("Remove") == generatedFilesPattern) .ShouldHaveSingleItem(); ((string?)removal.Parent?.Attribute("Condition")) .ShouldBe("'$(CompilerGeneratedFilesOutputPath)' != ''"); var repositoryRoot = GetRepositoryRoot(); var projectFiles = new[] { "src", "controlgallery", "tests", "tools" } .Select(directory => Path.Combine(repositoryRoot, directory)) .Where(Directory.Exists) .SelectMany(directory => Directory.EnumerateFiles( directory, "*.csproj", SearchOption.AllDirectories)); foreach (var projectFile in projectFiles) { var project = XDocument.Load(projectFile); if (!project.Descendants("CompilerGeneratedFilesOutputPath").Any()) { continue; } project.Descendants("Compile") .ShouldNotContain(element => (string?)element.Attribute("Remove") == generatedFilesPattern, projectFile); } } [Fact] public void Repository_Default_Items_Always_Exclude_Stale_Generated_Files() { var projectDefaults = XDocument.Load(GetRepoFile("build/ProjectDefaults.props")); var excludes = projectDefaults.Descendants("DefaultItemExcludes") .ShouldHaveSingleItem() .Value; excludes.ShouldContain("$(MSBuildProjectDirectory)/obj/**"); excludes.ShouldContain("$(MSBuildProjectDirectory)/GeneratedFiles/**"); } [Fact] public void Desktop_Project_Does_Not_Keep_Stale_Window_Directory_Exclusions() { var project = File.ReadAllText(GetRepoFile( "src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csproj")); project.ShouldNotContain("Window\\Reflection"); project.ShouldNotContain("Window\\Visuals"); } [Fact] public void NuGet_Generator_Entry_Points_Import_Flat_Feature_Files() { GetImports("build/AtomUI.Generator.props").ShouldBe([ "$(MSBuildThisFileDirectory)AtomUI.LinkedRegistration.props", "$(MSBuildThisFileDirectory)AtomUI.Localization.props" ]); GetImports("build/AtomUI.Generator.targets").ShouldBe([ "$(MSBuildThisFileDirectory)AtomUI.LinkedRegistration.targets", "$(MSBuildThisFileDirectory)AtomUI.Localization.targets", "$(MSBuildThisFileDirectory)AtomUI.ThemeAssets.targets" ]); } [Fact] public void Localization_Targets_Own_The_Complete_Build_Integration() { var targets = XDocument.Load(GetRepoFile("build/AtomUI.Localization.targets")); GetImports("build/AtomUI.Localization.targets").ShouldBeEmpty(); targets.Descendants("AdditionalFiles").ShouldNotBeEmpty(); targets.Descendants("CompilerVisibleItemMetadata").ShouldNotBeEmpty(); GetTargetNames(targets).ShouldBe([ "AtomUIGetLanguageModuleSourceAssets", "AtomUIResolveLanguageContractProjectReferences", "AtomUIInferLanguagePackageContractPolicy", "AtomUIGetLanguagePackProjectAssets", "AtomUIResolveLanguagePackProjectReferences", "AtomUIExportLanguageTemplates", "AtomUIPrepareLanguagePackage", "AtomUIPrepareLanguageModuleAssets" ]); } [Fact] public void Build_Root_Is_A_Flat_Explicit_MSBuild_Surface() { var buildRoot = Path.Combine(GetRepositoryRoot(), "build"); Directory.EnumerateDirectories(buildRoot).ShouldBeEmpty(); Directory.EnumerateFiles(buildRoot) .Select(Path.GetFileName) .ShouldBe(s_expectedBuildFiles, ignoreOrder: true); Directory.EnumerateFiles(buildRoot) .All(file => Path.GetExtension(file) is ".props" or ".targets") .ShouldBeTrue(); } private static string[] GetImports(string relativePath) { return XDocument.Load(GetRepoFile(relativePath)) .Descendants() .Where(element => element.Name.LocalName == "Import") .Select(element => (string?)element.Attribute("Project")) .Where(static path => path is not null) .Cast() .ToArray(); } private static string[] GetTargetNames(XDocument document) { return document.Descendants() .Where(element => element.Name.LocalName == "Target") .Select(element => (string?)element.Attribute("Name")) .Where(static name => name is not null) .Cast() .ToArray(); } private static string GetRepoFile(string relativePath) { var repositoryRoot = GetRepositoryRoot(); var candidate = Path.Combine(repositoryRoot, relativePath); if (File.Exists(candidate)) { return candidate; } throw new FileNotFoundException(relativePath); } private static string GetRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AtomUI.slnx"))) { return directory.FullName; } directory = directory.Parent; } throw new DirectoryNotFoundException("Could not locate the AtomUI repository root."); } private static bool IsIgnoredByGit(string relativePath) { using var process = Process.Start(new ProcessStartInfo { FileName = "git", WorkingDirectory = GetRepositoryRoot(), RedirectStandardOutput = true, RedirectStandardError = true, ArgumentList = { "check-ignore", "--quiet", "--no-index", "--", relativePath } }); process.ShouldNotBeNull(); process.WaitForExit(); process.ExitCode.ShouldBeOneOf(0, 1); return process.ExitCode == 0; } }