Skip to content

Commit bc742b0

Browse files
Add installer tests project (#46927)
Co-authored-by: Michael Simons <msimons@microsoft.com>
1 parent 9827576 commit bc742b0

File tree

4 files changed

+727
-0
lines changed

4 files changed

+727
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.IO;
8+
using System.Text.RegularExpressions;
9+
10+
namespace Microsoft.DotNet.Installer.Tests;
11+
12+
public static class Config
13+
{
14+
public static string AssetsDirectory { get; } = GetRuntimeConfig(AssetsDirectorySwitch);
15+
const string AssetsDirectorySwitch = RuntimeConfigSwitchPrefix + nameof(AssetsDirectory);
16+
17+
public static string PackagesDirectory { get; } = GetRuntimeConfig(PackagesDirectorySwitch);
18+
const string PackagesDirectorySwitch = RuntimeConfigSwitchPrefix + nameof(PackagesDirectory);
19+
20+
public static string ScenarioTestsNuGetConfigPath { get; } = GetRuntimeConfig(ScenarioTestsNuGetConfigSwitch);
21+
const string ScenarioTestsNuGetConfigSwitch = RuntimeConfigSwitchPrefix + nameof(ScenarioTestsNuGetConfigPath);
22+
23+
public static string Architecture { get; } = GetRuntimeConfig(ArchitectureSwitch);
24+
const string ArchitectureSwitch = RuntimeConfigSwitchPrefix + nameof(Architecture);
25+
26+
public static bool TestRpmPackages { get; } = TryGetRuntimeConfig(TestRpmPackagesSwitch, out bool value) ? value : false;
27+
const string TestRpmPackagesSwitch = RuntimeConfigSwitchPrefix + nameof(TestRpmPackages);
28+
29+
public static bool TestDebPackages { get; } = TryGetRuntimeConfig(TestDebPackagesSwitch, out bool value) ? value : false;
30+
const string TestDebPackagesSwitch = RuntimeConfigSwitchPrefix + nameof(TestDebPackages);
31+
32+
public static bool KeepDockerImages { get; } = TryGetRuntimeConfig(KeepDockerImagesSwitch, out bool value) ? value : false;
33+
const string KeepDockerImagesSwitch = RuntimeConfigSwitchPrefix + nameof(KeepDockerImages);
34+
35+
public const string RuntimeConfigSwitchPrefix = "Microsoft.DotNet.Installer.Tests.";
36+
37+
public static string GetRuntimeConfig(string key)
38+
{
39+
return TryGetRuntimeConfig(key, out string? value) ? value : throw new InvalidOperationException($"Runtime config setting '{key}' must be specified");
40+
}
41+
42+
public static bool TryGetRuntimeConfig(string key, out bool value)
43+
{
44+
string? rawValue = (string?)AppContext.GetData(key);
45+
if (string.IsNullOrEmpty(rawValue))
46+
{
47+
value = default!;
48+
return false;
49+
}
50+
value = bool.Parse(rawValue);
51+
return true;
52+
}
53+
54+
public static bool TryGetRuntimeConfig(string key, [NotNullWhen(true)] out string? value)
55+
{
56+
value = (string?)AppContext.GetData(key);
57+
if (string.IsNullOrEmpty(value))
58+
{
59+
return false;
60+
}
61+
return true;
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Text;
11+
using System.Threading;
12+
using Xunit.Abstractions;
13+
14+
namespace Microsoft.DotNet.Installer.Tests;
15+
16+
public class DockerHelper
17+
{
18+
public static string DockerOS => GetDockerOS();
19+
public static string DockerArchitecture => GetDockerArch();
20+
public static string ContainerWorkDir => IsLinuxContainerModeEnabled ? "/sandbox" : "c:\\sandbox";
21+
public static bool IsLinuxContainerModeEnabled => string.Equals(DockerOS, "linux", StringComparison.OrdinalIgnoreCase);
22+
public static string TestArtifactsDir { get; } = Path.Combine(Directory.GetCurrentDirectory(), "TestAppArtifacts");
23+
24+
private ITestOutputHelper OutputHelper { get; set; }
25+
26+
public DockerHelper(ITestOutputHelper outputHelper)
27+
{
28+
OutputHelper = outputHelper;
29+
}
30+
31+
public void Build(
32+
string tag,
33+
string? dockerfile = null,
34+
string? target = null,
35+
string contextDir = ".",
36+
bool pull = false,
37+
string? platform = null,
38+
params string[] buildArgs)
39+
{
40+
string buildArgsOption = string.Empty;
41+
if (buildArgs != null)
42+
{
43+
foreach (string arg in buildArgs)
44+
{
45+
buildArgsOption += $" --build-arg {arg}";
46+
}
47+
}
48+
49+
string platformOption = string.Empty;
50+
if (platform is not null)
51+
{
52+
platformOption = $" --platform {platform}";
53+
}
54+
55+
string targetArg = target == null ? string.Empty : $" --target {target}";
56+
string dockerfileArg = dockerfile == null ? string.Empty : $" -f {dockerfile}";
57+
string pullArg = pull ? " --pull" : string.Empty;
58+
59+
ExecuteWithLogging($"build -t {tag}{targetArg}{buildArgsOption}{dockerfileArg}{pullArg}{platformOption} {contextDir}");
60+
}
61+
62+
63+
public static bool ContainerExists(string name) => ResourceExists("container", $"-f \"name={name}\"");
64+
65+
public static bool ContainerIsRunning(string name) => Execute($"inspect --format=\"{{{{.State.Running}}}}\" {name}") == "true";
66+
67+
public void Copy(string src, string dest) => ExecuteWithLogging($"cp {src} {dest}");
68+
69+
public void DeleteContainer(string container, bool captureLogs = false)
70+
{
71+
if (ContainerExists(container))
72+
{
73+
if (captureLogs)
74+
{
75+
ExecuteWithLogging($"logs {container}", ignoreErrors: true);
76+
}
77+
78+
// If a container is already stopped, running `docker stop` again has no adverse effects.
79+
// This prevents some issues where containers could fail to be forcibly removed while they're running.
80+
// e.g. https://github.com/dotnet/dotnet-docker/issues/5127
81+
StopContainer(container);
82+
83+
ExecuteWithLogging($"container rm -f {container}");
84+
}
85+
}
86+
87+
public void DeleteImage(string tag)
88+
{
89+
if (ImageExists(tag))
90+
{
91+
ExecuteWithLogging($"image rm -f {tag}");
92+
}
93+
}
94+
95+
private void StopContainer(string container)
96+
{
97+
if (ContainerExists(container))
98+
{
99+
ExecuteWithLogging($"stop {container}", autoRetry: true);
100+
}
101+
}
102+
103+
private static string Execute(
104+
string args, bool ignoreErrors = false, bool autoRetry = false, ITestOutputHelper? outputHelper = null)
105+
{
106+
(Process Process, string StdOut, string StdErr) result;
107+
if (autoRetry)
108+
{
109+
result = ExecuteWithRetry(args, outputHelper!, ExecuteProcess);
110+
}
111+
else
112+
{
113+
result = ExecuteProcess(args, outputHelper!);
114+
}
115+
116+
if (!ignoreErrors && result.Process.ExitCode != 0)
117+
{
118+
ProcessStartInfo startInfo = result.Process.StartInfo;
119+
string msg = $"Failed to execute {startInfo.FileName} {startInfo.Arguments}" +
120+
$"{Environment.NewLine}Exit code: {result.Process.ExitCode}" +
121+
$"{Environment.NewLine}Standard Error: {result.StdErr}";
122+
throw new InvalidOperationException(msg);
123+
}
124+
125+
return result.StdOut;
126+
}
127+
128+
private static (Process Process, string StdOut, string StdErr) ExecuteProcess(
129+
string args, ITestOutputHelper outputHelper) => ExecuteProcess("docker", args, outputHelper);
130+
131+
private string ExecuteWithLogging(string args, bool ignoreErrors = false, bool autoRetry = false)
132+
{
133+
Stopwatch stopwatch = new Stopwatch();
134+
stopwatch.Start();
135+
136+
OutputHelper.WriteLine($"Executing: docker {args}");
137+
string result = Execute(args, outputHelper: OutputHelper, ignoreErrors: ignoreErrors, autoRetry: autoRetry);
138+
139+
stopwatch.Stop();
140+
OutputHelper.WriteLine($"Execution Elapsed Time: {stopwatch.Elapsed}");
141+
142+
return result;
143+
}
144+
145+
private static (Process Process, string StdOut, string StdErr) ExecuteWithRetry(
146+
string args,
147+
ITestOutputHelper outputHelper,
148+
Func<string, ITestOutputHelper, (Process Process, string StdOut, string StdErr)> executor)
149+
{
150+
const int maxRetries = 5;
151+
const int waitFactor = 5;
152+
153+
int retryCount = 0;
154+
155+
(Process Process, string StdOut, string StdErr) result = executor(args, outputHelper);
156+
while (result.Process.ExitCode != 0)
157+
{
158+
retryCount++;
159+
if (retryCount >= maxRetries)
160+
{
161+
break;
162+
}
163+
164+
int waitTime = Convert.ToInt32(Math.Pow(waitFactor, retryCount - 1));
165+
if (outputHelper != null)
166+
{
167+
outputHelper.WriteLine($"Retry {retryCount}/{maxRetries}, retrying in {waitTime} seconds...");
168+
}
169+
170+
Thread.Sleep(waitTime * 1000);
171+
result = executor(args, outputHelper!);
172+
}
173+
174+
return result;
175+
}
176+
177+
private static (Process Process, string StdOut, string StdErr) ExecuteProcess(
178+
string fileName, string args, ITestOutputHelper outputHelper)
179+
{
180+
Process process = new Process
181+
{
182+
EnableRaisingEvents = true,
183+
StartInfo =
184+
{
185+
FileName = fileName,
186+
Arguments = args,
187+
RedirectStandardOutput = true,
188+
RedirectStandardError = true,
189+
}
190+
};
191+
192+
StringBuilder stdOutput = new StringBuilder();
193+
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) => stdOutput.AppendLine(e.Data));
194+
195+
StringBuilder stdError = new StringBuilder();
196+
process.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => stdError.AppendLine(e.Data));
197+
198+
process.Start();
199+
process.BeginOutputReadLine();
200+
process.BeginErrorReadLine();
201+
process.WaitForExit();
202+
203+
string output = stdOutput.ToString().Trim();
204+
if (outputHelper != null && !string.IsNullOrWhiteSpace(output))
205+
{
206+
outputHelper.WriteLine(output);
207+
}
208+
209+
string error = stdError.ToString().Trim();
210+
if (outputHelper != null && !string.IsNullOrWhiteSpace(error))
211+
{
212+
outputHelper.WriteLine(error);
213+
}
214+
215+
return (process, output, error);
216+
}
217+
218+
private static string GetDockerOS() => Execute("version -f \"{{ .Server.Os }}\"");
219+
private static string GetDockerArch() => Execute("version -f \"{{ .Server.Arch }}\"");
220+
221+
public string GetImageUser(string image) => ExecuteWithLogging($"inspect -f \"{{{{ .Config.User }}}}\" {image}");
222+
223+
public static bool ImageExists(string tag) => ResourceExists("image", tag);
224+
225+
private static bool ResourceExists(string type, string filterArg)
226+
{
227+
string output = Execute($"{type} ls -a -q {filterArg}", true);
228+
return output != "";
229+
}
230+
231+
public string Run(
232+
string image,
233+
string name,
234+
string? command = null,
235+
string? workdir = null,
236+
string? optionalRunArgs = null,
237+
bool detach = false,
238+
string? runAsUser = null,
239+
bool skipAutoCleanup = false,
240+
bool useMountedDockerSocket = false,
241+
bool silenceOutput = false,
242+
bool tty = true)
243+
{
244+
string cleanupArg = skipAutoCleanup ? string.Empty : " --rm";
245+
string detachArg = detach ? " -d" : string.Empty;
246+
string ttyArg = detach && tty ? " -t" : string.Empty;
247+
string userArg = runAsUser != null ? $" -u {runAsUser}" : string.Empty;
248+
string workdirArg = workdir == null ? string.Empty : $" -w {workdir}";
249+
string mountedDockerSocketArg = useMountedDockerSocket ? " -v /var/run/docker.sock:/var/run/docker.sock" : string.Empty;
250+
if (silenceOutput)
251+
{
252+
return Execute(
253+
$"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}");
254+
}
255+
return ExecuteWithLogging(
256+
$"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}", ignoreErrors: true);
257+
}
258+
}

0 commit comments

Comments
 (0)