Skip to content

Add infrastructure for trimming and NativeAOT test apps. #48024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project>
<Project>
<Import Project="eng\Common.props" />

<PropertyGroup>
Expand Down Expand Up @@ -30,6 +30,9 @@
$(MSBuildProjectName.EndsWith('.Test')) OR
$(MSBuildProjectName.EndsWith('.FunctionalTest')) ) ">true</IsUnitTestProject>
<IsUnitTestProject Condition=" '$(IsUnitTestProject)' == '' ">false</IsUnitTestProject>
<IsTrimmingTestProject Condition="$(MSBuildProjectName.EndsWith('.TrimmingTests'))">true</IsTrimmingTestProject>
<IsNativeAotTestProject Condition="$(MSBuildProjectName.EndsWith('.NativeAotTests'))">true</IsNativeAotTestProject>
<IsPublishedAppTestProject Condition="'$(IsTrimmingTestProject)' == 'true' or '$(IsNativeAotTestProject)' == 'true'">true</IsPublishedAppTestProject>
<IsTestAssetProject Condition=" $(RepoRelativeProjectDir.Contains('testassets')) OR $(MSBuildProjectName.Contains('TestCommon'))">true</IsTestAssetProject>
<IsProjectTemplateProject Condition=" ($(RepoRelativeProjectDir.Contains('ProjectTemplates')) OR $(MSBuildProjectName.Contains('ProjectTemplates')) ) AND
'$(IsUnitTestProject)' != 'true' AND
Expand All @@ -39,6 +42,7 @@
<IsShipping Condition=" '$(IsSampleProject)' == 'true' OR
'$(IsTestAssetProject)' == 'true' OR
'$(IsBenchmarkProject)' == 'true' OR
'$(IsPublishedAppTestProject)' == 'true' OR
$(IsUnitTestProject) ">false</IsShipping>

<!--
Expand Down Expand Up @@ -260,6 +264,7 @@
<Import Project="eng\targets\Java.Common.props" Condition="'$(MSBuildProjectExtension)' == '.javaproj'" />
<Import Project="eng\targets\Helix.props" Condition=" $(IsTestProject) " />
<Import Project="eng\targets\FunctionalTestWithAssets.props" Condition=" $(IsTestProject) " />
<Import Project="eng\testing\linker\trimmingTests.props" Condition="'$(IsPublishedAppTestProject)' == 'true'" />

<!-- Keys used by InternalsVisibleTo attributes. -->
<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,5 @@
<Import Project="eng\targets\Helix.targets" Condition=" $(IsTestProject) " />
<Import Project="eng\targets\FunctionalTestWithAssets.targets"
Condition=" $(IsTestProject) AND $(ContainsFunctionalTestAssets) " />
<Import Project="eng\testing\linker\trimmingTests.targets" Condition="'$(IsPublishedAppTestProject)' == 'true'" />
</Project>
13 changes: 13 additions & 0 deletions eng/testing/linker/SupportFiles/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<!-- Workaround while there is no SDK available that understands the TFM; suppress unsupported version errors. -->
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>

<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<PublishSelfContained>true</PublishSelfContained>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Enable NuGet static graph evaluation to optimize incremental restore -->
<RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
</PropertyGroup>
</Project>
25 changes: 25 additions & 0 deletions eng/testing/linker/SupportFiles/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project>

<PropertyGroup>
<!-- Used to silence the warning caused by the workaround for https://github.com/dotnet/runtime/issues/81382 -->
<SuppressGenerateILCompilerExplicitPackageReferenceWarning>true</SuppressGenerateILCompilerExplicitPackageReferenceWarning>
</PropertyGroup>

<!-- needed to reference a specific version of NetCoreApp. Workaround https://github.com/dotnet/runtime/issues/81382 -->
<ItemGroup>
<FrameworkReference Update="Microsoft.NETCore.App"
RuntimeFrameworkVersion="$(MicrosoftNETCoreAppRuntimeVersion)" />

<PackageReference Include="Microsoft.Dotnet.ILCompiler"
Version="$(MicrosoftNETCoreAppRuntimeVersion)" />
</ItemGroup>

<!--
Reference the Microsoft.AspNetCore.App shared framework assemblies with ProjectReference.
This allows for easy dev workflow. Just need to update the product code, and re-run the tests.
-->
<ItemGroup>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did app.ref not work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, but it is a bit heavier than this approach.

For example, the "incremental" run of .\.dotnet\dotnet.exe test .\src\DefaultBuilder\test\Microsoft.AspNetCore.NativeAotTests\ takes ~15s using app.ref and takes ~10s with this approach.

We can always switch this as we go forward if we need to. But for now I'd rather do the minimal.

<ProjectReference Include="$(RepoRoot)src\DefaultBuilder\src\Microsoft.AspNetCore.csproj" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions eng/testing/linker/project.csproj.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>{TargetFramework}</TargetFramework>
<OutputType>Exe</OutputType>
<RuntimeIdentifier>{RuntimeIdentifier}</RuntimeIdentifier>
<PublishAot>{PublishAot}</PublishAot>
<MicrosoftNETCoreAppRuntimeVersion>{MicrosoftNETCoreAppRuntimeVersion}</MicrosoftNETCoreAppRuntimeVersion>
<RepoRoot>{RepoRoot}</RepoRoot>
<_ExtraTrimmerArgs>{ExtraTrimmerArgs} $(_ExtraTrimmerArgs)</_ExtraTrimmerArgs>
{AdditionalProperties}
</PropertyGroup>

<ItemGroup>
{RuntimeHostConfigurationOptions}
</ItemGroup>

<ItemGroup>
{AdditionalProjectReferences}
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions eng/testing/linker/trimmingTests.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<TrimmingTestDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'trimmingTests'))</TrimmingTestDir>
<TrimmingTestProjectsDir>$([MSBuild]::NormalizeDirectory('$(TrimmingTestDir)', 'projects'))</TrimmingTestProjectsDir>
<ProjectTemplate>$(MSBuildThisFileDirectory)project.csproj.template</ProjectTemplate>
</PropertyGroup>
</Project>
133 changes: 133 additions & 0 deletions eng/testing/linker/trimmingTests.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<Project>
<ItemGroup>
<TestConsoleAppSourceFiles Condition="'@(TestConsoleAppSourceFiles)' == ''" Include="$(MSBuildProjectDirectory)\*.cs" />

<TestSupportFiles Include="$(MSBuildThisFileDirectory)SupportFiles\Directory.Build.*">
<DestinationFolder>$(TrimmingTestDir)</DestinationFolder>
</TestSupportFiles>
</ItemGroup>

<Target Name="CreateTestDir"
Inputs="@(TestSupportFiles)"
Outputs="@(TestSupportFiles->'%(DestinationFolder)\%(FileName)%(Extension)')">
<MakeDir Directories="%(TestSupportFiles.DestinationFolder)" />
<Copy SourceFiles="@(TestSupportFiles)" DestinationFolder="%(TestSupportFiles.DestinationFolder)" />
</Target>

<Target Name="GetTestConsoleApps">
<ItemGroup>
<TestConsoleAppSourceFiles>
<ProjectDir>$([MSBuild]::NormalizeDirectory('$(TrimmingTestProjectsDir)', '$(MSBuildProjectName)', '%(Filename)', '$(PackageRID)'))</ProjectDir>
<TestRuntimeIdentifier>$(TargetRuntimeIdentifier)</TestRuntimeIdentifier>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework Condition="'%(TestConsoleAppSourceFiles.TargetOS)' != ''">$(DefaultNetCoreTargetFramework)-%(TestConsoleAppSourceFiles.TargetOS)</TargetFramework>
</TestConsoleAppSourceFiles>
<!-- We need to separate Item metadata declaration in two in order to be able to use ProjectDir and TestRuntimeIdentifier below -->
<TestConsoleAppSourceFiles>
<ProjectFile>%(ProjectDir)project.csproj</ProjectFile>
<TestCommand>$([MSBuild]::NormalizePath('%(ProjectDir)', 'bin', '$(Configuration)', '%(TargetFramework)', '%(TestRuntimeIdentifier)', 'publish', 'project'))</TestCommand>
<TestExecutionDirectory>$([MSBuild]::NormalizeDirectory('%(ProjectDir)', 'bin', '$(Configuration)', '%(TargetFramework)', '%(TestRuntimeIdentifier)', 'publish'))</TestExecutionDirectory>
</TestConsoleAppSourceFiles>
</ItemGroup>

<ItemGroup>
<TestConsoleApps Include="@(TestConsoleAppSourceFiles->'%(ProjectFile)')">
<ProjectCompileItems>%(FullPath)</ProjectCompileItems>
</TestConsoleApps>
<TestConsoleApps AdditionalProperties="MSBuildEnableWorkloadResolver=$(MSBuildEnableWorkloadResolver)" Condition="'$(MSBuildEnableWorkloadResolver)' != ''" />
</ItemGroup>
</Target>

<Target Name="GenerateProjects"
DependsOnTargets="GetTestConsoleApps;CreateTestDir"
Inputs="@(TestConsoleAppSourceFiles);$(ProjectTemplate);@(TestSupportFiles)"
Outputs="%(TestConsoleApps.Identity)">
<PropertyGroup>
<_projectDir>%(TestConsoleApps.ProjectDir)\</_projectDir>
<_projectFile>%(TestConsoleApps.ProjectFile)</_projectFile>
<_projectSourceFile>%(TestConsoleApps.ProjectCompileItems)</_projectSourceFile>
</PropertyGroup>

<ItemGroup Condition="'$(AdditionalProjectReferences)' != ''">
<_additionalProjectReferenceTemp Include="$(AdditionalProjectReferences)" />
<_additionalProjectReference Include="&lt;ProjectReference Include=&quot;$(LibrariesProjectRoot)%(_additionalProjectReferenceTemp.Identity)\src\%(_additionalProjectReferenceTemp.Identity).csproj&quot; SkipUseReferenceAssembly=&quot;true&quot; /&gt;" />
</ItemGroup>

<PropertyGroup>
<_additionalProjectReferencesString>@(_additionalProjectReference, '%0a')</_additionalProjectReferencesString>
</PropertyGroup>

<ItemGroup>
<_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" />
</ItemGroup>

<ItemGroup>
<_switchesAsItems Include="%(TestConsoleApps.DisabledFeatureSwitches)" Value="false" />
<_switchesAsItems Include="%(TestConsoleApps.EnabledFeatureSwitches)" Value="true" />

<_propertiesAsItems Include="%(TestConsoleApps.DisabledProperties)" Value="false" />
<_propertiesAsItems Include="%(TestConsoleApps.EnabledProperties)" Value="true" />
</ItemGroup>

<PropertyGroup>
<_runtimeHostConfigurationOptionsString>@(_switchesAsItems->'&lt;RuntimeHostConfigurationOption Include=&quot;%(Identity)&quot; Value=&quot;%(Value)&quot; Trim=&quot;true&quot; /&gt;', '%0a ')</_runtimeHostConfigurationOptionsString>
<_additionalPropertiesString>@(_propertiesAsItems->'&lt;%(Identity)&gt;%(Value)&lt;/%(Identity)&gt;', '%0a ')</_additionalPropertiesString>
</PropertyGroup>

<MakeDir Directories="$(_projectDir)" />
<WriteLinesToFile File="$(_projectFile)"
Lines="$([System.IO.File]::ReadAllText('$(ProjectTemplate)')
.Replace('{TargetFramework}', '%(TestConsoleApps.TargetFramework)')
.Replace('{RuntimeIdentifier}','%(TestConsoleApps.TestRuntimeIdentifier)')
.Replace('{PublishAot}','$(IsNativeAotTestProject)')
.Replace('{MicrosoftNETCoreAppRuntimeVersion}','$(MicrosoftNETCoreAppRuntimeVersion)')
.Replace('{RepoRoot}', '$(RepoRoot)')
.Replace('{ExtraTrimmerArgs}', '%(TestConsoleApps.ExtraTrimmerArgs)')
.Replace('{AdditionalProperties}', '$(_additionalPropertiesString)')
.Replace('{RuntimeHostConfigurationOptions}', '$(_runtimeHostConfigurationOptionsString)')
.Replace('{AdditionalProjectReferences}', '$(_additionalProjectReferencesString)')
)"
Overwrite="true" />
<Copy SourceFiles="$(_projectSourceFile);
@(_additionalProjectSourceFiles)"
DestinationFolder="$(_projectDir)" />
<Message Text="Generated $(_projectFile)" />
</Target>

<Target Name="GetTrimmingProjectsToRestore"
DependsOnTargets="GenerateProjects"
Returns="@(TestConsoleApps)" />

<Target Name="PublishTrimmedProjects"
DependsOnTargets="GenerateProjects">

<MSBuild Projects="@(TestConsoleApps)"
Targets="Restore"
Condition="'$(SkipTrimmingProjectsRestore)' != 'true'"
Properties="MSBuildRestoreSessionId=$([System.Guid]::NewGuid());Configuration=$(Configuration)" />

<MSBuild Projects="@(TestConsoleApps)"
Targets="Publish"
Properties="Configuration=$(Configuration);BuildProjectReferences=false" />
</Target>

<Target Name="ExecuteApplications"
DependsOnTargets="PublishTrimmedProjects"
Inputs="%(TestConsoleApps.Identity)"
Outputs="_unused"
Condition="'$(ArchiveTests)' != 'true'">

<Message Importance="High" Text="[Trimming Tests] Running test: %(TestConsoleApps.ProjectCompileItems) ..." />
<Exec IgnoreExitCode="true" Command="%(TestConsoleApps.TestCommand)" StandardOutputImportance="Low" WorkingDirectory="%(TestConsoleApps.TestExecutionDirectory)">
<Output TaskParameter="ExitCode" PropertyName="ExecutionExitCode" />
</Exec>

<Error Condition="'$(ExecutionExitCode)' != '100'" Text="Error: [Failed Test]: %(TestConsoleApps.ProjectCompileItems). The Command %(TestConsoleApps.TestCommand) return a non-success exit code." ContinueOnError="ErrorAndContinue" />
</Target>

<Target Name="Build" DependsOnTargets="ExecuteApplications" />

<!-- define test to do nothing, for this project Build does all the testing -->
<Target Name="Test" DependsOnTargets="Build" />
<Target Name="VSTest" DependsOnTargets="Build" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project DefaultTargets="Build">
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" />

<ItemGroup>
<TestConsoleAppSourceFiles Include="UseStartupThrowsForStructContainersTest.cs" />
</ItemGroup>

<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />
</Project>
Original file line number Diff line number Diff line change
@@ -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<MyContainerClass>
{
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<MyContainerStruct>
{
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);
}
Loading