/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/ProjectTemplates/Shared/AspNetProcess.cs
311 строк
12 KB
Youssef Fahmy
Update AngleSharp to latest (#67898)
20 июл 2026, 15:48
Не верифицирован
20 июл 2026, 15:48
fc4e6e0
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Net; using System.Net.Http; using System.Net.Security; using AngleSharp.Html.Dom; using AngleSharp.Html.Parser; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Playwright; using Xunit.Abstractions; namespace Templates.Test.Helpers; [DebuggerDisplay("{ToString(),nq}")] public class AspNetProcess : IDisposable { private const string ListeningMessagePrefix = "Now listening on: "; private readonly HttpClient _httpClient; private readonly ITestOutputHelper _output; private readonly DevelopmentCertificate _developmentCertificate; internal readonly Uri ListeningUri; internal ProcessEx Process { get; } public AspNetProcess( DevelopmentCertificate cert, ITestOutputHelper output, string workingDirectory, string dllPath, IDictionary<string, string> environmentVariables, bool published, bool hasListeningUri = true, bool usePublishedAppHost = false, ILogger logger = null) { _developmentCertificate = cert; _output = output; _httpClient = new HttpClient(new HttpClientHandler() { ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => (certificate.Subject != "CN=localhost" && errors == SslPolicyErrors.None) || certificate?.Thumbprint == _developmentCertificate.CertificateThumbprint, }) { Timeout = TimeSpan.FromMinutes(2) }; output.WriteLine("Running ASP.NET Core application..."); string process; string arguments; if (published) { if (usePublishedAppHost) { // When publishing, use the app host to run the app. This makes it easy to consistently run for regular and single-file publish process = Path.ChangeExtension(dllPath, OperatingSystem.IsWindows() ? ".exe" : null); arguments = null; } else { process = DotNetMuxer.MuxerPathOrDefault(); arguments = $"exec {dllPath}"; } } else { process = DotNetMuxer.MuxerPathOrDefault(); // When executing "dotnet run", the launch urls specified in the app's launchSettings.json have higher precedence // than ambient environment variables. We specify the urls using command line arguments instead to allow us // to continue binding to "port 0" and avoid test flakiness due to port conflicts. arguments = $"run --no-build --urls \"{environmentVariables["ASPNETCORE_URLS"]}\""; } if (logger is not null && logger.IsEnabled(LogLevel.Information)) { logger.LogInformation($"AspNetProcess - process: {process} arguments: {arguments}"); } var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables) { ["ASPNETCORE_Kestrel__Certificates__Default__Path"] = _developmentCertificate.CertificatePath, ["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _developmentCertificate.CertificatePassword, }; Process = ProcessEx.Run(output, workingDirectory, process, arguments, envVars: finalEnvironmentVariables); logger?.LogInformation("AspNetProcess - process started"); if (hasListeningUri) { logger?.LogInformation("AspNetProcess - Getting listening uri"); ListeningUri = ResolveListeningUrl(output); if (logger is not null && logger.IsEnabled(LogLevel.Information)) { logger?.LogInformation($"AspNetProcess - Got {ListeningUri}"); } } } public async Task VisitInBrowserAsync(IPage page) { _output.WriteLine($"Opening browser at {ListeningUri}..."); await page.GotoAsync(ListeningUri.AbsoluteUri); } public async Task AssertPagesOk(IEnumerable<Page> pages) { foreach (var page in pages) { await AssertOk(page.Url); if (page.Links?.Count() > 0) { await ContainsLinks(page); } } } public async Task AssertPagesNotFound(IEnumerable<string> urls) { foreach (var url in urls) { await AssertNotFound(url); } } public async Task ContainsLinks(Page page) { var response = await RetryHelper.RetryRequest(async () => { var request = new HttpRequestMessage( HttpMethod.Get, new Uri(ListeningUri, page.Url)); return await _httpClient.SendAsync(request); }, logger: NullLogger.Instance); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var parser = new HtmlParser(); var html = await parser.ParseDocumentAsync(await response.Content.ReadAsStreamAsync()); foreach (IHtmlLinkElement styleSheet in html.GetElementsByTagName("link")) { Assert.Equal("stylesheet", styleSheet.Relation); // Workaround for https://github.com/dotnet/aspnetcore/issues/31030#issuecomment-811334450 // Cleans up incorrectly generated filename for scoped CSS files var styleSheetHref = styleSheet.Href.Replace("_", string.Empty).Replace("about://", string.Empty); await AssertOk(styleSheetHref); } foreach (var script in html.Scripts) { if (!string.IsNullOrEmpty(script.Source)) { await AssertOk(script.Source); } } Assert.True(html.Links.Length == page.Links.Count(), $"Expected {page.Url} to have {page.Links.Count()} links but it had {html.Links.Length}"); foreach ((var link, var expectedLink) in html.Links.Zip(page.Links, Tuple.Create)) { IHtmlAnchorElement anchor = (IHtmlAnchorElement)link; if (string.Equals(anchor.Protocol, "about:")) { Assert.True(anchor.PathName.EndsWith(expectedLink, StringComparison.Ordinal), $"Expected next link on {page.Url} to be {expectedLink} but it was {anchor.PathName}: {html.Source.Text}"); await AssertOk(anchor.PathName); } else { Assert.True(string.Equals(anchor.Href, expectedLink), $"Expected next link to be {expectedLink} but it was {anchor.Href}."); if (!string.Equals(anchor.Protocol, "https:") || string.Equals(anchor.HostName, "localhost", StringComparison.OrdinalIgnoreCase)) { // This is a relative or same-site URI, so verify it goes to a real destination within the app. // We don't do this for external (absolute) URIs as it would introduce flakiness, and the code // above already checks that the URI is what we expect. var result = await RetryHelper.RetryRequest(async () => { return await _httpClient.GetAsync(anchor.Href); }, logger: NullLogger.Instance); Assert.True(IsSuccessStatusCode(result), $"{anchor.Href} is a broken link!"); } } } } private Uri ResolveListeningUrl(ITestOutputHelper output) { // Wait until the app is accepting HTTP requests output.WriteLine("Waiting until ASP.NET application is accepting connections..."); var listeningMessage = GetListeningMessage(); if (!string.IsNullOrEmpty(listeningMessage)) { listeningMessage = listeningMessage.Trim(); // Verify we have a valid URL to make requests to var listeningUrlString = listeningMessage.Substring(listeningMessage.IndexOf( ListeningMessagePrefix, StringComparison.Ordinal) + ListeningMessagePrefix.Length); output.WriteLine($"Detected that ASP.NET application is accepting connections on: {listeningUrlString}"); listeningUrlString = string.Concat(listeningUrlString.AsSpan(0, listeningUrlString.IndexOf(':')), "://localhost", listeningUrlString.AsSpan(listeningUrlString.LastIndexOf(':'))); output.WriteLine("Sending requests to " + listeningUrlString); return new Uri(listeningUrlString, UriKind.Absolute); } else { return null; } } private string GetListeningMessage() { var buffer = new List<string>(); try { foreach (var line in Process.OutputLinesAsEnumerable) { if (line != null) { buffer.Add(line); if (line.Trim().Contains(ListeningMessagePrefix, StringComparison.Ordinal)) { return line; } } } } catch (OperationCanceledException) { } throw new InvalidOperationException(@$"Couldn't find listening url: {string.Join(Environment.NewLine, buffer)}"); } private static bool IsSuccessStatusCode(HttpResponseMessage response) { return response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.Redirect; } public Task AssertOk(string requestUrl) => AssertStatusCode(requestUrl, HttpStatusCode.OK); public Task AssertNotFound(string requestUrl) => AssertStatusCode(requestUrl, HttpStatusCode.NotFound); internal Task<HttpResponseMessage> SendRequest(string path) => RetryHelper.RetryRequest(() => _httpClient.GetAsync(new Uri(ListeningUri, path)), logger: NullLogger.Instance); internal Task<HttpResponseMessage> SendRequest(Func<HttpRequestMessage> requestFactory) => RetryHelper.RetryRequest(() => _httpClient.SendAsync(requestFactory()), logger: NullLogger.Instance); public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null) { var response = await RetryHelper.RetryRequest(() => { var request = new HttpRequestMessage( HttpMethod.Get, new Uri(ListeningUri, requestUrl)); if (!string.IsNullOrEmpty(acceptContentType)) { request.Headers.Add("Accept", acceptContentType); } return _httpClient.SendAsync(request); }, logger: NullLogger.Instance); Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'."); } public void Dispose() { _httpClient.Dispose(); Process.Dispose(); } public override string ToString() { var result = ""; result += Process != null ? "Active: " : "Inactive"; if (Process != null) { if (!Process.HasExited) { result += $"(Listening on {ListeningUri.OriginalString}) PID: {Process.Id}"; } else { result += "(Already finished)"; } } return result; } } public class Page(string url) { public Page() : this(null) { } public string Url { get; set; } = url; public IEnumerable<string> Links { get; set; } }