296 lines
11 KiB
C#
296 lines
11 KiB
C#
using MsbuildAnalyzer.Models;
|
|
using MsbuildAnalyzer.Utilities;
|
|
using Xunit;
|
|
|
|
namespace MsbuildAnalyzer.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="TargetBuilder.BuildTargets"/> output path
|
|
/// computation. These exercise the scenarios reported in
|
|
/// https://github.com/nrwl/nx/issues/33971:
|
|
///
|
|
/// 1. The original bug: Microsoft.NET.Sdk.Web projects evaluate
|
|
/// BaseIntermediateOutputPath to an absolute path, and the analyzer
|
|
/// previously emitted "{projectRoot}/<ProjectName>/obj".
|
|
///
|
|
/// 2. The follow-up reported in the thread: repositories that set a
|
|
/// centralized "dist/" folder via Directory.Build.props (paths resolve
|
|
/// above the project directory) must use {workspaceRoot}-relative outputs
|
|
/// rather than {projectRoot}.
|
|
///
|
|
/// The tests drive the analyzer's pure-logic path via property dictionaries
|
|
/// that mirror what MSBuild would hand back, avoiding the cost of spinning up
|
|
/// a full MSBuild evaluation.
|
|
/// </summary>
|
|
public class TargetBuilderOutputPathsTests
|
|
{
|
|
private static readonly string WorkspaceRoot = Path.Combine(Path.GetTempPath(), "nx-dotnet-ws");
|
|
|
|
private static string ProjectDir(params string[] segments) =>
|
|
Path.Combine(new[] { WorkspaceRoot }.Concat(segments).ToArray());
|
|
|
|
private static Dictionary<string, Target> BuildTargets(
|
|
Dictionary<string, string> properties,
|
|
string projectDirectory,
|
|
string projectName = "MyProj",
|
|
bool isExe = false,
|
|
bool isTest = false,
|
|
List<string>? directoryBuildInputs = null) =>
|
|
TargetBuilder.BuildTargets(
|
|
projectName: projectName,
|
|
fileName: $"{projectName}.csproj",
|
|
isTest: isTest,
|
|
isExe: isExe,
|
|
packageRefs: new List<PackageReference>(),
|
|
properties: properties,
|
|
projectDirectory: projectDirectory,
|
|
workspaceRoot: WorkspaceRoot,
|
|
options: new PluginOptions(),
|
|
nxJson: null,
|
|
directoryBuildInputs: directoryBuildInputs ?? new List<string>());
|
|
|
|
// --- Original #33971: Microsoft.NET.Sdk.Web ---------------------------
|
|
|
|
[Fact]
|
|
public void Build_WebSdk_WithAbsoluteBaseIntermediatePath_EmitsProjectRootObj()
|
|
{
|
|
// Microsoft.NET.Sdk.Web runs BaseIntermediateOutputPath through
|
|
// [MSBuild]::NormalizePath, producing an absolute path anchored at
|
|
// the project directory. BaseOutputPath stays relative.
|
|
var projectDirectory = ProjectDir("MyWebApi");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["BaseOutputPath"] = "bin\\",
|
|
["BaseIntermediateOutputPath"] = projectDirectory + Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar,
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory);
|
|
|
|
Assert.Equal(
|
|
new[] { "{projectRoot}/bin", "{projectRoot}/obj" },
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_Sdk_WithRelativeDefaults_EmitsProjectRootBinAndObj()
|
|
{
|
|
var projectDirectory = ProjectDir("MyLibrary");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["BaseOutputPath"] = "bin\\",
|
|
["BaseIntermediateOutputPath"] = "obj\\",
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory);
|
|
|
|
Assert.Equal(
|
|
new[] { "{projectRoot}/bin", "{projectRoot}/obj" },
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_WebSdk_DoesNotDoubleCountProjectDirectory()
|
|
{
|
|
// Regression guard for the exact shape reported in #33971.
|
|
var projectDirectory = ProjectDir("MyWebApp");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["BaseOutputPath"] = "bin\\",
|
|
["BaseIntermediateOutputPath"] = Path.Combine(projectDirectory, "obj") + Path.DirectorySeparatorChar,
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "MyWebApp");
|
|
|
|
Assert.DoesNotContain(
|
|
targets["build"].Outputs!,
|
|
o => o.Contains("MyWebApp/obj") || o.Contains("MyWebApp\\obj"));
|
|
}
|
|
|
|
// --- Follow-up: centralized dist via Directory.Build.props ------------
|
|
|
|
[Fact]
|
|
public void Build_CentralizedDist_EmitsWorkspaceRootRelativeOutputs()
|
|
{
|
|
// Directory.Build.props configuration modeled after nx-dotnet:
|
|
// <BaseOutputPath>$(MSBuildThisFileDirectory)dist/$(MSBuildProjectName)/bin/</BaseOutputPath>
|
|
// <BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)dist/intermediates/$(MSBuildProjectName)/obj/</BaseIntermediateOutputPath>
|
|
// After MSBuild evaluation these are absolute paths rooted at the
|
|
// workspace root, not the project directory.
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var distBin = Path.Combine(WorkspaceRoot, "dist", "foo", "bin") + Path.DirectorySeparatorChar;
|
|
var distObj = Path.Combine(WorkspaceRoot, "dist", "intermediates", "foo", "obj") + Path.DirectorySeparatorChar;
|
|
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["BaseOutputPath"] = distBin,
|
|
["BaseIntermediateOutputPath"] = distObj,
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
"{workspaceRoot}/dist/foo/bin",
|
|
"{workspaceRoot}/dist/intermediates/foo/obj",
|
|
},
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_PartialRedirect_MixesProjectRootAndWorkspaceRoot()
|
|
{
|
|
// Only the obj tree is redirected; bin stays at the project default.
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var distObj = Path.Combine(WorkspaceRoot, "dist", "intermediates", "foo", "obj") + Path.DirectorySeparatorChar;
|
|
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["BaseOutputPath"] = "bin\\",
|
|
["BaseIntermediateOutputPath"] = distObj,
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
"{projectRoot}/bin",
|
|
"{workspaceRoot}/dist/intermediates/foo/obj",
|
|
},
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
// --- Sanity: SDK artifacts layout -------------------------------------
|
|
|
|
[Fact]
|
|
public void Build_ArtifactsOutput_EmitsWorkspaceRootOutputs()
|
|
{
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["UseArtifactsOutput"] = "true",
|
|
// ArtifactsPath defaults to "artifacts" relative to workspace root.
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
"{workspaceRoot}/artifacts/bin/foo",
|
|
"{workspaceRoot}/artifacts/obj/foo",
|
|
},
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_ArtifactsOutput_WithCustomArtifactsPath_EmitsWorkspaceRootOutputs()
|
|
{
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["UseArtifactsOutput"] = "true",
|
|
["ArtifactsPath"] = Path.Combine(WorkspaceRoot, "build-output"),
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
"{workspaceRoot}/build-output/bin/foo",
|
|
"{workspaceRoot}/build-output/obj/foo",
|
|
},
|
|
targets["build"].Outputs);
|
|
}
|
|
|
|
// --- Publish output: configuration is rewritten to match the target -----
|
|
|
|
[Fact]
|
|
public void Publish_RewritesEvaluatedDebugPublishDirToRelease()
|
|
{
|
|
// MSBuild evaluates PublishDir at the default (Debug) configuration, but
|
|
// the publish target runs --configuration Release. The declared output
|
|
// must point at bin/Release/publish (where the publish actually lands),
|
|
// not the evaluated bin/Debug/publish.
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["PublishDir"] = "bin\\Debug\\publish\\",
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo", isExe: true);
|
|
|
|
Assert.Equal(
|
|
new[] { "{projectRoot}/bin/Release/publish", "{projectRoot}/obj" },
|
|
targets["publish"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Publish_LeavesCustomPublishDirWithoutConfigurationSegmentAlone()
|
|
{
|
|
// A custom PublishDir that has no Debug/Release segment is passed through
|
|
// unchanged (only configuration segments are rewritten).
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["PublishDir"] = "dist-publish",
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo", isExe: true);
|
|
|
|
Assert.Equal(
|
|
new[] { "{projectRoot}/dist-publish", "{projectRoot}/obj" },
|
|
targets["publish"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Publish_ArtifactsLayout_EmitsWorkspaceRootPublishPath()
|
|
{
|
|
var projectDirectory = ProjectDir("apps", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["UseArtifactsOutput"] = "true",
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo", isExe: true);
|
|
|
|
Assert.Equal(
|
|
new[] { "{workspaceRoot}/artifacts/publish/foo", "{workspaceRoot}/artifacts/obj/foo" },
|
|
targets["publish"].Outputs);
|
|
}
|
|
|
|
// --- Pack output: nupkg glob plus the intermediate (obj) directory ------
|
|
|
|
[Fact]
|
|
public void Pack_EmitsNupkgGlobAndIntermediateObj()
|
|
{
|
|
// `dotnet pack` writes the .nupkg into the package output directory and
|
|
// intermediate state into obj, so both must be declared as outputs.
|
|
var projectDirectory = ProjectDir("libs", "foo");
|
|
|
|
var targets = BuildTargets(properties: new Dictionary<string, string>(), projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[] { "{projectRoot}/bin/*.nupkg", "{projectRoot}/obj" },
|
|
targets["pack"].Outputs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pack_ArtifactsLayout_EmitsWorkspaceRootPackageAndObjPaths()
|
|
{
|
|
var projectDirectory = ProjectDir("libs", "foo");
|
|
var properties = new Dictionary<string, string>
|
|
{
|
|
["UseArtifactsOutput"] = "true",
|
|
};
|
|
|
|
var targets = BuildTargets(properties, projectDirectory, projectName: "foo");
|
|
|
|
Assert.Equal(
|
|
new[] { "{workspaceRoot}/artifacts/package/*.nupkg", "{workspaceRoot}/artifacts/obj/foo" },
|
|
targets["pack"].Outputs);
|
|
}
|
|
}
|