Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add generic ITestHostLauncher extension point + PackagedApp reference extension - #9454

Merged
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc
Jun 29, 2026
Merged

Add generic ITestHostLauncher extension point + PackagedApp reference extension#9454
Amaury Levé (Evangelink) merged 20 commits into
mainfrom
evangelink-generic-testhost-launcher-rfc

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Implements RFC 017 (reworked to be generic — see #9349). Adds a public, experimental MTP extension point — ITestHostLauncher — that lets an extension control how the out-of-process test host is launched, instead of the platform always calling Process.Start. The platform keeps owning everything around the launch (argument/environment preparation, the controller↔host IPC pipe, PID tracking, ITestHostProcessLifetimeHandler callbacks, and exit-code reconciliation) and delegates only the single "create and start the test host" step.

The hook is deliberately agnostic of the launch mechanism: the launcher does not have to start a local OS process. It can deploy and activate a packaged app, launch a container, or start the host on a remote machine. The returned ITestHostHandle exposes only lifecycle (WaitForExitAsync(CancellationToken), ExitCode, HasExited, Terminate, and IDisposable); there is no Exited event — WaitForExitAsync is the lifecycle mechanism. An opaque diagnostics-only string? Identifier is optional (it is intentionally not a numeric process id, so it can carry whatever a non-process launcher has — a container id, an AUMID-activated token, etc., or null).

Motivating scenario: testing packaged Windows apps (UWP and packaged WinUI), which must be deployed and AUMID-activated rather than Process.Start-ed — the blocker behind #2784.

What's included

Platform extension point (Microsoft.Testing.Platform)

  • New experimental public types (all [Experimental("TPEXP")], no init):
    • ITestHostLauncherTask<ITestHostHandle> LaunchTestHostAsync(TestHostLaunchContext, CancellationToken)
    • ITestHostHandle — generic, mechanism-agnostic (IDisposable); lifecycle via WaitForExitAsync(CancellationToken) / ExitCode / HasExited / Terminate; optional opaque string? Identifier for diagnostics (no Exited event, no numeric process id)
    • TestHostLaunchContextFileName, Arguments, EnvironmentVariables, WorkingDirectory
  • ITestHostControllersManager.AddTestHostLauncher(...) (factory + composite overloads).
  • Registering a launcher forces the controller (process-restart) host, so a run with only a launcher still launches out-of-process. At most one launcher is allowed (fails fast with a localized error).
  • TestHostControllersTestHost delegates the launch at the process.Start site and adapts the handle to the internal IProcess monitoring contract.
  • Identifier-less support: the premature-exit check is gated on HasExited only (not on the availability of any id), so a launcher that returns no identifier (container/remote/AUMID) is monitored purely through the handle lifecycle + the IPC PID handshake. No behavior change for the default Process.Start path.

Reference consumer (Microsoft.Testing.Extensions.PackagedApp)

  • A real, packable extension that consumes the hook for packaged Windows apps (UWP/WinUI share the same MSIX deploy + AUMID-activate mechanism — VSTest uses a single UwpTestHostRuntimeProvider for both). The package ships an experimental (alpha) version.
  • builder.AddPackagedAppDeployment() registers a launcher that deploys (stages the loose layout into an isolated directory) and launches the deployed copy, returning a handle whose Identifier is null — exercising the mechanism-agnostic path end-to-end. Packaged AUMID activation is scaffolded as clearly-marked follow-up.

Tests

  • Unit (TestApplicationBuilderTests): launcher forces process restart, singleton enforcement, duplicate-id validation. ✅
  • Acceptance (TestHostLauncherTests): a custom launcher drives the host end-to-end (with an identifier). ✅
  • Acceptance (PackagedAppDeploymentTests, Windows-gated): deploy-to-separate-directory + run succeeds with an identifier-less handle. ✅
  • Acceptance (PackagedApp.MSBuildRegistration): the package's build/buildTransitive props auto-register its TestingPlatformBuilderHook (build-time, asserted from the binlog; OS-agnostic). ✅

Notes

  • Docs: docs/RFCs/017-TestHost-Launcher.md (generic rework of the RFC in Add RFC 017: Custom test host launcher #9349).
  • New public API is tracked in PublicAPI.Unshipped.txt. The PackagedAppExtensions consumer surface carries the [TPEXP] prefix; the TestingPlatformBuilderHook type/AddExtensions method is intentionally not experimental — it is the MSBuild self-registration code-gen entry point invoked by generated entry-point code, and it follows the exact same convention as the shipped CtrfReport/JUnitReport hooks (non-experimental hook, experimental *Extensions class).

Related: #2784, #9349

Introduce a public, experimental Microsoft.Testing.Platform extension point that
lets an extension control how the out-of-process test host is launched, replacing
the platform's default Process.Start. The abstraction is agnostic of the launch
mechanism (process, packaged/MSIX deploy+activate, container, remote): the launcher
returns an ITestHostHandle exposing only lifecycle (WaitForExitAsync, ExitCode,
HasExited, Exited, Terminate) with an optional ProcessId for diagnostics.
- New public types: ITestHostLauncher, ITestHostHandle, TestHostLaunchContext
(all [Experimental(TPEXP)], no init accessors).
- ITestHostControllersManager.AddTestHostLauncher overloads + manager wiring;
registering a launcher forces the controller host (RequireProcessRestart) and
at most one launcher is allowed (localized OnlyOneTestHostLauncherSupported).
- TestHostControllersTestHost delegates the launch to the registered launcher and
adapts the returned handle to the internal IProcess monitoring contract,
tolerating a null PID for container/remote launches.
- Unit tests for restart-forcing, singleton, and duplicate-id validation.
- Acceptance test with a real consuming launcher proving end-to-end delegation.
- Rework RFC 017 to the generic ITestHostLauncher/ITestHostHandle shape and a
package/deploy framing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove the ITestHostLauncher hook works for a launch that is not a plain
Process.Start: add a real shipping extension Microsoft.Testing.Extensions.AppDeployment
that deploys (stages) the test host into an isolated directory, launches the
deployed copy, and returns an ITestHostHandle that exposes no local process id.
- Platform fix: the test host controller gated premature-exit on
"testHostProcessId is null", which would reject a launcher that returns no PID
(AUMID/container/remote). Gate on HasExited only; the real test host PID still
arrives via the IPC handshake. No behavior change for the default Process.Start
path (a null PID there always coincides with HasExited).
- New extension: AddAppDeployment, AppDeploymentLauncher, DeployedTestHostHandle
(ProcessId => null), TestingPlatformBuilderHook, build props, PublicAPI, PACKAGE.md;
added to TestFx.slnx; targets the SupportedNetFrameworks set.
- Acceptance test AppDeploymentTests references the packed package and asserts the
host was deployed to a separate directory and the run succeeded with a PID-less
handle.
- RFC updated to describe the HasExited-only gating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "AppDeployment" name was too generic: it over-promised a broad deployment
feature while the implementation only demonstrated the ITestHostLauncher hook.
Rename the consuming extension to Microsoft.Testing.Extensions.WinUI, anchoring it
to the concrete motivating scenario (#2784) instead.
- Microsoft.Testing.Extensions.WinUI: AddWinUIDeployment, WinUITestHostLauncher,
WinUITestHostHandle (ProcessId => null), TestingPlatformBuilderHook (new GUID),
build props, PublicAPI, PACKAGE.md; renamed in TestFx.slnx.
- The launcher is framed for WinUI: it implements the unpackaged deploy-and-launch
path (stage the loose layout, launch the deployed app) and documents the packaged
AUMID-activation branch as clearly-marked follow-up.
- Acceptance test renamed to WinUIDeploymentTests and gated to Windows; still proves
end-to-end deploy + PID-less launch against the packed package.
- RFC non-goal updated: a reference WinUI consumer now exists (unpackaged path);
packaged AUMID activation remains a separate follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UWP and packaged WinUI are not distinct scenarios for launching the test host:
both produce MSIX packages and share the same deploy + AUMID-activate mechanism
(which is why VSTest exposes a single UwpTestHostRuntimeProvider for both). Naming
the extension after either app model is too narrow; name it after the shared
packaging format instead.
- Microsoft.Testing.Extensions.WinUI -> Microsoft.Testing.Extensions.Msix
(AddMsixDeployment, MsixTestHostLauncher, MsixTestHostHandle, MsixExtensions).
- Casing is PascalCase "Msix" (not "MSIX"), matching .NET guidelines and the repo
convention for 3+ letter acronyms (HtmlReport, TrxReport, CtrfReport).
- Docs/launcher text now describe UWP and packaged WinUI as the same MSIX mechanism;
packaged AUMID activation remains a clearly-marked follow-up.
- Acceptance test renamed to MsixDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Msix" reads like a tool that creates MSIX packages; this extension is about
running tests *inside* a packaged Windows app. Name it after the scenario instead
of the package format.
- Microsoft.Testing.Extensions.Msix -> Microsoft.Testing.Extensions.PackagedApp
(AddPackagedAppDeployment, PackagedAppTestHostLauncher, PackagedAppTestHostHandle,
PackagedAppExtensions).
- Still covers both UWP and packaged WinUI (both ship as MSIX and share the same
deploy + AUMID-activate mechanism); docs keep "MSIX" as the format acronym in prose
while the package/API name describes the scenario.
- Acceptance test renamed to PackagedAppDeploymentTests (Windows-gated); still proves
end-to-end deploy + PID-less launch against the packed package.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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

This PR implements RFC 017 by adding an experimental, public Microsoft.Testing.Platform (MTP) extension point (ITestHostLauncher + ITestHostHandle + TestHostLaunchContext) to allow extensions to control how the out-of-process test host is launched, and introduces a reference consumer extension (Microsoft.Testing.Extensions.PackagedApp) that exercises PID-less host monitoring.

Changes:

  • Added the experimental ITestHostLauncher launch hook (and related handle/context types) plus controller-manager registration and enforcement (single launcher, forces out-of-process controller host).
  • Updated TestHostControllersTestHost to delegate the launch step to a registered launcher and to support PID-less launchers by relying on lifecycle + IPC PID handshake.
  • Added acceptance/unit tests and a new packable extension (Microsoft.Testing.Extensions.PackagedApp) demonstrating deploy-to-isolated-directory + PID-less handle.
Show a summary per file
FileDescription
TestFx.slnxAdds the new PackagedApp extension project to the main solution.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.csAdds unit tests validating launcher registration, singleton enforcement, and process-restart forcing.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostLauncherTests.csAdds acceptance coverage for end-to-end custom launcher usage.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/PackagedAppDeploymentTests.csAdds Windows-gated acceptance coverage for PID-less launcher behavior via PackagedApp extension.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostLaunchContext.csIntroduces the launch context passed to a custom launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.csAdds registration/build pipeline for ITestHostLauncher and enforces single launcher.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllerConfiguration.csCarries the optional launcher through controller configuration.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostLauncher.csAdds the experimental launcher interface contract.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostHandle.csAdds the experimental handle contract used for monitoring launched hosts.
src/Platform/Microsoft.Testing.Platform/TestHostControllers/ITestHostControllerManager.csAdds public controller-manager APIs to register a launcher (factory + composite).
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlfAdds localized entry for single-launcher enforcement message.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resxAdds the OnlyOneTestHostLauncherSupported resource string.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks the new experimental public APIs.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.csDelegates host launch via ITestHostLauncher and supports PID-less launchers.
src/Platform/Microsoft.Testing.Platform/Helpers/System/TestHostHandleToProcessAdapter.csAdapts public ITestHostHandle to internal IProcess.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/TestingPlatformBuilderHook.csBuilder hook for MSBuild-driven extension registration.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Unshipped.txtTracks new public APIs for the PackagedApp extension.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PublicAPI/PublicAPI.Shipped.txtInitializes shipped API tracking file for the new package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostLauncher.csImplements deploy + launch using the new launcher extension point.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppTestHostHandle.csImplements a PID-less test host handle over a process.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PackagedAppExtensions.csAdds AddPackagedAppDeployment() builder extension to register the launcher.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/PACKAGE.mdDocuments the new experimental PackagedApp extension package.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/Microsoft.Testing.Extensions.PackagedApp.csprojAdds the new PackagedApp extension project and packaging layout.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildTransitive/Microsoft.Testing.Extensions.PackagedApp.propsWires transitive MSBuild import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/buildMultiTargeting/Microsoft.Testing.Extensions.PackagedApp.propsDeclares the TestingPlatformBuilderHook MSBuild item for extension discovery.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/build/Microsoft.Testing.Extensions.PackagedApp.propsWires non-transitive build import to the multi-targeting props.
src/Platform/Microsoft.Testing.Extensions.PackagedApp/BannedSymbols.txtAdds banned-symbols rules for the new PackagedApp extension project.
docs/RFCs/017-TestHost-Launcher.mdAdds/updates the RFC describing the launcher design and scenarios.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Low

- TestHostHandleToProcessAdapter: give the "no process id" InvalidOperationException
a descriptive message instead of an empty one, so the PID-less path is diagnosable.
- PackagedApp launcher: clean up the staged deployment directory once the host has
exited (the handle now owns the directory and best-effort deletes it on Dispose),
preventing temp-dir accumulation in CI. Update the acceptance test to no longer
require the deployment directory to remain on disk after the run.
- Mirror the RFC review fixes (env-var wording, packaged-app example) in the doc
shipped with the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entifier
Per design review on the RFC:
- Remove the redundant Exited event from ITestHostHandle; consumers use
WaitForExitAsync. The internal IProcess adapter synthesizes its informational
Exited event from the exit task instead.
- Replace int? ProcessId with an optional free-form string Identifier (diagnostics
only: PID, container id, remote host:pid, ...). The controller host logs it where
the handle is visible; the adapter no longer pretends to expose a numeric PID.
- Update the PackagedApp handle (Identifier => null) and the acceptance asset handle
(Identifier => process id string) accordingly, plus PublicAPI and the RFC doc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 26, 2026 12:46
CopilotAI review requested due to automatic review settings June 26, 2026 12:46
- PackagedAppTestHostHandle.Dispose: the two best-effort cleanup catches
(IOException, UnauthorizedAccessException) now log via Debug.WriteLine instead of
being empty, keeping cleanup non-fatal while satisfying the empty-catch rule.
- TestHostHandleToProcessAdapter.RaiseExitedWhenDoneAsync: replace the bare generic
catch with catch (Exception ex) and a Debug.WriteLine, addressing the generic
catch-clause finding while preserving the swallow-and-continue behavior for the
informational Exited event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 1
  • Review effort level: Low

…sthost-launcher-rfc
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
ITestHostHandle has no Exited event (consumers use WaitForExitAsync); drop the
stale "Exited" from the lifecycle-contract comment so it matches the interface and
the adapter comment. Also spell "queryable".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 13:18

CopilotAI 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.

Review details

  • Files reviewed: 40/40 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…isposable
Per RFC design review:
- ITestHostHandle now extends IDisposable so the platform deterministically releases
handle-held OS resources (it already wraps the handle in `using` via the adapter).
- WaitForExitAsync takes a CancellationToken (the runtime API and the repo's
netstandard2.0 polyfill both support one). Threaded through the internal IProcess
contract, SystemProcess, and the handle adapter.
- TestHostControllersTestHost passes its cancellation token when waiting for the host
to exit; on cancellation it terminates the host and waits (uncancelable) for full
exit so the existing exit-code reconciliation still observes a real OS exit code.
The normal (non-canceled) path is unchanged.
- Pre-existing internal callers (Retry, HangDump) pass CancellationToken.None to keep
their exact prior behavior; the adapter synthesizes its informational Exited event
with a dispose-linked token.
- Document ExitCode-before-HasExited as undefined, clarify Quote/PasteArguments are
placeholders, and fix the docker example so Terminate() tears down the container
rather than only killing the local client.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 26, 2026
- ITestHostHandle now extends IDisposable and WaitForExitAsync takes a
CancellationToken (both supported by the runtime and the netstandard2.0 polyfill).
The platform disposes the handle after exit and passes its cancellation token while
waiting, reconciling the real exit code afterwards.
- Document ExitCode-before-HasExited as undefined; note Quote/PasteArguments are
placeholders for proper argument quoting; fix the container example so Terminate()
tears down the container (docker stop) rather than only killing the local client.
- Record the design evolution in Alternatives.
Implemented in PR #9454.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

… WorkingDirectory
- Remove the PackagedApp launcher's write of a deployment-marker file into the
original output directory (a shipping-side effect that could fail on read-only dirs
and leave files behind). The acceptance test now proves deployment by having the
deployed test host self-report its AppContext.BaseDirectory via a platform-forwarded
env var, keeping the affordance in the test asset.
- Honor cancellation: ThrowIfCancellationRequested before the deploy and during the
recursive copy.
- Honor TestHostLaunchContext.WorkingDirectory when set, defaulting to the deployment
directory only when it is null.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 14:36

CopilotAI 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.

Review details

  • Files reviewed: 46/46 changed files
  • Comments generated: 3
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…ackage packs on Linux/macOS
The Linux/macOS CI legs build and pack via NonWindowsTests.slnf (NonWindowsBuild=true);
Microsoft.Testing.Platform.slnf is the matching dev filter. Because the PackagedApp project was
not a member of either filter, its NuGet package was never produced on those legs, so
artifacts/packages/Debug/Shipping had no Microsoft.Testing.Extensions.PackagedApp.*.nupkg.
AcceptanceTestBase's static constructor calls ExtractVersionFromPackage("Microsoft.Testing.Extensions.PackagedApp.")
which throws when the package is absent, failing the type initializer and therefore EVERY acceptance
test in the assembly (523 failures on Linux Debug). Windows legs build the full TestFx.slnx, so the
package was already present there.
Add the project alongside OpenTelemetry (its experimental sibling) in both filters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt asset
- TestHostControllersTestHost: when the wait is cancelled, the host may exit between cancellation
and the Kill() call (Kill throws InvalidOperationException when there is no process left to
terminate). Make termination best-effort by swallowing that exception; the subsequent
WaitForExitAsync still reconciles the real exit code.
- PackagedAppDeploymentTests asset: collapse the two consecutive <NoWarn> entries into a single
<NoWarn>$(NoWarn);TPEXP;NETSDK1201</NoWarn> to avoid any ambiguity about the effective value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 26, 2026 16:13

CopilotAI 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.

Review details

  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

This comment has been minimized.

…sthost-launcher-rfc
# Conflicts:
#	Directory.Build.props
#	test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs
…nd broaden cancel-time Kill catch
- Restore the parameterless IProcess.WaitForExitAsync() overload alongside the
WaitForExitAsync(CancellationToken) one. A previous commit replaced the parameterless method,
which is a binary-breaking change to the internal IProcess contract that previously shipped
extensions are compiled against. The shipped Retry extension calls IProcess.WaitForExitAsync(),
so the new platform threw MissingMethodException under ForwardCompatibilityTests
(NewerPlatform_WithPreviousExtensions_ShouldExecuteTests). Both overloads now exist; in-box
callers use the token overload, shipped extensions keep using the parameterless one.
- TestHostControllersTestHost: broaden the best-effort Kill() catch in the cancellation path from
InvalidOperationException to Exception (logged). Kill() can now delegate to a custom
ITestHostLauncher's Terminate(), which may throw arbitrary exceptions; termination must not mask
the cancellation teardown flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

…d version
- csproj: keep the default MIT license (drop PackageLicenseExpression cancel + License.txt
packaging). Matches CtrfReport/HangDump siblings.
- PackagedAppTestHostLauncher: localize DisplayName/Description via a new Resources/ExtensionResources.resx
(+ generated xlf for all locales), matching the HangDump convention; Version now uses
ExtensionVersion.DefaultSemVer (GenerateBuildInfo) instead of a hardcoded "1.0.0".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 10:28

CopilotAI 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.

Review details

  • Files reviewed: 62/62 changed files
  • Comments generated: 1
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9454

No new or modified test methods were identified in the changed regions
of this PR. Nothing to grade.

Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 297.7 AIC · ⌖ 13.2 AIC · ⊞ 43.8K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit 12d0754 into mainJun 29, 2026
56 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-generic-testhost-launcher-rfc branch June 29, 2026 12:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101