From 28602638d11f01a0e20a3d0a0aa1f3a9a84406d6 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 4 May 2023 10:51:10 -0500 Subject: [PATCH] Add infrastructure for trimming and NativeAOT test apps. (#48024) * Add infrastructure for trimming and NativeAOT test apps. This infrastructure was taken from https://github.com/dotnet/runtime/tree/c62f69be1405a8e41b56ffc05f22d791bf4c7d2d/eng/testing/linker and modified to work in dotnet/aspnetcore. Added the first test: - Generic host + value type container builder (verify that error is thrown) Contributes to #45860 * Skip IsPublishedAppTestProject projects when SkipTestBuild=true. Use `Sdk="Microsoft.NET.Sdk"` in order to skip the build correctly. Only run the AOT tests during Test. Skip "Build" and skip publishing for Helix. Ensure the IsPublishedAppTestProject projects aren't built until the shared fx is built using RequiresDelayedBuild. * Add workaround for https://github.com/dotnet/arcade/pull/13406/ --- Directory.Build.BeforeCommonTargets.targets | 1 + Directory.Build.props | 7 +- Directory.Build.targets | 1 + eng/RequiresDelayedBuildProjects.props | 1 + eng/common/tools.ps1 | 4 +- .../linker/SupportFiles/Directory.Build.props | 13 ++ .../SupportFiles/Directory.Build.targets | 25 ++++ eng/testing/linker/project.csproj.template | 22 +++ eng/testing/linker/trimmingTests.props | 20 +++ eng/testing/linker/trimmingTests.targets | 132 +++++++++++++++++ .../Microsoft.AspNetCore.NativeAotTests.proj | 7 + ...UseStartupThrowsForStructContainersTest.cs | 139 ++++++++++++++++++ .../src/Infrastructure/ISupportsStartup.cs | 2 +- .../Hosting/src/WebHostBuilderExtensions.cs | 2 +- 14 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 eng/testing/linker/SupportFiles/Directory.Build.props create mode 100644 eng/testing/linker/SupportFiles/Directory.Build.targets create mode 100644 eng/testing/linker/project.csproj.template create mode 100644 eng/testing/linker/trimmingTests.props create mode 100644 eng/testing/linker/trimmingTests.targets create mode 100644 src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/Microsoft.AspNetCore.NativeAotTests.proj create mode 100644 src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/UseStartupThrowsForStructContainersTest.cs diff --git a/Directory.Build.BeforeCommonTargets.targets b/Directory.Build.BeforeCommonTargets.targets index 8339d8e3ca0c..6395f6c74dc6 100644 --- a/Directory.Build.BeforeCommonTargets.targets +++ b/Directory.Build.BeforeCommonTargets.targets @@ -15,6 +15,7 @@ @@ -30,6 +30,9 @@ $(MSBuildProjectName.EndsWith('.Test')) OR $(MSBuildProjectName.EndsWith('.FunctionalTest')) ) ">true false + true + true + true true false + 99.9 + + true + full + true + true + + true + + diff --git a/eng/testing/linker/SupportFiles/Directory.Build.targets b/eng/testing/linker/SupportFiles/Directory.Build.targets new file mode 100644 index 000000000000..a2a18b071459 --- /dev/null +++ b/eng/testing/linker/SupportFiles/Directory.Build.targets @@ -0,0 +1,25 @@ + + + + + true + + + + + + + + + + + + + + + diff --git a/eng/testing/linker/project.csproj.template b/eng/testing/linker/project.csproj.template new file mode 100644 index 000000000000..59333742ae1c --- /dev/null +++ b/eng/testing/linker/project.csproj.template @@ -0,0 +1,22 @@ + + + + {TargetFramework} + Exe + {RuntimeIdentifier} + {PublishAot} + {MicrosoftNETCoreAppRuntimeVersion} + {RepoRoot} + <_ExtraTrimmerArgs>{ExtraTrimmerArgs} $(_ExtraTrimmerArgs) + {AdditionalProperties} + + + + {RuntimeHostConfigurationOptions} + + + + {AdditionalProjectReferences} + + + diff --git a/eng/testing/linker/trimmingTests.props b/eng/testing/linker/trimmingTests.props new file mode 100644 index 000000000000..4158daec9957 --- /dev/null +++ b/eng/testing/linker/trimmingTests.props @@ -0,0 +1,20 @@ + + + $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'trimmingTests')) + $([MSBuild]::NormalizeDirectory('$(TrimmingTestDir)', 'projects')) + $(MSBuildThisFileDirectory)project.csproj.template + + + false + false + + + + + $(DefaultNetCoreTargetFramework) + + + true + + + diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets new file mode 100644 index 000000000000..5f5fcb99736a --- /dev/null +++ b/eng/testing/linker/trimmingTests.targets @@ -0,0 +1,132 @@ + + + + + + + + $(TrimmingTestDir) + + + + + + + + + + + + $([MSBuild]::NormalizeDirectory('$(TrimmingTestProjectsDir)', '$(MSBuildProjectName)', '%(Filename)', '$(PackageRID)')) + $(TargetRuntimeIdentifier) + $(DefaultNetCoreTargetFramework) + $(DefaultNetCoreTargetFramework)-%(TestConsoleAppSourceFiles.TargetOS) + + + + %(ProjectDir)project.csproj + $([MSBuild]::NormalizePath('%(ProjectDir)', 'bin', '$(Configuration)', '%(TargetFramework)', '%(TestRuntimeIdentifier)', 'publish', 'project')) + $([MSBuild]::NormalizeDirectory('%(ProjectDir)', 'bin', '$(Configuration)', '%(TargetFramework)', '%(TestRuntimeIdentifier)', 'publish')) + + + + + + %(FullPath) + + + + + + + + <_projectDir>%(TestConsoleApps.ProjectDir)\ + <_projectFile>%(TestConsoleApps.ProjectFile) + <_projectSourceFile>%(TestConsoleApps.ProjectCompileItems) + + + + <_additionalProjectReferenceTemp Include="$(AdditionalProjectReferences)" /> + <_additionalProjectReference Include="<ProjectReference Include="$(LibrariesProjectRoot)%(_additionalProjectReferenceTemp.Identity)\src\%(_additionalProjectReferenceTemp.Identity).csproj" SkipUseReferenceAssembly="true" />" /> + + + + <_additionalProjectReferencesString>@(_additionalProjectReference, '%0a') + + + + <_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" /> + + + + <_switchesAsItems Include="%(TestConsoleApps.DisabledFeatureSwitches)" Value="false" /> + <_switchesAsItems Include="%(TestConsoleApps.EnabledFeatureSwitches)" Value="true" /> + + <_propertiesAsItems Include="%(TestConsoleApps.DisabledProperties)" Value="false" /> + <_propertiesAsItems Include="%(TestConsoleApps.EnabledProperties)" Value="true" /> + + + + <_runtimeHostConfigurationOptionsString>@(_switchesAsItems->'<RuntimeHostConfigurationOption Include="%(Identity)" Value="%(Value)" Trim="true" />', '%0a ') + <_additionalPropertiesString>@(_propertiesAsItems->'<%(Identity)>%(Value)</%(Identity)>', '%0a ') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/Microsoft.AspNetCore.NativeAotTests.proj b/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/Microsoft.AspNetCore.NativeAotTests.proj new file mode 100644 index 000000000000..b17fb2b6f5c3 --- /dev/null +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/Microsoft.AspNetCore.NativeAotTests.proj @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/UseStartupThrowsForStructContainersTest.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/UseStartupThrowsForStructContainersTest.cs new file mode 100644 index 000000000000..ef5951c9cbb3 --- /dev/null +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.NativeAotTests/UseStartupThrowsForStructContainersTest.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; + +int classTestResult = RunClassTest(); +if (classTestResult != 100) +{ + return classTestResult; +} + +return RunStructTest(); + +static int RunClassTest() +{ + var builder = Host.CreateDefaultBuilder(); + builder.UseServiceProviderFactory(new MyContainerClassFactory()); + + builder.ConfigureWebHost(webBuilder => + { + webBuilder.UseStartup(typeof(MyStartupWithClass)); + }); + + builder.Build(); + + if (!MyStartupWithClass.ConfigureServicesCalled) + { + return -1; + } + if (!MyStartupWithClass.ConfigureContainerCalled) + { + return -2; + } + + return 100; +} + +static int RunStructTest() +{ + var builder = Host.CreateDefaultBuilder(); + builder.UseServiceProviderFactory(new MyContainerStructFactory()); + + builder.ConfigureWebHost(webBuilder => + { + webBuilder.UseStartup(typeof(MyStartupWithStruct)); + }); + + try + { + builder.Build(); + return -3; + } + catch (InvalidOperationException e) + { + if (!e.Message.StartsWith("A ValueType TContainerBuilder isn't supported with AOT", StringComparison.Ordinal)) + { + return -4; + } + } + + if (!MyStartupWithStruct.ConfigureServicesCalled) + { + return -5; + } + + // ConfigureContainer should not have been called, since the exception should have been raised + if (MyStartupWithStruct.ConfigureContainerCalled) + { + return -6; + } + + return 100; +} + +public class MyStartupWithClass +{ + public static bool ConfigureServicesCalled; + public static bool ConfigureContainerCalled; + + public void ConfigureServices(IServiceCollection _) => ConfigureServicesCalled = true; + public void ConfigureContainer(MyContainerClass _) => ConfigureContainerCalled = true; + public void Configure(IApplicationBuilder _) { } +} + +public class MyContainerClassFactory : IServiceProviderFactory +{ + public MyContainerClass CreateBuilder(IServiceCollection services) => new MyContainerClass(services); + + public IServiceProvider CreateServiceProvider(MyContainerClass containerBuilder) + { + containerBuilder.Build(); + return containerBuilder; + } +} + +public class MyContainerClass : IServiceProvider +{ + private IServiceProvider _inner; + private IServiceCollection _services; + + public MyContainerClass(IServiceCollection services) => _services = services; + public void Build() => _inner = _services.BuildServiceProvider(); + public object GetService(Type serviceType) => _inner.GetService(serviceType); +} + +public class MyStartupWithStruct +{ + public static bool ConfigureServicesCalled; + public static bool ConfigureContainerCalled; + + public void ConfigureServices(IServiceCollection _) => ConfigureServicesCalled = true; + public void ConfigureContainer(MyContainerStruct _) => ConfigureContainerCalled = true; + public void Configure(IApplicationBuilder _) { } +} + +public class MyContainerStructFactory : IServiceProviderFactory +{ + public MyContainerStruct CreateBuilder(IServiceCollection services) => new MyContainerStruct(services); + + public IServiceProvider CreateServiceProvider(MyContainerStruct containerBuilder) + { + containerBuilder.Build(); + return containerBuilder; + } +} + +public struct MyContainerStruct : IServiceProvider +{ + private IServiceProvider _inner; + private IServiceCollection _services; + + public MyContainerStruct(IServiceCollection services) => _services = services; + public void Build() => _inner = _services.BuildServiceProvider(); + public object GetService(Type serviceType) => _inner.GetService(serviceType); +} diff --git a/src/Hosting/Hosting/src/Infrastructure/ISupportsStartup.cs b/src/Hosting/Hosting/src/Infrastructure/ISupportsStartup.cs index cf4dedf93e6c..6ee6cce59e22 100644 --- a/src/Hosting/Hosting/src/Infrastructure/ISupportsStartup.cs +++ b/src/Hosting/Hosting/src/Infrastructure/ISupportsStartup.cs @@ -40,6 +40,6 @@ public interface ISupportsStartup /// /// A delegate that specifies a factory for the startup class. /// The . - /// When using the IL linker, all public methods of are preserved. This should match the Startup type directly (and not a base type). + /// When in a trimmed app, all public methods of are preserved. This should match the Startup type directly (and not a base type). IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(Func startupFactory); } diff --git a/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs b/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs index 050b0c5f2ef8..af693bc07cea 100644 --- a/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs @@ -86,7 +86,7 @@ public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action /// The to configure. /// A delegate that specifies a factory for the startup class. /// The . - /// When using the il linker, all public methods of are preserved. This should match the Startup type directly (and not a base type). + /// When in a trimmed app, all public methods of are preserved. This should match the Startup type directly (and not a base type). public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(this IWebHostBuilder hostBuilder, Func startupFactory) where TStartup : class { ArgumentNullException.ThrowIfNull(startupFactory);