Skip to content

Add Native AOT Components testing harness - #68289

Merged
javiercn merged 7 commits into
mainfrom
javiercn-components-testing-native-aot
Aug 10, 2026
Merged

Add Native AOT Components testing harness#68289
javiercn merged 7 commits into
mainfrom
javiercn-components-testing-native-aot

Conversation

@javiercn

@javiercn javiercn commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

This document proposes adding Native AOT application testing to Microsoft.AspNetCore.Components.Testing through a build-only companion package. The feature lets an E2E test project publish an application with a compiled readiness and parent-process monitoring harness, describe it in a portable manifest, and launch it as a native executable without adding the testing library, MSTest, Playwright, YARP, startup hooks, or reflection-based test infrastructure to the application at runtime.

This PR is stacked on #67083 and extends its package-based application discovery, publishing, manifest, and launch model. The application remains responsible for its Native AOT configuration and matching restore; the test project selects which application reference is published, its mode, and the runtime identifier for the current invocation.

Goals

Enable external consumers to test Native AOT ASP.NET Core applications through the same Components.Testing manifest and server lifecycle used for managed applications.

Keep the companion package inert during ordinary builds and prevent it from contributing runtime lib or ref assemblies to the application.

Produce a portable published payload whose manifest identifies a compiled harness and whose native executable can signal readiness, terminate when its test parent exits, and serve requests without startup-hook dependencies.

Allow each application reference to select its own E2E preparation mode and runtime identifier while leaving Native AOT configuration, supported RIDs, self-contained behavior, and restore ownership in the application project.

Fail early and explicitly when the requested scenario cannot produce or safely launch a compiled Native AOT application.

Non-goals

This proposal does not make an application Native AOT compatible. The application must already satisfy Native AOT trimming, dependency, and platform requirements.

This proposal does not choose or restore runtime identifiers on behalf of the application. The application declares its supported RIDs and is restored for the RID selected by the test invocation.

This proposal does not cross-compile Native AOT applications or publish multiple RIDs in one test invocation.

This proposal does not replace the existing startup-hook harness for managed build or publish scenarios.

This proposal does not add repository-specific CI, Helix, source-build, or source-tree orchestration to the shipped package assets.

Proposed solution

Ship Microsoft.AspNetCore.Components.Testing.NativeAot as a build-only companion package containing the compiled-harness generator and transitive build assets. The package can be referenced unconditionally with PrivateAssets="all"; it activates only when Components.Testing publishes an application reference with E2ECompileTestHarness=true.

The application project owns PublishAot, RuntimeIdentifiers, self-contained configuration, and the matching restore. The test project marks the application as an E2E app, chooses publish mode for that reference, identifies it as Native AOT, and selects one RID. Components.Testing publishes the application with the compiled harness enabled, emits a manifest entry with harnessMode: "compiled", and launches the resulting native executable without startup-hook environment variables.

Reference the companion package without affecting normal builds

The application references the companion package privately and declares its Native AOT configuration:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <SelfContained>true</SelfContained>
  <RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Components.Testing.NativeAot"
                    Version="$(ComponentsTestingVersion)"
                    PrivateAssets="all" />
</ItemGroup>

A normal Build remains managed and does not generate the compiled harness or copy a Components.Testing Native AOT assembly into the application output.

Publish one application as Native AOT

The test project selects publish mode and one RID on the application reference:

<ItemGroup>
  <ProjectReference Include="..\App\App.csproj">
    <E2EApp>true</E2EApp>
    <E2EAppMode>publish</E2EAppMode>
    <E2ENativeAot>true</E2ENativeAot>
    <E2ERuntimeIdentifier>win-x64</E2ERuntimeIdentifier>
  </ProjectReference>
</ItemGroup>

The selected application is published for win-x64 with its compiled test harness. Other E2E application references may continue using the global default mode or specify their own E2EAppMode.

Restore and publish for a selected RID

Before the test project publishes the payload, the application is restored using the same Native AOT properties and selected RID that Publish will use:

<PropertyGroup>
  <RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
  <PublishAot>true</PublishAot>
  <SelfContained>true</SelfContained>
</PropertyGroup>

The consumer may restore all declared RIDs or restore only the RID selected by the test invocation. Components.Testing does not repair or replace an incompatible restore graph.

Start and use the compiled application in a test

Tests use the same server lifecycle as other Components.Testing applications:

var server = await StartServerAsync<App.Program>(TestRoot.Servers);
var context = await NewContext(new BrowserNewContextOptions().WithServerRouting(server));

The generated application harness reports readiness to the test host and watches the parent process automatically. ServerInstance exposes its Id, AppName, direct AppUrl, manifest PublicUrl, and proxied TestUrl for test scenarios that need routing, diagnostics, or externally registered callback URLs.

Produce a portable compiled manifest entry

The published manifest records the native executable, its relative working directory, and harnessMode: "compiled". The payload can be copied to another provisioned machine and launched without the source checkout, a managed application DLL fallback, DOTNET_STARTUP_HOOKS, or ASPNETCORE_HOSTINGSTARTUPASSEMBLIES.

Mix managed and compiled applications

A test project may combine build-mode managed applications, publish-mode managed applications, and publish-mode Native AOT applications. E2EAppMode on each ProjectReference overrides the global default, so only references explicitly configured for Native AOT publish require an E2ERuntimeIdentifier and compiled executable.

Extend application-owned Native AOT configuration

Applications may use normal SDK properties, publish profiles, conditional property groups, and RuntimeIdentifiers to control supported platforms and AOT behavior. Components.Testing forwards standard ProjectReference configuration, platform, target-framework, and additional properties while keeping the required compiled-harness flag and selected RID authoritative for the test publish.

Reject Native AOT outside publish mode

If E2ENativeAot=true is used with E2EAppMode=build or all, payload preparation fails with an error explaining that compiled Native AOT applications require publish mode.

Reject a missing runtime identifier

If a Native AOT ProjectReference does not provide E2ERuntimeIdentifier, payload preparation fails before publishing because one concrete RID is required for the invocation.

Reject missing or managed publish output

If publishing does not produce the expected native executable, manifest generation fails. A compiled entry never falls back to dotnet App.dll, because that would misrepresent the payload as Native AOT.

Reject managed service overrides for compiled applications

If a test tries to configure a compiled application through the managed ConfigureServices override mechanism, server startup fails with an explicit error. Compiled applications cannot load arbitrary managed test methods through the startup-hook mechanism; application customization must be expressed in AOT-compatible application configuration.

Surface SDK restore and publish failures

If the application was not restored for the selected RID, does not include the required Native AOT packs, or is not AOT compatible, the standard SDK restore or publish error is surfaced. Components.Testing does not silently restore with different settings or produce a success-shaped managed fallback.

Assumptions

The application targets a .NET SDK and ASP.NET Core version that support Native AOT for the selected RID.

The companion package version is compatible with the main Microsoft.AspNetCore.Components.Testing package version.

The application declares every RID that consumers may select and completes a matching restore before a no-restore publish path is used.

The machine performing Native AOT publish has the required native toolchain and supports the selected RID; cross-compilation is not assumed.

One test invocation selects exactly one RID for each Native AOT application reference.

The published payload is transferred only to a compatible operating system and architecture.

References

@javiercn
javiercn requested a review from a team as a code owner August 8, 2026 13:30
Copilot AI lite review requested due to automatic review settings August 8, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a Native AOT E2E testing harness to Microsoft.AspNetCore.Components.Testing, including a build-only Microsoft.AspNetCore.Components.Testing.NativeAot package (source generator + transitive targets), manifest support to distinguish “startupHook” vs “compiled” harness modes, and package-consumer integration coverage for both inert normal builds and Native AOT publish/launch.

Changes:

  • Introduces a build-only Native AOT harness generator/package and wires it into test assets and packing flow.
  • Extends E2E manifest generation + runtime loading/validation to support per-app E2EAppMode, Native AOT RIDs, and a harnessMode field.
  • Updates server launch environment construction to omit managed startup-hook injection for compiled harnesses, with new unit/integration tests.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Components/Testing/testassets/PackageConsumer/Tests/PackageConsumer.Tests.csproj Adds per-app E2E metadata (mode/nativeaot/RID) and forwards build properties for the package-consumer test scenario.
src/Components/Testing/testassets/PackageConsumer/App/PackageConsumer.App.csproj Enables compiled-harness publish settings (AOT, RIDs) when E2ECompileTestHarness is set; references the new Native AOT build-only package.
src/Components/Testing/testassets/Directory.Build.props Adds a repo/test-asset switch to disable E2E app preparation in excluded/source-only builds.
src/Components/Testing/test/ServerInstanceTests.cs Adds coverage for compiled-harness env var behavior and failure modes.
src/Components/Testing/test/PackageIntegrationTests.cs Adds package asset validation for the Native AOT package plus package-consumer coverage for inert/Native AOT publish + launch.
src/Components/Testing/test/Microsoft.AspNetCore.Components.Testing.Tests.csproj Packs the new Native AOT package for integration tests and adds Native AOT publish orchestration for the package consumer.
src/Components/Testing/test/Generators/NativeAotTestHarnessGeneratorTests.cs New unit tests validating generator gating and emitted source characteristics.
src/Components/Testing/test/GenerateE2EManifestTaskTests.cs Expands manifest task tests for per-app mode overrides and Native AOT validation/selection.
src/Components/Testing/test/E2EManifestTests.cs Adds manifest deserialize/validate coverage for the new harnessMode field.
src/Components/Testing/tasks/GenerateE2EManifest.cs Implements per-app mode resolution, Native AOT validation, exe selection by RID, and emits harnessMode.
src/Components/Testing/tasks/E2EManifestModel.cs Adds harnessMode to the serialized manifest model.
src/Components/Testing/src/Infrastructure/ServerInstance.cs Refactors env-var construction/application to support compiled harnesses (no managed injection variables).
src/Components/Testing/src/Infrastructure/E2EManifest.cs Adds manifest validation on load, including harness-mode validation.
src/Components/Testing/src/Infrastructure/E2EAppEntry.cs Adds harness-mode constants + JSON property for harnessMode.
src/Components/Testing/nativeaot/NativeAotTestHarnessGenerator.cs New incremental generator that emits a compiled-harness hosting startup + readiness/parent-process monitoring.
src/Components/Testing/nativeaot/Microsoft.AspNetCore.Components.Testing.NativeAot.csproj New build-only packable project for the Native AOT harness generator + transitive build assets.
src/Components/Testing/nativeaot/buildTransitive/Microsoft.AspNetCore.Components.Testing.NativeAot.targets Adds analyzer inclusion gated by E2ECompileTestHarness.
src/Components/Testing/nativeaot/buildTransitive/Microsoft.AspNetCore.Components.Testing.NativeAot.props Makes E2ECompileTestHarness visible to the generator via CompilerVisibleProperty.
src/Components/Testing/eng/targets/Microsoft.AspNetCore.Components.Testing.targets Updates E2E app preparation to support per-app modes, Native AOT publish wiring, and mode validation.
src/Components/Testing/eng/targets/Microsoft.AspNetCore.Components.Testing.props Adds EnableE2EAppPreparation defaulting for decoupled disabling of app prep.
AspNetCore.slnx Adds the new Native AOT project to the solution.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +69 to +79
var includeBuild = mode.Equals("build", StringComparison.OrdinalIgnoreCase)
|| mode.Equals("all", StringComparison.OrdinalIgnoreCase);
var includePublish = mode.Equals("publish", StringComparison.OrdinalIgnoreCase)
|| mode.Equals("all", StringComparison.OrdinalIgnoreCase);
var isBothMode = mode.Equals("all", StringComparison.OrdinalIgnoreCase);
var name = item.GetMetadata("Filename");
var projectPath = item.GetMetadata("FullPath");
var publicUrl = item.GetMetadata("E2EPublicUrl") ?? "";
var isCompiledHarness = item.GetMetadata("E2ENativeAot").Equals("true", StringComparison.OrdinalIgnoreCase);
var runtimeIdentifier = item.GetMetadata("E2ERuntimeIdentifier");

Comment on lines +179 to +184
if (completed != readinessTask)
{
var output = await process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken);
var error = await process.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken);
Assert.Fail($"Native app exited or timed out before readiness. stdout: {output}\nstderr: {error}");
}
@javiercn
javiercn force-pushed the javiercn-components-testing-native-aot branch 4 times, most recently from 5c3a7e9 to 0bf07bc Compare August 8, 2026 18:34
@javiercn
javiercn force-pushed the javiercn-components-testing-native-aot branch 3 times, most recently from 6fe3906 to 39bf2d2 Compare August 9, 2026 08:45
Base automatically changed from javiercn/migrate-to-mstest to main August 10, 2026 08:47
@javiercn
javiercn requested review from a team and wtgodbe as code owners August 10, 2026 08:47
javiercn and others added 7 commits August 10, 2026 10:47
Add a build-only Native AOT package, compiled application harness, manifest/runtime support, and package-consumer validation for native app publishing and launch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a5425cd4-aa5e-4199-846c-fbf11893985b
Run the publish in a fresh dotnet process after restore so newly generated Native AOT imports and ASP.NET runtime-pack inputs are available on clean consumers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Carry standard ProjectReference configuration and additional properties into the fresh publish evaluation while keeping the required harness, AOT, and RID properties authoritative.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Let each E2E project own its AOT, RID list, self-contained, and restore configuration. Add per-reference E2EAppMode overrides while keeping repository-specific gating and fixture restore outside the shipped targets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the isolated package consumer publish evaluation aligned with its RID-specific restore without adding repository behavior to shipped targets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep server identity, app metadata, direct URL, and public URL available to external E2E tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Suppress the source-tree Components.Testing target import when the outer build sets SkipTestBuild, preventing excluded E2E tests from traversing package assets without restore.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@javiercn
javiercn force-pushed the javiercn-components-testing-native-aot branch from f7cf4e0 to 5c893a6 Compare August 10, 2026 08:47
@javiercn
javiercn merged commit 330e89c into main Aug 10, 2026
29 checks passed
@javiercn
javiercn deleted the javiercn-components-testing-native-aot branch August 10, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants