Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

History on Extensions src/Hosting/IntegrationTesting #34077

Merged
merged 17 commits into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
public class ApplicationPublisher
{
public string ApplicationPath { get; }

public ApplicationPublisher(string applicationPath)
{
ApplicationPath = applicationPath;
}

public static readonly string DotnetCommandName = "dotnet";

public virtual Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
{
var publishDirectory = CreateTempDirectory();
using (logger.BeginScope("dotnet-publish"))
{
if (string.IsNullOrEmpty(deploymentParameters.TargetFramework))
{
throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment");
}

var parameters = $"publish "
+ $" --output \"{publishDirectory.FullName}\""
+ $" --framework {deploymentParameters.TargetFramework}"
+ $" --configuration {deploymentParameters.Configuration}"
// avoids triggering builds of dependencies of the test app which could cause issues like https://github.com/dotnet/arcade/issues/2941
+ $" --no-dependencies"
+ $" /p:TargetArchitecture={deploymentParameters.RuntimeArchitecture}"
+ " --no-restore";

if (deploymentParameters.ApplicationType == ApplicationType.Standalone)
{
parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}";
}
else
{
// Workaround for https://github.com/aspnet/websdk/issues/422
parameters += " -p:UseAppHost=false";
}

parameters += $" {deploymentParameters.AdditionalPublishParameters}";

var startInfo = new ProcessStartInfo
{
FileName = DotnetCommandName,
Arguments = parameters,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = deploymentParameters.ApplicationPath,
};

ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, deploymentParameters.PublishEnvironmentVariables, logger);

var hostProcess = new Process() { StartInfo = startInfo };

logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");

hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", logger);

// A timeout is passed to Process.WaitForExit() for two reasons:
//
// 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes
// are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe.
// With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes.
// https://stackoverflow.com/a/37983587/102052
//
// 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message.
const int timeoutMinutes = 5;
if (hostProcess.WaitForExit(milliseconds: timeoutMinutes * 60 * 1000))
{
if (hostProcess.ExitCode != 0)
{
var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
logger.LogError(message);
throw new Exception(message);
}
}
else
{
var message = $"{DotnetCommandName} publish failed to exit after {timeoutMinutes} minutes";
logger.LogError(message);
throw new Exception(message);
}

logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
}

return Task.FromResult(new PublishedApplication(publishDirectory.FullName, logger));
}

private static string GetRuntimeIdentifier(DeploymentParameters deploymentParameters)
{
var architecture = deploymentParameters.RuntimeArchitecture;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "win-" + architecture;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux-" + architecture;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "osx-" + architecture;
}
throw new InvalidOperationException("Unrecognized operation system platform");
}

protected static DirectoryInfo CreateTempDirectory()
{
var tempPath = Path.GetTempPath() + Guid.NewGuid().ToString("N");
var target = new DirectoryInfo(tempPath);
target.Create();
return target;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
public enum ApplicationType
{
/// <summary>
/// Does not target a specific platform. Requires the matching runtime to be installed.
/// </summary>
Portable,

/// <summary>
/// All dlls are published with the app for x-copy deploy. Net461 requires this because ASP.NET Core is not in the GAC.
/// </summary>
Standalone
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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.IO;
using System.Reflection;

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
/// <summary>
/// Parameters to control application deployment.
/// </summary>
public class DeploymentParameters
{
public DeploymentParameters()
{
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}
}

public DeploymentParameters(TestVariant variant)
{
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}

TargetFramework = variant.Tfm;
ApplicationType = variant.ApplicationType;
RuntimeArchitecture = variant.Architecture;
}

/// <summary>
/// Creates an instance of <see cref="DeploymentParameters"/>.
/// </summary>
/// <param name="applicationPath">Source code location of the target location to be deployed.</param>
/// <param name="runtimeFlavor">Flavor of the clr to run against.</param>
/// <param name="runtimeArchitecture">Architecture of the runtime to be used.</param>
public DeploymentParameters(
string applicationPath,
RuntimeFlavor runtimeFlavor,
RuntimeArchitecture runtimeArchitecture)
{
if (string.IsNullOrEmpty(applicationPath))
{
throw new ArgumentException("Value cannot be null.", nameof(applicationPath));
}

if (!Directory.Exists(applicationPath))
{
throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", applicationPath));
}

ApplicationPath = applicationPath;
ApplicationName = new DirectoryInfo(ApplicationPath).Name;
RuntimeFlavor = runtimeFlavor;

var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}
}

public DeploymentParameters(DeploymentParameters parameters)
{
foreach (var propertyInfo in typeof(DeploymentParameters).GetProperties())
{
if (propertyInfo.CanWrite)
{
propertyInfo.SetValue(this, propertyInfo.GetValue(parameters));
}
}

foreach (var kvp in parameters.EnvironmentVariables)
{
EnvironmentVariables.Add(kvp);
}

foreach (var kvp in parameters.PublishEnvironmentVariables)
{
PublishEnvironmentVariables.Add(kvp);
}
}

public ApplicationPublisher ApplicationPublisher { get; set; }

public RuntimeFlavor RuntimeFlavor { get; set; }

public RuntimeArchitecture RuntimeArchitecture { get; set; } = RuntimeArchitecture.x64;

public string EnvironmentName { get; set; }

public string ApplicationPath { get; set; }

/// <summary>
/// Gets or sets the name of the application. This is used to execute the application when deployed.
/// Defaults to the file name of <see cref="ApplicationPath"/>.
/// </summary>
public string ApplicationName { get; set; }

public string TargetFramework { get; set; }

/// <summary>
/// Configuration under which to build (ex: Release or Debug)
/// </summary>
public string Configuration { get; set; } = "Debug";

/// <summary>
/// Space separated command line arguments to be passed to dotnet-publish
/// </summary>
public string AdditionalPublishParameters { get; set; }

/// <summary>
/// To publish the application before deployment.
/// </summary>
public bool PublishApplicationBeforeDeployment { get; set; }

public bool PreservePublishedApplicationForDebugging { get; set; } = false;

public bool StatusMessagesEnabled { get; set; } = true;

public ApplicationType ApplicationType { get; set; }

public string PublishedApplicationRootPath { get; set; }

/// <summary>
/// Environment variables to be set before starting the host.
/// Not applicable for IIS Scenarios.
/// </summary>
public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();

/// <summary>
/// Environment variables used when invoking dotnet publish.
/// </summary>
public IDictionary<string, string> PublishEnvironmentVariables { get; } = new Dictionary<string, string>();

/// <summary>
/// For any application level cleanup to be invoked after performing host cleanup.
/// </summary>
public Action<DeploymentParameters> UserAdditionalCleanup { get; set; }

public override string ToString()
{
return string.Format(
"[Variation] :: Runtime={0}, Arch={1}, Publish={2}",
RuntimeFlavor,
RuntimeArchitecture,
PublishApplicationBeforeDeployment);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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.Net.Http;
using System.Threading;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
/// <summary>
/// Result of a deployment.
/// </summary>
public class DeploymentResult
{
private readonly ILoggerFactory _loggerFactory;

/// <summary>
/// The folder where the application is hosted. This path can be different from the
/// original application source location if published before deployment.
/// </summary>
public string ContentRoot { get; }

/// <summary>
/// Original deployment parameters used for this deployment.
/// </summary>
public DeploymentParameters DeploymentParameters { get; }

/// <summary>
/// Triggered when the host process dies or pulled down.
/// </summary>
public CancellationToken HostShutdownToken { get; }

public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters)
: this(loggerFactory, deploymentParameters: deploymentParameters, contentRoot: string.Empty, hostShutdownToken: CancellationToken.None)
{ }

public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters, string contentRoot, CancellationToken hostShutdownToken)
{
_loggerFactory = loggerFactory;

ContentRoot = contentRoot;
DeploymentParameters = deploymentParameters;
HostShutdownToken = hostShutdownToken;
}
}
}
Loading