| File: tests\Shared\TemplatesTesting\BuildEnvironment.cs | Web Access |
| Project: src\tests\Aspire.Templates.Tests\Aspire.Templates.Tests.csproj (Aspire.Templates.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; using Microsoft.Extensions.Diagnostics.Latency; using Xunit.Sdk; namespace Aspire.Templates.Tests; public class BuildEnvironment { public string DotNet { get; init; } public string DefaultBuildArgs { get; init; } public IDictionary<string, string> EnvVars { get; init; } public string LogRootPath { get; init; } public string BuiltNuGetsPath { get; init; } public bool UsesCustomDotNet { get; init; } public bool UsesSystemDotNet => !UsesCustomDotNet; public string? NuGetPackagesPath { get; init; } public DirectoryInfo? RepoRoot { get; init; } public TemplatesCustomHive? TemplatesCustomHive { get; init; } public static readonly string TempDir = IsRunningOnCI ? Path.GetTempPath() : Environment.GetEnvironmentVariable("DEV_TEMP") is { } devTemp && Path.Exists(devTemp) ? devTemp : Path.GetTempPath(); public static readonly TestTargetFramework DefaultTargetFramework = ComputeDefaultTargetFramework(); public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); public static readonly string TestRootPath = Path.Combine(TempDir, "templates-testroot"); public static bool IsRunningOnHelix => Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT") is not null; public static bool IsRunningOnCIBuildMachine => Environment.GetEnvironmentVariable("BUILD_BUILDID") is not null; public static bool IsRunningOnGithubActions => Environment.GetEnvironmentVariable("GITHUB_JOB") is not null; public static bool IsRunningOnCI => IsRunningOnHelix || IsRunningOnCIBuildMachine || IsRunningOnGithubActions; public static bool ShouldRunPlaywrightTests => PlaywrightProvider.HasPlaywrightSupport && !EnvironmentVariables.RunOnlyBasicBuildTemplatesTests; private static readonly Lazy<BuildEnvironment> s_net8Sdk = new(() => new BuildEnvironment(sdkDirName: "dotnet-8")); private static readonly Lazy<BuildEnvironment> s_net9Sdk = new(() => new BuildEnvironment(sdkDirName: "dotnet-9")); private static readonly Lazy<BuildEnvironment> s_net10Sdk = new(() => new BuildEnvironment(sdkDirName: "dotnet-10")); private static readonly Lazy<BuildEnvironment> s_net11Sdk = new(() => new BuildEnvironment(sdkDirName: "dotnet-11")); private static readonly Lazy<BuildEnvironment> s_net11SdkWithAllSupportedRuntimes = new(() => new BuildEnvironment(sdkDirName: "dotnet-tests")); public static BuildEnvironment ForNet8SdkOnly => s_net8Sdk.Value; public static BuildEnvironment ForNet9SdkOnly => s_net9Sdk.Value; public static BuildEnvironment ForNet10SdkOnly => s_net10Sdk.Value; public static BuildEnvironment ForNet11SdkOnly => s_net11Sdk.Value; public static BuildEnvironment ForNet11SdkWithAllSupportedRuntimes => s_net11SdkWithAllSupportedRuntimes.Value; public static BuildEnvironment ForDefaultFramework => DefaultTargetFramework switch { TestTargetFramework.Net8 => ForNet8SdkOnly, TestTargetFramework.Net9 or TestTargetFramework.Net10 => ForNet11SdkWithAllSupportedRuntimes, TestTargetFramework.Net11 => ForNet11SdkOnly, _ => throw new ArgumentOutOfRangeException(nameof(DefaultTargetFramework)) }; public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotnet-tests") { UsesCustomDotNet = !useSystemDotNet; RepoRoot = TestUtils.FindRepoRoot(); string sdkForTemplatePath; if (RepoRoot is not null) { // Local run if (!useSystemDotNet) { var sdkFromArtifactsPath = Path.Combine(RepoRoot!.FullName, "artifacts", "bin", sdkDirName); if (Directory.Exists(sdkFromArtifactsPath)) { sdkForTemplatePath = Path.GetFullPath(sdkFromArtifactsPath); } else { string buildCmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".\\build.cmd" : "./build.sh"; string workloadsProjString = Path.Combine("tests", "workloads.proj"); throw new XunitException( $"Could not find a SDK with the necessary components installed at {sdkFromArtifactsPath} computed from {nameof(RepoRoot)}={RepoRoot}." + $" Build all the packages with '{buildCmd} -pack'." + $" Then install the SDK with 'dotnet build {workloadsProjString}'." + " See https://github.com/microsoft/aspire/tree/main/tests/Aspire.Templates.Tests#readme for more details."); } } else { string? dotnetPath = Environment.GetEnvironmentVariable("PATH")! .Split(Path.PathSeparator) .Select(path => Path.Combine(path, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet")) .FirstOrDefault(File.Exists); if (dotnetPath is null) { throw new ArgumentException($"Could not find dotnet.exe in PATH={Environment.GetEnvironmentVariable("PATH")}"); } sdkForTemplatePath = Path.GetDirectoryName(dotnetPath)!; } #if RELEASE BuiltNuGetsPath = Path.Combine(RepoRoot.FullName, "artifacts", "packages", "Release", "Shipping"); #else BuiltNuGetsPath = Path.Combine(RepoRoot.FullName, "artifacts", "packages", "Debug", "Shipping"); #endif PlaywrightProvider.DetectAndSetInstalledPlaywrightDependenciesPath(RepoRoot); } else { if (useSystemDotNet) { string? dotnetPath = Environment.GetEnvironmentVariable("PATH")! .Split(Path.PathSeparator) .Select(path => Path.Combine(path, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet")) .FirstOrDefault(File.Exists); if (dotnetPath is null) { throw new ArgumentException($"Could not find dotnet.exe in PATH={Environment.GetEnvironmentVariable("PATH")}"); } sdkForTemplatePath = Path.GetDirectoryName(dotnetPath)!; } else { // CI - helix if (string.IsNullOrEmpty(EnvironmentVariables.SdkForTemplateTestingPath)) { throw new ArgumentException($"Environment variable SDK_FOR_TEMPLATES_TESTING_PATH is unset"); } string? baseDir = Path.GetDirectoryName(EnvironmentVariables.SdkForTemplateTestingPath); if (baseDir is null) { throw new ArgumentException($"Cannot find base directory for SDK_FOR_TEMPLATES_TESTING_PATH - {baseDir}"); } sdkForTemplatePath = Path.Combine(baseDir, sdkDirName); } if (string.IsNullOrEmpty(EnvironmentVariables.BuiltNuGetsPath) || !Directory.Exists(EnvironmentVariables.BuiltNuGetsPath)) { throw new ArgumentException($"Cannot find 'BUILT_NUGETS_PATH={EnvironmentVariables.BuiltNuGetsPath}' or {BuiltNuGetsPath}"); } BuiltNuGetsPath = EnvironmentVariables.BuiltNuGetsPath; } if (!Directory.Exists(TestAssetsPath)) { throw new ArgumentException($"Cannot find TestAssetsPath={TestAssetsPath}"); } sdkForTemplatePath = Path.GetFullPath(sdkForTemplatePath); // The generated AppHost project opts into the CLI bundle, so an environment variable // cannot disable it: project properties override properties imported from the environment. // Template tests use repo-built DCP and dashboard packages instead of an installed bundle, // so pass the opt-out as a global MSBuild property that the project cannot override. DefaultBuildArgs = "/p:AspireUseCliBundle=false"; NuGetPackagesPath = UsesCustomDotNet ? Path.Combine(AppContext.BaseDirectory, $"nuget-cache-{Guid.NewGuid()}") : null; EnvVars = new Dictionary<string, string>(); if (UsesCustomDotNet) { EnvVars["DOTNET_ROOT"] = sdkForTemplatePath; EnvVars["DOTNET_INSTALL_DIR"] = sdkForTemplatePath; EnvVars["DOTNET_MULTILEVEL_LOOKUP"] = "0"; EnvVars["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"; EnvVars["PATH"] = $"{sdkForTemplatePath}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}"; } EnvVars["NUGET_PACKAGES"] = NuGetPackagesPath!; EnvVars["BUILT_NUGETS_PATH"] = BuiltNuGetsPath; EnvVars["TreatWarningsAsErrors"] = "true"; // Set DEBUG_SESSION_PORT='' to avoid the app from the tests connecting // to the IDE EnvVars["DEBUG_SESSION_PORT"] = ""; // Avoid using the msbuild terminal logger, so the output can be read // in the tests EnvVars["_MSBUILDTLENABLED"] = "0"; EnvVars["SkipAspireWorkloadManifest"] = "true"; // Template tests build generated apps from repo-built packages, not from an installed // Aspire CLI bundle layout, so keep bundle resolution disabled for these test builds. EnvVars["AspireUseCliBundle"] = "false"; EnvVars["NoWarn"] = "ASPIRE010"; if (OperatingSystem.IsMacOS()) { // Disable developer certificate trust and HTTPS termination in macOS template tests to avoid keychain prompts. EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_TRUST"] = "false"; EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_HTTPS_TERMINATION"] = "false"; } DotNet = Path.Combine(sdkForTemplatePath!, "dotnet"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { DotNet += ".exe"; } if (!string.IsNullOrEmpty(EnvironmentVariables.TestLogPath)) { LogRootPath = Path.GetFullPath(EnvironmentVariables.TestLogPath); Directory.CreateDirectory(LogRootPath); } else { LogRootPath = Path.Combine(AppContext.BaseDirectory, "logs"); } Console.WriteLine($"*** Using path for projects: {TestRootPath}"); CleanupTestRootPath(); Directory.CreateDirectory(TestRootPath); Console.WriteLine($"*** Using Sdk path: {sdkForTemplatePath}"); if (UsesCustomDotNet) { if (EnvironmentVariables.IsRunningOnCI) { Console.WriteLine($"*** Using NuGet cache: {NuGetPackagesPath}"); if (Directory.Exists(NuGetPackagesPath)) { Directory.Delete(NuGetPackagesPath, recursive: true); } } else { if (NuGetPackagesPath is not null && Directory.Exists(NuGetPackagesPath)) { foreach (var dir in Directory.GetDirectories(NuGetPackagesPath, "aspire*")) { Directory.Delete(dir, recursive: true); } } Console.WriteLine($"*** Using NuGet cache (never deleted automatically): {NuGetPackagesPath}"); } } TemplatesCustomHive = TemplatesCustomHive.TemplatesHive; TemplatesCustomHive?.EnsureInstalledAsync(this).Wait(); static void CleanupTestRootPath() { if (!Directory.Exists(TestRootPath)) { return; } try { Directory.Delete(TestRootPath, recursive: true); } catch (IOException) when (!EnvironmentVariables.IsRunningOnCI) { // there might be lingering processes that are holding onto the files // try deleting the subdirectories instead Console.WriteLine($"\tFailed to delete {TestRootPath} . Deleting subdirectories."); foreach (var dir in Directory.GetDirectories(TestRootPath)) { try { Directory.Delete(dir, recursive: true); } catch (IOException ioex) { // ignore Console.WriteLine($"\tFailed to delete {dir} : {ioex.Message}. Ignoring."); } } } catch (Exception ex) { throw new InvalidOperationException($"Error deleting '{TestRootPath}'.", ex); } } } public BuildEnvironment(BuildEnvironment otherBuildEnvironment) { DotNet = otherBuildEnvironment.DotNet; DefaultBuildArgs = otherBuildEnvironment.DefaultBuildArgs; EnvVars = new Dictionary<string, string>(otherBuildEnvironment.EnvVars); LogRootPath = otherBuildEnvironment.LogRootPath; BuiltNuGetsPath = otherBuildEnvironment.BuiltNuGetsPath; UsesCustomDotNet = otherBuildEnvironment.UsesCustomDotNet; NuGetPackagesPath = otherBuildEnvironment.NuGetPackagesPath; RepoRoot = otherBuildEnvironment.RepoRoot; TemplatesCustomHive = otherBuildEnvironment.TemplatesCustomHive; } private static TestTargetFramework ComputeDefaultTargetFramework() => EnvironmentVariables.DefaultTFMForTesting?.ToLowerInvariant() switch { null or "" or "net9.0" => TestTargetFramework.Net9, "net8.0" => TestTargetFramework.Net8, "net10.0" => TestTargetFramework.Net10, "net11.0" => TestTargetFramework.Net11, _ => throw new ArgumentOutOfRangeException(nameof(EnvironmentVariables.DefaultTFMForTesting), EnvironmentVariables.DefaultTFMForTesting, "Invalid value") }; } public enum TestTargetFramework { Net8, Net9, Net10, Net11, None } public static class TestTargetFrameworkExtensions { public static string ToTFMString(this TestTargetFramework tfm) => tfm switch { TestTargetFramework.Net8 => "net8.0", TestTargetFramework.Net9 => "net9.0", TestTargetFramework.Net10 => "net10.0", TestTargetFramework.Net11 => "net11.0", _ => throw new ArgumentOutOfRangeException(nameof(tfm)) }; }