Add full IIS tests (#979)

This commit is contained in:
Justin Kotalik 2018-07-02 12:15:15 -07:00 committed by GitHub
parent 65d3787fc4
commit dfd75e939d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 629 additions and 12 deletions

View File

@ -8,6 +8,7 @@ branches:
install:
- ps: .\tools\update_schema.ps1
- git submodule update --init --recursive
- net start w3svc
build_script:
- ps: .\run.ps1 default-build
clone_depth: 1

View File

@ -6,9 +6,9 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using IISIntegration.FunctionalTests.Utilities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IIS.Performance

View File

@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
@ -38,8 +38,10 @@
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview1-26618-02</MicrosoftNETCoreApp22PackageVersion>
<MicrosoftNetHttpHeadersPackageVersion>2.2.0-preview1-34530</MicrosoftNetHttpHeadersPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<MicrosoftWebAdministrationPackageVersion>11.1.0</MicrosoftWebAdministrationPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<SystemBuffersPackageVersion>4.6.0-preview1-26617-01</SystemBuffersPackageVersion>
<SystemDiagnosticsEventLogPackageVersion>4.6.0-preview1-26617-01</SystemDiagnosticsEventLogPackageVersion>
<SystemIOPipelinesPackageVersion>4.6.0-preview1-26617-01</SystemIOPipelinesPackageVersion>
<SystemMemoryPackageVersion>4.6.0-preview1-26617-01</SystemMemoryPackageVersion>
<SystemNetWebSocketsWebSocketProtocolPackageVersion>4.6.0-preview1-26617-01</SystemNetWebSocketsWebSocketProtocolPackageVersion>

View File

@ -334,7 +334,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToDotnet(
const fs::path & requestedPath
)
{
WLOG_INFOF(L"Resolving absolute path do dotnet.exe from %s", requestedPath.c_str());
WLOG_INFOF(L"Resolving absolute path to dotnet.exe from %s", requestedPath.c_str());
//
// If we are given an absolute path to dotnet.exe, we are done
@ -368,7 +368,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToDotnet(
const auto dotnetViaWhere = InvokeWhereToFindDotnet();
if (dotnetViaWhere.has_value())
{
WLOG_INFOF(L"Found dotnet.exe wia where.exe invocation at %s", dotnetViaWhere.value().c_str());
WLOG_INFOF(L"Found dotnet.exe via where.exe invocation at %s", dotnetViaWhere.value().c_str());
return dotnetViaWhere;
}
@ -394,7 +394,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToHostFxr(
std::vector<std::wstring> versionFolders;
const auto hostFxrBase = dotnetPath.parent_path() / "host" / "fxr";
WLOG_INFOF(L"Resolving absolute path do hostfxr.dll from %s", dotnetPath.c_str());
WLOG_INFOF(L"Resolving absolute path to hostfxr.dll from %s", dotnetPath.c_str());
if (!is_directory(hostFxrBase))
{

View File

@ -36,7 +36,9 @@
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
<PackageReference Include="Microsoft.Web.Administration" Version="$(MicrosoftWebAdministrationPackageVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
<PackageReference Include="System.Diagnostics.EventLog" Version="$(SystemDiagnosticsEventLogPackageVersion)" />
<PackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
[SkipIfIISCannotRun]
public class IISTests : FunctionalTestsBase
{
[ConditionalFact]
public Task HelloWorld_IIS_CoreClr_X64_Standalone()
{
return HelloWorld(RuntimeFlavor.CoreClr, ApplicationType.Standalone);
}
[ConditionalFact]
public Task HelloWorld_IIS_CoreClr_X64_Portable()
{
return HelloWorld(RuntimeFlavor.CoreClr, ApplicationType.Portable);
}
private async Task HelloWorld(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
{
var deploymentParameters = Helpers.GetBaseDeploymentParameters();
deploymentParameters.ServerType = ServerType.IIS;
deploymentParameters.ApplicationType = applicationType;
var deploymentResult = await DeployAsync(deploymentParameters);
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World", responseText);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using IISIntegration.FunctionalTests.Utilities;
using Microsoft.AspNetCore.Http;
@ -11,7 +9,7 @@ using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
namespace IISIntegration.FunctionalTests.Inprocess
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
[SkipIfHostableWebCoreNotAvailible]
public class TestServerTest: LoggedTest

View File

@ -21,10 +21,18 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
if (!parameters.EnvironmentVariables.ContainsKey(DebugEnvironmentVariable))
{
// enable debug output
parameters.EnvironmentVariables[DebugEnvironmentVariable] = "4";
}
_deployer = ApplicationDeployerFactory.Create(parameters, LoggerFactory);
// Currently hosting throws if the Servertype = IIS.
if (parameters.ServerType == ServerType.IIS)
{
_deployer = new IISDeployer(parameters, LoggerFactory);
}
else
{
_deployer = ApplicationDeployerFactory.Create(parameters, LoggerFactory);
}
var result = await _deployer.DeployAsync();

View File

@ -0,0 +1,310 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Web.Administration;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
/// <summary>
/// Represents the IIS website registered in the global applicationHost.config
/// </summary>
internal class IISApplication
{
private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(2);
private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(100);
private readonly ServerManager _serverManager = new ServerManager();
private readonly DeploymentParameters _deploymentParameters;
private readonly ILogger _logger;
private readonly string _ancmVersion;
private readonly object _syncLock = new object();
private readonly string _apphostConfigBackupPath;
private static readonly string _apphostConfigPath = Path.Combine(
Environment.SystemDirectory,
"inetsrv",
"config",
"applicationhost.config");
public IISApplication(DeploymentParameters deploymentParameters, ILogger logger)
{
_deploymentParameters = deploymentParameters;
_logger = logger;
_ancmVersion = deploymentParameters.AncmVersion.ToString();
WebSiteName = CreateTestSiteName();
AppPoolName = $"{WebSiteName}Pool";
_apphostConfigBackupPath = Path.Combine(
Environment.SystemDirectory,
"inetsrv",
"config",
$"applicationhost.config.{WebSiteName}backup");
}
public string WebSiteName { get; }
public string AppPoolName { get; }
public async Task StartIIS(Uri uri, string contentRoot)
{
// Backup currently deployed apphost.config file
using (_logger.BeginScope("StartIIS"))
{
AddTemporaryAppHostConfig();
var port = uri.Port;
if (port == 0)
{
throw new NotSupportedException("Cannot set port 0 for IIS.");
}
ConfigureAppPool(contentRoot);
ConfigureSite(contentRoot, port);
ConfigureAppHostConfig(contentRoot);
if (_deploymentParameters.ApplicationType == ApplicationType.Portable)
{
ModifyAspNetCoreSectionInWebConfig("processPath", DotNetMuxer.MuxerPathOrDefault());
}
_serverManager.CommitChanges();
await WaitUntilSiteStarted();
}
}
private void ModifyAspNetCoreSectionInWebConfig(string key, string value)
{
var webConfigFile = Path.Combine(_deploymentParameters.PublishedApplicationRootPath, "web.config");
var config = XDocument.Load(webConfigFile);
var element = config.Descendants("aspNetCore").FirstOrDefault();
element.SetAttributeValue(key, value);
config.Save(webConfigFile);
}
private async Task WaitUntilSiteStarted()
{
var sw = Stopwatch.StartNew();
while (sw.Elapsed < _timeout)
{
try
{
var site = _serverManager.Sites.FirstOrDefault(s => s.Name.Equals(WebSiteName));
if (site.State == ObjectState.Started)
{
_logger.LogInformation($"Site {WebSiteName} has started.");
return;
}
else if (site.State != ObjectState.Starting)
{
_logger.LogInformation($"Site hasn't started with state: {site.State.ToString()}");
var state = site.Start();
_logger.LogInformation($"Tried to start site, state: {state.ToString()}");
}
}
catch (COMException comException)
{
// Accessing the site.State property while the site
// is starting up returns the COMException
// The object identifier does not represent a valid object.
// (Exception from HRESULT: 0x800710D8)
// This also means the site is not started yet, so catch and retry
// after waiting.
_logger.LogWarning($"ComException: {comException.Message}");
}
await Task.Delay(_retryDelay);
}
throw new TimeoutException($"IIS failed to start site.");
}
public async Task StopAndDeleteAppPool()
{
if (string.IsNullOrEmpty(WebSiteName))
{
return;
}
RestoreAppHostConfig();
_serverManager.CommitChanges();
await WaitUntilSiteStopped();
}
private async Task WaitUntilSiteStopped()
{
var site = _serverManager.Sites.Where(element => element.Name == WebSiteName).FirstOrDefault();
if (site == null)
{
return;
}
var sw = Stopwatch.StartNew();
while (sw.Elapsed < _timeout)
{
try
{
if (site.State == ObjectState.Stopped)
{
_logger.LogInformation($"Site {WebSiteName} has stopped successfully.");
return;
}
}
catch (COMException)
{
// Accessing the site.State property while the site
// is shutdown down returns the COMException
return;
}
_logger.LogWarning($"IIS has not stopped after {sw.Elapsed.TotalMilliseconds}");
await Task.Delay(_retryDelay);
}
throw new TimeoutException($"IIS failed to stop site {site}.");
}
private void AddTemporaryAppHostConfig()
{
File.Copy(_apphostConfigPath, _apphostConfigBackupPath);
_logger.LogInformation($"Backed up {_apphostConfigPath} to {_apphostConfigBackupPath}");
}
private void RestoreAppHostConfig()
{
RetryHelper.RetryOperation(
() => File.Delete(_apphostConfigPath),
e => _logger.LogWarning($"Failed to delete directory : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
File.Move(_apphostConfigBackupPath, _apphostConfigPath);
_logger.LogInformation($"Restored {_apphostConfigPath}.");
}
private ApplicationPool ConfigureAppPool(string contentRoot)
{
var pool = _serverManager.ApplicationPools.Add(AppPoolName);
pool.ProcessModel.IdentityType = ProcessModelIdentityType.LocalSystem;
pool.ManagedRuntimeVersion = string.Empty;
var envCollection = pool.GetCollection("environmentVariables");
AddEnvironmentVariables(contentRoot, envCollection);
_logger.LogInformation($"Configured AppPool {AppPoolName}");
return pool;
}
private void AddEnvironmentVariables(string contentRoot, ConfigurationElementCollection envCollection)
{
foreach (var tuple in _deploymentParameters.EnvironmentVariables)
{
AddEnvironmentVariableToAppPool(envCollection, tuple.Key, tuple.Value);
}
}
private static void AddEnvironmentVariableToAppPool(ConfigurationElementCollection envCollection, string key, string value)
{
var addElement = envCollection.CreateElement("add");
addElement["name"] = key;
addElement["value"] = value;
envCollection.Add(addElement);
}
private Site ConfigureSite(string contentRoot, int port)
{
var site = _serverManager.Sites.Add(WebSiteName, contentRoot, port);
site.Applications.Single().ApplicationPoolName = AppPoolName;
_logger.LogInformation($"Configured Site {WebSiteName} with AppPool {AppPoolName}");
return site;
}
private Configuration ConfigureAppHostConfig(string dllRoot)
{
var config = _serverManager.GetApplicationHostConfiguration();
SetGlobalModuleSection(config, dllRoot);
SetModulesSection(config);
return config;
}
private void SetGlobalModuleSection(Configuration config, string dllRoot)
{
var ancmFile = GetAncmLocation(dllRoot);
var globalModulesSection = config.GetSection("system.webServer/globalModules");
var globalConfigElement = globalModulesSection
.GetCollection()
.Where(element => (string)element["name"] == _ancmVersion)
.FirstOrDefault();
if (globalConfigElement == null)
{
_logger.LogInformation($"Could not find {_ancmVersion} section in global modules; creating section.");
var addElement = globalModulesSection.GetCollection().CreateElement("add");
addElement["name"] = _ancmVersion;
addElement["image"] = ancmFile;
globalModulesSection.GetCollection().Add(addElement);
}
else
{
_logger.LogInformation($"Replacing {_ancmVersion} section in global modules with {ancmFile}");
globalConfigElement["image"] = ancmFile;
}
}
private void SetModulesSection(Configuration config)
{
var modulesSection = config.GetSection("system.webServer/modules");
var moduleConfigElement = modulesSection.GetCollection().Where(element => (string)element["name"] == _ancmVersion).FirstOrDefault();
if (moduleConfigElement == null)
{
_logger.LogInformation($"Could not find {_ancmVersion} section in modules; creating section.");
var moduleElement = modulesSection.GetCollection().CreateElement("add");
moduleElement["name"] = _ancmVersion;
modulesSection.GetCollection().Add(moduleElement);
}
}
private string CreateTestSiteName()
{
if (!string.IsNullOrEmpty(_deploymentParameters.SiteName))
{
return $"{_deploymentParameters.SiteName}{DateTime.Now.ToString("yyyyMMddHHmmss")}";
}
else
{
return $"testsite{DateTime.Now.ToString("yyyyMMddHHmmss")}";
}
}
private string GetAncmLocation(string dllRoot)
{
var arch = _deploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x64 ? @"x64\aspnetcorev2.dll" : @"x86\aspnetcorev2.dll";
var ancmFile = Path.Combine(dllRoot, arch);
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
{
ancmFile = Path.Combine(dllRoot, "aspnetcorev2.dll");
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
{
throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile);
}
}
return ancmFile;
}
}
}

View File

@ -0,0 +1,95 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
/// <summary>
/// Deployer for IIS.
/// </summary>
public partial class IISDeployer : ApplicationDeployer
{
private IISApplication _application;
private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource();
public IISDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
: base(deploymentParameters, loggerFactory)
{
}
public override void Dispose()
{
if (_application != null)
{
_application.StopAndDeleteAppPool().GetAwaiter().GetResult();
TriggerHostShutdown(_hostShutdownToken);
}
GetLogsFromFile($"{_application.WebSiteName}.txt");
GetLogsFromFile("web.config");
CleanPublishedOutput();
InvokeUserApplicationCleanup();
StopTimer();
}
public override async Task<DeploymentResult> DeployAsync()
{
using (Logger.BeginScope("Deployment"))
{
StartTimer();
var contentRoot = string.Empty;
_application = new IISApplication(DeploymentParameters, Logger);
// For now, only support using published output
DeploymentParameters.PublishApplicationBeforeDeployment = true;
if (DeploymentParameters.PublishApplicationBeforeDeployment)
{
DotnetPublish();
contentRoot = DeploymentParameters.PublishedApplicationRootPath;
}
var uri = TestIISUriHelper.BuildTestUri(ServerType.IIS, DeploymentParameters.ApplicationBaseUriHint);
// To prevent modifying the IIS setup concurrently.
await _application.StartIIS(uri, contentRoot);
// Warm up time for IIS setup.
Logger.LogInformation("Successfully finished IIS application directory setup.");
return new DeploymentResult(
LoggerFactory,
DeploymentParameters,
applicationBaseUri: uri.ToString(),
contentRoot: contentRoot,
hostShutdownToken: _hostShutdownToken.Token
);
}
}
private void GetLogsFromFile(string file)
{
var arr = new string[0];
RetryHelper.RetryOperation(() => arr = File.ReadAllLines(Path.Combine(DeploymentParameters.PublishedApplicationRootPath, file)),
(ex) => Logger.LogError("Could not read log file"),
5,
200);
foreach (var line in arr)
{
Logger.LogInformation(line);
}
}
}
}

View File

@ -1,3 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net.Http;
using Microsoft.Extensions.Logging;

View File

@ -5,7 +5,7 @@ using System;
using System.IO;
using Microsoft.AspNetCore.Testing.xunit;
namespace IISIntegration.FunctionalTests.Utilities
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public sealed class SkipIfHostableWebCoreNotAvailibleAttribute : Attribute, ITestCondition

View File

@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Xml.Linq;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing.xunit;
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public sealed class SkipIfIISCannotRunAttribute : Attribute, ITestCondition
{
private static readonly bool _isMet;
public static readonly string _skipReason;
public bool IsMet => _isMet;
public string SkipReason => _skipReason;
static SkipIfIISCannotRunAttribute()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_skipReason = "IIS tests can only be run on Windows";
return;
}
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
if (!principal.IsInRole(WindowsBuiltInRole.Administrator))
{
_skipReason += "The current console is not running as admin.";
return;
}
if (!File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", "w3wp.exe")))
{
_skipReason += "The machine does not have IIS installed.";
return;
}
var ancmConfigPath = Path.Combine(Environment.SystemDirectory, "inetsrv", "config", "schema", "aspnetcore_schema_v2.xml");
if (!File.Exists(ancmConfigPath))
{
_skipReason = "IIS Schema is not installed.";
return;
}
XDocument ancmConfig;
try
{
ancmConfig = XDocument.Load(ancmConfigPath);
}
catch
{
_skipReason = "Could not read ANCM schema configuration";
return;
}
_isMet = ancmConfig
.Root
.Descendants("attribute")
.Any(n => "hostingModel".Equals(n.Attribute("name")?.Value, StringComparison.Ordinal));
_skipReason = _isMet ? null : "IIS schema needs to be upgraded to support ANCM.";
}
}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net;
using System.Net.Sockets;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
// Copied from Hosting for now https://github.com/aspnet/Hosting/blob/970bc8a30d66dd6894f8f662e5fdab9e68d57777/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs
internal static class TestIISUriHelper
{
internal static Uri BuildTestUri(ServerType serverType)
{
return BuildTestUri(serverType, hint: null);
}
internal static Uri BuildTestUri(ServerType serverType, string hint)
{
// Assume status messages are enabled for Kestrel and disabled for all other servers.
return BuildTestUri(serverType, hint, statusMessagesEnabled: serverType == ServerType.Kestrel);
}
internal static Uri BuildTestUri(ServerType serverType, string hint, bool statusMessagesEnabled)
{
if (string.IsNullOrEmpty(hint))
{
if (serverType == ServerType.Kestrel && statusMessagesEnabled)
{
// Most functional tests use this codepath and should directly bind to dynamic port "0" and scrape
// the assigned port from the status message, which should be 100% reliable since the port is bound
// once and never released. Binding to dynamic port "0" on "localhost" (both IPv4 and IPv6) is not
// supported, so the port is only bound on "127.0.0.1" (IPv4). If a test explicitly requires IPv6,
// it should provide a hint URL with "localhost" (IPv4 and IPv6) or "[::1]" (IPv6-only).
return new UriBuilder("http", "127.0.0.1", 0).Uri;
}
else
{
// If the server type is not Kestrel, or status messages are disabled, there is no status message
// from which to scrape the assigned port, so the less reliable GetNextPort() must be used. The
// port is bound on "localhost" (both IPv4 and IPv6), since this is supported when using a specific
// (non-zero) port.
return new UriBuilder("http", "localhost", GetNextPort()).Uri;
}
}
else
{
var uriHint = new Uri(hint);
if (uriHint.Port == 0)
{
// Only a few tests use this codepath, so it's fine to use the less reliable GetNextPort() for simplicity.
// The tests using this codepath will be reviewed to see if they can be changed to directly bind to dynamic
// port "0" on "127.0.0.1" and scrape the assigned port from the status message (the default codepath).
return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri;
}
else
{
// If the hint contains a specific port, return it unchanged.
return uriHint;
}
}
}
// Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508
//
// This method is an attempt to safely get a free port from the OS. Most of the time,
// when binding to dynamic port "0" the OS increments the assigned port, so it's safe
// to re-use the assigned port in another process. However, occasionally the OS will reuse
// a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse
// exceptions. This method should only be used when the application itself cannot use
// dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel
// (with status messages enabled) should directly bind to dynamic port "0" and scrape
// the assigned port from the status message, which should be 100% reliable since the port
// is bound once and never released.
internal static int GetNextPort()
{
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint).Port;
}
}
}
}

View File

@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IISIntegration.FunctionalTests.Utilities
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
public class TestServer: IDisposable, IStartup
{