Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion eng/testing/RunnerTemplate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
/// <summary>
/// Gets the full path of the muxer that portable applications are launched with, or
/// <see langword="null"/> when the current test environment has not got one.
/// </summary>
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ namespace Microsoft.Extensions.Hosting.IntegrationTesting
/// </summary>
public abstract class ApplicationDeployer : IDisposable
{
public static readonly string DotnetCommandName = "dotnet";

private readonly Stopwatch _stopwatch = new Stopwatch();

private PublishedApplication _publishedApplication;
Expand Down Expand Up @@ -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);
}
Comment thread
rosebyte marked this conversation as resolved.
else
{
Expand All @@ -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<string, string> environmentVariables)
{
var environment = startInfo.Environment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ protected async Task<CancellationToken> StartSelfHostAsync()

if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable)
{
executableName = GetDotNetExeForArchitecture();
executableName = GetDotNetMuxerPath();
executableArgs = executable;
}
else
Expand All @@ -121,7 +121,8 @@ protected async Task<CancellationToken> StartSelfHostAsync()

AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables);

var started = new TaskCompletionSource<object>();
var started = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var hostExitTokenSource = new CancellationTokenSource();

HostProcess = new Process() { StartInfo = startInfo };
HostProcess.EnableRaisingEvents = true;
Expand All @@ -134,7 +135,6 @@ protected async Task<CancellationToken> StartSelfHostAsync()

OutputReceived?.Invoke(sender, dataArgs);
};
var hostExitTokenSource = new CancellationTokenSource();
HostProcess.Exited += (sender, e) =>
{
Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id);
Expand All @@ -151,7 +151,9 @@ protected async Task<CancellationToken> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()
{
Expand All @@ -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,
Expand Down
6 changes: 0 additions & 6 deletions src/libraries/tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,6 @@
</ItemGroup>

<ItemGroup Condition="'$(TestNativeAot)' == 'true' and '$(RunDisabledNativeAotTests)' != 'true'">
<!-- https://github.com/dotnet/runtime/issues/129832 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj" />

<!-- https://github.com/dotnet/runtime/issues/119380 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Text.Json\tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj"
Condition="'$(TargetOS)' == 'osx' and '$(TargetArchitecture)' == 'arm64'" />
Expand Down Expand Up @@ -663,9 +660,6 @@
<ItemGroup Condition="'$(TestReadyToRun)' == 'true'">
<!-- https://github.com/dotnet/runtime/issues/95928 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Text.RegularExpressions/tests/FunctionalTests/System.Text.RegularExpressions.Tests.csproj" />

<!-- https://github.com/dotnet/runtime/issues/129832 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading