|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests
{
public abstract partial class DotNetSdkTestBase : TestBase
{
public sealed class DotNetSdkAvailable : ExecutionCondition
{
public override bool ShouldSkip => s_dotnetSdkPath == null;
public override string SkipReason => "The location of dotnet SDK can't be determined (DOTNET_INSTALL_DIR environment variable is unset)";
}
private static readonly string s_dotnetExeName;
private static readonly string? s_dotnetInstallDir;
private static readonly string s_dotnetSdkVersion;
private static readonly string? s_dotnetSdkPath;
private static readonly string s_projectSource =
@"<Project Sdk='Microsoft.NET.Sdk'>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
";
private static readonly string s_classSource =
@"using System;
public class TestClass
{
public void F()
{
Console.WriteLine(123);
}
}
";
protected readonly ITestOutputHelper TestOutputHelper;
protected readonly TempDirectory ProjectDir;
protected readonly TempDirectory ObjDir;
protected readonly TempDirectory OutDir;
protected readonly TempFile Project;
protected readonly string ProjectName;
protected readonly string ProjectFileName;
protected readonly string Configuration;
protected readonly string TargetFramework;
protected readonly string DotNetPath;
protected readonly IReadOnlyDictionary<string, string> EnvironmentVariables;
private static string GetSdkPath(string dotnetInstallDir, string version)
=> Path.Combine(dotnetInstallDir, "sdk", version);
static DotNetSdkTestBase()
{
s_dotnetExeName = "dotnet" + (Path.DirectorySeparatorChar == '/' ? "" : ".exe");
s_dotnetSdkVersion = typeof(DotNetSdkTests).Assembly.GetCustomAttribute<DotNetSdkVersionAttribute>()?.Version
?? throw new InvalidOperationException($"Couldn't find {nameof(DotNetSdkVersionAttribute)}");
static bool isMatchingDotNetInstance(string? dotnetDir)
=> dotnetDir != null && File.Exists(Path.Combine(dotnetDir, s_dotnetExeName)) && Directory.Exists(GetSdkPath(dotnetDir, s_dotnetSdkVersion));
var dotnetInstallDir = Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR");
if (!isMatchingDotNetInstance(dotnetInstallDir))
{
dotnetInstallDir = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator).FirstOrDefault(isMatchingDotNetInstance);
}
if (dotnetInstallDir != null)
{
s_dotnetInstallDir = dotnetInstallDir;
s_dotnetSdkPath = GetSdkPath(dotnetInstallDir, s_dotnetSdkVersion);
}
}
private const string EmptyValueMarker = "--{empty}--";
private static void EmitTestHelperProps(
string objDirectory,
string projectFileName,
string? content,
ArtifactUploadUtil? uploadUtil)
{
// Common.props automatically import {project-name}.*.props files from MSBuildProjectExtensionsPath directory,
// which is by default set to the IntermediateOutputPath:
var filePath = Path.Combine(objDirectory, projectFileName + ".TestHelpers.g.props");
File.WriteAllText(filePath,
$@"<Project>
{content}
</Project>");
uploadUtil?.AddFile(filePath);
}
private static void EmitTestHelperTargets(
string objDirectory,
string outputFile,
string projectFileName,
IEnumerable<string> expressions,
string? additionalContent,
ArtifactUploadUtil? uploadUtil)
{
// Common.targets automatically import {project-name}.*.targets files from MSBuildProjectExtensionsPath directory,
// which is by defautl set to the IntermediateOutputPath:
var filePath = Path.Combine(objDirectory, projectFileName + ".TestHelpers.g.targets");
File.WriteAllText(filePath,
$@"<Project>
<Target Name=""Test_EvaluateExpressions"">
<PropertyGroup>
{string.Join(Environment.NewLine + " ", expressions.SelectWithIndex((e, i) => $@"<_Value{i}>{e}</_Value{i}><_Value{i} Condition=""'$(_Value{i})' == ''"">{EmptyValueMarker}</_Value{i}>"))}
</PropertyGroup>
<ItemGroup>
<LinesToWrite Include=""{string.Join(";", expressions.SelectWithIndex((e, i) => $"$(_Value{i})"))}""/>
</ItemGroup>
<MakeDir Directories=""{Path.GetDirectoryName(outputFile)}"" />
<WriteLinesToFile File=""{outputFile}""
Lines=""@(LinesToWrite)""
Overwrite=""true""
Encoding=""UTF-8"" />
</Target>
<!-- Overwrite CoreCompile target to avoid triggering the compiler -->
<Target Name=""CoreCompile""
DependsOnTargets=""$(CoreCompileDependsOn);_BeforeVBCSCoreCompile"">
</Target>
<Target Name=""InitializeSourceControlInformation""/>
{additionalContent}
</Project>");
uploadUtil?.AddFile(filePath);
}
public DotNetSdkTestBase(ITestOutputHelper testOutputHelper)
{
Assert.True(s_dotnetInstallDir is object, $"SDK not found. Use {nameof(ConditionalFactAttribute)}(typeof({nameof(DotNetSdkAvailable)})) to skip the test if the SDK is not found.");
Debug.Assert(s_dotnetInstallDir is object);
DotNetPath = Path.Combine(s_dotnetInstallDir, s_dotnetExeName);
var testBinDirectory = Path.GetDirectoryName(typeof(DotNetSdkTests).Assembly.Location) ?? string.Empty;
var sdksDir = Path.Combine(s_dotnetSdkPath ?? string.Empty, "Sdks");
TestOutputHelper = testOutputHelper;
ProjectName = "test";
ProjectFileName = ProjectName + ".csproj";
Configuration = "Debug";
TargetFramework = "netstandard2.0";
ProjectDir = Temp.CreateDirectory();
ObjDir = ProjectDir.CreateDirectory("obj");
OutDir = ProjectDir.CreateDirectory("bin").CreateDirectory(Configuration).CreateDirectory(TargetFramework);
Project = ProjectDir.CreateFile(ProjectFileName).WriteAllText(s_projectSource);
ProjectDir.CreateFile("TestClass.cs").WriteAllText(s_classSource);
// avoid accidental dependency on files outside of the project directory:
ProjectDir.CreateFile("Directory.Build.props").WriteAllText("<Project/>");
ProjectDir.CreateFile("Directory.Build.targets").WriteAllText("<Project/>");
ProjectDir.CreateFile(".editorconfig").WriteAllText("root = true");
var csharpCoreTargets = Path.Combine(testBinDirectory, "Microsoft.CSharp.Core.targets");
var visualBasicCoreTargets = Path.Combine(testBinDirectory, "Microsoft.VisualBasic.Core.targets");
Assert.True(File.Exists(csharpCoreTargets));
Assert.True(File.Exists(visualBasicCoreTargets));
EnvironmentVariables = new Dictionary<string, string>()
{
{ "CSharpCoreTargetsPath", csharpCoreTargets },
{ "VisualBasicCoreTargetsPath", visualBasicCoreTargets },
{ "MSBuildSDKsPath", sdksDir },
{ "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR", sdksDir },
// Disable the server until it's production ready so it doesn't cause CI flakiness
{ "DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER", "1" }
};
RunMSBuild(
Project.Path,
"/t:restore",
binlogName: "restore.binlog",
additionalEnvironmentVars: EnvironmentVariables);
Assert.True(File.Exists(Path.Combine(ObjDir.Path, "project.assets.json")));
Assert.True(File.Exists(Path.Combine(ObjDir.Path, ProjectFileName + ".nuget.g.props")));
Assert.True(File.Exists(Path.Combine(ObjDir.Path, ProjectFileName + ".nuget.g.targets")));
}
protected void RunMSBuild(
string projectFilePath,
string arguments,
string? binlogName = null,
IEnumerable<KeyValuePair<string, string>>? additionalEnvironmentVars = null)
{
var workingDirectory = Path.GetDirectoryName(projectFilePath)!;
using var uploadUtil = new ArtifactUploadUtil(TestOutputHelper);
uploadUtil.AddDirectory(workingDirectory);
var projectFileName = Path.GetFileName(projectFilePath);
binlogName ??= $"{Guid.NewGuid()}.binlog";
arguments = $@"msbuild /bl:{binlogName} ""{projectFileName}"" {arguments}";
var result = ProcessUtilities.Run(DotNetPath, arguments, workingDirectory, additionalEnvironmentVars);
Assert.True(result.ExitCode == 0, $"MSBuild failed with exit code {result.ExitCode}: {result.Output}");
uploadUtil.SetSucceeded();
}
protected void VerifyValues(string? customProps, string? customTargets, string[] targets, string[] expressions, string[] expectedResults)
{
using var uploadUtil = new ArtifactUploadUtil(TestOutputHelper);
var evaluationResultsFile = Path.Combine(OutDir.Path, "EvaluationResult.txt");
EmitTestHelperProps(ObjDir.Path, ProjectFileName, customProps, uploadUtil);
EmitTestHelperTargets(ObjDir.Path, evaluationResultsFile, ProjectFileName, expressions, customTargets, uploadUtil);
var targetsArg = string.Join(";", targets.Concat(new[] { "Test_EvaluateExpressions" }));
var testBinDirectory = Path.GetDirectoryName(typeof(DotNetSdkTests).Assembly.Location);
// RoslynTargetsPath is a path to the built-in Roslyn compilers in the .NET SDK.
// For testing we are using compilers from custom location (this emulates usage of Microsoft.Net.Compilers package.
// The core targets should be imported from CSharpCoreTargetsPath and VisualBasicCoreTargetsPath and the compiler tasks from the same location.
RunMSBuild(
Project.Path,
arguments: $@"/t:{targetsArg} /p:RoslynTargetsPath=""<nonexistent directory>"" /p:Configuration={Configuration}",
additionalEnvironmentVars: EnvironmentVariables);
var evaluationResult = File.ReadAllLines(evaluationResultsFile).Select(l => (l != EmptyValueMarker) ? l : "");
AssertEx.Equal(expectedResults, evaluationResult);
uploadUtil.SetSucceeded();
}
}
}
|