Skip to content

Commit 25fdaa8

Browse files
authored
Merge pull request #34077 from maryamariyan/fromoldmaster-integration
History on Extensions src/Hosting/IntegrationTesting
2 parents dcbee66 + 3a4cf26 commit 25fdaa8

22 files changed

+1529
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Runtime.InteropServices;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.Logging;
11+
12+
namespace Microsoft.Extensions.Hosting.IntegrationTesting
13+
{
14+
public class ApplicationPublisher
15+
{
16+
public string ApplicationPath { get; }
17+
18+
public ApplicationPublisher(string applicationPath)
19+
{
20+
ApplicationPath = applicationPath;
21+
}
22+
23+
public static readonly string DotnetCommandName = "dotnet";
24+
25+
public virtual Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
26+
{
27+
var publishDirectory = CreateTempDirectory();
28+
using (logger.BeginScope("dotnet-publish"))
29+
{
30+
if (string.IsNullOrEmpty(deploymentParameters.TargetFramework))
31+
{
32+
throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment");
33+
}
34+
35+
var parameters = $"publish "
36+
+ $" --output \"{publishDirectory.FullName}\""
37+
+ $" --framework {deploymentParameters.TargetFramework}"
38+
+ $" --configuration {deploymentParameters.Configuration}"
39+
// avoids triggering builds of dependencies of the test app which could cause issues like https://github.com/dotnet/arcade/issues/2941
40+
+ $" --no-dependencies"
41+
+ $" /p:TargetArchitecture={deploymentParameters.RuntimeArchitecture}"
42+
+ " --no-restore";
43+
44+
if (deploymentParameters.ApplicationType == ApplicationType.Standalone)
45+
{
46+
parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}";
47+
}
48+
else
49+
{
50+
// Workaround for https://github.com/aspnet/websdk/issues/422
51+
parameters += " -p:UseAppHost=false";
52+
}
53+
54+
parameters += $" {deploymentParameters.AdditionalPublishParameters}";
55+
56+
var startInfo = new ProcessStartInfo
57+
{
58+
FileName = DotnetCommandName,
59+
Arguments = parameters,
60+
UseShellExecute = false,
61+
CreateNoWindow = true,
62+
RedirectStandardError = true,
63+
RedirectStandardOutput = true,
64+
WorkingDirectory = deploymentParameters.ApplicationPath,
65+
};
66+
67+
ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, deploymentParameters.PublishEnvironmentVariables, logger);
68+
69+
var hostProcess = new Process() { StartInfo = startInfo };
70+
71+
logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
72+
73+
hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", logger);
74+
75+
// A timeout is passed to Process.WaitForExit() for two reasons:
76+
//
77+
// 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes
78+
// are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe.
79+
// With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes.
80+
// https://stackoverflow.com/a/37983587/102052
81+
//
82+
// 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message.
83+
const int timeoutMinutes = 5;
84+
if (hostProcess.WaitForExit(milliseconds: timeoutMinutes * 60 * 1000))
85+
{
86+
if (hostProcess.ExitCode != 0)
87+
{
88+
var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
89+
logger.LogError(message);
90+
throw new Exception(message);
91+
}
92+
}
93+
else
94+
{
95+
var message = $"{DotnetCommandName} publish failed to exit after {timeoutMinutes} minutes";
96+
logger.LogError(message);
97+
throw new Exception(message);
98+
}
99+
100+
logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
101+
}
102+
103+
return Task.FromResult(new PublishedApplication(publishDirectory.FullName, logger));
104+
}
105+
106+
private static string GetRuntimeIdentifier(DeploymentParameters deploymentParameters)
107+
{
108+
var architecture = deploymentParameters.RuntimeArchitecture;
109+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
110+
{
111+
return "win-" + architecture;
112+
}
113+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
114+
{
115+
return "linux-" + architecture;
116+
}
117+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
118+
{
119+
return "osx-" + architecture;
120+
}
121+
throw new InvalidOperationException("Unrecognized operation system platform");
122+
}
123+
124+
protected static DirectoryInfo CreateTempDirectory()
125+
{
126+
var tempPath = Path.GetTempPath() + Guid.NewGuid().ToString("N");
127+
var target = new DirectoryInfo(tempPath);
128+
target.Create();
129+
return target;
130+
}
131+
}
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.Extensions.Hosting.IntegrationTesting
6+
{
7+
public enum ApplicationType
8+
{
9+
/// <summary>
10+
/// Does not target a specific platform. Requires the matching runtime to be installed.
11+
/// </summary>
12+
Portable,
13+
14+
/// <summary>
15+
/// All dlls are published with the app for x-copy deploy. Net461 requires this because ASP.NET Core is not in the GAC.
16+
/// </summary>
17+
Standalone
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Reflection;
9+
10+
namespace Microsoft.Extensions.Hosting.IntegrationTesting
11+
{
12+
/// <summary>
13+
/// Parameters to control application deployment.
14+
/// </summary>
15+
public class DeploymentParameters
16+
{
17+
public DeploymentParameters()
18+
{
19+
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
20+
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
21+
{
22+
Configuration = configAttribute.Configuration;
23+
}
24+
}
25+
26+
public DeploymentParameters(TestVariant variant)
27+
{
28+
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
29+
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
30+
{
31+
Configuration = configAttribute.Configuration;
32+
}
33+
34+
TargetFramework = variant.Tfm;
35+
ApplicationType = variant.ApplicationType;
36+
RuntimeArchitecture = variant.Architecture;
37+
}
38+
39+
/// <summary>
40+
/// Creates an instance of <see cref="DeploymentParameters"/>.
41+
/// </summary>
42+
/// <param name="applicationPath">Source code location of the target location to be deployed.</param>
43+
/// <param name="runtimeFlavor">Flavor of the clr to run against.</param>
44+
/// <param name="runtimeArchitecture">Architecture of the runtime to be used.</param>
45+
public DeploymentParameters(
46+
string applicationPath,
47+
RuntimeFlavor runtimeFlavor,
48+
RuntimeArchitecture runtimeArchitecture)
49+
{
50+
if (string.IsNullOrEmpty(applicationPath))
51+
{
52+
throw new ArgumentException("Value cannot be null.", nameof(applicationPath));
53+
}
54+
55+
if (!Directory.Exists(applicationPath))
56+
{
57+
throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", applicationPath));
58+
}
59+
60+
ApplicationPath = applicationPath;
61+
ApplicationName = new DirectoryInfo(ApplicationPath).Name;
62+
RuntimeFlavor = runtimeFlavor;
63+
64+
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
65+
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
66+
{
67+
Configuration = configAttribute.Configuration;
68+
}
69+
}
70+
71+
public DeploymentParameters(DeploymentParameters parameters)
72+
{
73+
foreach (var propertyInfo in typeof(DeploymentParameters).GetProperties())
74+
{
75+
if (propertyInfo.CanWrite)
76+
{
77+
propertyInfo.SetValue(this, propertyInfo.GetValue(parameters));
78+
}
79+
}
80+
81+
foreach (var kvp in parameters.EnvironmentVariables)
82+
{
83+
EnvironmentVariables.Add(kvp);
84+
}
85+
86+
foreach (var kvp in parameters.PublishEnvironmentVariables)
87+
{
88+
PublishEnvironmentVariables.Add(kvp);
89+
}
90+
}
91+
92+
public ApplicationPublisher ApplicationPublisher { get; set; }
93+
94+
public RuntimeFlavor RuntimeFlavor { get; set; }
95+
96+
public RuntimeArchitecture RuntimeArchitecture { get; set; } = RuntimeArchitecture.x64;
97+
98+
public string EnvironmentName { get; set; }
99+
100+
public string ApplicationPath { get; set; }
101+
102+
/// <summary>
103+
/// Gets or sets the name of the application. This is used to execute the application when deployed.
104+
/// Defaults to the file name of <see cref="ApplicationPath"/>.
105+
/// </summary>
106+
public string ApplicationName { get; set; }
107+
108+
public string TargetFramework { get; set; }
109+
110+
/// <summary>
111+
/// Configuration under which to build (ex: Release or Debug)
112+
/// </summary>
113+
public string Configuration { get; set; } = "Debug";
114+
115+
/// <summary>
116+
/// Space separated command line arguments to be passed to dotnet-publish
117+
/// </summary>
118+
public string AdditionalPublishParameters { get; set; }
119+
120+
/// <summary>
121+
/// To publish the application before deployment.
122+
/// </summary>
123+
public bool PublishApplicationBeforeDeployment { get; set; }
124+
125+
public bool PreservePublishedApplicationForDebugging { get; set; } = false;
126+
127+
public bool StatusMessagesEnabled { get; set; } = true;
128+
129+
public ApplicationType ApplicationType { get; set; }
130+
131+
public string PublishedApplicationRootPath { get; set; }
132+
133+
/// <summary>
134+
/// Environment variables to be set before starting the host.
135+
/// Not applicable for IIS Scenarios.
136+
/// </summary>
137+
public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();
138+
139+
/// <summary>
140+
/// Environment variables used when invoking dotnet publish.
141+
/// </summary>
142+
public IDictionary<string, string> PublishEnvironmentVariables { get; } = new Dictionary<string, string>();
143+
144+
/// <summary>
145+
/// For any application level cleanup to be invoked after performing host cleanup.
146+
/// </summary>
147+
public Action<DeploymentParameters> UserAdditionalCleanup { get; set; }
148+
149+
public override string ToString()
150+
{
151+
return string.Format(
152+
"[Variation] :: Runtime={0}, Arch={1}, Publish={2}",
153+
RuntimeFlavor,
154+
RuntimeArchitecture,
155+
PublishApplicationBeforeDeployment);
156+
}
157+
}
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Net.Http;
7+
using System.Threading;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Microsoft.Extensions.Hosting.IntegrationTesting
11+
{
12+
/// <summary>
13+
/// Result of a deployment.
14+
/// </summary>
15+
public class DeploymentResult
16+
{
17+
private readonly ILoggerFactory _loggerFactory;
18+
19+
/// <summary>
20+
/// The folder where the application is hosted. This path can be different from the
21+
/// original application source location if published before deployment.
22+
/// </summary>
23+
public string ContentRoot { get; }
24+
25+
/// <summary>
26+
/// Original deployment parameters used for this deployment.
27+
/// </summary>
28+
public DeploymentParameters DeploymentParameters { get; }
29+
30+
/// <summary>
31+
/// Triggered when the host process dies or pulled down.
32+
/// </summary>
33+
public CancellationToken HostShutdownToken { get; }
34+
35+
public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters)
36+
: this(loggerFactory, deploymentParameters: deploymentParameters, contentRoot: string.Empty, hostShutdownToken: CancellationToken.None)
37+
{ }
38+
39+
public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters, string contentRoot, CancellationToken hostShutdownToken)
40+
{
41+
_loggerFactory = loggerFactory;
42+
43+
ContentRoot = contentRoot;
44+
DeploymentParameters = deploymentParameters;
45+
HostShutdownToken = hostShutdownToken;
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)