From 366664f089ebc861514251e4965771f75a95e8e1 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Tue, 18 Feb 2025 22:58:01 +0000 Subject: [PATCH 1/9] Add installer tests project --- .../Config.cs | 61 +++ .../DockerHelper.cs | 270 ++++++++++++ .../LinuxInstallerTests.cs | 386 ++++++++++++++++++ .../Microsoft.DotNet.Installer.Tests.csproj | 38 ++ 4 files changed, 755 insertions(+) create mode 100755 src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs create mode 100755 src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs create mode 100755 src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs create mode 100755 src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs new file mode 100755 index 000000000000..167d72b199dd --- /dev/null +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs @@ -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; + } +} diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs new file mode 100755 index 000000000000..4768280e70c4 --- /dev/null +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs @@ -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 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 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); + } +} diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs new file mode 100755 index 000000000000..90d99ef39336 --- /dev/null +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -0,0 +1,386 @@ +// 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 Microsoft.VisualStudio.TestPlatform.Utilities; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Runtime.Intrinsics.Arm; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; +using Xunit.Abstractions; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Microsoft.DotNet.Installer.Tests; + +public class LinuxInstallerTests : IDisposable +{ + private readonly DockerHelper _dockerHelper; + private readonly string _tmpDir; + private readonly string _contextDir; + + private readonly string[] RpmDistroImages = + [ + "mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-40" + ]; + + private readonly string[] DebDistroImages = + [ + "mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04" + ]; + + private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; + private const string NetStandard21DebPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.deb"; + + private enum DistroType + { + Rpm, + Deb + } + + private ITestOutputHelper OutputHelper { get; set; } + + public LinuxInstallerTests(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper; + _dockerHelper = new DockerHelper(OutputHelper); + + _tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(_tmpDir); + _contextDir = Path.Combine(_tmpDir, Path.GetRandomFileName()); + Directory.CreateDirectory(_contextDir); + + InitializeContext(); + } + + public void Dispose() + { + try + { + Directory.Delete(_tmpDir, recursive: true); + } + catch + { + } + } + + [Fact] + public void RunScenarioTestsForAllDistros() + { + if (Config.TestRpmPackages) + { + TestAllDistros(DistroType.Rpm); + } + + + if (Config.TestDebPackages) + { + TestAllDistros(DistroType.Deb); + } + } + + private void TestAllDistros(DistroType distroType) + { + foreach (string image in (distroType == DistroType.Rpm ? RpmDistroImages : DebDistroImages)) + { + try + { + OutputHelper.WriteLine($"Begin testing distro: {image}"); + DistroTest(image, distroType); + OutputHelper.WriteLine($"Finished testing distro: {image}"); + } + catch (Exception ex) + { + Assert.Fail($"Test failed for {image}: {ex}"); + } + } + } + + private void InitializeContext() + { + // For rpm enumerate RPM packages, excluding those that contain ".cm." in the name + List rpmPackages = Directory.GetFiles(Config.AssetsDirectory, "*.rpm", SearchOption.AllDirectories) + .Where(p => !Path.GetFileName(p).Contains("-cm.") && !Path.GetFileName(p).EndsWith("azl.rpm")) + .ToList(); + + foreach (string rpmPackage in rpmPackages) + { + File.Copy(rpmPackage, Path.Combine(_contextDir, Path.GetFileName(rpmPackage))); + } + + // Copy all DEB packages as well + foreach (string debPackage in Directory.GetFiles(Config.AssetsDirectory, "*.deb", SearchOption.AllDirectories)) + { + File.Copy(debPackage, Path.Combine(_contextDir, Path.GetFileName(debPackage))); + } + + // Download NetStandard 2.1 packages + DownloadFileAsync(NetStandard21RpmPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21RpmPackage))).Wait(); + DownloadFileAsync(NetStandard21DebPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21DebPackage))).Wait(); + + // Copy nuget packages + string nugetPackagesDir = Path.Combine(_contextDir, "packages"); + Directory.CreateDirectory(nugetPackagesDir); + foreach (string package in Directory.GetFiles(Config.PackagesDirectory, "*.nupkg", SearchOption.AllDirectories)) + { + File.Copy(package, Path.Combine(nugetPackagesDir, Path.GetFileName(package))); + } + + // Copy and update NuGet.config from scenario-tests repo + string newNuGetConfig = Path.Combine(_contextDir, "NuGet.config"); + File.Copy(Config.ScenarioTestsNuGetConfigPath, newNuGetConfig); + InsertLocalPackagesPathToNuGetConfig(newNuGetConfig, "/packages"); + + // Find the scenario-tests package and unpack it to the context dir, subfolder "scenario-tests" + string? scenarioTestsPackage = Directory.GetFiles(nugetPackagesDir, "Microsoft.DotNet.ScenarioTests.SdkTemplateTests*.nupkg", SearchOption.AllDirectories).FirstOrDefault(); + if (scenarioTestsPackage == null) + { + Assert.Fail("Scenario tests package not found"); + } + + ZipFile.ExtractToDirectory(scenarioTestsPackage, Path.Combine(_contextDir, "scenario-tests")); + } + + private void InsertLocalPackagesPathToNuGetConfig(string nuGetConfig, string localPackagesPath) + { + XDocument doc = XDocument.Load(nuGetConfig); + if (doc.Root != null) + { + XElement? packageSourcesElement = doc.Root.Element("packageSources"); + if (packageSourcesElement != null) + { + XElement? clearElement = packageSourcesElement.Element("clear"); + if (clearElement != null) + { + XElement newAddElement = new XElement("add", + new XAttribute("key", "local-packages"), + new XAttribute("value", localPackagesPath)); + + clearElement.AddAfterSelf(newAddElement); + } + } + + doc.Save(nuGetConfig); + } + } + + private void DistroTest(string baseImage, DistroType distroType) + { + // Order of installation is important as we do not want to use "--nodeps" + // We install in correct order, so package dependencies are present. + + // Prepare the package list in correct install order + List packageList = + [ + // Deps package should be installed first + Path.GetFileName(GetMatchingDepsPackage(baseImage, distroType)) + ]; + + // Add all other packages in correct install order + AddPackage(packageList, "dotnet-host-", distroType); + AddPackage(packageList, "dotnet-hostfxr-", distroType); + AddPackage(packageList, "dotnet-runtime-", distroType); + AddPackage(packageList, "dotnet-targeting-pack-", distroType); + AddPackage(packageList, "aspnetcore-runtime-", distroType); + AddPackage(packageList, "aspnetcore-targeting-pack-", distroType); + AddPackage(packageList, "dotnet-apphost-pack-", distroType); + if (Config.Architecture == "x64") + { + // netstandard package exists for x64 only + AddPackage(packageList, "netstandard-targeting-pack-", distroType); + } + AddPackage(packageList, "dotnet-sdk-", distroType); + + string dockerfile = GenerateDockerfile(packageList, baseImage, distroType); + + string tag = $"test-{Path.GetRandomFileName()}"; + string output = ""; + + try + { + // Build docker image and run the tests + _dockerHelper.Build(tag, dockerfile: dockerfile, contextDir: _contextDir); + output = _dockerHelper.Run(tag, tag); + + int testResultsSummaryIndex = output.IndexOf("Tests run: "); + if (testResultsSummaryIndex >= 0) + { + string testResultsSummary = output[testResultsSummaryIndex..]; + Assert.False(AnyTestFailures(testResultsSummary), testResultsSummary); + } + else + { + Assert.Fail("Test summary not found"); + } + } + catch (Exception e) + { + if (string.IsNullOrEmpty(output)) + { + output = e.Message; + } + Assert.Fail($"Build failed: {output}"); + } + finally + { + _dockerHelper.DeleteImage(tag); + } + } + + private string GenerateDockerfile(List rpmPackageList, string baseImage, DistroType distroType) + { + StringBuilder sb = new(); + sb.AppendLine("FROM " + baseImage); + sb.AppendLine(""); + sb.AppendLine("# Copy NuGet.config"); + sb.AppendLine($"COPY NuGet.config ."); + + sb.AppendLine(""); + sb.AppendLine("# Copy scenario-tests content"); + sb.AppendLine($"COPY scenario-tests scenario-tests"); + + sb.AppendLine(""); + sb.AppendLine("# Copy nuget packages"); + sb.AppendLine($"COPY packages packages"); + + sb.AppendLine(""); + sb.AppendLine("# Copy RPM packages"); + foreach (string package in rpmPackageList) + { + sb.AppendLine($"COPY {package} {package}"); + } + sb.AppendLine(""); + sb.AppendLine("# Install RPM packages and Microsoft.DotNet.ScenarioTests.SdkTemplateTests tool"); + sb.Append("RUN"); + + // TODO: remove --force-all when aspnet package versioning issue have been resolved - https://github.com/dotnet/source-build/issues/4895 + string packageInstallationCommand = distroType == DistroType.Deb ? "dpkg -i --force-all" : "rpm -i"; + bool useAndOperator = false; + foreach (string package in rpmPackageList) + { + sb.AppendLine(" \\"); + sb.Append($" {(useAndOperator ? "&&" : "")} {packageInstallationCommand} {package}"); + useAndOperator = true; + } + sb.AppendLine(""); + + // Set environment for nuget.config + sb.AppendLine(""); + sb.AppendLine("# Set custom nuget.config"); + sb.AppendLine("ENV RestoreConfigFile=/NuGet.config"); + + // Find scenario-tests binary in context/scenario-tests + string? scenarioTestsBinary = Directory.GetFiles(Path.Combine(_contextDir, "scenario-tests"), "Microsoft.DotNet.ScenarioTests.SdkTemplateTests.dll", SearchOption.AllDirectories).FirstOrDefault(); + if (scenarioTestsBinary == null) + { + throw new Exception("Scenario tests binary not found"); + } + scenarioTestsBinary = scenarioTestsBinary.Replace(_contextDir, "").Replace("\\", "/"); + + // Set entry point + sb.AppendLine(""); + sb.AppendLine($"ENTRYPOINT [ \"dotnet\", \"{scenarioTestsBinary}\", \"--dotnet-root\", \"/usr/share/dotnet\" ]"); + + string dockerfile = Path.Combine(_contextDir, Path.GetRandomFileName()); + File.WriteAllText(dockerfile, sb.ToString()); + return dockerfile; + } + + private bool AnyTestFailures(string testResultSummary) + { + var parts = testResultSummary.Split(',') + .Select(part => part.Split(':').Select(p => p.Trim()).ToArray()) + .Where(p => p.Length == 2) + .ToDictionary(p => p[0], p => int.Parse(p[1])); + + return parts["Errors"] > 0 || parts["Failures"] > 0; + } + + private void AddPackage(List packageList, string prefix, DistroType distroType) + { + packageList.Add(Path.GetFileName(GetContentPackage(prefix, distroType))); + } + + private string GetContentPackage(string prefix, DistroType distroType) + { + string matchPattern = DistroType.Deb == distroType ? "*.deb" : "*.rpm"; + string[] rpmFiles = Directory.GetFiles(_contextDir, prefix + matchPattern, SearchOption.AllDirectories) + .Where(p => !Path.GetFileName(p).Contains("dotnet-runtime-deps-")) + .ToArray(); + if (rpmFiles.Length == 0) + { + throw new Exception($"RPM package with prefix '{prefix}' not found"); + } + + return rpmFiles.OrderByDescending(f => f).First(); + } + + private string GetMatchingDepsPackage(string baseImage, DistroType distroType) + { + string matchPattern = "dotnet-runtime-deps-*.deb"; + if (distroType == DistroType.Rpm) + { + string depsId = "fedora"; + if (baseImage.Contains("fedora")) + { + depsId = "fedora"; + } + else if (baseImage.Contains("centos")) + { + depsId = "centos"; + } + else if (baseImage.Contains("rhel")) + { + depsId = "rhel"; + } + else if (baseImage.Contains("opensuse")) + { + depsId = "opensuse"; + } + else if (baseImage.Contains("sles")) + { + depsId = "sles"; + } + else if (baseImage.Contains("azurelinux")) + { + depsId = "azl"; + } + else + { + throw new Exception($"Unknown distro: {baseImage}"); + } + + matchPattern = $"dotnet-runtime-deps-*{depsId}*.rpm"; + } + + string[] files = Directory.GetFiles(_contextDir, matchPattern, SearchOption.AllDirectories); + if (files.Length == 0) + { + throw new Exception($"Did not find the DEPS package."); + } + + return files.OrderByDescending(f => f).First(); + } + + private static async Task DownloadFileAsync(string url, string filePath) + { + using (HttpClient client = new HttpClient()) + { + HttpResponseMessage response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + await response.Content.CopyToAsync(fileStream); + } + } + } +} + diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj new file mode 100755 index 000000000000..b7b3fdde1384 --- /dev/null +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(NetCurrent) + console%3bverbosity=normal;trx%3bverbosity=diagnostic%3bLogFileName=$(MSBuildProjectName).trx + $(VSTestCLIRunSettings);RunConfiguration.DotNetHostPath=$(DotnetTool) + + + + + + + + + + + $(ArtifactsAssetsDir) + + + $(ArtifactsPackagesDir) + + + $(RepoRoot)src\scenario-tests\NuGet.config + + + $(HostArchitecture) + + + $(TestRpmPackages) + + + $(TestDebPackages) + + + + + From 054366b087810078c0e6e27f6f47edbbb5f765bd Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Wed, 19 Feb 2025 17:43:48 +0000 Subject: [PATCH 2/9] Addressing PR comments --- .../LinuxInstallerTests.cs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index 90d99ef39336..a6611b925b16 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -28,12 +28,12 @@ public class LinuxInstallerTests : IDisposable private readonly string[] RpmDistroImages = [ - "mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-40" + "mcr.microsoft.com/dotnet/runtime-deps:9.0-azurelinux3.0" ]; private readonly string[] DebDistroImages = [ - "mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04" + "mcr.microsoft.com/dotnet/runtime-deps:9.0-bookworm-slim" ]; private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; @@ -327,20 +327,8 @@ private string GetMatchingDepsPackage(string baseImage, DistroType distroType) string matchPattern = "dotnet-runtime-deps-*.deb"; if (distroType == DistroType.Rpm) { - string depsId = "fedora"; - if (baseImage.Contains("fedora")) - { - depsId = "fedora"; - } - else if (baseImage.Contains("centos")) - { - depsId = "centos"; - } - else if (baseImage.Contains("rhel")) - { - depsId = "rhel"; - } - else if (baseImage.Contains("opensuse")) + string? depsId = null; + if (baseImage.Contains("opensuse")) { depsId = "opensuse"; } From 08a423071d13796dd1ac99a900ccae15ce1705c6 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Wed, 19 Feb 2025 23:36:34 +0000 Subject: [PATCH 3/9] Address more review comments --- .../Config.cs | 1 - .../DockerHelper.cs | 12 --- .../LinuxInstallerTests.cs | 77 +++++++++---------- .../Microsoft.DotNet.Installer.Tests.csproj | 4 - 4 files changed, 38 insertions(+), 56 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs index 167d72b199dd..cd3bfc196d5e 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.RegularExpressions; -using Newtonsoft.Json.Linq; namespace Microsoft.DotNet.Installer.Tests; diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs index 4768280e70c4..678fe02afe2b 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/DockerHelper.cs @@ -9,8 +9,6 @@ using System.Linq; using System.Text; using System.Threading; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Xunit.Abstractions; namespace Microsoft.DotNet.Installer.Tests; @@ -222,16 +220,6 @@ private static (Process Process, string StdOut, string StdErr) ExecuteProcess( public string GetImageUser(string image) => ExecuteWithLogging($"inspect -f \"{{{{ .Config.User }}}}\" {image}"); - public IDictionary 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) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index a6611b925b16..1fbf35a1592e 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestPlatform.Utilities; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; @@ -28,18 +27,18 @@ public class LinuxInstallerTests : IDisposable private readonly string[] RpmDistroImages = [ - "mcr.microsoft.com/dotnet/runtime-deps:9.0-azurelinux3.0" + "mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-preview-azurelinux3.0" ]; private readonly string[] DebDistroImages = [ - "mcr.microsoft.com/dotnet/runtime-deps:9.0-bookworm-slim" + "mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-preview-trixie-slim" ]; private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; private const string NetStandard21DebPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.deb"; - private enum DistroType + private enum PackageType { Rpm, Deb @@ -76,25 +75,25 @@ public void RunScenarioTestsForAllDistros() { if (Config.TestRpmPackages) { - TestAllDistros(DistroType.Rpm); + TestAllDistros(PackageType.Rpm); } if (Config.TestDebPackages) { - TestAllDistros(DistroType.Deb); + TestAllDistros(PackageType.Deb); } } - private void TestAllDistros(DistroType distroType) + private void TestAllDistros(PackageType packageType) { - foreach (string image in (distroType == DistroType.Rpm ? RpmDistroImages : DebDistroImages)) + foreach (string image in (packageType == PackageType.Rpm ? RpmDistroImages : DebDistroImages)) { try { - OutputHelper.WriteLine($"Begin testing distro: {image}"); - DistroTest(image, distroType); - OutputHelper.WriteLine($"Finished testing distro: {image}"); + OutputHelper.WriteLine($"Begin testing installer packages on distro: {image}"); + DistroTest(image, packageType); + OutputHelper.WriteLine($"Finished testing installer packages on distro: {image}"); } catch (Exception ex) { @@ -171,7 +170,7 @@ private void InsertLocalPackagesPathToNuGetConfig(string nuGetConfig, string loc } } - private void DistroTest(string baseImage, DistroType distroType) + private void DistroTest(string baseImage, PackageType packageType) { // Order of installation is important as we do not want to use "--nodeps" // We install in correct order, so package dependencies are present. @@ -180,25 +179,25 @@ private void DistroTest(string baseImage, DistroType distroType) List packageList = [ // Deps package should be installed first - Path.GetFileName(GetMatchingDepsPackage(baseImage, distroType)) + Path.GetFileName(GetMatchingDepsPackage(baseImage, packageType)) ]; // Add all other packages in correct install order - AddPackage(packageList, "dotnet-host-", distroType); - AddPackage(packageList, "dotnet-hostfxr-", distroType); - AddPackage(packageList, "dotnet-runtime-", distroType); - AddPackage(packageList, "dotnet-targeting-pack-", distroType); - AddPackage(packageList, "aspnetcore-runtime-", distroType); - AddPackage(packageList, "aspnetcore-targeting-pack-", distroType); - AddPackage(packageList, "dotnet-apphost-pack-", distroType); + AddPackage(packageList, "dotnet-host-", packageType); + AddPackage(packageList, "dotnet-hostfxr-", packageType); + AddPackage(packageList, "dotnet-runtime-", packageType); + AddPackage(packageList, "dotnet-targeting-pack-", packageType); + AddPackage(packageList, "aspnetcore-runtime-", packageType); + AddPackage(packageList, "aspnetcore-targeting-pack-", packageType); + AddPackage(packageList, "dotnet-apphost-pack-", packageType); if (Config.Architecture == "x64") { // netstandard package exists for x64 only - AddPackage(packageList, "netstandard-targeting-pack-", distroType); + AddPackage(packageList, "netstandard-targeting-pack-", packageType); } - AddPackage(packageList, "dotnet-sdk-", distroType); + AddPackage(packageList, "dotnet-sdk-", packageType); - string dockerfile = GenerateDockerfile(packageList, baseImage, distroType); + string dockerfile = GenerateDockerfile(packageList, baseImage, packageType); string tag = $"test-{Path.GetRandomFileName()}"; string output = ""; @@ -234,7 +233,7 @@ private void DistroTest(string baseImage, DistroType distroType) } } - private string GenerateDockerfile(List rpmPackageList, string baseImage, DistroType distroType) + private string GenerateDockerfile(List packageList, string baseImage, PackageType packageType) { StringBuilder sb = new(); sb.AppendLine("FROM " + baseImage); @@ -251,19 +250,19 @@ private string GenerateDockerfile(List rpmPackageList, string baseImage, sb.AppendLine($"COPY packages packages"); sb.AppendLine(""); - sb.AppendLine("# Copy RPM packages"); - foreach (string package in rpmPackageList) + sb.AppendLine("# Copy installer packages"); + foreach (string package in packageList) { sb.AppendLine($"COPY {package} {package}"); } sb.AppendLine(""); - sb.AppendLine("# Install RPM packages and Microsoft.DotNet.ScenarioTests.SdkTemplateTests tool"); + sb.AppendLine("# Install the installer packages and Microsoft.DotNet.ScenarioTests.SdkTemplateTests tool"); sb.Append("RUN"); // TODO: remove --force-all when aspnet package versioning issue have been resolved - https://github.com/dotnet/source-build/issues/4895 - string packageInstallationCommand = distroType == DistroType.Deb ? "dpkg -i --force-all" : "rpm -i"; + string packageInstallationCommand = packageType == PackageType.Deb ? "dpkg -i --force-all" : "rpm -i"; bool useAndOperator = false; - foreach (string package in rpmPackageList) + foreach (string package in packageList) { sb.AppendLine(" \\"); sb.Append($" {(useAndOperator ? "&&" : "")} {packageInstallationCommand} {package}"); @@ -288,7 +287,7 @@ private string GenerateDockerfile(List rpmPackageList, string baseImage, sb.AppendLine(""); sb.AppendLine($"ENTRYPOINT [ \"dotnet\", \"{scenarioTestsBinary}\", \"--dotnet-root\", \"/usr/share/dotnet\" ]"); - string dockerfile = Path.Combine(_contextDir, Path.GetRandomFileName()); + string dockerfile = Path.Combine(_contextDir, $"Dockerfile-{Path.GetRandomFileName()}"); File.WriteAllText(dockerfile, sb.ToString()); return dockerfile; } @@ -303,29 +302,29 @@ private bool AnyTestFailures(string testResultSummary) return parts["Errors"] > 0 || parts["Failures"] > 0; } - private void AddPackage(List packageList, string prefix, DistroType distroType) + private void AddPackage(List packageList, string prefix, PackageType packageType) { - packageList.Add(Path.GetFileName(GetContentPackage(prefix, distroType))); + packageList.Add(Path.GetFileName(GetContentPackage(prefix, packageType))); } - private string GetContentPackage(string prefix, DistroType distroType) + private string GetContentPackage(string prefix, PackageType packageType) { - string matchPattern = DistroType.Deb == distroType ? "*.deb" : "*.rpm"; - string[] rpmFiles = Directory.GetFiles(_contextDir, prefix + matchPattern, SearchOption.AllDirectories) + string matchPattern = PackageType.Deb == packageType ? "*.deb" : "*.rpm"; + string[] files = Directory.GetFiles(_contextDir, prefix + matchPattern, SearchOption.AllDirectories) .Where(p => !Path.GetFileName(p).Contains("dotnet-runtime-deps-")) .ToArray(); - if (rpmFiles.Length == 0) + if (files.Length == 0) { throw new Exception($"RPM package with prefix '{prefix}' not found"); } - return rpmFiles.OrderByDescending(f => f).First(); + return files.OrderByDescending(f => f).First(); } - private string GetMatchingDepsPackage(string baseImage, DistroType distroType) + private string GetMatchingDepsPackage(string baseImage, PackageType packageType) { string matchPattern = "dotnet-runtime-deps-*.deb"; - if (distroType == DistroType.Rpm) + if (packageType == PackageType.Rpm) { string? depsId = null; if (baseImage.Contains("opensuse")) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj index b7b3fdde1384..2973542bf230 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj @@ -6,10 +6,6 @@ $(VSTestCLIRunSettings);RunConfiguration.DotNetHostPath=$(DotnetTool) - - - - From 64fa1b8e6d800687ab49eae0b2a483e937a83e73 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Thu, 20 Feb 2025 17:16:52 +0000 Subject: [PATCH 4/9] Split the tests and use conditional theory --- .../LinuxInstallerTests.cs | 53 ++++++------------- .../Microsoft.DotNet.Installer.Tests.csproj | 4 ++ 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index 1fbf35a1592e..db40c8b1ede6 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -13,9 +13,9 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using TestUtilities; using Xunit; using Xunit.Abstractions; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Microsoft.DotNet.Installer.Tests; @@ -25,19 +25,12 @@ public class LinuxInstallerTests : IDisposable private readonly string _tmpDir; private readonly string _contextDir; - private readonly string[] RpmDistroImages = - [ - "mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-preview-azurelinux3.0" - ]; - - private readonly string[] DebDistroImages = - [ - "mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-preview-trixie-slim" - ]; - private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; private const string NetStandard21DebPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.deb"; + public static bool IncludeRpmTests => Config.TestRpmPackages; + public static bool IncludeDebTests => Config.TestDebPackages; + private enum PackageType { Rpm, @@ -70,36 +63,18 @@ public void Dispose() } } - [Fact] - public void RunScenarioTestsForAllDistros() + [ConditionalTheory(typeof(LinuxInstallerTests), nameof(IncludeRpmTests))] + [InlineData("mcr.microsoft.com/dotnet/nightly/runtime-deps", "10.0-preview-azurelinux3.0")] + public void RpmTest(string repo, string tag) { - if (Config.TestRpmPackages) - { - TestAllDistros(PackageType.Rpm); - } - - - if (Config.TestDebPackages) - { - TestAllDistros(PackageType.Deb); - } + DistroTest($"{repo}:{tag}", PackageType.Rpm); } - private void TestAllDistros(PackageType packageType) + [ConditionalTheory(typeof(LinuxInstallerTests), nameof(IncludeDebTests))] + [InlineData("mcr.microsoft.com/dotnet/nightly/runtime-deps", "10.0-preview-trixie-slim")] + public void DebTest(string repo, string tag) { - foreach (string image in (packageType == PackageType.Rpm ? RpmDistroImages : DebDistroImages)) - { - try - { - OutputHelper.WriteLine($"Begin testing installer packages on distro: {image}"); - DistroTest(image, packageType); - OutputHelper.WriteLine($"Finished testing installer packages on distro: {image}"); - } - catch (Exception ex) - { - Assert.Fail($"Test failed for {image}: {ex}"); - } - } + DistroTest($"{repo}:{tag}", PackageType.Deb); } private void InitializeContext() @@ -201,11 +176,13 @@ private void DistroTest(string baseImage, PackageType packageType) string tag = $"test-{Path.GetRandomFileName()}"; string output = ""; + bool buildCompleted = false; try { // Build docker image and run the tests _dockerHelper.Build(tag, dockerfile: dockerfile, contextDir: _contextDir); + buildCompleted = true; output = _dockerHelper.Run(tag, tag); int testResultsSummaryIndex = output.IndexOf("Tests run: "); @@ -225,7 +202,7 @@ private void DistroTest(string baseImage, PackageType packageType) { output = e.Message; } - Assert.Fail($"Build failed: {output}"); + Assert.Fail($"{(buildCompleted ? "Build" : "Test")} failed: {output}"); } finally { diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj index 2973542bf230..f9e572605deb 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj @@ -6,6 +6,10 @@ $(VSTestCLIRunSettings);RunConfiguration.DotNetHostPath=$(DotnetTool) + + + + From 1f69d814a1c420251a8c18b94ada25608f2b2487 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Fri, 21 Feb 2025 05:02:38 +0000 Subject: [PATCH 5/9] Few tweaks --- .../test/Microsoft.DotNet.Installer.Tests/Config.cs | 3 +++ .../LinuxInstallerTests.cs | 11 ++++++++--- .../Microsoft.DotNet.Installer.Tests.csproj | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs index cd3bfc196d5e..663cd347d712 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Config.cs @@ -29,6 +29,9 @@ public static class Config public static bool TestDebPackages { get; } = TryGetRuntimeConfig(TestDebPackagesSwitch, out bool value) ? value : false; const string TestDebPackagesSwitch = RuntimeConfigSwitchPrefix + nameof(TestDebPackages); + public static bool KeepDockerImages { get; } = TryGetRuntimeConfig(KeepDockerImagesSwitch, out bool value) ? value : false; + const string KeepDockerImagesSwitch = RuntimeConfigSwitchPrefix + nameof(KeepDockerImages); + public const string RuntimeConfigSwitchPrefix = "Microsoft.DotNet.Installer.Tests."; public static string GetRuntimeConfig(string key) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index db40c8b1ede6..c10258abfde6 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -27,6 +27,8 @@ public class LinuxInstallerTests : IDisposable private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; private const string NetStandard21DebPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.deb"; + private const string RuntimeDepsRepo = "mcr.microsoft.com/dotnet/nightly/runtime-deps"; + private const string RuntimeDepsVersion = "10.0-preview"; public static bool IncludeRpmTests => Config.TestRpmPackages; public static bool IncludeDebTests => Config.TestDebPackages; @@ -64,14 +66,14 @@ public void Dispose() } [ConditionalTheory(typeof(LinuxInstallerTests), nameof(IncludeRpmTests))] - [InlineData("mcr.microsoft.com/dotnet/nightly/runtime-deps", "10.0-preview-azurelinux3.0")] + [InlineData(RuntimeDepsRepo, $"{RuntimeDepsVersion}-azurelinux3.0")] public void RpmTest(string repo, string tag) { DistroTest($"{repo}:{tag}", PackageType.Rpm); } [ConditionalTheory(typeof(LinuxInstallerTests), nameof(IncludeDebTests))] - [InlineData("mcr.microsoft.com/dotnet/nightly/runtime-deps", "10.0-preview-trixie-slim")] + [InlineData(RuntimeDepsRepo, $"{RuntimeDepsVersion}-trixie-slim")] public void DebTest(string repo, string tag) { DistroTest($"{repo}:{tag}", PackageType.Deb); @@ -206,7 +208,10 @@ private void DistroTest(string baseImage, PackageType packageType) } finally { - _dockerHelper.DeleteImage(tag); + if (!Config.KeepDockerImages) + { + _dockerHelper.DeleteImage(tag); + } } } diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj index f9e572605deb..3e3707bb6cdf 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/Microsoft.DotNet.Installer.Tests.csproj @@ -32,6 +32,9 @@ $(TestDebPackages) + + $(KeepDockerImages) + From a459cb9f457ca67b2f79ae11705fcb24b7314adb Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Fri, 21 Feb 2025 18:11:25 +0000 Subject: [PATCH 6/9] Refactor context initialization --- .../LinuxInstallerTests.cs | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index c10258abfde6..409aeb77d5a8 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -25,6 +25,10 @@ public class LinuxInstallerTests : IDisposable private readonly string _tmpDir; private readonly string _contextDir; + private bool _rpmContextInitialized = false; + private bool _debContextInitialized = false; + private bool _sharedContextInitialized = false; + private const string NetStandard21RpmPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.rpm"; private const string NetStandard21DebPackage = @"https://dotnetcli.blob.core.windows.net/dotnet/Runtime/3.1.0/netstandard-targeting-pack-2.1.0-x64.deb"; private const string RuntimeDepsRepo = "mcr.microsoft.com/dotnet/nightly/runtime-deps"; @@ -50,8 +54,6 @@ public LinuxInstallerTests(ITestOutputHelper outputHelper) Directory.CreateDirectory(_tmpDir); _contextDir = Path.Combine(_tmpDir, Path.GetRandomFileName()); Directory.CreateDirectory(_contextDir); - - InitializeContext(); } public void Dispose() @@ -69,6 +71,8 @@ public void Dispose() [InlineData(RuntimeDepsRepo, $"{RuntimeDepsVersion}-azurelinux3.0")] public void RpmTest(string repo, string tag) { + InitializeContext(PackageType.Rpm); + DistroTest($"{repo}:{tag}", PackageType.Rpm); } @@ -76,52 +80,66 @@ public void RpmTest(string repo, string tag) [InlineData(RuntimeDepsRepo, $"{RuntimeDepsVersion}-trixie-slim")] public void DebTest(string repo, string tag) { + InitializeContext(PackageType.Deb); + DistroTest($"{repo}:{tag}", PackageType.Deb); } - private void InitializeContext() + private void InitializeContext(PackageType packageType) { - // For rpm enumerate RPM packages, excluding those that contain ".cm." in the name - List rpmPackages = Directory.GetFiles(Config.AssetsDirectory, "*.rpm", SearchOption.AllDirectories) - .Where(p => !Path.GetFileName(p).Contains("-cm.") && !Path.GetFileName(p).EndsWith("azl.rpm")) - .ToList(); - - foreach (string rpmPackage in rpmPackages) + if (packageType == PackageType.Rpm && !_rpmContextInitialized) { - File.Copy(rpmPackage, Path.Combine(_contextDir, Path.GetFileName(rpmPackage))); - } + // For rpm enumerate RPM packages, excluding those that contain ".cm." in the name + List rpmPackages = Directory.GetFiles(Config.AssetsDirectory, "*.rpm", SearchOption.AllDirectories) + .Where(p => !Path.GetFileName(p).Contains("-cm.") && !Path.GetFileName(p).EndsWith("azl.rpm")) + .ToList(); - // Copy all DEB packages as well - foreach (string debPackage in Directory.GetFiles(Config.AssetsDirectory, "*.deb", SearchOption.AllDirectories)) - { - File.Copy(debPackage, Path.Combine(_contextDir, Path.GetFileName(debPackage))); + foreach (string rpmPackage in rpmPackages) + { + File.Copy(rpmPackage, Path.Combine(_contextDir, Path.GetFileName(rpmPackage))); + } + + DownloadFileAsync(NetStandard21RpmPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21RpmPackage))).Wait(); + _rpmContextInitialized = true; } + else if (!_debContextInitialized) + { + // Copy all DEB packages as well + foreach (string debPackage in Directory.GetFiles(Config.AssetsDirectory, "*.deb", SearchOption.AllDirectories)) + { + File.Copy(debPackage, Path.Combine(_contextDir, Path.GetFileName(debPackage))); + } - // Download NetStandard 2.1 packages - DownloadFileAsync(NetStandard21RpmPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21RpmPackage))).Wait(); - DownloadFileAsync(NetStandard21DebPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21DebPackage))).Wait(); + // Download NetStandard 2.1 packages + DownloadFileAsync(NetStandard21DebPackage, Path.Combine(_contextDir, Path.GetFileName(NetStandard21DebPackage))).Wait(); + _debContextInitialized = true; + } - // Copy nuget packages - string nugetPackagesDir = Path.Combine(_contextDir, "packages"); - Directory.CreateDirectory(nugetPackagesDir); - foreach (string package in Directory.GetFiles(Config.PackagesDirectory, "*.nupkg", SearchOption.AllDirectories)) + if (!_sharedContextInitialized) { - File.Copy(package, Path.Combine(nugetPackagesDir, Path.GetFileName(package))); - } + // Copy nuget packages + string nugetPackagesDir = Path.Combine(_contextDir, "packages"); + Directory.CreateDirectory(nugetPackagesDir); + foreach (string package in Directory.GetFiles(Config.PackagesDirectory, "*.nupkg", SearchOption.AllDirectories)) + { + File.Copy(package, Path.Combine(nugetPackagesDir, Path.GetFileName(package))); + } - // Copy and update NuGet.config from scenario-tests repo - string newNuGetConfig = Path.Combine(_contextDir, "NuGet.config"); - File.Copy(Config.ScenarioTestsNuGetConfigPath, newNuGetConfig); - InsertLocalPackagesPathToNuGetConfig(newNuGetConfig, "/packages"); + // Copy and update NuGet.config from scenario-tests repo + string newNuGetConfig = Path.Combine(_contextDir, "NuGet.config"); + File.Copy(Config.ScenarioTestsNuGetConfigPath, newNuGetConfig); + InsertLocalPackagesPathToNuGetConfig(newNuGetConfig, "/packages"); - // Find the scenario-tests package and unpack it to the context dir, subfolder "scenario-tests" - string? scenarioTestsPackage = Directory.GetFiles(nugetPackagesDir, "Microsoft.DotNet.ScenarioTests.SdkTemplateTests*.nupkg", SearchOption.AllDirectories).FirstOrDefault(); - if (scenarioTestsPackage == null) - { - Assert.Fail("Scenario tests package not found"); - } + // Find the scenario-tests package and unpack it to the context dir, subfolder "scenario-tests" + string? scenarioTestsPackage = Directory.GetFiles(nugetPackagesDir, "Microsoft.DotNet.ScenarioTests.SdkTemplateTests*.nupkg", SearchOption.AllDirectories).FirstOrDefault(); + if (scenarioTestsPackage == null) + { + Assert.Fail("Scenario tests package not found"); + } - ZipFile.ExtractToDirectory(scenarioTestsPackage, Path.Combine(_contextDir, "scenario-tests")); + ZipFile.ExtractToDirectory(scenarioTestsPackage, Path.Combine(_contextDir, "scenario-tests")); + _sharedContextInitialized = true; + } } private void InsertLocalPackagesPathToNuGetConfig(string nuGetConfig, string localPackagesPath) From 6ac9570f431c1a7281f45c101e10b2dc02f83195 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Mon, 24 Feb 2025 13:31:30 -0800 Subject: [PATCH 7/9] Update src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs Co-authored-by: Michael Simons --- .../Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index 409aeb77d5a8..9e2dbb2139bc 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -154,7 +154,7 @@ private void InsertLocalPackagesPathToNuGetConfig(string nuGetConfig, string loc if (clearElement != null) { XElement newAddElement = new XElement("add", - new XAttribute("key", "local-packages"), + new XAttribute("key", "local-packages"), new XAttribute("value", localPackagesPath)); clearElement.AddAfterSelf(newAddElement); From 7dd918feef4c476fe34ed8ec3c51da831c3ad621 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Tue, 25 Feb 2025 15:46:42 +0000 Subject: [PATCH 8/9] Address few more comments --- .../LinuxInstallerTests.cs | 105 ++++++++---------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index 9e2dbb2139bc..83697f6d5ca4 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -24,6 +24,7 @@ public class LinuxInstallerTests : IDisposable private readonly DockerHelper _dockerHelper; private readonly string _tmpDir; private readonly string _contextDir; + private readonly ITestOutputHelper _outputHelper; private bool _rpmContextInitialized = false; private bool _debContextInitialized = false; @@ -43,12 +44,10 @@ private enum PackageType Deb } - private ITestOutputHelper OutputHelper { get; set; } - public LinuxInstallerTests(ITestOutputHelper outputHelper) { - OutputHelper = outputHelper; - _dockerHelper = new DockerHelper(OutputHelper); + _outputHelper = outputHelper; + _dockerHelper = new DockerHelper(_outputHelper); _tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(_tmpDir); @@ -60,7 +59,7 @@ public void Dispose() { try { - Directory.Delete(_tmpDir, recursive: true); + //Directory.Delete(_tmpDir, recursive: true); } catch { @@ -71,6 +70,12 @@ public void Dispose() [InlineData(RuntimeDepsRepo, $"{RuntimeDepsVersion}-azurelinux3.0")] public void RpmTest(string repo, string tag) { + if (!tag.Contains("azurelinux")) + { + // Only Azure Linux is currently supported for RPM tests + Assert.Fail("Only Azure Linux is currently supported for RPM tests"); + } + InitializeContext(PackageType.Rpm); DistroTest($"{repo}:{tag}", PackageType.Rpm); @@ -167,31 +172,7 @@ private void InsertLocalPackagesPathToNuGetConfig(string nuGetConfig, string loc private void DistroTest(string baseImage, PackageType packageType) { - // Order of installation is important as we do not want to use "--nodeps" - // We install in correct order, so package dependencies are present. - - // Prepare the package list in correct install order - List packageList = - [ - // Deps package should be installed first - Path.GetFileName(GetMatchingDepsPackage(baseImage, packageType)) - ]; - - // Add all other packages in correct install order - AddPackage(packageList, "dotnet-host-", packageType); - AddPackage(packageList, "dotnet-hostfxr-", packageType); - AddPackage(packageList, "dotnet-runtime-", packageType); - AddPackage(packageList, "dotnet-targeting-pack-", packageType); - AddPackage(packageList, "aspnetcore-runtime-", packageType); - AddPackage(packageList, "aspnetcore-targeting-pack-", packageType); - AddPackage(packageList, "dotnet-apphost-pack-", packageType); - if (Config.Architecture == "x64") - { - // netstandard package exists for x64 only - AddPackage(packageList, "netstandard-targeting-pack-", packageType); - } - AddPackage(packageList, "dotnet-sdk-", packageType); - + List packageList = GetPackageList(baseImage, packageType); string dockerfile = GenerateDockerfile(packageList, baseImage, packageType); string tag = $"test-{Path.GetRandomFileName()}"; @@ -233,6 +214,36 @@ private void DistroTest(string baseImage, PackageType packageType) } } + private List GetPackageList(string baseImage, PackageType packageType) + { + // Order of installation is important as we do not want to use "--nodeps" + // We install in correct order, so package dependencies are present. + + // Prepare the package list in correct install order + List packageList = + [ + // Deps package should be installed first + Path.GetFileName(GetMatchingDepsPackage(baseImage, packageType)) + ]; + + // Add all other packages in correct install order + AddPackage(packageList, "dotnet-host-", packageType); + AddPackage(packageList, "dotnet-hostfxr-", packageType); + AddPackage(packageList, "dotnet-runtime-", packageType); + AddPackage(packageList, "dotnet-targeting-pack-", packageType); + AddPackage(packageList, "aspnetcore-runtime-", packageType); + AddPackage(packageList, "aspnetcore-targeting-pack-", packageType); + AddPackage(packageList, "dotnet-apphost-pack-", packageType); + if (Config.Architecture == "x64") + { + // netstandard package exists for x64 only + AddPackage(packageList, "netstandard-targeting-pack-", packageType); + } + AddPackage(packageList, "dotnet-sdk-", packageType); + + return packageList; + } + private string GenerateDockerfile(List packageList, string baseImage, PackageType packageType) { StringBuilder sb = new(); @@ -259,7 +270,7 @@ private string GenerateDockerfile(List packageList, string baseImage, Pa sb.AppendLine("# Install the installer packages and Microsoft.DotNet.ScenarioTests.SdkTemplateTests tool"); sb.Append("RUN"); - // TODO: remove --force-all when aspnet package versioning issue have been resolved - https://github.com/dotnet/source-build/issues/4895 + // TODO: remove --force-all after deps image issue has been resolved - https://github.com/dotnet/dotnet-docker/issues/6271 string packageInstallationCommand = packageType == PackageType.Deb ? "dpkg -i --force-all" : "rpm -i"; bool useAndOperator = false; foreach (string package in packageList) @@ -295,9 +306,9 @@ private string GenerateDockerfile(List packageList, string baseImage, Pa private bool AnyTestFailures(string testResultSummary) { var parts = testResultSummary.Split(',') - .Select(part => part.Split(':').Select(p => p.Trim()).ToArray()) - .Where(p => p.Length == 2) - .ToDictionary(p => p[0], p => int.Parse(p[1])); + .Select(part => part.Split(':').Select(p => p.Trim()).ToArray()) + .Where(p => p.Length == 2) + .ToDictionary(p => p[0], p => int.Parse(p[1])); return parts["Errors"] > 0 || parts["Failures"] > 0; } @@ -323,29 +334,9 @@ private string GetContentPackage(string prefix, PackageType packageType) private string GetMatchingDepsPackage(string baseImage, PackageType packageType) { - string matchPattern = "dotnet-runtime-deps-*.deb"; - if (packageType == PackageType.Rpm) - { - string? depsId = null; - if (baseImage.Contains("opensuse")) - { - depsId = "opensuse"; - } - else if (baseImage.Contains("sles")) - { - depsId = "sles"; - } - else if (baseImage.Contains("azurelinux")) - { - depsId = "azl"; - } - else - { - throw new Exception($"Unknown distro: {baseImage}"); - } - - matchPattern = $"dotnet-runtime-deps-*{depsId}*.rpm"; - } + string matchPattern = packageType == PackageType.Deb + ? "dotnet-runtime-deps-*.deb" + : "dotnet-runtime-deps-*azl*.rpm"; // We currently only support Azure Linux deps image string[] files = Directory.GetFiles(_contextDir, matchPattern, SearchOption.AllDirectories); if (files.Length == 0) From 004efb282d93a60c9893edb6656a4f20d89430da Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Tue, 25 Feb 2025 17:04:23 +0000 Subject: [PATCH 9/9] Specify test command in docker run command --- .../LinuxInstallerTests.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs index 83697f6d5ca4..236c6ddc8828 100755 --- a/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.Installer.Tests/LinuxInstallerTests.cs @@ -174,6 +174,7 @@ private void DistroTest(string baseImage, PackageType packageType) { List packageList = GetPackageList(baseImage, packageType); string dockerfile = GenerateDockerfile(packageList, baseImage, packageType); + string testCommand = $"dotnet {GetScenarioTestsBinaryPath()} --dotnet-root /usr/share/dotnet/"; string tag = $"test-{Path.GetRandomFileName()}"; string output = ""; @@ -184,7 +185,7 @@ private void DistroTest(string baseImage, PackageType packageType) // Build docker image and run the tests _dockerHelper.Build(tag, dockerfile: dockerfile, contextDir: _contextDir); buildCompleted = true; - output = _dockerHelper.Run(tag, tag); + output = _dockerHelper.Run(tag, tag, testCommand); int testResultsSummaryIndex = output.IndexOf("Tests run: "); if (testResultsSummaryIndex >= 0) @@ -214,6 +215,18 @@ private void DistroTest(string baseImage, PackageType packageType) } } + private string GetScenarioTestsBinaryPath() + { + // Find scenario-tests binary in context/scenario-tests + string? scenarioTestsBinary = Directory.GetFiles(Path.Combine(_contextDir, "scenario-tests"), "Microsoft.DotNet.ScenarioTests.SdkTemplateTests.dll", SearchOption.AllDirectories).FirstOrDefault(); + if (scenarioTestsBinary == null) + { + throw new Exception("Scenario tests binary not found"); + } + + return scenarioTestsBinary.Replace(_contextDir, "").Replace("\\", "/"); + } + private List GetPackageList(string baseImage, PackageType packageType) { // Order of installation is important as we do not want to use "--nodeps" @@ -286,18 +299,6 @@ private string GenerateDockerfile(List packageList, string baseImage, Pa sb.AppendLine("# Set custom nuget.config"); sb.AppendLine("ENV RestoreConfigFile=/NuGet.config"); - // Find scenario-tests binary in context/scenario-tests - string? scenarioTestsBinary = Directory.GetFiles(Path.Combine(_contextDir, "scenario-tests"), "Microsoft.DotNet.ScenarioTests.SdkTemplateTests.dll", SearchOption.AllDirectories).FirstOrDefault(); - if (scenarioTestsBinary == null) - { - throw new Exception("Scenario tests binary not found"); - } - scenarioTestsBinary = scenarioTestsBinary.Replace(_contextDir, "").Replace("\\", "/"); - - // Set entry point - sb.AppendLine(""); - sb.AppendLine($"ENTRYPOINT [ \"dotnet\", \"{scenarioTestsBinary}\", \"--dotnet-root\", \"/usr/share/dotnet\" ]"); - string dockerfile = Path.Combine(_contextDir, $"Dockerfile-{Path.GetRandomFileName()}"); File.WriteAllText(dockerfile, sb.ToString()); return dockerfile;