diff --git a/eng/testing/RunnerTemplate.sh b/eng/testing/RunnerTemplate.sh index 0e6e0b778bd4e5..c990b67d06496f 100644 --- a/eng/testing/RunnerTemplate.sh +++ b/eng/testing/RunnerTemplate.sh @@ -22,7 +22,9 @@ while [[ $# -gt 0 ]]; do exit 0 ;; --runtime-path|-r) - RUNTIME_PATH=$2 + # Exported so that tests which need to launch a portable application of their own can find the + # same host the test run itself was handed. RunnerTemplate.cmd's "set" already does this. + export RUNTIME_PATH=$2 shift ;; --rsp-file) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index 7282154c33498c..3e6c5eabde7ac4 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -9,69 +9,42 @@ namespace Microsoft.Extensions.Hosting.IntegrationTesting { public static class DotNetCommands { - private const string _dotnetFolderName = ".dotnet"; + // Set by the test run script to the testhost it was handed via --runtime-path: the locally built + // testhost for a local run, the Helix correlation payload in CI. See eng/testing/RunnerTemplate.sh + // and eng/testing/RunnerTemplate.cmd. + private const string RuntimePathVariableName = "RUNTIME_PATH"; - internal static string DotNetHome { get; } = GetDotNetHome(); + /// + /// Gets the full path of the muxer that portable applications are launched with, or + /// when the current test environment has not got one. + /// + public static string DotNetMuxerPath { get; } = FindDotNetMuxer(); - // Compare to https://github.com/aspnet/BuildTools/blob/314c98e4533217a841ff9767bb38e144eb6c93e4/tools/KoreBuild.Console/Commands/CommandContext.cs#L76 - public static string GetDotNetHome() - { - var dotnetHome = Environment.GetEnvironmentVariable("DOTNET_HOME"); - var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); - var home = Environment.GetEnvironmentVariable("HOME"); - - var result = Path.Combine(Directory.GetCurrentDirectory(), _dotnetFolderName); - if (!string.IsNullOrEmpty(dotnetHome)) - { - result = dotnetHome; - } - else if (!string.IsNullOrEmpty(dotnetRoot)) - { - // DOTNET_ROOT has x64 appended to the path, which we append again in GetDotNetInstallDir - result = dotnetRoot.Substring(0, dotnetRoot.Length - 3); - } - else if (!string.IsNullOrEmpty(userProfile)) - { - result = Path.Combine(userProfile, _dotnetFolderName); - } - else if (!string.IsNullOrEmpty(home)) - { - result = home; - } - - return result; - } + public static string DotNetExecutableName + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - public static string GetDotNetInstallDir(RuntimeArchitecture arch) + private static string FindDotNetMuxer() { - var dotnetDir = DotNetHome; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + var runtimePath = Environment.GetEnvironmentVariable(RuntimePathVariableName); + if (!string.IsNullOrEmpty(runtimePath)) { - dotnetDir = Path.Combine(dotnetDir, arch.ToString()); + var fromRunScript = Path.Combine(runtimePath, DotNetExecutableName); + if (File.Exists(fromRunScript)) + { + return fromRunScript; + } } - return dotnetDir; - } - - public static string GetDotNetExecutable(RuntimeArchitecture arch) - { - var dotnetDir = GetDotNetInstallDir(arch); - - var dotnetFile = "dotnet"; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - dotnetFile += ".exe"; - } - - return Path.Combine(dotnetDir, dotnetFile); - } - - public static bool IsRunningX86OnX64(RuntimeArchitecture arch) - { - return (RuntimeInformation.OSArchitecture == Architecture.X64 || RuntimeInformation.OSArchitecture == Architecture.Arm64) - && arch == RuntimeArchitecture.x86; +#if NETFRAMEWORK + return null; +#else + // Outside the run script the only host we can vouch for is the one running the tests, which is + // the muxer itself on every leg that does not publish the tests as a self-contained application. + var processPath = Environment.ProcessPath; + return string.Equals(Path.GetFileName(processPath), DotNetExecutableName, StringComparison.OrdinalIgnoreCase) + ? processPath + : null; +#endif } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index 6a96659f406835..31b689d3f1114a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -18,8 +18,6 @@ namespace Microsoft.Extensions.Hosting.IntegrationTesting /// public abstract class ApplicationDeployer : IDisposable { - public static readonly string DotnetCommandName = "dotnet"; - private readonly Stopwatch _stopwatch = new Stopwatch(); private PublishedApplication _publishedApplication; @@ -100,33 +98,21 @@ protected void CleanPublishedOutput() } } - protected string GetDotNetExeForArchitecture() - { - var executableName = DotnetCommandName; - // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. - if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) - { - executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); - if (!File.Exists(executableName)) - { - throw new Exception($"Unable to find '{executableName}'.'"); - } - } - - return executableName; - } + protected static string GetDotNetMuxerPath() + => DotNetCommands.DotNetMuxerPath + ?? throw new Exception($"Unable to find '{DotNetCommands.DotNetExecutableName}'."); protected void ShutDownIfAnyHostProcess(Process hostProcess) { - if (hostProcess != null && !hostProcess.HasExited) + if (hostProcess is not null && IsRunning(hostProcess)) { Logger.LogInformation("Attempting to cancel process {0}", hostProcess.Id); // Shutdown the host process. hostProcess.KillTree(); - if (!hostProcess.HasExited) + if (IsRunning(hostProcess)) { - Logger.LogWarning("Unable to terminate the host process with process Id '{processId}", hostProcess.Id); + Logger.LogWarning("Unable to terminate the host process with process Id '{processId}'", hostProcess.Id); } else { @@ -139,6 +125,22 @@ protected void ShutDownIfAnyHostProcess(Process hostProcess) } } + // Process.HasExited throws InvalidOperationException ("No process is associated with this object") + // when the process was never started (and also after the Process has been disposed, which disassociates + // it). Treat that as "not running" so shutdown cleanup stays non-throwing rather than masking the + // original start failure with a misleading exception. + private static bool IsRunning(Process hostProcess) + { + try + { + return !hostProcess.HasExited; + } + catch (InvalidOperationException) + { + return false; + } + } + protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary environmentVariables) { var environment = startInfo.Environment; diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs index b5ef69dee0664d..9cd579b1886962 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -96,7 +96,7 @@ protected async Task StartSelfHostAsync() if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) { - executableName = GetDotNetExeForArchitecture(); + executableName = GetDotNetMuxerPath(); executableArgs = executable; } else @@ -121,7 +121,8 @@ protected async Task StartSelfHostAsync() AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - var started = new TaskCompletionSource(); + var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostExitTokenSource = new CancellationTokenSource(); HostProcess = new Process() { StartInfo = startInfo }; HostProcess.EnableRaisingEvents = true; @@ -134,7 +135,6 @@ protected async Task StartSelfHostAsync() OutputReceived?.Invoke(sender, dataArgs); }; - var hostExitTokenSource = new CancellationTokenSource(); HostProcess.Exited += (sender, e) => { Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); @@ -151,7 +151,9 @@ protected async Task StartSelfHostAsync() } catch (Exception ex) { + // Surface the real launch failure instead of letting it be masked later during disposal. Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); + throw; } if (HostProcess.HasExited) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs index 5ab3a61adfdc6a..ed84d22abc6701 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Hosting.IntegrationTesting; @@ -22,21 +23,40 @@ public class ShutdownTests "Stopped firing\n" + "Stopped end"; private static readonly TimeSpan s_shutdownExitTimeout = TimeSpan.FromSeconds(30); + private const string TestApplicationName = "Microsoft.Extensions.Hosting.TestApp"; private readonly ITestOutputHelper _output; + // The deployer launches the test application as a portable application: that needs a muxer, plus the + // application's assembly and its dependency manifest sitting next to the tests. Legs that publish the + // tests (single file, NativeAOT, ReadyToRun) run from a self-contained publish layout which carries + // the assembly but no deps.json for it, so there is nothing there to launch portably. + public static bool CanDeployTestApplication + { + get + { + if (DotNetCommands.DotNetMuxerPath is null) + { + return false; + } + + var applicationPath = Path.Combine(AppContext.BaseDirectory, TestApplicationName); + return File.Exists(applicationPath + ".dll") && File.Exists(applicationPath + ".deps.json"); + } + } + public ShutdownTests(ITestOutputHelper output) { _output = output; } - [Fact] + [ConditionalFact(typeof(ShutdownTests), nameof(CanDeployTestApplication))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestRun() { await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } - [Fact] + [ConditionalFact(typeof(ShutdownTests), nameof(CanDeployTestApplication))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestWaitForShutdown() { @@ -59,7 +79,7 @@ private async Task ExecuteShutdownTest(string testName, string shutdownMechanic) RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { - ApplicationName = "Microsoft.Extensions.Hosting.TestApp", + ApplicationName = TestApplicationName, TargetFramework = $"net{version.Major}.{version.Minor}", ApplicationType = ApplicationType.Portable, PublishApplicationBeforeDeployment = true, diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 0dd94483146f90..9f48d7b47b225b 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -394,9 +394,6 @@ - - - @@ -663,9 +660,6 @@ - - -