using MsbuildAnalyzer.Utilities; using Xunit; namespace MsbuildAnalyzer.Tests; /// /// Unit tests for . The function /// walks ancestors of a project file and returns the closest existing occurrence of each /// canonical Directory.* filename — mirroring MSBuild's "first ancestor wins" import rule /// for Directory.Build.props/.targets/.rsp, Directory.Solution.{props,targets}, and /// Directory.Packages.props. Inputs are returned as "{workspaceRoot}/..."-prefixed paths /// so Nx hashes the right files for sandboxed task execution. /// public class ProjectUtilitiesDirectoryBuildInputsTests { private static readonly string WorkspaceRoot = Path.Combine(Path.GetTempPath(), "nx-dotnet-ws"); /// /// Build the directory→filenames index the same way Analyzer.AnalyzeWorkspace does /// when it sees Directory.* files come in over stdin (workspace-root-relative paths, /// forward-slashed; "." denotes the workspace root itself). /// private static Dictionary> IndexByDir(params string[] relativePaths) { var index = new Dictionary>(); foreach (var rel in relativePaths) { var normalized = rel.Replace('\\', '/'); var lastSlash = normalized.LastIndexOf('/'); var dir = lastSlash < 0 ? "." : normalized.Substring(0, lastSlash); var name = lastSlash < 0 ? normalized : normalized.Substring(lastSlash + 1); if (!index.TryGetValue(dir, out var set)) { set = new HashSet(StringComparer.OrdinalIgnoreCase); index[dir] = set; } set.Add(name); } return index; } private static string ProjectPath(params string[] segments) => Path.Combine(new[] { WorkspaceRoot }.Concat(segments).ToArray()); [Fact] public void ReturnsEmpty_WhenNoDirectoryFilesIndexed() { var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: new Dictionary>()); Assert.Empty(inputs); } [Fact] public void ReturnsWorkspaceRootFile_WhenOnlyRootHasOne() { var index = IndexByDir("Directory.Build.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Equal(new[] { "{workspaceRoot}/Directory.Build.props" }, inputs); } [Fact] public void ClosestAncestorWins_PerFileName() { // The same filename exists at two depths. MSBuild stops at the first // ancestor it finds (closest to the project), so we should too. var index = IndexByDir( "Directory.Build.props", "apps/Directory.Build.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Equal(new[] { "{workspaceRoot}/apps/Directory.Build.props" }, inputs); } [Fact] public void DifferentFileNames_PickedUpFromDifferentAncestors() { // props sits at the project directory, targets only exists at the root. // Each filename is resolved independently to its own closest ancestor. var index = IndexByDir( "apps/foo/Directory.Build.props", "Directory.Build.targets"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Contains("{workspaceRoot}/apps/foo/Directory.Build.props", inputs); Assert.Contains("{workspaceRoot}/Directory.Build.targets", inputs); Assert.Equal(2, inputs.Count); } [Fact] public void ProjectAtWorkspaceRoot_FindsRootLevelFiles() { // A project file living at the workspace root still needs its sibling // Directory.* files declared as inputs. var index = IndexByDir( "Directory.Build.props", "Directory.Packages.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("root.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Contains("{workspaceRoot}/Directory.Build.props", inputs); Assert.Contains("{workspaceRoot}/Directory.Packages.props", inputs); Assert.Equal(2, inputs.Count); } [Fact] public void MissingIntermediateAncestor_IsTransparentlySkipped() { // Project at apps/foo/bar/bar.csproj with files only at the workspace root // and at apps/foo (gap at apps/foo/bar and apps/). The walk should keep // climbing past empty levels rather than stopping at the first one. var index = IndexByDir( "apps/foo/Directory.Build.targets", "Directory.Build.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "bar", "bar.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Contains("{workspaceRoot}/apps/foo/Directory.Build.targets", inputs); Assert.Contains("{workspaceRoot}/Directory.Build.props", inputs); Assert.Equal(2, inputs.Count); } [Fact] public void OnlyCanonicalFileNames_ArePicked() { // The index could theoretically contain extra entries (e.g. an unrelated // file in the same directory). The walker should ignore anything that // isn't in the canonical list. var index = IndexByDir( "Directory.Build.props", "Directory.Build.targets", "Directory.Build.rsp", "Directory.Solution.props", "Directory.Solution.targets", "Directory.Packages.props", "Directory.NotRecognized.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Equal(6, inputs.Count); Assert.DoesNotContain( inputs, i => i.Contains("Directory.NotRecognized.props")); } [Fact] public void AllSixCanonicalFileNames_AreSupportedAtSameDirectory() { var index = IndexByDir( "Directory.Build.props", "Directory.Build.targets", "Directory.Build.rsp", "Directory.Solution.props", "Directory.Solution.targets", "Directory.Packages.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.Contains("{workspaceRoot}/Directory.Build.props", inputs); Assert.Contains("{workspaceRoot}/Directory.Build.targets", inputs); Assert.Contains("{workspaceRoot}/Directory.Build.rsp", inputs); Assert.Contains("{workspaceRoot}/Directory.Solution.props", inputs); Assert.Contains("{workspaceRoot}/Directory.Solution.targets", inputs); Assert.Contains("{workspaceRoot}/Directory.Packages.props", inputs); } [Fact] public void StopsWalkingOnceAllFileNamesFound() { // Every canonical filename exists at the project's own directory. The // walker should NOT continue to ancestors and pick up shadowed copies. var index = IndexByDir( "apps/foo/Directory.Build.props", "apps/foo/Directory.Build.targets", "apps/foo/Directory.Build.rsp", "apps/foo/Directory.Solution.props", "apps/foo/Directory.Solution.targets", "apps/foo/Directory.Packages.props", // These ancestors should be shadowed and NOT included: "apps/Directory.Build.props", "Directory.Build.props"); var inputs = ProjectUtilities.GetDirectoryBuildInputs( projectPath: ProjectPath("apps", "foo", "foo.csproj"), workspaceRoot: WorkspaceRoot, filesByDir: index); Assert.All(inputs, i => Assert.StartsWith("{workspaceRoot}/apps/foo/", i)); Assert.Equal(6, inputs.Count); } }