dotnet-installer/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/DotNetHelper.cs

351 lines
13 KiB
C#
Raw Normal View History

// 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.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.SourceBuild.SmokeTests;
internal class DotNetHelper
{
private static readonly object s_lockObj = new();
public static string DotNetPath { get; } = Path.Combine(Config.DotNetDirectory, "dotnet");
public static string LogsDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), "logs");
public static string PackagesDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), "packages");
public static string ProjectsDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), $"projects-{DateTime.Now:yyyyMMddHHmmssffff}");
private ITestOutputHelper OutputHelper { get; }
public bool IsMonoRuntime { get; }
public DotNetHelper(ITestOutputHelper outputHelper)
{
OutputHelper = outputHelper;
lock (s_lockObj)
{
if (!Directory.Exists(Config.DotNetDirectory))
{
if (!File.Exists(Config.SdkTarballPath))
{
throw new InvalidOperationException($"Tarball path '{Config.SdkTarballPath}' specified in {Config.SdkTarballPath} does not exist.");
}
Directory.CreateDirectory(Config.DotNetDirectory);
Utilities.ExtractTarball(Config.SdkTarballPath, Config.DotNetDirectory, outputHelper);
}
IsMonoRuntime = DetermineIsMonoRuntime(Config.DotNetDirectory);
if (!Directory.Exists(ProjectsDirectory))
{
Directory.CreateDirectory(ProjectsDirectory);
InitNugetConfig();
}
if (!Directory.Exists(PackagesDirectory))
{
Directory.CreateDirectory(PackagesDirectory);
}
if (!Directory.Exists(LogsDirectory))
{
Directory.CreateDirectory(LogsDirectory);
}
}
}
private static void InitNugetConfig()
{
bool useLocalPackages = !string.IsNullOrEmpty(Config.PrereqsPath);
string nugetConfigPrefix = useLocalPackages ? "local" : "online";
string nugetConfigPath = Path.Combine(ProjectsDirectory, "NuGet.Config");
File.Copy(
Path.Combine(BaselineHelper.GetAssetsDirectory(), $"{nugetConfigPrefix}.NuGet.Config"),
nugetConfigPath);
if (useLocalPackages)
{
// When using local packages this feed is always required. It contains packages that are
// not produced by source-build but are required by the various project templates.
if (!Directory.Exists(Config.PrereqsPath))
{
throw new InvalidOperationException(
$"Prereqs path '{Config.PrereqsPath}' specified in {Config.PrereqsPathEnv} does not exist.");
}
string nugetConfig = File.ReadAllText(nugetConfigPath);
nugetConfig = nugetConfig.Replace("SMOKE_TEST_PACKAGE_FEED", Config.PrereqsPath);
// This package feed is optional. You can use an additional feed of source-built packages to run the
// smoke-tests as offline as possible.
if (Config.CustomPackagesPath != null)
{
if (!Directory.Exists(Config.CustomPackagesPath))
{
throw new ArgumentException($"Specified --with-packages {Config.CustomPackagesPath} does not exist.");
}
nugetConfig = nugetConfig.Replace("CUSTOM_PACKAGE_FEED", Config.CustomPackagesPath);
}
else
{
nugetConfig = string.Join(Environment.NewLine, nugetConfig.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Where(s => !s.Contains("CUSTOM_PACKAGE_FEED")).ToArray());
}
File.WriteAllText(nugetConfigPath, nugetConfig);
}
}
public void ExecuteCmd(string args, string? workingDirectory = null, Action<Process>? processConfigCallback = null,
int? expectedExitCode = 0, int millisecondTimeout = -1)
{
(Process Process, string StdOut, string StdErr) executeResult = ExecuteHelper.ExecuteProcess(
DotNetPath,
args,
OutputHelper,
configureCallback: (process) => configureProcess(process, workingDirectory),
millisecondTimeout: millisecondTimeout);
if (expectedExitCode != null) {
ExecuteHelper.ValidateExitCode(executeResult, (int) expectedExitCode);
}
void configureProcess(Process process, string? workingDirectory)
{
ConfigureProcess(process, workingDirectory);
processConfigCallback?.Invoke(process);
}
}
public static void ConfigureProcess(Process process, string? workingDirectory, bool setPath = false)
{
if (workingDirectory != null)
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
process.StartInfo.EnvironmentVariables["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
process.StartInfo.EnvironmentVariables["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
process.StartInfo.EnvironmentVariables["DOTNET_ROOT"] = Config.DotNetDirectory;
process.StartInfo.EnvironmentVariables["NUGET_PACKAGES"] = PackagesDirectory;
if (setPath)
{
process.StartInfo.EnvironmentVariables["PATH"] = $"{Config.DotNetDirectory}:{Environment.GetEnvironmentVariable("PATH")}";
}
}
public void ExecuteBuild(string projectName) =>
ExecuteCmd($"build {GetBinLogOption(projectName, "build")}", GetProjectDirectory(projectName));
/// <summary>
/// Create a new .NET project and return the path to the created project folder.
/// </summary>
public string ExecuteNew(string projectType, string name, string? language = null, string? customArgs = null)
{
string projectDirectory = GetProjectDirectory(name);
string options = $"--name {name} --output {projectDirectory}";
if (language != null)
{
options += $" --language \"{language}\"";
}
if (string.IsNullOrEmpty(customArgs))
{
options += $" {customArgs}";
}
ExecuteCmd($"new {projectType} {options}");
return projectDirectory;
}
public void ExecutePublish(string projectName, DotNetTemplate template, bool? selfContained = null, string? rid = null, bool trimmed = false, bool readyToRun = false)
{
string options = string.Empty;
string binlogDifferentiator = string.Empty;
if (selfContained.HasValue)
{
options += $"--self-contained {selfContained.Value.ToString().ToLowerInvariant()}";
if (selfContained.Value)
{
binlogDifferentiator += "self-contained";
if (!string.IsNullOrEmpty(rid))
{
options += $" -r {rid}";
binlogDifferentiator += $"-{rid}";
}
if (trimmed)
{
options += " /p:PublishTrimmed=true";
binlogDifferentiator += "-trimmed";
}
if (readyToRun)
{
options += " /p:PublishReadyToRun=true";
binlogDifferentiator += "-R2R";
}
}
}
string projDir = GetProjectDirectory(projectName);
string publishDir = Path.Combine(projDir, "bin", "publish");
ExecuteCmd(
$"publish {options} {GetBinLogOption(projectName, "publish", binlogDifferentiator)} -o {publishDir}",
projDir);
if (template == DotNetTemplate.Console)
{
ExecuteCmd($"{projectName}.dll", publishDir, expectedExitCode: 0);
}
else if (template == DotNetTemplate.ClassLib || template == DotNetTemplate.BlazorWasm)
{
// Can't run the published output of classlib (no entrypoint) or WASM (needs a server)
}
// Assume it is a web-based template
else
{
ExecuteWebDll(projectName, publishDir, template);
}
}
public void ExecuteRun(string projectName) =>
ExecuteCmd($"run {GetBinLogOption(projectName, "run")}", GetProjectDirectory(projectName));
public void ExecuteRunWeb(string projectName, DotNetTemplate template)
{
// 'dotnet run' exit code differs between CoreCLR and Mono (https://github.com/dotnet/sdk/issues/30095).
int expectedExitCode = IsMonoRuntime ? 143 : 0;
ExecuteWeb(
projectName,
$"run --no-launch-profile {GetBinLogOption(projectName, "run")}",
GetProjectDirectory(projectName),
template,
expectedExitCode);
}
public void ExecuteWebDll(string projectName, string workingDirectory, DotNetTemplate template) =>
ExecuteWeb(projectName, $"{projectName}.dll", workingDirectory, template, expectedExitCode: 0);
public void ExecuteTest(string projectName) =>
ExecuteCmd($"test {GetBinLogOption(projectName, "test")}", GetProjectDirectory(projectName));
private void ExecuteWeb(string projectName, string args, string workingDirectory, DotNetTemplate template, int expectedExitCode)
{
WebAppValidator validator = new(OutputHelper, template);
ExecuteCmd(
args,
workingDirectory,
processConfigCallback: validator.Validate,
expectedExitCode: expectedExitCode,
millisecondTimeout: 30000);
Assert.True(validator.IsValidated);
if (validator.ValidationException is not null)
{
throw validator.ValidationException;
}
}
private static string GetBinLogOption(string projectName, string command, string? differentiator = null)
{
string fileName = $"{projectName}-{command}";
if (!string.IsNullOrEmpty(differentiator))
{
fileName += $"-{differentiator}";
}
return $"/bl:{Path.Combine(LogsDirectory, $"{fileName}.binlog")}";
}
private static bool DetermineIsMonoRuntime(string dotnetRoot)
{
string sharedFrameworkRoot = Path.Combine(dotnetRoot, "shared", "Microsoft.NETCore.App");
if (!Directory.Exists(sharedFrameworkRoot))
{
return false;
}
string? version = Directory.GetDirectories(sharedFrameworkRoot).FirstOrDefault();
if (version is null)
{
return false;
}
string sharedFramework = Path.Combine(sharedFrameworkRoot, version);
// Check the presence of one of the mono header files.
return File.Exists(Path.Combine(sharedFramework, "mono-gc.h"));
}
private static string GetProjectDirectory(string projectName) => Path.Combine(ProjectsDirectory, projectName);
private class WebAppValidator
{
private readonly ITestOutputHelper _outputHelper;
private readonly DotNetTemplate _template;
public WebAppValidator(ITestOutputHelper outputHelper, DotNetTemplate template)
{
_outputHelper = outputHelper;
_template = template;
}
public bool IsValidated { get; set; }
public Exception? ValidationException { get; set; }
private static int GetAvailablePort()
{
TcpListener listener = new(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
public void Validate(Process process)
{
int port = GetAvailablePort();
process.StartInfo.EnvironmentVariables.Add("ASPNETCORE_HTTP_PORTS", port.ToString());
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
{
try
{
if (e.Data?.Contains("Application started. Press Ctrl+C to shut down.") ?? false)
{
_outputHelper.WriteLine("Detected app has started. Sending web request to validate...");
using HttpClient httpClient = new();
string url = $"http://localhost:{port}";
if (_template == DotNetTemplate.WebApi)
{
url += "/WeatherForecast";
}
using HttpResponseMessage resultMsg = httpClient.GetAsync(new Uri(url)).Result;
_outputHelper.WriteLine($"Status code returned: {resultMsg.StatusCode}");
resultMsg.EnsureSuccessStatusCode();
IsValidated = true;
ExecuteHelper.ExecuteProcessValidateExitCode("kill", $"-s TERM {process.Id}", _outputHelper);
}
}
catch (Exception ex)
{
ValidationException = ex;
}
});
}
}
}