Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add installer tests project #46927

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;

namespace Microsoft.DotNet.Installer.Tests;

public static class Config
{
public static string AssetsDirectory { get; } = GetRuntimeConfig(AssetsDirectorySwitch);
const string AssetsDirectorySwitch = RuntimeConfigSwitchPrefix + nameof(AssetsDirectory);

public static string PackagesDirectory { get; } = GetRuntimeConfig(PackagesDirectorySwitch);
const string PackagesDirectorySwitch = RuntimeConfigSwitchPrefix + nameof(PackagesDirectory);

public static string ScenarioTestsNuGetConfigPath { get; } = GetRuntimeConfig(ScenarioTestsNuGetConfigSwitch);
const string ScenarioTestsNuGetConfigSwitch = RuntimeConfigSwitchPrefix + nameof(ScenarioTestsNuGetConfigPath);

public static string Architecture { get; } = GetRuntimeConfig(ArchitectureSwitch);
const string ArchitectureSwitch = RuntimeConfigSwitchPrefix + nameof(Architecture);

public static bool TestRpmPackages { get; } = TryGetRuntimeConfig(TestRpmPackagesSwitch, out bool value) ? value : false;
const string TestRpmPackagesSwitch = RuntimeConfigSwitchPrefix + nameof(TestRpmPackages);

public static bool TestDebPackages { get; } = TryGetRuntimeConfig(TestDebPackagesSwitch, out bool value) ? value : false;
const string TestDebPackagesSwitch = RuntimeConfigSwitchPrefix + nameof(TestDebPackages);

public const string RuntimeConfigSwitchPrefix = "Microsoft.DotNet.Installer.Tests.";

public static string GetRuntimeConfig(string key)
{
return TryGetRuntimeConfig(key, out string? value) ? value : throw new InvalidOperationException($"Runtime config setting '{key}' must be specified");
}

public static bool TryGetRuntimeConfig(string key, out bool value)
{
string? rawValue = (string?)AppContext.GetData(key);
if (string.IsNullOrEmpty(rawValue))
{
value = default!;
return false;
}
value = bool.Parse(rawValue);
return true;
}

public static bool TryGetRuntimeConfig(string key, [NotNullWhen(true)] out string? value)
{
value = (string?)AppContext.GetData(key);
if (string.IsNullOrEmpty(value))
{
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// 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.Text;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Installer.Tests;

public class DockerHelper
{
public static string DockerOS => GetDockerOS();
public static string DockerArchitecture => GetDockerArch();
public static string ContainerWorkDir => IsLinuxContainerModeEnabled ? "/sandbox" : "c:\\sandbox";
public static bool IsLinuxContainerModeEnabled => string.Equals(DockerOS, "linux", StringComparison.OrdinalIgnoreCase);
public static string TestArtifactsDir { get; } = Path.Combine(Directory.GetCurrentDirectory(), "TestAppArtifacts");

private ITestOutputHelper OutputHelper { get; set; }

public DockerHelper(ITestOutputHelper outputHelper)
{
OutputHelper = outputHelper;
}

public void Build(
string tag,
string? dockerfile = null,
string? target = null,
string contextDir = ".",
bool pull = false,
string? platform = null,
params string[] buildArgs)
{
string buildArgsOption = string.Empty;
if (buildArgs != null)
{
foreach (string arg in buildArgs)
{
buildArgsOption += $" --build-arg {arg}";
}
}

string platformOption = string.Empty;
if (platform is not null)
{
platformOption = $" --platform {platform}";
}

string targetArg = target == null ? string.Empty : $" --target {target}";
string dockerfileArg = dockerfile == null ? string.Empty : $" -f {dockerfile}";
string pullArg = pull ? " --pull" : string.Empty;

ExecuteWithLogging($"build -t {tag}{targetArg}{buildArgsOption}{dockerfileArg}{pullArg}{platformOption} {contextDir}");
}


public static bool ContainerExists(string name) => ResourceExists("container", $"-f \"name={name}\"");

public static bool ContainerIsRunning(string name) => Execute($"inspect --format=\"{{{{.State.Running}}}}\" {name}") == "true";

public void Copy(string src, string dest) => ExecuteWithLogging($"cp {src} {dest}");

public void DeleteContainer(string container, bool captureLogs = false)
{
if (ContainerExists(container))
{
if (captureLogs)
{
ExecuteWithLogging($"logs {container}", ignoreErrors: true);
}

// If a container is already stopped, running `docker stop` again has no adverse effects.
// This prevents some issues where containers could fail to be forcibly removed while they're running.
// e.g. https://github.com/dotnet/dotnet-docker/issues/5127
StopContainer(container);

ExecuteWithLogging($"container rm -f {container}");
}
}

public void DeleteImage(string tag)
{
if (ImageExists(tag))
{
ExecuteWithLogging($"image rm -f {tag}");
}
}

private void StopContainer(string container)
{
if (ContainerExists(container))
{
ExecuteWithLogging($"stop {container}", autoRetry: true);
}
}

private static string Execute(
string args, bool ignoreErrors = false, bool autoRetry = false, ITestOutputHelper? outputHelper = null)
{
(Process Process, string StdOut, string StdErr) result;
if (autoRetry)
{
result = ExecuteWithRetry(args, outputHelper!, ExecuteProcess);
}
else
{
result = ExecuteProcess(args, outputHelper!);
}

if (!ignoreErrors && result.Process.ExitCode != 0)
{
ProcessStartInfo startInfo = result.Process.StartInfo;
string msg = $"Failed to execute {startInfo.FileName} {startInfo.Arguments}" +
$"{Environment.NewLine}Exit code: {result.Process.ExitCode}" +
$"{Environment.NewLine}Standard Error: {result.StdErr}";
throw new InvalidOperationException(msg);
}

return result.StdOut;
}

private static (Process Process, string StdOut, string StdErr) ExecuteProcess(
string args, ITestOutputHelper outputHelper) => ExecuteProcess("docker", args, outputHelper);

private string ExecuteWithLogging(string args, bool ignoreErrors = false, bool autoRetry = false)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

OutputHelper.WriteLine($"Executing: docker {args}");
string result = Execute(args, outputHelper: OutputHelper, ignoreErrors: ignoreErrors, autoRetry: autoRetry);

stopwatch.Stop();
OutputHelper.WriteLine($"Execution Elapsed Time: {stopwatch.Elapsed}");

return result;
}

private static (Process Process, string StdOut, string StdErr) ExecuteWithRetry(
string args,
ITestOutputHelper outputHelper,
Func<string, ITestOutputHelper, (Process Process, string StdOut, string StdErr)> executor)
{
const int maxRetries = 5;
const int waitFactor = 5;

int retryCount = 0;

(Process Process, string StdOut, string StdErr) result = executor(args, outputHelper);
while (result.Process.ExitCode != 0)
{
retryCount++;
if (retryCount >= maxRetries)
{
break;
}

int waitTime = Convert.ToInt32(Math.Pow(waitFactor, retryCount - 1));
if (outputHelper != null)
{
outputHelper.WriteLine($"Retry {retryCount}/{maxRetries}, retrying in {waitTime} seconds...");
}

Thread.Sleep(waitTime * 1000);
result = executor(args, outputHelper!);
}

return result;
}

private static (Process Process, string StdOut, string StdErr) ExecuteProcess(
string fileName, string args, ITestOutputHelper outputHelper)
{
Process process = new Process
{
EnableRaisingEvents = true,
StartInfo =
{
FileName = fileName,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
}
};

StringBuilder stdOutput = new StringBuilder();
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) => stdOutput.AppendLine(e.Data));

StringBuilder stdError = new StringBuilder();
process.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => stdError.AppendLine(e.Data));

process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();

string output = stdOutput.ToString().Trim();
if (outputHelper != null && !string.IsNullOrWhiteSpace(output))
{
outputHelper.WriteLine(output);
}

string error = stdError.ToString().Trim();
if (outputHelper != null && !string.IsNullOrWhiteSpace(error))
{
outputHelper.WriteLine(error);
}

return (process, output, error);
}

private static string GetDockerOS() => Execute("version -f \"{{ .Server.Os }}\"");
private static string GetDockerArch() => Execute("version -f \"{{ .Server.Arch }}\"");

public string GetImageUser(string image) => ExecuteWithLogging($"inspect -f \"{{{{ .Config.User }}}}\" {image}");

public IDictionary<string, string> GetEnvironmentVariables(string image)
{
string envVarsStr = ExecuteWithLogging($"inspect -f \"{{{{json .Config.Env }}}}\" {image}");
JArray? envVarsArray = (JArray?)JsonConvert.DeserializeObject(envVarsStr);
return envVarsArray!
.ToDictionary(
item => item.ToString().Split('=')[0],
item => item.ToString().Split('=')[1]);
}

public static bool ImageExists(string tag) => ResourceExists("image", tag);

private static bool ResourceExists(string type, string filterArg)
{
string output = Execute($"{type} ls -a -q {filterArg}", true);
return output != "";
}

public string Run(
string image,
string name,
string? command = null,
string? workdir = null,
string? optionalRunArgs = null,
bool detach = false,
string? runAsUser = null,
bool skipAutoCleanup = false,
bool useMountedDockerSocket = false,
bool silenceOutput = false,
bool tty = true)
{
string cleanupArg = skipAutoCleanup ? string.Empty : " --rm";
string detachArg = detach ? " -d" : string.Empty;
string ttyArg = detach && tty ? " -t" : string.Empty;
string userArg = runAsUser != null ? $" -u {runAsUser}" : string.Empty;
string workdirArg = workdir == null ? string.Empty : $" -w {workdir}";
string mountedDockerSocketArg = useMountedDockerSocket ? " -v /var/run/docker.sock:/var/run/docker.sock" : string.Empty;
if (silenceOutput)
{
return Execute(
$"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}");
}
return ExecuteWithLogging(
$"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}", ignoreErrors: true);
}
}
Loading