|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.Runtime.CompilerServices;
namespace Microsoft.NET.Build.Tests
{
using System.Runtime.InteropServices;
using ArtifactsTestExtensions;
public class ArtifactsOutputPathTests : SdkTest
{
public ArtifactsOutputPathTests(ITestOutputHelper log) : base(log)
{
}
(List<TestProject> testProjects, TestAsset testAsset) GetTestProjects(bool putArtifactsInProjectFolder = false, [CallerMemberName] string callingMethod = "")
{
var testProject1 = new TestProject()
{
Name = "App1",
IsExe = true
};
var testProject2 = new TestProject()
{
Name = "App2",
IsExe = true
};
var testLibraryProject = new TestProject()
{
Name = "Library",
};
testProject1.ReferencedProjects.Add(testLibraryProject);
testProject2.ReferencedProjects.Add(testLibraryProject);
List<TestProject> testProjects = new() { testProject1, testProject2, testLibraryProject };
foreach (var testProject in testProjects)
{
testProject.UseArtifactsOutput = true;
}
var testAsset = _testAssetsManager.CreateTestProjects(testProjects, callingMethod: callingMethod, identifier: putArtifactsInProjectFolder.ToString());
if (putArtifactsInProjectFolder)
{
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
"""
<Project>
<PropertyGroup>
<ArtifactsPath>$(MSBuildProjectDirectory)\artifacts</ArtifactsPath>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
</PropertyGroup>
</Project>
""");
}
else
{
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
"""
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
""");
}
return (testProjects, testAsset);
}
[Fact]
public void ItUsesArtifactsOutputPathForBuild()
{
var (testProjects, testAsset) = GetTestProjects();
new DotnetCommand(Log, "build")
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
ValidateIntermediatePaths(testAsset, testProjects);
foreach (var testProject in testProjects)
{
OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(), testProject.Name + ".dll"))
.Should()
.Exist();
}
}
[Fact(Skip = "https://github.com/dotnet/sdk/issues/45057")]
public void ItUsesArtifactsOutputPathForPublish()
{
var (testProjects, testAsset) = GetTestProjects();
new DotnetCommand(Log, "publish")
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
ValidateIntermediatePaths(testAsset, testProjects, "release");
foreach (var testProject in testProjects)
{
OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(configuration: "release"), testProject.Name + ".dll"))
.Should()
.Exist();
new FileInfo(Path.Combine(outputPathCalculator.GetPublishDirectory(configuration: "release"), testProject.Name + ".dll"))
.Should()
.Exist();
}
}
[Fact]
public void ItUseArtifactsOutputPathForPack()
{
var (testProjects, testAsset) = GetTestProjects();
new DotnetCommand(Log, "pack")
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
ValidateIntermediatePaths(testAsset, testProjects, "release");
foreach (var testProject in testProjects)
{
OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(configuration: "release"), testProject.Name + ".dll"))
.Should()
.Exist();
new FileInfo(Path.Combine(outputPathCalculator.GetPackageDirectory(configuration: "release"), testProject.Name + ".1.0.0.nupkg"))
.Should()
.Exist();
}
}
void ValidateIntermediatePaths(TestAsset testAsset, IEnumerable<TestProject> testProjects, string configuration = "debug")
{
foreach (var testProject in testProjects)
{
new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name))
.Should()
.NotHaveSubDirectories();
new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts", "obj", testProject.Name, configuration))
.Should()
.Exist();
}
}
[Fact]
public void ArtifactsPathCanBeInProjectFolder()
{
var (testProjects, testAsset) = GetTestProjects(putArtifactsInProjectFolder: true);
new DotnetCommand(Log, "build")
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
foreach (var testProject in testProjects)
{
var outputPathCalculator = OutputPathCalculator.FromProject(testAsset.Path, testProject);
outputPathCalculator.IncludeProjectNameInArtifactsPaths = false;
outputPathCalculator.ArtifactsPath = Path.Combine(testAsset.Path, testProject.Name, "artifacts");
new DirectoryInfo(outputPathCalculator.GetIntermediateDirectory())
.Should()
.Exist();
new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(), testProject.Name + ".dll"))
.Should()
.Exist();
}
}
[Fact]
public void ProjectsCanSwitchOutputFormats()
{
var testProject = new TestProject()
{
IsExe = true,
};
var testAsset = _testAssetsManager.CreateTestProject(testProject);
// Build without artifacts format
new BuildCommand(testAsset)
.Execute()
.Should()
.Pass();
new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetOutputDirectory())
.Should()
.Exist();
// Now add a Directory.Build.props file setting UseArtifactsOutput to true
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"), """
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
""");
new BuildCommand(testAsset)
.Execute()
.Should()
.Pass();
new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetOutputDirectory())
.Should()
.Exist();
// Now go back to not using artifacts output format
File.Delete(Path.Combine(testAsset.Path, "Directory.Build.props"));
new BuildCommand(testAsset)
.Execute()
.Should()
.Pass();
}
[Fact]
public void ProjectsCanCustomizeOutputPathBasedOnTargetFramework()
{
var testProject = new TestProject("CustomizeArtifactsPath")
{
IsExe = true,
TargetFrameworks = "net7.0;net8.0;netstandard2.0"
};
var testAsset = _testAssetsManager.CreateTestProject(testProject);
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"), """
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
<AfterTargetFrameworkInferenceTargets>$(MSBuildThisFileDirectory)\Directory.AfterTargetFrameworkInference.targets</AfterTargetFrameworkInferenceTargets>
</PropertyGroup>
</Project>
""");
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.AfterTargetFrameworkInference.targets"), """
<Project>
<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<ArtifactsPivots Condition="'$(_TargetFrameworkVersionWithoutV)' == '8.0'">NET8_$(Configuration)</ArtifactsPivots>
<ArtifactsPivots Condition="'$(_TargetFrameworkVersionWithoutV)' == '7.0'">NET7_$(Configuration)</ArtifactsPivots>
</PropertyGroup>
</Project>
""");
new BuildCommand(testAsset)
.Execute()
.Should()
.Pass();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "NET8_Debug")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "NET7_Debug")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_netstandard2.0")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_net8.0")).Should().NotExist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_net7.0")).Should().NotExist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "NET8_Debug")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "NET7_Debug")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_netstandard2.0")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_net8.0")).Should().NotExist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_net7.0")).Should().NotExist();
foreach (var targetFramework in testProject.TargetFrameworks.Split(';'))
{
new DotnetPublishCommand(Log, "-f", targetFramework)
.WithWorkingDirectory(Path.Combine(testAsset.Path, testProject.Name))
.Execute()
.Should()
.Pass();
}
// Note that publish defaults to release configuration for .NET 8 but not prior TargetFrameworks
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "NET8_Release")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "NET7_Debug")).Should().Exist();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "debug_netstandard2.0")).Should().Exist();
new DotnetPackCommand(Log)
.WithWorkingDirectory(Path.Combine(testAsset.Path, testProject.Name))
.Execute()
.Should()
.Pass();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "package", "release")).Should().Exist();
new FileInfo(Path.Combine(testAsset.Path, "artifacts", "package", "release", testProject.Name + ".1.0.0.nupkg")).Should().Exist();
}
TestAsset CreateCustomizedTestProject(string propertyName, string propertyValue, [CallerMemberName] string callingMethod = "")
{
var testProject = new TestProject("App")
{
IsExe = true,
UseArtifactsOutput = true
};
var testAsset = _testAssetsManager.CreateTestProjects(new[] { testProject }, callingMethod: callingMethod);
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
$"""
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
<{propertyName}>{propertyValue}</{propertyName}>
</PropertyGroup>
</Project>
""");
return testAsset;
}
[Fact]
public void ArtifactsPathCanBeSet()
{
var artifactsFolder = _testAssetsManager.CreateTestDirectory(identifier: "ArtifactsPath").Path;
var testAsset = CreateCustomizedTestProject("ArtifactsPath", artifactsFolder);
new DotnetBuildCommand(testAsset)
.Execute()
.Should()
.Pass();
// If ArtifactsPath is set, even in the project file itself, we still include the project name in the path,
// as the path used is likely to be shared between multiple projects
new FileInfo(Path.Combine(artifactsFolder, "bin", "App", "debug", "App.dll"))
.Should()
.Exist();
}
[Fact]
public void BinOutputNameCanBeSet()
{
var testAsset = CreateCustomizedTestProject("ArtifactsBinOutputName", "binaries");
new DotnetBuildCommand(testAsset)
.Execute()
.Should()
.Pass();
new FileInfo(Path.Combine(testAsset.Path, "artifacts", "binaries", "App", "debug", "App.dll"))
.Should()
.Exist();
}
[Fact]
public void PublishOutputNameCanBeSet()
{
var testAsset = CreateCustomizedTestProject("ArtifactsPublishOutputName", "published_app");
new DotnetPublishCommand(Log)
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
new FileInfo(Path.Combine(testAsset.Path, "artifacts", "published_app", "App", "release", "App.dll"))
.Should()
.Exist();
}
[Fact]
public void PackageOutputNameCanBeSet()
{
var testAsset = CreateCustomizedTestProject("ArtifactsPackageOutputName", "package_output");
new DotnetPackCommand(Log)
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
new FileInfo(Path.Combine(testAsset.Path, "artifacts", "package_output", "release", "App.1.0.0.nupkg"))
.Should()
.Exist();
}
[Fact]
public void ProjectNameCanBeSet()
{
var testAsset = CreateCustomizedTestProject("ArtifactsProjectName", "Apps\\MyApp");
new DotnetBuildCommand(Log)
.WithWorkingDirectory(testAsset.Path)
.Execute()
.Should()
.Pass();
new FileInfo(Path.Combine(testAsset.Path, "artifacts", "bin", "Apps", "MyApp", "debug", "App.dll"))
.Should()
.Exist();
}
[Fact]
public void PackageValidationSucceeds()
{
var testProject = new TestProject()
{
TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net7.0"
};
testProject.AdditionalProperties["EnablePackageValidation"] = "True";
testProject.UseArtifactsOutput = true;
var testAsset = _testAssetsManager.CreateTestProject(testProject);
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
$"""
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
""");
new DotnetPackCommand(Log)
.WithWorkingDirectory(Path.Combine(testAsset.TestRoot, testProject.Name))
.Execute()
.Should()
.Pass();
}
[Fact]
public void ItErrorsIfArtifactsPathIsSetInProject()
{
var testProject = new TestProject();
testProject.AdditionalProperties["ArtifactsPath"] = "$(MSBuildThisFileDirectory)\\..\\artifacts";
var testAsset = _testAssetsManager.CreateTestProject(testProject);
new BuildCommand(testAsset)
.Execute()
.Should()
.Fail()
.And
.HaveStdOutContaining("NETSDK1199");
new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts"))
.Should()
.NotExist();
}
[Fact]
public void ItErrorsIfUseArtifactsOutputIsSetInProject()
{
var testProject = new TestProject();
testProject.AdditionalProperties["UseArtifactsOutput"] = "true";
var testAsset = _testAssetsManager.CreateTestProject(testProject);
new BuildCommand(testAsset)
.Execute()
.Should()
.Fail()
.And
.HaveStdOutContaining("NETSDK1199");
new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name, "artifacts"))
.Should()
.NotExist();
}
[Fact]
public void ItErrorsIfUseArtifactsOutputIsSetAndThereIsNoDirectoryBuildProps()
{
var testProject = new TestProject();
var testAsset = _testAssetsManager.CreateTestProject(testProject);
new BuildCommand(testAsset)
.DisableDirectoryBuildProps()
.Execute("/p:UseArtifactsOutput=true")
.Should()
.Fail()
.And
.HaveStdOutContaining("NETSDK1200");
}
[Fact(Skip = "https://github.com/dotnet/sdk/issues/40160")]
public void ItCanBuildWithMicrosoftBuildArtifactsSdk()
{
var testAsset = _testAssetsManager.CopyTestAsset("ArtifactsSdkTest")
.WithSource();
new DotnetBuildCommand(testAsset)
.Execute()
.Should()
.Pass();
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "MSBuildSdk", ToolsetInfo.CurrentTargetFramework))
.Should()
.OnlyHaveFiles(new[] { "MSBuildSdk.dll" });
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Microsoft.Build.Artifacts doesn't appear to copy the (extensionless) executable to the artifacts folder on non-Windows platforms
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "PackageReference", ToolsetInfo.CurrentTargetFramework))
.Should()
.OnlyHaveFiles(new[] { "PackageReference.dll", $"PackageReference{EnvironmentInfo.ExecutableExtension}" });
}
else
{
new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "PackageReference", ToolsetInfo.CurrentTargetFramework))
.Should()
.OnlyHaveFiles(new[] { "PackageReference.dll" });
}
// Verify that default bin and obj folders still exist (which wouldn't be the case if using the .NET SDKs artifacts output functianality
new FileInfo(Path.Combine(testAsset.Path, "MSBuildSdk", "bin", "Debug", ToolsetInfo.CurrentTargetFramework, "MSBuildSdk.dll")).Should().Exist();
new FileInfo(Path.Combine(testAsset.Path, "MSBuildSdk", "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "MSBuildSdk.dll")).Should().Exist();
}
[Fact(Skip = "https://github.com/dotnet/sdk/issues/50140")]
public void PublishingRegistersWrittenFilesForProperCleanup()
{
var testProject = new TestProject()
{
IsExe = true,
UseArtifactsOutput = true,
};
var hostfxrName =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "hostfxr.dll" :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "libhostfxr.so" :
"libhostfxr.dylib";
var testAsset = _testAssetsManager.CreateTestProject(testProject);
// Now add a Directory.Build.props file setting UseArtifactsOutput to true
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"), """
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
""");
var projectDir = Path.Combine(testAsset.Path, testAsset.TestProject.Name);
// publish the app
// we publish self-contained so that we include hostfxr.dll.
// if we don't clean up this file, when we build in Release,
// the generated exe will pick up the hostfxr and fail to run.
// so the only way to successfully run the exe is to clean up
// the hostfxr.dll and other self-contained files.
new DotnetPublishCommand(Log)
.WithWorkingDirectory(projectDir)
.Execute("--self-contained")
.Should()
.Pass();
var outputDir = new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetOutputDirectory(configuration: "release"));
outputDir.Should().Exist().And.HaveFile(hostfxrName);
LocateAndRunApp(outputDir);
var publishDir = new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetPublishDirectory(configuration: "release"));
publishDir.Should().Exist().And.HaveFile(hostfxrName);
LocateAndRunApp(publishDir);
// now build the app in Release configuration.
// not self-contained, so that we are forced to clean up the runtime
// files that were published.
new DotnetBuildCommand(Log)
.WithWorkingDirectory(projectDir)
.Execute("-c", "Release")
.Should()
.Pass();
outputDir.Should().Exist();
outputDir.Should().NotHaveFiles([hostfxrName]);
LocateAndRunApp(outputDir);
void LocateAndRunApp(DirectoryInfo root)
{
var appBinaryName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"{testProject.Name}.exe"
: testProject.Name;
root.Should().HaveFiles([appBinaryName]);
var binary = root.GetFiles(appBinaryName).First();
new RunExeCommand(Log, binary.FullName)
.Execute()
.Should()
.Pass();
}
}
}
namespace ArtifactsTestExtensions
{
static class Extensions
{
public static TestCommand DisableDirectoryBuildProps(this TestCommand command)
{
// There is an empty Directory.Build.props file in the test execution root, to stop other files further up in the repo from
// impacting the tests. So if a project set UseArtifactsOutput to true, the logic would find that file and put the output
// in that folder. To simulate the situation where there is no Directory.Build.props, we turn it off via an environment
// variable.
return command.WithEnvironmentVariable("ImportDirectoryBuildProps", "false");
}
}
}
}
|