diff --git a/.azure/pipelines/richnav.yml b/.azure/pipelines/richnav.yml index 46d8b9e3cb75c9f74b4e8b705ae762f45c5b6972..e5fe8756d315e0991920164f4b10f28d975d5632 100644 --- a/.azure/pipelines/richnav.yml +++ b/.azure/pipelines/richnav.yml @@ -6,59 +6,69 @@ trigger: branches: include: - - blazor-wasm - main - release/* - - internal/release/* + +# Do not run this pipeline for PR validation. +pr: none variables: - name: _BuildArgs value: '/p:SkipTestBuild=true' -- name: Windows86LogArgs +- name: WindowsNonX64LogArgs value: -ExcludeCIBinaryLog stages: - stage: build displayName: Build jobs: - # Build Windows (x64/x86) + # Build Windows (x64/x86/arm64) - template: jobs/default-build.yml parameters: codeSign: false jobName: Windows_build - jobDisplayName: "Build: Windows x64/x86" + jobDisplayName: "Build: Windows x64/x86/arm64" enableRichCodeNavigation: true agentOs: Windows steps: - - script: ./build.cmd + - script: ./eng/build.cmd -ci - -all -arch x64 - /p:EnableRichCodeNavigation=true + -buildNative + /p:EnableRichCodeNavigation=false + $(_BuildArgs) + displayName: Build x64 native assets + + - script: ./eng/build.cmd + -ci + -arch x64 + -all + -noBuildNative + -noBuildRepoTasks $(_BuildArgs) displayName: Build x64 # Build the x86 shared framework # This is going to actually build x86 native assets. - - script: ./build.cmd + - script: ./eng/build.cmd -ci - -noBuildRepoTasks -arch x86 -all -noBuildJava -noBuildNative - /p:EnableRichCodeNavigation=true + -noBuildRepoTasks $(_BuildArgs) - $(Windows86LogArgs) + $(WindowsNonX64LogArgs) displayName: Build x86 - # Windows installers bundle both x86 and x64 assets - - script: ./build.cmd + # Build the arm64 shared framework + - script: ./eng/build.cmd -ci - -noBuildRepoTasks - -buildInstallers + -arch arm64 + -noBuildJava -noBuildNative - /p:AssetManifestFileName=aspnetcore-win-x64-x86.xml - /p:EnableRichCodeNavigation=true + -noBuildRepoTasks $(_BuildArgs) - displayName: Build Installers + $(WindowsNonX64LogArgs) + displayName: Build ARM64 + diff --git a/Directory.Build.props b/Directory.Build.props index 8f0ac5942284cb62724017bdaa2c5700e4b1ce37..721f099a7734bd344434f3eeb2100fba83bd62b3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,6 +30,9 @@ $(MSBuildProjectName.EndsWith('.Test')) OR $(MSBuildProjectName.EndsWith('.FunctionalTest')) ) ">true</IsUnitTestProject> <IsTestAssetProject Condition=" $(RepoRelativeProjectDir.Contains('testassets')) OR $(MSBuildProjectName.Contains('TestCommon'))">true</IsTestAssetProject> + <IsProjectTemplateProject Condition=" ($(RepoRelativeProjectDir.Contains('ProjectTemplates')) OR $(MSBuildProjectName.Contains('ProjectTemplates')) ) AND + '$(IsUnitTestProject)' != 'true' AND + '$(IsTestAssetProject)' != 'true' ">true</IsProjectTemplateProject> <IsSampleProject Condition=" $(RepoRelativeProjectDir.ToUpperInvariant().Contains('SAMPLE')) ">true</IsSampleProject> <IsAnalyzersProject Condition="$(MSBuildProjectName.EndsWith('.Analyzers'))">true</IsAnalyzersProject> <IsShipping Condition=" '$(IsSampleProject)' == 'true' OR diff --git a/Directory.Build.targets b/Directory.Build.targets index a195c0fb2ea0e22d72f7972c6d6938784a3942c6..fd388a1176d2762dbe99ba7519ca2f8f03391f4c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,10 +1,15 @@ <Project> <PropertyGroup> - <!-- Only build Microsoft.AspNetCore.App, Microsoft.AspNetCore.App.Ref, and ref/ assemblies in source build. --> + <!-- Only build Microsoft.AspNetCore.App, Microsoft.AspNetCore.App.Ref, ref/ assemblies, and ProjectTemplates in source build. --> <!-- Analyzer package are needed in source build for WebSDK --> <ExcludeFromSourceBuild - Condition="'$(ExcludeFromSourceBuild)' == '' and '$(DotNetBuildFromSource)' == 'true' and '$(IsAspNetCoreApp)' != 'true' and '$(MSBuildProjectName)' != '$(TargetingPackName)' and '$(IsAnalyzersProject)' != 'true'">true</ExcludeFromSourceBuild> + Condition="'$(ExcludeFromSourceBuild)' == '' and + '$(DotNetBuildFromSource)' == 'true' and + '$(IsAspNetCoreApp)' != 'true' and + '$(MSBuildProjectName)' != '$(TargetingPackName)' and + '$(IsAnalyzersProject)' != 'true' and + '$(IsProjectTemplateProject)' != 'true'">true</ExcludeFromSourceBuild> <!-- If the user has specified that they want to skip building any test related projects with SkipTestBuild, suppress all targets for TestProjects using ExcludeFromBuild. --> diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a15e7e7e18252fba5d8178387ea8ae52d37b3bcb..640bbfa0defb6cf2765556fb7c3a88a080ff690a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -280,22 +280,22 @@ <Uri>https://dev.azure.com/dnceng/internal/_git/dotnet-runtime</Uri> <Sha>be98e88c760526452df94ef452fff4602fb5bded</Sha> </Dependency> - <Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="6.0.0-beta.22161.1"> + <Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="6.0.0-beta.22178.5"> <Uri>https://github.com/dotnet/arcade</Uri> - <Sha>879df783283dfb44c7653493fdf7fd7b07ba6b01</Sha> + <Sha>f8c0d51185208227e582f76ac3c5003db237b689</Sha> <SourceBuild RepoName="arcade" ManagedOnly="true" /> </Dependency> - <Dependency Name="Microsoft.DotNet.Build.Tasks.Installers" Version="6.0.0-beta.22161.1"> + <Dependency Name="Microsoft.DotNet.Build.Tasks.Installers" Version="6.0.0-beta.22178.5"> <Uri>https://github.com/dotnet/arcade</Uri> - <Sha>879df783283dfb44c7653493fdf7fd7b07ba6b01</Sha> + <Sha>f8c0d51185208227e582f76ac3c5003db237b689</Sha> </Dependency> - <Dependency Name="Microsoft.DotNet.Build.Tasks.Templating" Version="6.0.0-beta.22161.1"> + <Dependency Name="Microsoft.DotNet.Build.Tasks.Templating" Version="6.0.0-beta.22178.5"> <Uri>https://github.com/dotnet/arcade</Uri> - <Sha>879df783283dfb44c7653493fdf7fd7b07ba6b01</Sha> + <Sha>f8c0d51185208227e582f76ac3c5003db237b689</Sha> </Dependency> - <Dependency Name="Microsoft.DotNet.Helix.Sdk" Version="6.0.0-beta.22161.1"> + <Dependency Name="Microsoft.DotNet.Helix.Sdk" Version="6.0.0-beta.22178.5"> <Uri>https://github.com/dotnet/arcade</Uri> - <Sha>879df783283dfb44c7653493fdf7fd7b07ba6b01</Sha> + <Sha>f8c0d51185208227e582f76ac3c5003db237b689</Sha> </Dependency> </ToolsetDependencies> </Dependencies> diff --git a/eng/Versions.props b/eng/Versions.props index 2d3e47d26e4157f7296c91910bd40ca534570899..72ca8c5f00473f37d36adbfa780b9ea33bdf85d5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,8 +8,8 @@ <PropertyGroup Label="Version settings"> <AspNetCoreMajorVersion>6</AspNetCoreMajorVersion> <AspNetCoreMinorVersion>0</AspNetCoreMinorVersion> - <AspNetCorePatchVersion>4</AspNetCorePatchVersion> - <ValidateBaseline>true</ValidateBaseline> + <AspNetCorePatchVersion>5</AspNetCorePatchVersion> + <ValidateBaseline>false</ValidateBaseline> <!-- When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages --> @@ -131,8 +131,8 @@ <MicrosoftEntityFrameworkCoreVersion>6.0.4</MicrosoftEntityFrameworkCoreVersion> <MicrosoftEntityFrameworkCoreDesignVersion>6.0.4</MicrosoftEntityFrameworkCoreDesignVersion> <!-- Packages from dotnet/arcade --> - <MicrosoftDotNetBuildTasksInstallersVersion>6.0.0-beta.22161.1</MicrosoftDotNetBuildTasksInstallersVersion> - <MicrosoftDotNetBuildTasksTemplatingVersion>6.0.0-beta.22161.1</MicrosoftDotNetBuildTasksTemplatingVersion> + <MicrosoftDotNetBuildTasksInstallersVersion>6.0.0-beta.22178.5</MicrosoftDotNetBuildTasksInstallersVersion> + <MicrosoftDotNetBuildTasksTemplatingVersion>6.0.0-beta.22178.5</MicrosoftDotNetBuildTasksTemplatingVersion> </PropertyGroup> <!-- diff --git a/eng/common/templates/steps/source-build.yml b/eng/common/templates/steps/source-build.yml index ba40dc82f1411b9fb54db50a4c315d07a27aa305..abb1b2bcda42b84352ff6ead9d449f959e27dfc8 100644 --- a/eng/common/templates/steps/source-build.yml +++ b/eng/common/templates/steps/source-build.yml @@ -43,8 +43,8 @@ steps: # In that case, add variables to allow the download of internal runtimes if the specified versions are not found # in the default public locations. internalRuntimeDownloadArgs= - if [ '$(dotnetclimsrc-read-sas-token-base64)' != '$''(dotnetclimsrc-read-sas-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetclimsrc.blob.core.windows.net/dotnet /p:DotNetRuntimeSourceFeedKey=$(dotnetclimsrc-read-sas-token-base64) --runtimesourcefeed https://dotnetclimsrc.blob.core.windows.net/dotnet --runtimesourcefeedkey $(dotnetclimsrc-read-sas-token-base64)' + if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release diff --git a/global.json b/global.json index 05ed9cd7faca029f874fb92c428b51d5f4bd4c55..ecf50a40254a6674389e826d308e3b0537d9907d 100644 --- a/global.json +++ b/global.json @@ -29,7 +29,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.22161.1", - "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.22161.1" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.22178.5", + "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.22178.5" } } diff --git a/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj b/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj index 8ae5d1c43d7b6df28fc0c7c7f649590f499fca71..1db0368b882c85dbbe4973be06857ee348f9d3f2 100644 --- a/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj +++ b/src/Components/WebAssembly/Authentication.Msal/src/Microsoft.Authentication.WebAssembly.Msal.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk.Razor"> - <Sdk Name="Yarn.MSBuild" /> + <Import Project="Sdk.props" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" /> <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> @@ -25,6 +25,7 @@ <PropertyGroup> <YarnWorkingDir>$(MSBuildThisFileDirectory)Interop\</YarnWorkingDir> <ResolveStaticWebAssetsInputsDependsOn> + CheckForSourceBuild; CompileInterop; IncludeCompileInteropOutput; $(ResolveStaticWebAssetsInputsDependsOn) @@ -91,5 +92,11 @@ <FileWrites Include="$(_InteropBuildOutput)" /> </ItemGroup> </Target> + + <Target Name="CheckForSourceBuild" Condition=" '$(DotNetBuildFromSource)' == 'true'"> + <Error Text="The Yarn.Msbuild SDK is currently excluded from SourceBuild. If you are enabling this project for SourceBuild, remove the condition on the Yarn.Msbuild SDK above." /> + </Target> + <Import Project="Sdk.targets" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" /> + </Project> diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj b/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj index 8d6a000d74f0d646a51119c6c53c8137169c30df..35c79a73eb8b10ac60c15ef1dd85ef1e155bb500 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk.Razor"> - <Sdk Name="Yarn.MSBuild" /> + <Import Project="Sdk.props" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" /> <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> @@ -26,6 +26,7 @@ <PropertyGroup> <YarnWorkingDir>$(MSBuildThisFileDirectory)Interop\</YarnWorkingDir> <ResolveStaticWebAssetsInputsDependsOn> + CheckForSourceBuild; CompileInterop; IncludeCompileInteropOutput; $(ResolveStaticWebAssetsInputsDependsOn) @@ -93,4 +94,10 @@ </ItemGroup> </Target> + <Target Name="CheckForSourceBuild" Condition=" '$(DotNetBuildFromSource)' == 'true'"> + <Error Text="The Yarn.Msbuild SDK is currently excluded from SourceBuild. If you are enabling this project for SourceBuild, remove the condition on the Yarn.Msbuild SDK above." /> + </Target> + + <Import Project="Sdk.targets" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" /> + </Project> diff --git a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs index 11ff6b17d8999af93de231af5bbb07344b285f70..ccfb60a40bb4aa962c6f58f89b087b8c0e899295 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs @@ -1,14 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Encodings.Web; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; using Microsoft.AspNetCore.TestHost; @@ -16,7 +12,6 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -24,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests /// <summary> /// Functional test to verify the error reporting of Razor compilation by diagnostic middleware. /// </summary> - public class ErrorPageTests : IClassFixture<MvcTestFixture<ErrorPageMiddlewareWebSite.Startup>>, IDisposable + public class ErrorPageTests : IClassFixture<MvcTestFixture<ErrorPageMiddlewareWebSite.Startup>> { private static readonly string PreserveCompilationContextMessage = HtmlEncoder.Default.Encode( "One or more compilation references may be missing. " + @@ -189,10 +184,5 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Contains(nullReferenceException, content); Assert.Contains(indexOutOfRangeException, content); } - - public void Dispose() - { - _assemblyTestLog.Dispose(); - } } } diff --git a/src/Servers/HttpSys/HttpSysServer.slnf b/src/Servers/HttpSys/HttpSysServer.slnf index 3990e33925cbff07c901e51ce3ccde3c33a99007..c546d5797a4acf062a54e3b020589b01bc241768 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -4,9 +4,11 @@ "projects": [ "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", + "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", diff --git a/src/Servers/HttpSys/src/DelegationRule.cs b/src/Servers/HttpSys/src/DelegationRule.cs index 1f57f82985584022be8ac93664b00f9420d85fbf..c454952eea9e3bc280e7f08f7ea56335bd9d6b63 100644 --- a/src/Servers/HttpSys/src/DelegationRule.cs +++ b/src/Servers/HttpSys/src/DelegationRule.cs @@ -13,17 +13,19 @@ namespace Microsoft.AspNetCore.Server.HttpSys public class DelegationRule : IDisposable { private readonly ILogger _logger; - private readonly UrlGroup _urlGroup; private readonly UrlGroup _sourceQueueUrlGroup; private bool _disposed; + /// <summary> /// The name of the Http.Sys request queue /// </summary> public string QueueName { get; } + /// <summary> /// The URL of the Http.Sys Url Prefix /// </summary> public string UrlPrefix { get; } + internal RequestQueue Queue { get; } internal DelegationRule(UrlGroup sourceQueueUrlGroup, string queueName, string urlPrefix, ILogger logger) @@ -32,8 +34,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys _logger = logger; QueueName = queueName; UrlPrefix = urlPrefix; - Queue = new RequestQueue(queueName, UrlPrefix, _logger, receiver: true); - _urlGroup = Queue.UrlGroup; + Queue = new RequestQueue(queueName, _logger); } /// <inheritdoc /> @@ -51,7 +52,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys _sourceQueueUrlGroup.UnSetDelegationProperty(Queue, throwOnError: false); } catch (ObjectDisposedException) { /* Server may have been shutdown */ } - _urlGroup.Dispose(); Queue.Dispose(); } } diff --git a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs index 8e25a8c21c37cc558db42be59130e70d1aa19ef1..89638a222575ee9de312ddcb681a493770efaed2 100644 --- a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs +++ b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs @@ -19,25 +19,16 @@ namespace Microsoft.AspNetCore.Server.HttpSys private readonly ILogger _logger; private bool _disposed; - internal RequestQueue(string requestQueueName, string urlPrefix, ILogger logger, bool receiver) - : this(urlGroup: null!, requestQueueName, RequestQueueMode.Attach, logger, receiver) + internal RequestQueue(string requestQueueName, ILogger logger) + : this(urlGroup: null, requestQueueName, RequestQueueMode.Attach, logger, receiver: true) { - try - { - UrlGroup = new UrlGroup(this, UrlPrefix.Create(urlPrefix), logger); - } - catch - { - Dispose(); - throw; - } } internal RequestQueue(UrlGroup urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger) : this(urlGroup, requestQueueName, mode, logger, false) { } - private RequestQueue(UrlGroup urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver) + private RequestQueue(UrlGroup? urlGroup, string? requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver) { _mode = mode; UrlGroup = urlGroup; @@ -117,10 +108,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal SafeHandle Handle { get; } internal ThreadPoolBoundHandle BoundHandle { get; } - internal UrlGroup UrlGroup { get; } + internal UrlGroup? UrlGroup { get; } internal unsafe void AttachToUrlGroup() { + if (UrlGroup == null) + { + throw new NotSupportedException("Can't attach when UrlGroup is null"); + } + Debug.Assert(Created); CheckDisposed(); // Set the association between request queue and url group. After this, requests for registered urls will @@ -138,6 +134,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal unsafe void DetachFromUrlGroup() { + if (UrlGroup == null) + { + throw new NotSupportedException("Can't detach when UrlGroup is null"); + } + Debug.Assert(Created); CheckDisposed(); // Break the association between request queue and url group. After this, requests for registered urls diff --git a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs index d13264889dddebec2b00d3ae938c8d5269f5aa6d..59a67ca43195617b26954ede805d3d429216ac63 100644 --- a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs +++ b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs @@ -41,24 +41,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys Id = urlGroupId; } - internal unsafe UrlGroup(RequestQueue requestQueue, UrlPrefix url, ILogger logger) - { - _logger = logger; - - ulong urlGroupId = 0; - _created = false; - var statusCode = HttpApi.HttpFindUrlGroupId( - url.FullPrefix, requestQueue.Handle, &urlGroupId); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - throw new HttpSysException((int)statusCode); - } - - Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup"); - Id = urlGroupId; - } - internal ulong Id { get; private set; } internal unsafe void SetMaxConnections(long maxConnections) diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index ffffc050672cce7df8cba9bd1482b7ca86069981..880beebf562d686115a681dea15d3b860f4ef3c5 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal ulong RawConnectionId { get; } // No ulongs in public APIs... - public long ConnectionId => (long)RawConnectionId; + public long ConnectionId => RawConnectionId != 0 ? (long)RawConnectionId : (long)UConnectionId; internal ulong RequestId { get; } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index fabae04d35e597fd9dc28333870ad1c27c3f495f..9c45a262dac8fd981d3ccab5360f2e1937afdaea 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -289,10 +289,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys PropertyInfoLength = (uint)System.Text.Encoding.Unicode.GetByteCount(destination.UrlPrefix) }; + // Passing 0 for delegateUrlGroupId allows http.sys to find the right group for the + // URL passed in via the property above. If we passed in the receiver's URL group id + // instead of 0, then delegation would fail if the receiver restarted. statusCode = HttpApi.HttpDelegateRequestEx(source.Handle, destination.Queue.Handle, Request.RequestId, - destination.Queue.UrlGroup.Id, + delegateUrlGroupId: 0, propertyInfoSetSize: 1, &property); } diff --git a/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs b/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs index 14aee50615c90497b6137037d19650cccfbbe5c6..a97520a837d796d01b94a1d7cb51bf3d8de6eefe 100644 --- a/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs +++ b/src/Servers/HttpSys/src/ServerDelegationPropertyFeature.cs @@ -14,18 +14,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal class ServerDelegationPropertyFeature : IServerDelegationFeature { private readonly ILogger _logger; - private readonly RequestQueue _queue; + private readonly UrlGroup _urlGroup; public ServerDelegationPropertyFeature(RequestQueue queue, ILogger logger) { - _queue = queue; + if (queue.UrlGroup == null) + { + throw new ArgumentException($"{nameof(queue)}.UrlGroup can't be null"); + } + + _urlGroup = queue.UrlGroup; _logger = logger; } public DelegationRule CreateDelegationRule(string queueName, string uri) { - var rule = new DelegationRule(_queue.UrlGroup, queueName, uri, _logger); - _queue.UrlGroup.SetDelegationProperty(rule.Queue); + var rule = new DelegationRule(_urlGroup, queueName, uri, _logger); + _urlGroup.SetDelegationProperty(rule.Queue); return rule; } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs index ca9dcf3a0736e18915f26d539f73b745b02cda30..9e38c7da8b0651c7c27e65fec2fb50260d231351 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/DelegateTests.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; using System.Net.Http; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Testing; -using Xunit; namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests { @@ -198,6 +196,72 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests destination?.Dispose(); } + [ConditionalFact] + [DelegateSupportedCondition(true)] + public async Task DelegateAfterReceiverRestart() + { + var queueName = Guid.NewGuid().ToString(); + using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext => + { + await httpContext.Response.WriteAsync(_expectedResponseString); + }, + options => + { + options.RequestQueueName = queueName; + }); + + DelegationRule destination = default; + using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext => + { + var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>(); + delegateFeature.DelegateRequest(destination); + return Task.CompletedTask; + }); + + var delegationProperty = delegator.Features.Get<IServerDelegationFeature>(); + destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress); + + var responseString = await SendRequestAsync(delegatorAddress); + Assert.Equal(_expectedResponseString, responseString); + + // Stop the receiver + receiver?.Dispose(); + + // Start the receiver again but this time we need to attach to the existing queue. + // Due to https://github.com/dotnet/aspnetcore/issues/40359, we have to manually + // register URL prefixes and attach the server's queue to them. + using var receiverRestarted = (MessagePump)Utilities.CreateHttpServer(out receiverAddress, async httpContext => + { + await httpContext.Response.WriteAsync(_expectedResponseString); + }, + options => + { + options.RequestQueueName = queueName; + options.RequestQueueMode = RequestQueueMode.Attach; + options.UrlPrefixes.Clear(); + options.UrlPrefixes.Add(receiverAddress); + }); + AttachToUrlGroup(receiverRestarted.Listener.RequestQueue); + receiverRestarted.Listener.Options.UrlPrefixes.RegisterAllPrefixes(receiverRestarted.Listener.UrlGroup); + + responseString = await SendRequestAsync(delegatorAddress); + Assert.Equal(_expectedResponseString, responseString); + + destination?.Dispose(); + } + + private unsafe void AttachToUrlGroup(RequestQueue requestQueue) + { + var info = new HttpApiTypes.HTTP_BINDING_INFO(); + info.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; + info.RequestQueueHandle = requestQueue.Handle.DangerousGetHandle(); + + var infoptr = new IntPtr(&info); + + requestQueue.UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, + infoptr, (uint)Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>()); + } + private async Task<string> SendRequestAsync(string uri) { using var client = new HttpClient(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs index e0b0ab828131bc6129d9ffba2b94de3b54462822..e6acee1a6d43e27d664889ed26d86e55a3162832 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/StartupTests.cs @@ -468,7 +468,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests [ConditionalFact] [RequiresNewHandler] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/40036")] public async Task StartupTimeoutIsApplied() { // From what we can tell, this failure is due to ungraceful shutdown. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 064da12cb6346c87d4eba523da33829f1375e762..3c66964cafd472a866c44ff948c33e8be632d0a6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -38,6 +38,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public bool ParseRequestLine(TRequestHandler handler, ref SequenceReader<byte> reader) { + // Skip any leading \r or \n on the request line. This is not technically allowed, + // but apparently there are enough clients relying on this that it's worth allowing. + // Peek first as a minor performance optimization; it's a quick inlined check. + if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF)) + { + reader.AdvancePastAny(ByteCR, ByteLF); + } + if (reader.TryReadTo(out ReadOnlySpan<byte> requestLine, ByteLF, advancePastDelimiter: true)) { ParseRequestLine(handler, requestLine); diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index 4c65611e95e4b16b71d0580940211050277d0052..59fdf6dd0cc1bca027df08e013ba396eb06988f9 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.IO.Pipelines; using System.Text; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -516,6 +517,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests DifferentFormsWorkTogether(); } + public static IEnumerable<object[]> GetCrLfAndMethodCombinations() + { + // HTTP methods to test + var methods = new string[] { + HttpMethods.Connect, + HttpMethods.Delete, + HttpMethods.Get, + HttpMethods.Head, + HttpMethods.Options, + HttpMethods.Patch, + HttpMethods.Post, + HttpMethods.Put, + HttpMethods.Trace + }; + + // Prefixes to test + var crLfPrefixes = new string[] { + "\r", + "\n", + "\r\r\r\r\r", + "\r\n", + "\n\r" + }; + + foreach (var method in methods) + { + foreach (var prefix in crLfPrefixes) + { + yield return new object[] { prefix, method }; + } + } + } + + [Theory] + [MemberData(nameof(GetCrLfAndMethodCombinations))] + public void LeadingCrLfAreAllowed(string startOfRequestLine, string httpMethod) + { + var rawTarget = "http://localhost/path1?q=123&w=xyzw"; + Http1Connection.Reset(); + // RawTarget, Path, QueryString are null after reset + Assert.Null(Http1Connection.RawTarget); + Assert.Null(Http1Connection.Path); + Assert.Null(Http1Connection.QueryString); + + var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"{startOfRequestLine}{httpMethod} {rawTarget} HTTP/1.1\r\n")); + var reader = new SequenceReader<byte>(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); + } + public StartLineTests() { MemoryPool = PinnedBlockMemoryPoolFactory.Create(); diff --git a/src/Testing/src/AssemblyTestLog.cs b/src/Testing/src/AssemblyTestLog.cs index 0e3d06f2eb3ead47233c139a87c758fe846744ae..4e03f7ca303527b584463a8a6cd36e7b73e6fad5 100644 --- a/src/Testing/src/AssemblyTestLog.cs +++ b/src/Testing/src/AssemblyTestLog.cs @@ -7,10 +7,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; @@ -22,20 +20,21 @@ using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Microsoft.AspNetCore.Testing { - public class AssemblyTestLog : IDisposable + public class AssemblyTestLog : IAcceptFailureReports, IDisposable { private const string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH"; private const string LogFileExtension = ".log"; private static readonly int MaxPathLength = GetMaxPathLength(); - private static readonly object _lock = new object(); - private static readonly Dictionary<Assembly, AssemblyTestLog> _logs = new Dictionary<Assembly, AssemblyTestLog>(); + private static readonly object _lock = new(); + private static readonly Dictionary<Assembly, AssemblyTestLog> _logs = new(); private readonly ILoggerFactory _globalLoggerFactory; private readonly ILogger _globalLogger; private readonly string _baseDirectory; private readonly Assembly _assembly; private readonly IServiceProvider _serviceProvider; + private bool _testFailureReported; private static int GetMaxPathLength() { @@ -53,6 +52,9 @@ namespace Microsoft.AspNetCore.Testing _serviceProvider = serviceProvider; } + // internal for testing + internal bool OnCI { get; set; } = SkipOnCIAttribute.OnCI(); + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName); @@ -178,11 +180,8 @@ namespace Microsoft.AspNetCore.Testing return serviceCollection.BuildServiceProvider(); } - // For back compat - public static AssemblyTestLog Create(string assemblyName, string baseDirectory) - => Create(Assembly.Load(new AssemblyName(assemblyName)), baseDirectory); - - public static AssemblyTestLog Create(Assembly assembly, string baseDirectory) + // internal for testing. Expectation is AspNetTestAssembly runner calls ForAssembly() first for every Assembly. + internal static AssemblyTestLog Create(Assembly assembly, string baseDirectory) { var logStart = DateTimeOffset.UtcNow; SerilogLoggerProvider serilogLoggerProvider = null; @@ -224,26 +223,46 @@ namespace Microsoft.AspNetCore.Testing { if (!_logs.TryGetValue(assembly, out var log)) { - var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly); + var stackTrace = Environment.StackTrace; + if (!stackTrace.Contains( + "Microsoft.AspNetCore.Testing" +#if NETCOREAPP + , StringComparison.Ordinal +#endif + )) + { + throw new InvalidOperationException($"Unexpected initial {nameof(ForAssembly)} caller."); + } - log = Create(assembly, baseDirectory); - _logs[assembly] = log; + var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly); - // Try to clear previous logs, continue if it fails. + // Try to clear previous logs, continue if it fails. Do this before creating new global logger. var assemblyBaseDirectory = TestFileOutputContext.GetAssemblyBaseDirectory(assembly); - if (!string.IsNullOrEmpty(assemblyBaseDirectory) && !TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly)) + if (!string.IsNullOrEmpty(assemblyBaseDirectory) && + !TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly)) { try { Directory.Delete(assemblyBaseDirectory, recursive: true); } - catch { } + catch + { + } } + + log = Create(assembly, baseDirectory); + _logs[assembly] = log; } + return log; } } + public void ReportTestFailure() + { + _testFailureReported = true; + } + private static TestFrameworkFileLoggerAttribute GetFileLoggerAttribute(Assembly assembly) => assembly.GetCustomAttribute<TestFrameworkFileLoggerAttribute>() ?? throw new InvalidOperationException($"No {nameof(TestFrameworkFileLoggerAttribute)} found on the assembly {assembly.GetName().Name}. " @@ -269,13 +288,32 @@ namespace Microsoft.AspNetCore.Testing .MinimumLevel.Verbose() .WriteTo.File(fileName, outputTemplate: "[{TimestampOffset}] [{SourceContext}] [{Level}] {Message:l}{NewLine}{Exception}", flushToDiskInterval: TimeSpan.FromSeconds(1), shared: true) .CreateLogger(); + return new SerilogLoggerProvider(serilogger, dispose: true); } - public void Dispose() + void IDisposable.Dispose() { (_serviceProvider as IDisposable)?.Dispose(); _globalLoggerFactory.Dispose(); + + // Clean up if no tests failed and we're not running local tests. (Ignoring tests of this class, OnCI is + // true on both build and Helix agents.) In particular, remove the directory containing the global.log + // file. All test class log files for this assembly are in subdirectories of this location. + if (!_testFailureReported && + OnCI && + _baseDirectory is not null && + Directory.Exists(_baseDirectory)) + { + try + { + Directory.Delete(_baseDirectory, recursive: true); + } + catch + { + // Best effort. Ignore problems deleting locked logged files. + } + } } private class AssemblyLogTimestampOffsetEnricher : ILogEventEnricher diff --git a/src/Testing/src/AssemblyTestLogFixtureAttribute.cs b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..e4a4452cd458ba50c83db4e0a37b37180d8b1f18 --- /dev/null +++ b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Testing; + +public class AssemblyTestLogFixtureAttribute : AssemblyFixtureAttribute +{ + public AssemblyTestLogFixtureAttribute() : base(typeof(AssemblyTestLog)) + { + } +} diff --git a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props index 063e9094d172ed930ec3ca4a721ef3a50724be3e..47d06dfef7a7ce53d212a11993fe368e95343227 100644 --- a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props +++ b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props @@ -11,8 +11,8 @@ </PropertyGroup> <Target Name="SetLoggingTestingAssemblyAttributes" - BeforeTargets="GetAssemblyAttributes" - Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'"> + BeforeTargets="GetAssemblyAttributes" + Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'"> <PropertyGroup> <PreserveExistingLogsInOutput Condition="'$(PreserveExistingLogsInOutput)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">true</PreserveExistingLogsInOutput> <PreserveExistingLogsInOutput Condition="'$(PreserveExistingLogsInOutput)' == ''">false</PreserveExistingLogsInOutput> @@ -24,6 +24,7 @@ <_Parameter2>Microsoft.AspNetCore.Testing</_Parameter2> </AssemblyAttribute> + <AssemblyAttribute Include="Microsoft.AspNetCore.Testing.AssemblyTestLogFixtureAttribute" /> <AssemblyAttribute Include="Microsoft.AspNetCore.Testing.TestFrameworkFileLoggerAttribute"> <_Parameter1>$(PreserveExistingLogsInOutput)</_Parameter1> <_Parameter2>$(TargetFramework)</_Parameter2> diff --git a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs index a83446375ddd8cb8374b05abf4d35d5ab97fdc03..1d71bdf939bed7beff854f73b73dc2d9ce2feaaf 100644 --- a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs +++ b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Testing { public class AspNetTestAssemblyRunner : XunitTestAssemblyRunner { - private readonly Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>(); + private readonly Dictionary<Type, object> _assemblyFixtureMappings = new(); public AspNetTestAssemblyRunner( ITestAssembly testAssembly, @@ -26,6 +27,9 @@ namespace Microsoft.AspNetCore.Testing { } + // internal for testing + internal IEnumerable<object> Fixtures => _assemblyFixtureMappings.Values; + protected override async Task AfterTestAssemblyStartingAsync() { await base.AfterTestAssemblyStartingAsync(); @@ -33,8 +37,8 @@ namespace Microsoft.AspNetCore.Testing // Find all the AssemblyFixtureAttributes on the test assembly await Aggregator.RunAsync(async () => { - var fixturesAttributes = ((IReflectionAssemblyInfo)TestAssembly.Assembly) - .Assembly + var assembly = ((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly; + var fixturesAttributes = assembly .GetCustomAttributes(typeof(AssemblyFixtureAttribute), false) .Cast<AssemblyFixtureAttribute>() .ToList(); @@ -42,15 +46,30 @@ namespace Microsoft.AspNetCore.Testing // Instantiate all the fixtures foreach (var fixtureAttribute in fixturesAttributes) { - var ctorWithDiagnostics = fixtureAttribute.FixtureType.GetConstructor(new[] { typeof(IMessageSink) }); object instance = null; - if (ctorWithDiagnostics != null) + var staticCreator = fixtureAttribute.FixtureType.GetMethod( + name: "ForAssembly", + bindingAttr: BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new[] { typeof(Assembly) }, + modifiers: null); + if (staticCreator is null) { - instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink); + var ctorWithDiagnostics = fixtureAttribute + .FixtureType + .GetConstructor(new[] { typeof(IMessageSink) }); + if (ctorWithDiagnostics is null) + { + instance = Activator.CreateInstance(fixtureAttribute.FixtureType); + } + else + { + instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink); + } } else { - instance = Activator.CreateInstance(fixtureAttribute.FixtureType); + instance = staticCreator.Invoke(obj: null, parameters: new[] { assembly }); } _assemblyFixtureMappings[fixtureAttribute.FixtureType] = instance; @@ -66,12 +85,12 @@ namespace Microsoft.AspNetCore.Testing protected override async Task BeforeTestAssemblyFinishedAsync() { // Dispose fixtures - foreach (var disposable in _assemblyFixtureMappings.Values.OfType<IDisposable>()) + foreach (var disposable in Fixtures.OfType<IDisposable>()) { Aggregator.Run(disposable.Dispose); } - foreach (var disposable in _assemblyFixtureMappings.Values.OfType<IAsyncLifetime>()) + foreach (var disposable in Fixtures.OfType<IAsyncLifetime>()) { await Aggregator.RunAsync(disposable.DisposeAsync); } @@ -79,12 +98,13 @@ namespace Microsoft.AspNetCore.Testing await base.BeforeTestAssemblyFinishedAsync(); } - protected override Task<RunSummary> RunTestCollectionAsync( + protected override async Task<RunSummary> RunTestCollectionAsync( IMessageBus messageBus, ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases, CancellationTokenSource cancellationTokenSource) - => new AspNetTestCollectionRunner( + { + var runSummary = await new AspNetTestCollectionRunner( _assemblyFixtureMappings, testCollection, testCases, @@ -92,6 +112,17 @@ namespace Microsoft.AspNetCore.Testing messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), - cancellationTokenSource).RunAsync(); + cancellationTokenSource) + .RunAsync(); + if (runSummary.Failed != 0) + { + foreach (var fixture in Fixtures.OfType<IAcceptFailureReports>()) + { + fixture.ReportTestFailure(); + } + } + + return runSummary; + } } } diff --git a/src/Testing/src/xunit/IAcceptFailureReports.cs b/src/Testing/src/xunit/IAcceptFailureReports.cs new file mode 100644 index 0000000000000000000000000000000000000000..30ca366b3f1bf10783b4a81d382bc4d23792a79d --- /dev/null +++ b/src/Testing/src/xunit/IAcceptFailureReports.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Testing; + +internal interface IAcceptFailureReports +{ + void ReportTestFailure(); +} diff --git a/src/Testing/test/AspNetTestAssemblyRunnerTest.cs b/src/Testing/test/AspNetTestAssemblyRunnerTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..dbce4c69bb82af78017083f9c83e04b62b10b816 --- /dev/null +++ b/src/Testing/test/AspNetTestAssemblyRunnerTest.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Testing; + +public class AspNetTestAssemblyRunnerTest +{ + private const int NotCalled = -1; + + [Fact] + public async Task ForAssemblyHasHigherPriorityThanConstructors() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithAll)); + + await runner.AfterTestAssemblyStartingAsync_Public(); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TestAssemblyFixtureWithAll>(fixtureObject); + Assert.False(fixture.ConstructorWithMessageSinkCalled); + Assert.True(fixture.ForAssemblyCalled); + Assert.False(fixture.ParameterlessConstructorCalled); + } + + [Fact] + public async Task ConstructorWithMessageSinkHasHigherPriorityThanParameterlessConstructor() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithMessageSink)); + + await runner.AfterTestAssemblyStartingAsync_Public(); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TestAssemblyFixtureWithMessageSink>(fixtureObject); + Assert.True(fixture.ConstructorWithMessageSinkCalled); + Assert.False(fixture.ParameterlessConstructorCalled); + } + + [Fact] + public async Task CalledInExpectedOrder_SuccessWithDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TextAssemblyFixtureWithDispose)); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(0, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TextAssemblyFixtureWithDispose>(fixtureObject); + Assert.Equal(NotCalled, fixture.ReportTestFailureCalledAt); + Assert.Equal(0, fixture.DisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_FailedWithDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create( + typeof(TextAssemblyFixtureWithDispose), + failTestCase: true); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(1, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TextAssemblyFixtureWithDispose>(fixtureObject); + Assert.Equal(0, fixture.ReportTestFailureCalledAt); + Assert.Equal(1, fixture.DisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_SuccessWithAsyncDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create(typeof(TestAssemblyFixtureWithAsyncDispose)); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(0, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TestAssemblyFixtureWithAsyncDispose>(fixtureObject); + Assert.Equal(0, fixture.InitializeAsyncCalledAt); + Assert.Equal(NotCalled, fixture.ReportTestFailureCalledAt); + Assert.Equal(1, fixture.AsyncDisposeCalledAt); + } + + [Fact] + public async Task CalledInExpectedOrder_FailedWithAsyncDispose() + { + var runner = TestableAspNetTestAssemblyRunner.Create( + typeof(TestAssemblyFixtureWithAsyncDispose), + failTestCase: true); + + var runSummary = await runner.RunAsync(); + + Assert.NotNull(runSummary); + Assert.Equal(1, runSummary.Failed); + Assert.Equal(0, runSummary.Skipped); + Assert.Equal(1, runSummary.Total); + + Assert.NotNull(runner.Fixtures); + var fixtureObject = Assert.Single(runner.Fixtures); + var fixture = Assert.IsType<TestAssemblyFixtureWithAsyncDispose>(fixtureObject); + Assert.Equal(0, fixture.InitializeAsyncCalledAt); + Assert.Equal(1, fixture.ReportTestFailureCalledAt); + Assert.Equal(2, fixture.AsyncDisposeCalledAt); + } + + private class TestAssemblyFixtureWithAll + { + private TestAssemblyFixtureWithAll(bool forAssemblyCalled) + { + ForAssemblyCalled = forAssemblyCalled; + } + + public TestAssemblyFixtureWithAll() + { + ParameterlessConstructorCalled = true; + } + + public TestAssemblyFixtureWithAll(IMessageSink messageSink) + { + ConstructorWithMessageSinkCalled = true; + } + + public static TestAssemblyFixtureWithAll ForAssembly(Assembly assembly) + { + return new TestAssemblyFixtureWithAll(forAssemblyCalled: true); + } + + public bool ParameterlessConstructorCalled { get; } + + public bool ConstructorWithMessageSinkCalled { get; } + + public bool ForAssemblyCalled { get; } + } + + private class TestAssemblyFixtureWithMessageSink + { + public TestAssemblyFixtureWithMessageSink() + { + ParameterlessConstructorCalled = true; + } + + public TestAssemblyFixtureWithMessageSink(IMessageSink messageSink) + { + ConstructorWithMessageSinkCalled = true; + } + + public bool ParameterlessConstructorCalled { get; } + + public bool ConstructorWithMessageSinkCalled { get; } + } + + private class TextAssemblyFixtureWithDispose : IAcceptFailureReports, IDisposable + { + private int _position; + + public int ReportTestFailureCalledAt { get; private set; } = NotCalled; + + public int DisposeCalledAt { get; private set; } = NotCalled; + + void IAcceptFailureReports.ReportTestFailure() + { + ReportTestFailureCalledAt = _position++; + } + + void IDisposable.Dispose() + { + DisposeCalledAt = _position++; + } + } + + private class TestAssemblyFixtureWithAsyncDispose : IAcceptFailureReports, IAsyncLifetime + { + private int _position; + + public int InitializeAsyncCalledAt { get; private set; } = NotCalled; + + public int ReportTestFailureCalledAt { get; private set; } = NotCalled; + + public int AsyncDisposeCalledAt { get; private set; } = NotCalled; + + Task IAsyncLifetime.InitializeAsync() + { + InitializeAsyncCalledAt = _position++; + return Task.CompletedTask; + } + + void IAcceptFailureReports.ReportTestFailure() + { + ReportTestFailureCalledAt = _position++; + } + + Task IAsyncLifetime.DisposeAsync() + { + AsyncDisposeCalledAt = _position++; + return Task.CompletedTask; + } + } +} diff --git a/src/Testing/test/AssemblyTestLogTests.cs b/src/Testing/test/AssemblyTestLogTests.cs index 57c3b87b16819360220011b7a1c9a345e56bda3c..bd3bbb1b8e2905f168b92bedee8b1d9bfb943029 100644 --- a/src/Testing/test/AssemblyTestLogTests.cs +++ b/src/Testing/test/AssemblyTestLogTests.cs @@ -4,21 +4,17 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing.Tests; using Xunit; -namespace Microsoft.Extensions.Logging.Testing.Tests +namespace Microsoft.AspNetCore.Testing { public class AssemblyTestLogTests : LoggedTest { - private static readonly Assembly ThisAssembly = typeof(AssemblyTestLogTests).GetTypeInfo().Assembly; - private static readonly string ThisAssemblyName = ThisAssembly.GetName().Name; - private static readonly string TFM = ThisAssembly.GetCustomAttributes().OfType<TestOutputDirectoryAttribute>().FirstOrDefault().TargetFramework; - [Fact] public void FunctionalLogs_LogsPreservedFromNonQuarantinedTest() { @@ -35,15 +31,52 @@ namespace Microsoft.Extensions.Logging.Testing.Tests public void ForAssembly_ReturnsSameInstanceForSameAssembly() { Assert.Same( - AssemblyTestLog.ForAssembly(ThisAssembly), - AssemblyTestLog.ForAssembly(ThisAssembly)); + AssemblyTestLog.ForAssembly(TestableAssembly.ThisAssembly), + AssemblyTestLog.ForAssembly(TestableAssembly.ThisAssembly)); } + [Fact] + public Task ForAssemblyWritesToAssemblyBaseDirectory() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + + var assembly = TestableAssembly.Create(typeof(AssemblyTestLog), logDirectory: tempDir); + var assemblyName = assembly.GetName().Name; + var testName = $"{TestableAssembly.TestClassName}.{TestableAssembly.TestMethodName}"; + + var tfmPath = Path.Combine(tempDir, assemblyName, TestableAssembly.TFM); + var globalLogPath = Path.Combine(tfmPath, "global.log"); + var testLog = Path.Combine(tfmPath, TestableAssembly.TestClassName, $"{testName}.log"); + + using var testAssemblyLog = AssemblyTestLog.ForAssembly(assembly); + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{assemblyName}.{TestableAssembly.TestClassName}", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: testName)) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + }); + [Fact] public void TestLogWritesToITestOutputHelper() { var output = new TestTestOutputHelper(); - var assemblyLog = AssemblyTestLog.Create(ThisAssemblyName, baseDirectory: null); + + using var assemblyLog = AssemblyTestLog.Create(TestableAssembly.ThisAssembly, baseDirectory: null); using (assemblyLog.StartTestLog(output, "NonExistant.Test.Class", out var loggerFactory)) { @@ -69,11 +102,19 @@ namespace Microsoft.Extensions.Logging.Testing.Tests { var illegalTestName = "T:e/s//t"; var escapedTestName = "T_e_s_t"; - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, baseDirectory: tempDir)) - using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, resolvedTestName: out var resolvedTestname, out var _, testName: illegalTestName)) - { - Assert.Equal(escapedTestName, resolvedTestname); - } + + using var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir); + using var disposable = testAssemblyLog.StartTestLog( + output: null, + className: "FakeTestAssembly.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + resolvedTestName: out var resolvedTestname, + out var _, + testName: illegalTestName); + Assert.Equal(escapedTestName, resolvedTestname); }); [Fact] @@ -84,11 +125,19 @@ namespace Microsoft.Extensions.Logging.Testing.Tests // but it's also testing the test logging facility. So this is pretty meta ;) var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) { var testLogger = testLoggerFactory.CreateLogger("TestLogger"); testLogger.LogInformation("Information!"); @@ -98,8 +147,17 @@ namespace Microsoft.Extensions.Logging.Testing.Tests logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - var globalLogPath = Path.Combine(tempDir, ThisAssemblyName, TFM, "global.log"); - var testLog = Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", "FakeTestName.log"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist"); Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist"); @@ -120,31 +178,139 @@ namespace Microsoft.Extensions.Logging.Testing.Tests ", testLogContent, ignoreLineEndingDifferences: true); }); + [Fact] + public Task TestLogCleansLogFiles_AfterSuccessfulRun() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); + + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) + { + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + } + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + + Assert.True(!File.Exists(globalLogPath), $"Expected no global log file {globalLogPath} to exist."); + Assert.True(!File.Exists(testLog), $"Expected no test log file {testLog} to exist."); + }); + + [Fact] + public Task TestLogDoesNotCleanLogFiles_AfterFailedRun() => + RunTestLogFunctionalTest((tempDir) => + { + var logger = LoggerFactory.CreateLogger("Test"); + var globalLogPath = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "global.log"); + var testLog = Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"); + + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) + { + testAssemblyLog.OnCI = true; + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } + + testAssemblyLog.ReportTestFailure(); + } + + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist."); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist."); + }); + [Fact] public Task TestLogTruncatesTestNameToAvoidLongPaths() => RunTestLogFunctionalTest((tempDir) => { - var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50); + var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + + new string('3', 50) + new string('4', 50); var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName)) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: longTestName)) { testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); } } + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass")).EnumerateFiles(); + var testLogFiles = new DirectoryInfo( + Path.Combine(tempDir, TestableAssembly.ThisAssemblyName, TestableAssembly.TFM, "FakeTestClass")) + .EnumerateFiles(); var testLog = Assert.Single(testLogFiles); var testFileName = Path.GetFileNameWithoutExtension(testLog.Name); // The first half of the file comes from the beginning of the test name passed to the logger - Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2)); + Assert.Equal( + longTestName.Substring(0, testFileName.Length / 2), + testFileName.Substring(0, testFileName.Length / 2)); + // The last half of the file comes from the ending of the test name passed to the logger - Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); + Assert.Equal( + longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), + testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); }); [Fact] @@ -152,27 +318,46 @@ namespace Microsoft.Extensions.Logging.Testing.Tests RunTestLogFunctionalTest((tempDir) => { var logger = LoggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create(ThisAssemblyName, tempDir)) + using (var testAssemblyLog = AssemblyTestLog.Create( + TestableAssembly.ThisAssembly, + baseDirectory: tempDir)) { + testAssemblyLog.OnCI = false; logger.LogInformation("Created test log in {baseDirectory}", tempDir); for (var i = 0; i < 10; i++) { - using (testAssemblyLog.StartTestLog(output: null, className: $"{ThisAssemblyName}.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + using (testAssemblyLog.StartTestLog( + output: null, + className: $"{TestableAssembly.ThisAssemblyName}.FakeTestClass", + loggerFactory: out var testLoggerFactory, + minLogLevel: LogLevel.Trace, + testName: "FakeTestName")) { testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); } } } + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); // The first log file exists - Assert.True(File.Exists(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", "FakeTestName.log"))); + Assert.True(File.Exists(Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + "FakeTestName.log"))); // Subsequent files exist for (var i = 0; i < 9; i++) { - Assert.True(File.Exists(Path.Combine(tempDir, ThisAssemblyName, TFM, "FakeTestClass", $"FakeTestName.{i}.log"))); + Assert.True(File.Exists(Path.Combine( + tempDir, + TestableAssembly.ThisAssemblyName, + TestableAssembly.TFM, + "FakeTestClass", + $"FakeTestName.{i}.log"))); } }); diff --git a/src/Testing/test/TestableAspNetTestAssemblyRunner.cs b/src/Testing/test/TestableAspNetTestAssemblyRunner.cs new file mode 100644 index 0000000000000000000000000000000000000000..17f9373b34c2886b94d5e1bd740ea95bbebb92d5 --- /dev/null +++ b/src/Testing/test/TestableAspNetTestAssemblyRunner.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Moq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing; + +public class TestableAspNetTestAssemblyRunner : AspNetTestAssemblyRunner +{ + private TestableAspNetTestAssemblyRunner( + ITestAssembly testAssembly, + IEnumerable<IXunitTestCase> testCases, + IMessageSink diagnosticMessageSink, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base( + testAssembly, + testCases, + diagnosticMessageSink, + executionMessageSink, + executionOptions) + { + } + + public static TestableAspNetTestAssemblyRunner Create(Type fixtureType, bool failTestCase = false) + { + var assembly = TestableAssembly.Create(fixtureType, failTestCase: failTestCase); + var testAssembly = GetTestAssembly(assembly); + var testCase = GetTestCase(assembly, testAssembly); + + return new TestableAspNetTestAssemblyRunner( + testAssembly, + new[] { testCase }, + diagnosticMessageSink: new NullMessageSink(), + executionMessageSink: new NullMessageSink(), + executionOptions: Mock.Of<ITestFrameworkExecutionOptions>()); + + // Do not call Xunit.Sdk.Reflector.Wrap(assembly) because it uses GetTypes() and that method + // throws NotSupportedException for a dynamic assembly. + IAssemblyInfo GetAssemblyInfo(Assembly assembly) + { + var testAssemblyName = assembly.GetName().Name; + var assemblyInfo = new Mock<IReflectionAssemblyInfo>(); + assemblyInfo.SetupGet(r => r.Assembly).Returns(assembly); + assemblyInfo.SetupGet(r => r.Name).Returns(testAssemblyName); + assemblyInfo + .SetupGet(r => r.AssemblyPath) + .Returns(Path.Combine(Directory.GetCurrentDirectory(), $"{testAssemblyName}.dll")); + + foreach (var attribute in CustomAttributeData.GetCustomAttributes(assembly)) + { + var attributeInfo = Reflector.Wrap(attribute); + var attributeName = attribute.AttributeType.AssemblyQualifiedName; + assemblyInfo + .Setup(r => r.GetCustomAttributes(attributeName)) + .Returns(new[] { attributeInfo }); + } + + var typeInfo = Reflector.Wrap(assembly.GetType(TestableAssembly.TestClassName)); + assemblyInfo.Setup(r => r.GetType(TestableAssembly.TestClassName)).Returns(typeInfo); + assemblyInfo.Setup(r => r.GetTypes(It.IsAny<bool>())).Returns(new[] { typeInfo }); + + return assemblyInfo.Object; + } + + ITestAssembly GetTestAssembly(Assembly assembly) + { + var assemblyInfo = GetAssemblyInfo(assembly); + + return new TestAssembly(assemblyInfo); + } + + IXunitTestCase GetTestCase(Assembly assembly, ITestAssembly testAssembly) + { + var testAssemblyName = assembly.GetName().Name; + var testCollection = new TestCollection( + testAssembly, + collectionDefinition: null, + displayName: $"Mock collection for '{testAssemblyName}'."); + + var type = assembly.GetType(TestableAssembly.TestClassName); + var testClass = new TestClass(testCollection, Reflector.Wrap(type)); + var method = type.GetMethod(TestableAssembly.TestMethodName); + var methodInfo = Reflector.Wrap(method); + var testMethod = new TestMethod(testClass, methodInfo); + + return new XunitTestCase( + diagnosticMessageSink: new NullMessageSink(), + defaultMethodDisplay: TestMethodDisplay.ClassAndMethod, + defaultMethodDisplayOptions: TestMethodDisplayOptions.None, + testMethod: testMethod); + } + } + + public Task AfterTestAssemblyStartingAsync_Public() + { + return base.AfterTestAssemblyStartingAsync(); + } +} diff --git a/src/Testing/test/TestableAssembly.cs b/src/Testing/test/TestableAssembly.cs new file mode 100644 index 0000000000000000000000000000000000000000..fd4b557cca12506fe4adae6e7436adc60c4c9f38 --- /dev/null +++ b/src/Testing/test/TestableAssembly.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Xunit; + +namespace Microsoft.AspNetCore.Testing; + +/* Creates a very simple dynamic assembly containing + * + * [Assembly: TestFramework( + * typeName: "Microsoft.AspNetCore.Testing.AspNetTestFramework", + * assemblyName: "Microsoft.AspNetCore.Testing")] + * [assembly: AssemblyFixture(typeof({fixtureType}))] + * [assembly: TestOutputDirectory( + * preserveExistingLogsInOutput: "false", + * targetFramework: TFM, + * baseDirectory: logDirectory)] // logdirectory is passed into Create(...). + * + * public class MyTestClass + * { + * public MyTestClass() { } + * + * [Fact] + * public MyTestMethod() + * { + * if (failTestCase) // Not exactly; condition checked during generation. + * { + * Assert.True(condition: false); + * } + * } + * } + */ +public static class TestableAssembly +{ + public static readonly Assembly ThisAssembly = typeof(TestableAssembly).GetTypeInfo().Assembly; + public static readonly string ThisAssemblyName = ThisAssembly.GetName().Name; + + private static readonly TestOutputDirectoryAttribute ThisOutputDirectoryAttribute = + ThisAssembly.GetCustomAttributes().OfType<TestOutputDirectoryAttribute>().FirstOrDefault(); + public static readonly string BaseDirectory = ThisOutputDirectoryAttribute.BaseDirectory; + public static readonly string TFM = ThisOutputDirectoryAttribute.TargetFramework; + + public const string TestClassName = "MyTestClass"; + public const string TestMethodName = "MyTestMethod"; + + public static Assembly Create(Type fixtureType, string logDirectory = null, bool failTestCase = false) + { + var frameworkConstructor = typeof(TestFrameworkAttribute) + .GetConstructor(new[] { typeof(string), typeof(string) }); + var frameworkBuilder = new CustomAttributeBuilder( + frameworkConstructor, + new[] { "Microsoft.AspNetCore.Testing.AspNetTestFramework", "Microsoft.AspNetCore.Testing" }); + + var fixtureConstructor = typeof(AssemblyFixtureAttribute).GetConstructor(new[] { typeof(Type) }); + var fixtureBuilder = new CustomAttributeBuilder(fixtureConstructor, new[] { fixtureType }); + + var outputConstructor = typeof(TestOutputDirectoryAttribute).GetConstructor( + new[] { typeof(string), typeof(string), typeof(string) }); + var outputBuilder = new CustomAttributeBuilder(outputConstructor, new[] { "false", TFM, logDirectory }); + + var testAssemblyName = $"Test{Guid.NewGuid():n}"; + var assemblyName = new AssemblyName(testAssemblyName); + var assembly = AssemblyBuilder.DefineDynamicAssembly( + assemblyName, + AssemblyBuilderAccess.Run, + new[] { frameworkBuilder, fixtureBuilder, outputBuilder }); + + var module = assembly.DefineDynamicModule(testAssemblyName); + var type = module.DefineType(TestClassName, TypeAttributes.Public); + type.DefineDefaultConstructor(MethodAttributes.Public); + + var method = type.DefineMethod(TestMethodName, MethodAttributes.Public); + var factConstructor = typeof(FactAttribute).GetConstructor(Array.Empty<Type>()); + var factBuilder = new CustomAttributeBuilder(factConstructor, Array.Empty<object>()); + method.SetCustomAttribute(factBuilder); + + var generator = method.GetILGenerator(); + if (failTestCase) + { + // Assert.True(condition: false); + generator.Emit(OpCodes.Ldc_I4_0); + var trueInfo = typeof(Assert).GetMethod("True", new[] { typeof(bool) }); + generator.EmitCall(OpCodes.Call, trueInfo, optionalParameterTypes: null); + } + + generator.Emit(OpCodes.Ret); + type.CreateType(); + + return assembly; + } +} diff --git a/src/submodules/googletest b/src/submodules/googletest index c9461a9b55ba954df0489bab6420eb297bed846b..af29db7ec28d6df1c7f0f745186884091e602e07 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit c9461a9b55ba954df0489bab6420eb297bed846b +Subproject commit af29db7ec28d6df1c7f0f745186884091e602e07