Skip to content

Run each test assembly in a test host process of its own - #45

Merged
TorreyBetts merged 4 commits into
mainfrom
fix/runner-test-isolation
Oct 25, 2026
Merged

TorreyBetts merged 4 commits into
mainfrom
fix/runner-test-isolation

Conversation

@TorreyBetts

@TorreyBetts TorreyBetts commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

motus run and the visual runner loaded each test assembly into their own process, so a suite got the runner's copy of anything both depend on (SkiaSharp 2.88 instead of the app's 3.x) and the runner's install folder as AppContext.BaseDirectory. WebApplicationFactory reads the app's .deps.json from the base directory and its manifest from the current directory, so a suite that hosts an ASP.NET Core app could not run at all, though it passes under dotnet test.

Each test assembly now runs in a test host process of its own, started the way dotnet test starts its test host: dotnet exec of Motus.Runner.dll with the suite's own .runtimeconfig.json and .deps.json, with the suite's output folder as base and current directory. motus.config.json is still read from the directory motus run was started in, so the runner and its test hosts see the same settings. Discovery, the assembly, class and test lifecycle, and the accessibility, performance and coverage sinks run in the test host, using the execution code the two runners already shared, which is now one copy of discovery and lifecycle instead of two. Scheduling, retries, quarantine, sharding, filters, reporters, flaky history and enforcement stay in the runner and work on the report each test run sends back over a named pipe. The visual runner starts a test host per run and gets the screencast, the timeline and step debugging from it over the same pipe. A suite built for an older .NET than the runner rolls forward to the newest installed one, since Motus.Runner.dll cannot load on an older runtime.

Because the suite runs on its own Motus, which can be older than the runner's, the runner never names Motus types across the pipe (what a run collects travels as JSON) and makes every call into Motus through a guard that, when the suite's Motus lacks something, warns once and goes on without that extra. Suites on every published package back to 1.0.1 run with their own results. Each test case also has an identity apart from its display name (class, method with parameter types, and for a data row its data source and row), so a fresh test host finds a [DynamicData] row whose name changes on every read, sharding agrees across processes, and overloaded tests each run.

A collectible AssemblyLoadContext per assembly was rejected, even inside a child process: besides leaving the base directory alone, it splits types whenever a suite carries newer Microsoft.Extensions packages than the installed ASP.NET Core (as Mvc.Testing does), because ASP.NET Core in the default context binds the framework's copies while the suite's context loads its own. Only the host's own resolution from the suite's .deps.json unifies them the way dotnet test does.

Behavior changes: tests run in a separate process per assembly, on the dependency versions, frameworks and Motus version their project references rather than motus's own, and motus run warns when that Motus version differs from its own. The current directory for tests is the assembly's output folder, so relative paths used by tests and relative failure.tracePath and failure.screenshotPath settings now resolve there; failure traces from motus run land under the output folder's test-results/traces unless an absolute path is set. A test that exits or crashes its process now fails with the exit code, along with the tests of that assembly still to run, instead of ending motus run. An assembly named on the command line that cannot be loaded, or whose test host cannot start or list its tests, now makes motus run exit 1 (it used to be skipped with a message and exit 0), and that includes a non-test file picked up by a shell glob such as bin//.dll; the visual runner fails that assembly's tests and runs the others. On an older Motus package, accessibility reports, performance metrics, coverage or retry of lost browsers that the package cannot support are left out with a warning. Shards are assigned by test identity rather than display name, so shard membership differs from 1.1. Overloaded test methods without data rows are named with their parameter types, such as Class.Method(Int32). Reporter callbacks are made one at a time, so every result reaches the JUnit, TRX and HTML files under several workers. The visual runner starts a fresh test host for each run, so statics do not carry over and a rebuilt assembly is picked up. Each test assembly adds about a third of a second of startup.

Fixes #27

Signed-off-by: Torrey Betts torrey.betts@gmail.com

motus run and the visual runner loaded test assemblies into their own
process, so a suite saw the runner's copies of anything both depend on
(SkiaSharp 2.88 rather than the app's 3.x), the runner's install folder as
AppContext.BaseDirectory, and the user's directory as the current one.
WebApplicationFactory reads the app's .deps.json and manifest from those
folders, so a suite that hosts an ASP.NET Core app could not run at all,
though it passes under dotnet test.

Each test assembly now runs in a child process started the way dotnet
test starts its test host: dotnet exec of Motus.Runner.dll with the
assembly's own .runtimeconfig.json and .deps.json, with its output folder
as base and current directory. Discovery and each test run go over a
named pipe; reporters, retries, quarantine, sharding and flaky history stay
in the runner and work on the reports that come back. The visual runner
starts one per run and gets the screencast, the timeline and step
debugging from it over the same pipe.

A collectible AssemblyLoadContext per assembly was not enough even inside
a child: a test that carries newer Microsoft.Extensions packages than the
installed ASP.NET Core would load those into its own context while ASP.NET
Core bound the framework's, splitting their types. Only the host's own
dependency resolution unifies them the way dotnet test does.

A test that ends its process now fails with the exit code instead of
taking the runner down, and the tests of that assembly after it fail
saying they did not run.

Fixes #27

Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
@TorreyBetts

Copy link
Copy Markdown
Contributor Author

Design notes and the tests run, to go with the description above.

motus run and the visual runner loaded each test assembly into their own process with Assembly.LoadFrom. The suite then saw the runner's copy of anything both depend on, and the runner's install folder as AppContext.BaseDirectory, so a suite that hosts an ASP.NET Core app could not run although it passes under dotnet test. With a suite shaped like the one in #27 (an app on SkiaSharp 3, started with WebApplicationFactory<Program>), all four tests in the new fixture fail on main:

  • the app and the suite get Could not load file or assembly 'SkiaSharp, Version=3.119.0.0', because the runner's SkiaSharp 2.88 is already in the default load context;
  • WebApplicationFactory throws Can't find '<motus install folder>/Motus.Fixtures.WebApp.deps.json';
  • once the base directory is fixed, it still fails with "Solution root could not be located", because it looks for MvcTestingAppManifest.json in the current directory, which dotnet test sets to the output folder.

What changed

Each test assembly now runs in a test host process of its own, started the way dotnet test starts its test host: dotnet exec --runtimeconfig <suite>.runtimeconfig.json --depsfile <suite>.deps.json Motus.Runner.dll --motus-test-host <pipe> <suite.dll>. The host resolves the suite's dependencies and frameworks exactly as it would for the suite itself, and makes the suite's output folder the base directory. The test host reads motus.config.json from the directory motus run was started in (where the runner reads it, so both see the same settings) and then moves to the output folder.

Motus.Runner.dll is the test host: its Program.cs already had an entry point for the standalone runner and now checks for the test host command first. Before anything touches a Motus type it installs a resolver that loads the runner's own assemblies from beside it when the suite doesn't have them, and takes the suite's copy when the runner asks for a different version. If that version differs from the one the runner was built with, motus run prints a warning.

The runner and the test host talk over a named pipe, one JSON message per line (TestHostProtocol.cs). The work splits like this:

  • In the test host (TestHostServer, InProcessTestHost): discovery, [AssemblyInitialize], each test case with [ClassInitialize] on first use, the class and assembly cleanups, and the accessibility, performance and coverage sinks around each test. All of this is the code from Give MSTest tests under motus run a TestContext, data rows and MSTest's cleanup order #40 (TestCaseInvoker, MSTestContextFactory, TestCaseExpander), moved out of the two runners rather than rewritten. The two copies of discovery and of the lifecycle code are now one each (TestCaseDiscovery, InProcessTestHost).
  • In the runner (TestHostClient, TestRunner, TestExecutionService): scheduling under --workers, retries and their policy, quarantine, sharding, filters, reporters, flaky history, a11y and perf enforcement, and coverage aggregation and thresholds. These work on the report each run sends back (pass/fail, duration, message and stack, whether the failure was a lost browser, plus what the sinks collected), so their code barely changed.
  • The visual runner starts one test host per run, which also picks up a rebuilt assembly without restarting the runner. In visual mode the test host installs the page hook the runner used to install in its own process, then streams screencast frames and timeline entries back. Before each browser action it asks the runner whether to go on, which is how step debugging still holds the test.

Tests built by hand from reflection metadata (the existing runner unit tests do this) still run in-process, through the same InProcessTestHost.

Startup cost. On this machine a test host adds about 0.35 s per assembly: motus run on a three-test filter of Motus.Cli.Tests takes 0.51 s, against 0.15 s on main. A full run starts one host per assembly and keeps it for the whole run.

A test host that crashes. Its stdout and stderr are the runner's own, so whatever the process wrote before it ended (a stack overflow report, say) is on the console. The runner sees the pipe close and waits briefly for the exit code. The test that was running fails with The test host for X.dll exited unexpectedly with code N. The test was running at the time., each later test of that assembly fails with ... The test did not run., and other assemblies carry on. Before, the same test took motus down with it and no report was written. If the host can't start, or the file isn't a .NET assembly, discovery reports Failed to load assembly <path>: <reason> and skips it, as before.

A suite built for an older .NET. Motus.Runner.dll can't load on a runtime older than the one it was built for. When the suite's runtimeconfig targets an older major version, the test host gets --roll-forward LatestMajor, so the suite runs on the newest installed runtime, as it effectively did inside the runner before. The CI step that runs the net10.0 motus on the net8.0 samples takes this path.

Alternatives considered

A collectible AssemblyLoadContext per assembly with an AssemblyDependencyResolver (the issue's lighter option). Besides not fixing the base and current directories, it doesn't fix the dependency clash either, even inside a child process. The fixture suite carries Microsoft.Extensions.* 10.0.12 from Mvc.Testing, newer than the installed ASP.NET Core 10.0.3. The host resolves that by keeping the higher version once for everyone. A load context would load the suite's copies for the suite while ASP.NET Core, which lives in the default context, binds the framework's, and the hosting types end up split across two copies. Only the host's own resolution, from the suite's .deps.json, gets this right, so that is what the test host uses.

Keeping the runner's Motus for the tests. Making the tests run on motus's own Motus, as they did before, would need the suite's Motus-dependent assemblies loaded into a separate context in the child, with the same splitting risk for anything they share with the framework. Running on the Motus the suite references is what dotnet test does, and it also removes today's mismatch between a suite's Motus.Testing package and the runner's Motus. The cost is that the runner reaches into the suite's Motus for its sinks and page hooks, so a large gap between motus and the packages can lose reports. Hence the warning.

Behavior changes

  • Tests run in a separate process per test assembly, on the dependency versions, frameworks and Motus version their project references, not motus's. motus run warns when the suite's Motus version differs from its own.
  • The current directory for tests is the test assembly's output folder, as under dotnet test. A relative path a test uses is now resolved there, and so are relative paths in motus.config.json such as failure.tracePath (default test-results/traces) and failure.screenshotPath. Failure traces from motus run now land under bin/<config>/<tfm>/test-results/traces instead of under the directory motus run was started in. An absolute path keeps them where they were.
  • motus.config.json is still found from the directory motus run was started in, for both the runner and the tests.
  • A suite built for an older .NET than motus runs on the newest .NET installed (--roll-forward LatestMajor).
  • A test that exits or crashes its process fails, along with the later tests of that assembly, instead of ending motus run.
  • The visual runner starts a fresh test host for each run, so statics don't carry over between runs and a rebuilt assembly is used on the next run.
  • motus run takes about a third of a second longer per test assembly.

No public library API changes. Motus.Cli and Motus.Runner ship only inside the tool. Within them, DiscoveredTest in both gains name fields, and its reflection members are null for discovered tests. TestDiscovery.Discover became DiscoverAsync, and the CLI's TestDiscovery is IAsyncDisposable, since it owns the test hosts.

Docs: the motus run section of docs/reference/cli.md gets a "Where tests run" note. docs/guides/configuration.md (config file discovery) and docs/guides/testing-frameworks.md (where relative trace paths land) are updated.

Tests

  • TestHostIsolationTests (Motus.Cli.Tests) runs a new fixture suite, tests/Fixtures/Motus.Fixtures.WebApp.Tests: an ASP.NET Core app on SkiaSharp 3 hosted with WebApplicationFactory<Program>, with tests for the app's and the suite's SkiaSharp, the base directory and the current directory. The fixture isn't in the solution: Motus.Cli.Tests builds it through a ReferenceOutputAssembly="false" project reference and finds it through assembly metadata. All 4 fixture tests fail under the main runner and pass here. The class also covers the net8.0 build of the fixture from the net10.0 runner (roll-forward), a test host that exits with code 3 mid-run (that test and the one after it fail with the exit code), and a file that isn't an assembly.
  • VisualTestHostIntegrationTests (Motus.Tests, Integration) runs a browser test through the visual runner's session and executor in a test host. It checks the screencast frames, the timeline entry for the click (numbered, and filed under the right test), the step debugger being asked, and the browser view stopping afterwards.
  • TestHostLauncherTests (Motus.Tests): the command line and the roll-forward decision.
  • Existing tests that went through discovery now go through a test host: TestDiscoveryTests, the data-row and quarantine tests in TestRunnerMSTestTests, TestExecutionServiceTests discovery plus a new run of discovered data rows, and TrxRerunServiceTests. The data-row fixture now asserts its own arguments, since the runner can't see statics in another process.

Run locally on macOS:

  • dotnet build Motus.sln: 0 errors, no new warnings.
  • Unit tests (TestCategory!=Integration, Samples excluded) on net8.0 and net10.0: all pass. Motus.Tests 1255, Motus.Cli.Tests 328 (2 skipped on net10.0, 3 on net8.0, where the roll-forward test is inconclusive), Mcp 368, Recorder 105, Codegen 59, Analyzers 29, MSTest.v4 12, xUnit 8.
  • Under the browser lock, on net8.0 and net10.0: the Motus.Tests.Runner namespace including the new integration test (68), and FailureTraceIntegrationTests, TestRunner* and TestHostIsolationTests in Motus.Cli.Tests (40). All pass.
  • motus run (net10.0) on the net8.0 Motus.Samples, as CI does: 48 passed, 0 failed, 3 ignored, the same as before.
  • By hand, motus run --visual on the browser fixture: tests listed, the Browser View streams the page from the test host, the timeline fills in, and Step Mode pauses before the click and advances on Step.

Firefox wasn't run. Nothing here touches the protocol layer, and the test host is browser-agnostic.

Left for later

  • A test host that crashes isn't restarted, so the rest of that assembly fails rather than running in a new host.
  • There is still no per-test timeout: a hung test hangs its test host and the run, as it hung motus before. The process boundary would make a timeout easy to add.
  • Step debugging makes a round trip to the runner before every browser action, even with Step Mode off. It's cheap next to the two screenshots the timeline already takes per action, but it could be skipped while Step Mode is off.
  • motus run --visual still ignores --a11y, --perf-budget and --coverage (unchanged here).

The fixture suite that TestHostIsolationTests runs was built only
through a project reference from Motus.Cli.Tests. A project outside the
solution is built in its default configuration, so the Release build in
CI produced it under bin/Debug while the test looked under bin/Release.
In the solution it builds in the solution's configuration, and its own
tests run under dotnet test, which shows the suite passes there too.

Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Summary

Summary
Generated on: 10/24/2026 - 23:52:43
Coverage date: 10/24/2026 - 23:47:37 - 10/24/2026 - 23:52:33
Parser: MultiReport (36x Cobertura)
Assemblies: 12
Classes: 580
Files: 292
Line coverage: 82.7% (9921 of 11991)
Covered lines: 9921
Uncovered lines: 2070
Coverable lines: 11991
Total lines: 42938
Branch coverage: 71.2% (3763 of 5282)
Covered branches: 3763
Total branches: 5282
Method coverage: Feature is only available for sponsors

Coverage

Motus - 81.6%
Name Line Branch
Motus 81.6% 72.9%
Motus.AccessibilityAuditHook 62% 40%
Motus.AccessibilityAXNode 100%
Motus.AccessibilityAXNodeSimple 100%
Motus.AccessibilityAXProperty 100%
Motus.AccessibilityAXRelatedNode 100%
Motus.AccessibilityAXValue 100%
Motus.AccessibilityAXValueSimple 100%
Motus.AccessibilityEnableResult 100%
Motus.AccessibilityGetFullAXTreeParams 100%
Motus.AccessibilityQueryAXTreeParams 100%
Motus.AccessibilityRoleNames 100% 50%
Motus.AccessibilityRuleCollection 100%
Motus.AccessibilityRuleEngine 100% 100%
Motus.AccessibilityRulesPlugin 75%
Motus.AccessibilitySelectorHelper 0% 0%
Motus.AccessibilityTreeQuery 98.8% 98.6%
Motus.AccessibilityTreeResult 100%
Motus.AccessibilityViolationSink 100% 100%
Motus.ActionContext 100%
Motus.AltTextAccessibilityRule 94.1% 100%
Motus.Assertions.AccessibilityAssertionOptions 100%
Motus.Assertions.AssertionRetryHelper 100% 75%
Motus.Assertions.Expect 100%
Motus.Assertions.LocatorAssertions 90.9% 100%
Motus.Assertions.PageAssertions 98.1% 85.7%
Motus.Assertions.ResponseAssertions 100% 100%
Motus.AttachedToTargetTranslation 100% 50%
Motus.BackendNodeIdSelectorStrategy 50% 0%
Motus.BiDiBrowsingContextCloseParams 0%
Motus.BiDiBrowsingContextCreateParams 0%
Motus.BiDiBrowsingContextCreateResult 0%
Motus.BiDiBrowsingContextNavigateParams 0%
Motus.BiDiBrowsingContextNavigateResult 0%
Motus.BiDiBrowsingContextReloadParams 0%
Motus.BiDiCommandEnvelope 100%
Motus.BiDiDisconnectedException 50%
Motus.BiDiEventMap 100% 83.3%
Motus.BiDiInboundDiscriminator 100%
Motus.BiDiInputAction 100%
Motus.BiDiInputActionSequence 100%
Motus.BiDiInputPerformActionsParams 100%
Motus.BiDiInputSetFilesParams 100%
Motus.BiDiInputSourceParameters 100%
Motus.BiDiKeyValues 100% 100%
Motus.BiDiNetworkAddInterceptParams 100%
Motus.BiDiNetworkAddInterceptResult 0%
Motus.BiDiNetworkBytesValue 100%
Motus.BiDiNetworkContinueRequestParams 100%
Motus.BiDiNetworkFailRequestParams 100%
Motus.BiDiNetworkHeader 0%
Motus.BiDiNetworkProvideResponseParams 100%
Motus.BiDiNetworkUrlPattern 0%
Motus.BiDiProtocolException 75%
Motus.BiDiScriptCallFunctionParams 100%
Motus.BiDiScriptCallFunctionResult 0%
Motus.BiDiScriptEvaluateParams 0%
Motus.BiDiScriptEvaluateResult 0%
Motus.BiDiScriptExceptionDetails 0%
Motus.BiDiScriptRemoteValue 0%
Motus.BiDiScriptSerializationOptions 100%
Motus.BiDiScriptTarget 100%
Motus.BiDiScriptValues 84% 57.9%
Motus.BiDiSession 100% 100%
Motus.BiDiSessionCapabilitiesRequest 0%
Motus.BiDiSessionNewParams 0%
Motus.BiDiSessionNewResult 0%
Motus.BiDiSessionRegistry 84.6%
Motus.BiDiSessionStatusResult 0%
Motus.BiDiSessionSubscribeParams 100%
Motus.BiDiTranslationRegistry 100%
Motus.BiDiTransport 98.2% 90%
Motus.Browser 54% 42.8%
Motus.BrowserCloseResult 100%
Motus.BrowserCloseTranslation 50%
Motus.BrowserContext 74% 41.1%
Motus.BrowserFinder 50.4% 65.8%
Motus.BrowserGetVersionResult 100%
Motus.BrowserGetVersionTranslation 83.3% 50%
Motus.BrowserGrantPermissionsParams 100%
Motus.BrowserGrantPermissionsResult 100%
Motus.BrowserHeartbeat 100%
Motus.BrowserLease 100%
Motus.BrowserOutputDrain 100% 100%
Motus.BrowserPool 20% 0%
Motus.BrowserResetPermissionsResult 0%
Motus.BuiltinSelectorsPlugin 75%
Motus.CapabilityGuard 78.2% 68.7%
Motus.CdpCommandEnvelope 100%
Motus.CdpDisconnectedException 50%
Motus.CdpEndpointPoller 75% 50%
Motus.CdpErrorPayload 100%
Motus.CdpInboundEnvelope 100%
Motus.CdpPipeSocket 96.5% 87.5%
Motus.CdpProtocolException 75%
Motus.CdpSession 100% 83.3%
Motus.CdpSessionRegistry 100%
Motus.CdpSocket 75% 0%
Motus.CdpTransport 100% 97%
Motus.ChromiumArgs 100% 100%
Motus.ColorContrastRule 93.1% 89.2%
Motus.ComputedStyleCollector 100%
Motus.ConfigMerge 37.7% 45.8%
Motus.ConsoleApiCalledTranslation 84.2% 25%
Motus.ContrastCalculator 91% 72.5%
Motus.CoverageAggregator 95% 87.2%
Motus.CoverageCollector 95.2% 100%
Motus.CoverageRemapper 96.2% 87.8%
Motus.CoverageSink 72.7% 50%
Motus.CoverageThresholdResult 100%
Motus.CoverageThresholds 100% 100%
Motus.CssEnableResult 0%
Motus.CssRuleUsageEntry 0%
Motus.CssSelectorStrategy 100%
Motus.CssStartRuleUsageTrackingResult 0%
Motus.DebuggerDisableResult 0%
Motus.DetachedFromTargetTranslation 25% 0%
Motus.Dialog 100%
Motus.DialogClosedTranslation 100%
Motus.DialogOpeningTranslation 100% 50%
Motus.DomBoxModel 0%
Motus.DomContentLoadedTranslation 50%
Motus.DomDebuggerEventListener 0%
Motus.DomDebuggerGetEventListenersParams 0%
Motus.DomDebuggerGetEventListenersResult 0%
Motus.DomDescribeNodeParams 100%
Motus.DomDocumentNode 0%
Motus.DomEnableResult 0%
Motus.DomFocusResult 0%
Motus.DomGetBoxModelParams 0%
Motus.DomGetNodeForLocationParams 100%
Motus.DomGetNodeForLocationResult 100%
Motus.DomGetOuterHtmlParams 100%
Motus.DomHideHighlightResult 0%
Motus.DomHighlightConfig 0%
Motus.DomHighlightNodeParams 0%
Motus.DomHighlightNodeResult 0%
Motus.DomNodeDescription 100%
Motus.DomResolveNodeParams 100%
Motus.DomScrollIntoViewIfNeededResult 0%
Motus.DomSetFileInputFilesParams 100%
Motus.DomSetFileInputFilesResult 100%
Motus.DomSetFileInputFilesTranslation 87.5% 50%
Motus.Download 40% 0%
Motus.DuplicateIdRule 88.8% 90%
Motus.ElementHandle 100%
Motus.EmptyButtonRule 94.1% 100%
Motus.EmptyLinkRule 94.1% 100%
Motus.EmulationSetDeviceMetricsOverrideParams 100%
Motus.EmulationSetDeviceMetricsOverrideResult 100%
Motus.EmulationSetEmulatedMediaParams 100%
Motus.EmulationSetEmulatedMediaResult 100%
Motus.EmulationSetFocusEmulationEnabledResult 100%
Motus.EmulationSetGeolocationOverrideParams 0%
Motus.EmulationSetGeolocationOverrideResult 0%
Motus.EmulationSetLocaleOverrideResult 100%
Motus.EmulationSetTimezoneOverrideResult 100%
Motus.EmulationSetUserAgentOverrideParams 100%
Motus.EmulationSetUserAgentOverrideResult 100%
Motus.FailureArtifactName 91.3% 94.7%
Motus.FetchAuthChallenge 0%
Motus.FetchAuthChallengeResponse 0%
Motus.FetchAuthRequiredEvent 0%
Motus.FetchContinueRequestParams 100%
Motus.FetchContinueRequestResult 100%
Motus.FetchContinueRequestTranslation 67.8% 42.8%
Motus.FetchContinueWithAuthParams 0%
Motus.FetchContinueWithAuthResult 0%
Motus.FetchDisableResult 100%
Motus.FetchEnableParams 100%
Motus.FetchEnableResult 100%
Motus.FetchEnableTranslation 58.8% 30%
Motus.FetchFailRequestParams 100%
Motus.FetchFailRequestResult 100%
Motus.FetchFailRequestTranslation 71.4% 50%
Motus.FetchFulfillRequestParams 100%
Motus.FetchFulfillRequestResult 100%
Motus.FetchFulfillRequestTranslation 67.8% 42.8%
Motus.FetchRequestPattern 100%
Motus.FetchRequestPausedEvent 100%
Motus.FileChooser 0% 0%
Motus.FirefoxArgs 100% 100%
Motus.FirefoxProfileManager 100% 100%
Motus.FragmentNavigatedTranslation 100% 50%
Motus.Frame 45.9% 12.5%
Motus.FrameNavigatedTranslation 100% 50%
Motus.GetBySelectors 100% 100%
Motus.GetBySelectorStrategy 96.9% 96.1%
Motus.HarContent 100%
Motus.HarEntry 100%
Motus.HarLog 100%
Motus.HarPage 0%
Motus.HarRecorder 96.4% 79.5%
Motus.HarRequest 100%
Motus.HarResponse 100%
Motus.HarTimings 100%
Motus.HeaderCollection 95.8% 100%
Motus.HeadingHierarchyRule 91.1% 80%
Motus.HistoryUpdatedTranslation 100% 50%
Motus.IBiDiEventTranslation 100%
Motus.IMotusSession 0%
Motus.InputDispatchKeyEventParams 100%
Motus.InputDispatchKeyEventResult 100%
Motus.InputDispatchKeyEventTranslation 100% 83.3%
Motus.InputDispatchMouseEventParams 100%
Motus.InputDispatchMouseEventResult 100%
Motus.InputDispatchMouseEventTranslation 72.5% 33.3%
Motus.InputDispatchTouchEventParams 100%
Motus.InputDispatchTouchEventResult 100%
Motus.InputDispatchTouchEventTranslation 83.7% 50%
Motus.InputInsertTextResult 100%
Motus.InputTouchPoint 100%
Motus.InstalledBrowsers 81.8% 100%
Motus.IoCloseResult 100%
Motus.IoReadParams 100%
Motus.IoReadResult 100%
Motus.JsHandle 66.6% 30%
Motus.JsonBuilder 100% 72.2%
Motus.Keyboard 100% 100%
Motus.KeyDefinitions 100% 100%
Motus.LifecycleHookCollection 100%
Motus.LoadEventFiredTranslation 100%
Motus.Locator 88.6% 78.1%
Motus.MappingSegment 100%
Motus.MissingDocumentLanguageRule 92.8% 100%
Motus.MissingLandmarkRule 93.7% 100%
Motus.MjpegAviWriter 100% 50%
Motus.MotusAccessibilityConfig 100%
Motus.MotusAssertionsConfig 100%
Motus.MotusConfigLoader 89.4% 88.5%
Motus.MotusContextConfig 100%
Motus.MotusCoverageConfig 100%
Motus.MotusCoverageCssConfig 0%
Motus.MotusCoverageJsConfig 0%
Motus.MotusFailureConfig 100%
Motus.MotusFlakyConfig 100%
Motus.MotusLaunchConfig 100%
Motus.MotusLauncher 56.2% 100%
Motus.MotusLocatorConfig 100%
Motus.MotusMcpConfig 100%
Motus.MotusPerformanceConfig 100%
Motus.MotusRecorderConfig 100%
Motus.MotusReporterConfig 100%
Motus.MotusRequest 100%
Motus.MotusResponse 100% 50%
Motus.MotusRootConfig 100%
Motus.MotusRoute 58% 45.6%
Motus.MotusShardConfig 100%
Motus.MotusVideo 100%
Motus.MotusViewportConfig 100%
Motus.Mouse 92.8% 78.5%
Motus.NetworkClearBrowserCookiesResult 0%
Motus.NetworkCookieData 100%
Motus.NetworkEmulateNetworkConditionsParams 100%
Motus.NetworkEmulateNetworkConditionsResult 100%
Motus.NetworkEnableParams 100%
Motus.NetworkEnableResult 100%
Motus.NetworkLoadingFailedEvent 100%
Motus.NetworkLoadingFinishedEvent 100%
Motus.NetworkManager 98% 75%
Motus.NetworkRequestData 100%
Motus.NetworkRequestWillBeSentEvent 100%
Motus.NetworkResponseData 100%
Motus.NetworkResponseReceivedEvent 100%
Motus.NetworkSetCookieParams 100%
Motus.NetworkSetExtraHttpHeadersParams 100%
Motus.NetworkSetExtraHttpHeadersResult 100%
Motus.NoOpTranslation 60%
Motus.NullMotusLogger 40%
Motus.NullTracing 0%
Motus.Page 73.6% 71.3%
Motus.PageBringToFrontResult 0%
Motus.PageCaptureScreenshotParams 0%
Motus.PageCaptureScreenshotWithClipParams 100%
Motus.PageClipRect 100%
Motus.PageCreateIsolatedWorldParams 0%
Motus.PageDownloadProgressEvent 0%
Motus.PageDownloadWillBeginEvent 100%
Motus.PageEnableResult 100%
Motus.PageFileChooserOpenedEvent 0%
Motus.PageFrameInfo 100%
Motus.PageFrameTreeNode 100%
Motus.PageGetFrameTreeTranslation 100% 75%
Motus.PageGetNavigationHistoryResult 0%
Motus.PageHandleJavaScriptDialogResult 0%
Motus.PageJavascriptDialogOpeningEvent 100%
Motus.PageNavigatedWithinDocumentEvent 100%
Motus.PageNavigateParams 100%
Motus.PageNavigateResult 100%
Motus.PageNavigateToHistoryEntryResult 0%
Motus.PageNavigateTranslation 100% 50%
Motus.PageReloadResult 100%
Motus.PageReloadTranslation 75% 50%
Motus.PageScreencastFrameAckResult 0%
Motus.PageScreencastFrameEvent 0%
Motus.PageScreencastFrameMetadata 0%
Motus.PageSetInterceptFileChooserDialogResult 0%
Motus.PageStartScreencastParams 100%
Motus.PageStartScreencastResult 100%
Motus.PageStopScreencastResult 0%
Motus.PerformanceBudgetContext 100% 100%
Motus.PerformanceEnableResult 100%
Motus.PerformanceMetricsCollector 94.7% 100%
Motus.PerformanceMetricsSink 100%
Motus.PluginContext 87.5%
Motus.PluginHost 100%
Motus.ProcessStderrAdapter 0%
Motus.ProfilerCoverageRange 0%
Motus.ProfilerEnableResult 0%
Motus.ProfilerFunctionCoverage 0%
Motus.ProfilerScriptCoverage 0%
Motus.ProfilerStartPreciseCoverageParams 0%
Motus.ProfilerStopPreciseCoverageResult 0%
Motus.ProfilerTakePreciseCoverageResult 0%
Motus.ProtocolLog 77.7% 81.4%
Motus.ProtocolLogFile 88.4% 68.7%
Motus.ProtocolLogFormatter 96.5% 93.7%
Motus.ProtocolLogSettings 100% 95.4%
Motus.ProtocolLogSink 86.6% 76.4%
Motus.ReporterCollection 100%
Motus.RequestPausedTranslation 100% 60%
Motus.RequestWillBeSentTranslation 13.3% 0%
Motus.ResponseReceivedTranslation 7.4% 0%
Motus.RoleSelectorStrategy 95.2% 94.4%
Motus.RuntimeAddBindingResult 100%
Motus.RuntimeBindingCalledEvent 0%
Motus.RuntimeCallArgument 100%
Motus.RuntimeCallFunctionOnParams 100%
Motus.RuntimeCallFunctionOnResult 100%
Motus.RuntimeCallFunctionOnTranslation 95.8% 85.7%
Motus.RuntimeConsoleApiCalledEvent 100%
Motus.RuntimeEnableResult 100%
Motus.RuntimeEvaluateParams 100%
Motus.RuntimeEvaluateResult 100%
Motus.RuntimeEvaluateTranslation 92.3% 50%
Motus.RuntimeExceptionDetails 100%
Motus.RuntimeExceptionThrownEvent 100%
Motus.RuntimeExecutionContextCreatedEvent 100%
Motus.RuntimeExecutionContextDescription 100%
Motus.RuntimeExecutionContextDestroyedEvent 100%
Motus.RuntimeGetPropertiesParams 100%
Motus.RuntimeGetPropertiesTranslation 89.4% 78.1%
Motus.RuntimePropertyDescriptor 100%
Motus.RuntimeReleaseObjectResult 0%
Motus.RuntimeReleaseObjectTranslation 50%
Motus.RuntimeRemoteObject 100%
Motus.ScriptResultTranslator 92.8% 75%
Motus.SecurityEnableResult 100%
Motus.SecuritySetIgnoreCertificateErrorsResult 100%
Motus.Selectors.DomFingerprint 100%
Motus.Selectors.DomFingerprintBuilder 98% 91.6%
Motus.Selectors.SelectorEntry 100%
Motus.SelectorStrategyHelpers 100%
Motus.SelectorStrategyRegistry 100%
Motus.SourceMap 100%
Motus.SourceMapFetcher 0% 0%
Motus.SourceMapJsonDto 100%
Motus.SourceMapParser 90.3% 77.5%
Motus.SourceMapResolver 93.3% 87.5%
Motus.TargetAttachedToTargetEvent 100%
Motus.TargetAttachToTargetTranslation 62.5% 100%
Motus.TargetCloseTargetTranslation 60% 50%
Motus.TargetCreateBrowserContextParams 100%
Motus.TargetCreateBrowserContextTranslation 50%
Motus.TargetCreateTargetParams 100%
Motus.TargetCreateTargetTranslation 60% 50%
Motus.TargetDetachedFromTargetEvent 100%
Motus.TargetDisposeBrowserContextResult 100%
Motus.TargetDisposeBrowserContextTranslation 50%
Motus.TargetInfo 100%
Motus.TargetSetAutoAttachParams 100%
Motus.TargetSetAutoAttachResult 100%
Motus.TargetSetAutoAttachTranslation 50%
Motus.TargetSetDiscoverTargetsResult 100%
Motus.TestIdSelectorStrategy 100%
Motus.TestMethodNameContext 100%
Motus.TextSelectorStrategy 100%
Motus.Touchscreen 100%
Motus.Tracing 82.6% 77%
Motus.TracingEndResult 100%
Motus.TracingStartParams 100%
Motus.TracingStartResult 100%
Motus.TracingTraceConfig 100%
Motus.TracingTracingCompleteEvent 100%
Motus.UnlabeledFormControlRule 95.4% 100%
Motus.VideoRecorder 90.9% 100%
Motus.Vlq 100% 100%
Motus.WindowsProcessGuard 0% 0%
Motus.XPathSelectorStrategy 100%
System.Text.Json.Serialization.JsonStringEnumConverter`1 0% 0%
System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute 0%
Motus.Abstractions - 87.7%
Name Line Branch
Motus.Abstractions 87.7% 100%
Motus.Abstractions.AccessibilityAuditContext 100%
Motus.Abstractions.AccessibilityAuditResult 100%
Motus.Abstractions.AccessibilityNode 100%
Motus.Abstractions.AccessibilitySnapshot 100%
Motus.Abstractions.AccessibilityViolation 100%
Motus.Abstractions.ActionTimeoutException 50%
Motus.Abstractions.AmbiguousSelectorException 50%
Motus.Abstractions.BrowserFailure 100% 100%
Motus.Abstractions.ComputedStyleInfo 100%
Motus.Abstractions.Cookie 100%
Motus.Abstractions.CoverageData 100% 100%
Motus.Abstractions.CoverageSummary 100%
Motus.Abstractions.ElementNotFoundException 100%
Motus.Abstractions.IBrowser 0%
Motus.Abstractions.LayoutShiftEntry 100%
Motus.Abstractions.MotusAssertionException 50%
Motus.Abstractions.MotusException 100%
Motus.Abstractions.MotusNavigationException 50%
Motus.Abstractions.MotusProtocolException 100%
Motus.Abstractions.MotusSelectorException 100%
Motus.Abstractions.MotusTargetClosedException 100%
Motus.Abstractions.MotusTimeoutException 50%
Motus.Abstractions.MouseButtonOptions 100%
Motus.Abstractions.MouseMoveOptions 100%
Motus.Abstractions.NavigationTimeoutException 50%
Motus.Abstractions.OriginalFileCoverage 100%
Motus.Abstractions.PerformanceBudget 100%
Motus.Abstractions.PerformanceBudgetAttribute 100% 100%
Motus.Abstractions.PerformanceBudgetEntry 100%
Motus.Abstractions.PerformanceBudgetResult 100%
Motus.Abstractions.PerformanceMetrics 100%
Motus.Abstractions.ProxySettings 100%
Motus.Abstractions.RouteContinueOptions 100%
Motus.Abstractions.ScriptCoverage 100%
Motus.Abstractions.StorageState 100%
Motus.Abstractions.StylesheetCoverage 100%
Motus.Abstractions.TestInfo 100%
Motus.Abstractions.TestResult 100%
Motus.Abstractions.TestRunSummary 100%
Motus.Abstractions.TestSuiteInfo 100%
Motus.Abstractions.WaitTimeoutException 50%
Motus.Analyzers - 91%
Name Line Branch
Motus.Analyzers 91% 72.4%
Motus.Analyzers.Analyzers.DeprecatedSelectorAnalyzer 90.6% 61.1%
Motus.Analyzers.Analyzers.FragileSelectorAnalyzer 92.1% 65%
Motus.Analyzers.Analyzers.HardcodedDelayAnalyzer 96.1% 81.2%
Motus.Analyzers.Analyzers.MissingDisposalAnalyzer 100% 100%
Motus.Analyzers.Analyzers.NavigationWaitAnalyzer 93.4% 77%
Motus.Analyzers.Analyzers.NonAwaitedCallAnalyzer 90.9% 75%
Motus.Analyzers.Analyzers.UnusedLocatorAnalyzer 94.5% 64.2%
Motus.Analyzers.CodeFixes.AddAwaitCodeFix 0%
Motus.Analyzers.CodeFixes.ReplaceDelayCodeFix 56.2% 50%
Motus.Analyzers.CodeFixes.WrapAwaitUsingCodeFix 0%
Motus.Analyzers.SymbolHelper 100% 82.1%
Motus.Cli - 82.1%
Name Line Branch
Motus.Cli 82.1% 60.4%
Motus.Cli.Commands.BrowserPathHelper 88.8% 50%
Motus.Cli.Commands.CheckSelectorsCommand 100%
Motus.Cli.Commands.CodegenCommand 93.8% 0%
Motus.Cli.Commands.InstallCommand 100%
Motus.Cli.Commands.McpCommand 96.8% 60.8%
Motus.Cli.Commands.PdfCommand 100%
Motus.Cli.Commands.RecordCommand 94.2%
Motus.Cli.Commands.RunCommand 95.9% 25%
Motus.Cli.Commands.ScreenshotCommand 100%
Motus.Cli.Commands.ShardCommand 0%
Motus.Cli.Commands.TraceCommand 0%
Motus.Cli.Commands.TrxCommand 100%
Motus.Cli.Commands.UpdateProtocolCommand 100%
Motus.Cli.Services.BrowserInstaller 2.1% 0%
Motus.Cli.Services.CheckSelectorsRunner 0% 0%
Motus.Cli.Services.DiscoveredTest
Motus.Cli.Services.DomainDiff 100%
Motus.Cli.Services.FingerprintCandidate 100%
Motus.Cli.Services.FingerprintMatch 0%
Motus.Cli.Services.FingerprintScanner 0% 0%
Motus.Cli.Services.FlakeHistoryReporter 86.6% 100%
Motus.Cli.Services.InteractiveRepairApplier 85.7% 75%
Motus.Cli.Services.LocatorDispatcher 26.3% 10.8%
Motus.Cli.Services.ParsedSelector 100%
Motus.Cli.Services.ProtocolDiff 100% 100%
Motus.Cli.Services.ProtocolDiffer 97.2% 95.4%
Motus.Cli.Services.ProtocolUpdater 0% 0%
Motus.Cli.Services.RepairSuggestion 100%
Motus.Cli.Services.RepairSuggestionPipeline 75% 75%
Motus.Cli.Services.Reporters.CoberturaReporter 62.5% 54.1%
Motus.Cli.Services.Reporters.CompositeReporter 100%
Motus.Cli.Services.Reporters.ConsoleReporter 63.1% 50%
Motus.Cli.Services.Reporters.CoverageConsoleReporter 88.6% 70.8%
Motus.Cli.Services.Reporters.CoverageHtmlReporter 91.4% 88.2%
Motus.Cli.Services.Reporters.CoverageReporterFactory 100% 95.8%
Motus.Cli.Services.Reporters.HtmlReporter 92.3% 100%
Motus.Cli.Services.Reporters.JUnitReporter 100% 100%
Motus.Cli.Services.Reporters.ReporterFactory 100% 100%
Motus.Cli.Services.Reporters.TrxReporter 100%
Motus.Cli.Services.SelectorCheckResult 100%
Motus.Cli.Services.SelectorCheckTablePrinter 93.6% 78.3%
Motus.Cli.Services.SelectorManifestWriter 100% 87.5%
Motus.Cli.Services.SelectorParser 94.2% 88.8%
Motus.Cli.Services.SelectorParseResult 100%
Motus.Cli.Services.SelectorParseWarning 100%
Motus.Cli.Services.SelectorRewriter 77.6% 67.1%
Motus.Cli.Services.ShardMerger 91.3% 66.4%
Motus.Cli.Services.ShardMergeResult 100% 100%
Motus.Cli.Services.ShardSelector 93.3% 86.3%
Motus.Cli.Services.SuggestionBuilder 100% 91.6%
Motus.Cli.Services.TestDiscovery 100%
Motus.Cli.Services.TestRunner 80.9% 83.3%
Motus.Codegen - 89.8%
Name Line Branch
Motus.Codegen 89.8% 70.6%
Motus.Codegen.CdpGenerator 100%
Motus.Codegen.Emit.DomainEmitter 84.9% 71.4%
Motus.Codegen.Emit.NamingHelper 100% 92.8%
Motus.Codegen.Emit.PluginRegistryEmitter 100% 100%
Motus.Codegen.Emit.TypeResolver 75% 55%
Motus.Codegen.Model.PluginTypeInfo 44.4% 0%
Motus.Codegen.Parser.CdpSchemaParser 93.4% 85.2%
Motus.Codegen.PluginDiscoveryGenerator 97.5% 91.1%
Motus.Mcp - 91.9%
Name Line Branch
Motus.Mcp 91.9% 80.2%
Motus.Mcp.AccessibilityTools 100% 50%
Motus.Mcp.ActionReport 89.7% 69.4%
Motus.Mcp.ActionRunner 100%
Motus.Mcp.ActivePageService 88.8% 100%
Motus.Mcp.BrowserSessionManager 78.2% 75%
Motus.Mcp.ConsoleEntry 50% 0%
Motus.Mcp.ConsoleService 96.8% 94.4%
Motus.Mcp.ConsoleTools 100% 100%
Motus.Mcp.ContextTools 92.8% 87.5%
Motus.Mcp.CoordinateTools 100%
Motus.Mcp.CoreTools 100%
Motus.Mcp.DialogNotice 97.3% 94.7%
Motus.Mcp.DialogService 100% 95%
Motus.Mcp.FrameCollector 100% 100%
Motus.Mcp.InlineFrames 75% 100%
Motus.Mcp.InteractionTools 66.6% 13.8%
Motus.Mcp.LogText 90% 83.3%
Motus.Mcp.McpServerConfiguration 100% 81.8%
Motus.Mcp.McpServerHost 0%
Motus.Mcp.McpServerLaunchOptions 85% 87.5%
Motus.Mcp.McpSessionBundle 100% 50%
Motus.Mcp.McpSessionRegistry 83.3% 50%
Motus.Mcp.NetworkEntry 100% 87.5%
Motus.Mcp.NetworkService 91.5% 90.6%
Motus.Mcp.NetworkTools 100% 100%
Motus.Mcp.PageDescription 100% 100%
Motus.Mcp.PageSnapshotService 94.1% 92.8%
Motus.Mcp.PageTools 100% 100%
Motus.Mcp.PerformanceTools 100% 100%
Motus.Mcp.SecurityPolicy 88.1% 82.2%
Motus.Mcp.SerializedSnapshot 100%
Motus.Mcp.SnapshotNotTakenException 100%
Motus.Mcp.SnapshotSerializer 94.9% 91.3%
Motus.Mcp.StaleRefException 100%
Motus.Mcp.ToolArguments 92% 96.8%
Motus.Mcp.ToolCapabilities 100% 80%
Motus.Mcp.ToolResultHelper 100% 100%
Motus.Mcp.Http - 98.6%
Name Line Branch
Motus.Mcp.Http 98.6% 80%
Motus.Mcp.Http.McpHttpServerHost 98.6% 80%
Motus.Recorder - 77.8%
Name Line Branch
Motus.Recorder 77.8% 70.8%
Motus.Recorder.ActionCapture.ActionCaptureEngine 45.1% 28.2%
Motus.Recorder.ActionCapture.InputStateMachine 99.4% 91.7%
Motus.Recorder.ActionCapture.RecorderScript 100%
Motus.Recorder.CodeEmit.ActionLineEmitter 79.6% 77.5%
Motus.Recorder.CodeEmit.CodeEmitter 83.3% 78.9%
Motus.Recorder.CodeEmit.EmittedLocator 100%
Motus.Recorder.CodeEmit.FrameworkTemplate 97.4% 83.3%
Motus.Recorder.PageAnalysis.DiscoveredElement 100%
Motus.Recorder.PageAnalysis.MemberNameDeriver 94.8% 90.7%
Motus.Recorder.PageAnalysis.PageAnalysisEngine 0.7% 0%
Motus.Recorder.PageAnalysis.PageClassNameDeriver 91.3% 88.8%
Motus.Recorder.PageAnalysis.PageElementInfo 100%
Motus.Recorder.PageAnalysis.SelectorStrategyOrdering 100% 100%
Motus.Recorder.PomEmit.FormGrouper 90.3% 49.3%
Motus.Recorder.PomEmit.PomEmitter 95.6% 86.3%
Motus.Recorder.Records.ActionRecord 100%
Motus.Recorder.Records.CheckAction 100%
Motus.Recorder.Records.ClickAction 100%
Motus.Recorder.Records.DialogAction 100%
Motus.Recorder.Records.FileUploadAction 100%
Motus.Recorder.Records.FillAction 100%
Motus.Recorder.Records.KeyPressAction 100%
Motus.Recorder.Records.NavigationAction 100%
Motus.Recorder.Records.ResolvedAction 100%
Motus.Recorder.Records.ScrollAction 100%
Motus.Recorder.Records.SelectAction 100%
Motus.Recorder.SelectorInference.SelectorInferenceEngine 100% 100%
Motus.Recorder.SelectorInference.SelectorInferenceResult 100%
Motus.Testing - 75.4%
Name Line Branch
Motus.Testing 75.4% 61.1%
Motus.Testing.BrowserFixture 88.6% 78.5%
Motus.Testing.FailureTracing 30.7% 0%
Motus.Testing.MSTest - 79%
Name Line Branch
Motus.Testing.MSTest 79% 79.4%
Motus.Testing.MSTest.MotusTestBase 70.4% 75%
Motus.Testing.MSTest.MotusTestClassAttribute 100% 100%
Motus.Testing.MSTest.MotusTestMethodAttribute 100% 87.5%
Motus.Testing.MSTest.v4 - 22.8%
Name Line Branch
Motus.Testing.MSTest.v4 22.8% 22.2%
Motus.Testing.MSTest.MotusTestBase 0% 0%
Motus.Testing.MSTest.MotusTestClassAttribute 100% 100%
Motus.Testing.MSTest.MotusTestMethodAttribute 100% 60%
Motus.Testing.xUnit - 63.6%
Name Line Branch
Motus.Testing.xUnit 63.6% 33.3%
Motus.Testing.xUnit.BrowserContextFixture 71.4% 33.3%
Motus.Testing.xUnit.BrowserContextFixture`1 100%
Motus.Testing.xUnit.DefaultBrowser 0%
Motus.Testing.xUnit.SharedBrowserFixture 25%

@TorreyBetts

Copy link
Copy Markdown
Contributor Author

Follow-up to the notes above: the first CI run failed only WebAppSuite_RunsWithItsOwnDependenciesAndFolder, because the fixture outside the solution was built as Debug in CI's Release build. 28eff8c adds both fixture projects to Motus.sln under tests, so they build in the solution's configuration and their own four tests now also run under dotnet test. CI is green on 28eff8c across all three .NET legs and both integration jobs.

The test host runs a suite on the Motus its project references, but it
named Motus types directly: the test report carried accessibility,
performance and coverage records, and the host called the sinks, the
test method name context and the lost browser checks. A suite on an older
published package lacks some of those, so on 1.0.10 the host crashed
reading its first message and no test ran, and on 1.0.11 to 1.0.14 every
test failed with a TypeLoadException. What a run collects now crosses the
pipe as JSON that the runner reads into its own types, and every call into
Motus goes through a guard that makes it from a method of its own and, when
the suite's Motus cannot satisfy it, says so once and goes on without it.
The visual runner's browser hooks are compiled up front for the same
reason. A suite on 1.0.1, the oldest published package, now runs with its
own results; a fixture pinned to it keeps that true.

An assembly whose test host could not start, load it or list its tests
was reported on standard error and skipped, and the run exited 0 when
nothing else failed, so a broken run looked green. It now fails the run.
The visual runner fails that assembly's tests and runs the others, where
it used to stop and leave them as they were.

A fresh test host reads each data source again, and a row that names
itself differently on each read (a Guid or a time in it) was then not
found. Each test case now has an identity apart from its name: its class,
its method with the parameter types, and for a data row, which data
source and which row of it. Runs ask for tests by it, a row the source no
longer returns fails saying so, and sharding sorts by it so that separate
shard processes agree. Overloads, which share a name, no longer collapse
into one test run twice.

Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
@TorreyBetts

Copy link
Copy Markdown
Contributor Author

58a4446 fixes the review findings.

Older Motus packages. The test host runs on the suite's own Motus. I ran the reviewer's suites and a 1.0.1 suite against the runner before the fix. These members of Motus.Runner are missing from older packages:

  • before 1.0.10: the sinks and TestMethodNameContext;
  • before 1.0.11: CoverageData and the other coverage types;
  • before 1.0.15: BrowserFailure.

Now:

  • TestCaseReport carries what a run collected as JSON, and the timeline entries the visual runner gets carry their metrics the same way. The runner reads both into its own types (TestCaseReportExtras, TimelineEntryMessage).
  • MotusInterop makes every call into Motus from its own non-inlined method, inside a guard. The calls are the sinks, the test method name, the lost-browser check, the config load and the visual hooks. When the suite's Motus lacks a member, the guard warns once per feature and skips it. The lost-browser check falls back to the exception type names. The visual hooks are JIT-compiled before they're installed, so a missing member turns the browser view off instead of failing a test.

Results:

Suite Plain tests Browser test Warnings
reviewer's 1.0.10 correct pass/fail fails with "Page is not initialized.", same as dotnet test on that package coverage, lost-browser detection
reviewer's 1.0.14 correct pass/fail same as above lost-browser detection
reviewer's 1.1.0 correct pass/fail same as above none
1.0.5 and 1.0.1 (plain tests only) all correct n/a every extra

The new tests/Fixtures/Motus.Fixtures.OldMotus.Tests is pinned to Motus.Testing.MSTest 1.0.1, the oldest published package, and is in the solution. Two tests run it:

  • SuiteOnTheOldestPublishedMotus_RunsWithItsOwnResults (Motus.Cli.Tests) runs it through motus run, with one test asked to fail.
  • SuiteOnTheOldestPublishedMotus_RunsInTheVisualRunner (Motus.Tests) runs it through the visual runner, hooks included.

Its own six tests also run under dotnet test. The package comes from nuget.org at restore and is fixed, so it doesn't make CI fragile.

Assemblies that fail to load now fail the run.

  • TestDiscovery.FailedAssemblies lists them, and motus run exits 1 with an error line naming them. The other assemblies still run.
  • The visual runner fails the broken assembly's tests and runs the rest. It used to return early on a startup or [AssemblyInitialize] failure, which left the other assemblies' tests untouched.
  • Tests: AssemblyThatCannotBeLoaded_FailsTheRun (exit code from the run command) and AssemblyThatCannotStart_FailsItsTests_AndTheOthersRun.

Stable test identity.

  • Each case has an id: {Class}.{Method}({parameter types}), plus #{source}:{row} for a data row.
  • The visual runner's fresh host looks tests up by id and falls back to the name. A row the source no longer returns fails with "it was row N of its data source, which now returns M row(s)".
  • motus run keeps one host for discovery and the whole run, so it never looks a test up again. Sharding did key on display names, though, so separate shard processes could split a nondeterministic data source differently. The shard key is now assembly path plus id, and docs/guides/sharding.md says so.
  • The same ids fix overloads: two methods with one name each run, instead of one of them twice.
  • Tests: DataRowsNamedDifferentlyEachRead_RunInAFreshTestHost, DataRowTheSourceNoLongerReturns_SaysSo, Shards_AreTheSameInEveryProcess_WhenRowNamesChange and OverloadedTests_EachRun.

Runs (macOS, net8.0 and net10.0):

  • Unit tests, CI filter: all pass. Motus.Tests 1269, Motus.Cli.Tests 340 (2 or 3 skipped), Mcp 368, Recorder 105, Codegen 59, Analyzers 29, MSTest.v4 12, xUnit 8, fixtures 4 and 6.
  • Under the browser lock: Motus.Tests.Runner (82) and the Cli runner and isolation classes (44) all pass.
  • motus run on the net8.0 samples: 48 passed, 0 failed, 3 ignored.

motus run made each reporter's per-test callbacks from whichever worker
finished the test, and the JUnit, TRX and HTML reporters append to plain
lists. On main, tests that finished one after another hid that; with the
test host, every test finishes on a pool thread after a pipe round trip,
and a quick suite under --workers auto lost most of its results from the
JUnit and TRX files. The runner now makes those callbacks one at a time.

The visual runner looked a test up by its id before its name. After a
rebuild that removed a [DataRow], the old row's id belonged to the row
that moved up, which then ran and reported under the removed row's name.
A test is now found by its name when exactly one case has it, by its id
otherwise, and a [DataRow], whose name never changes, is never taken from
its id under a different name; that case fails saying the rows changed.

Overloaded test methods without data rows now get their parameter types
in their names, so reporters and results can tell them apart.

Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
@TorreyBetts

Copy link
Copy Markdown
Contributor Author

79d08e3 fixes the re-review findings.

Reporter results lost under several workers (HIGH). TestRunner now makes every per-test reporter callback through one SemaphoreSlim(1). That covers test start and end, coverage collected (for both the coverage reporters and an ICoverageReporter test reporter), accessibility violations, and performance metrics. The run start and end callbacks were already sequential.

I checked the other per-test paths:

  • Coverage aggregation and the results list were already under a lock.
  • FlakeHistoryReporter locks its own state.
  • CompositeReporter fans out sequentially.
  • The visual runner runs one test at a time (MaxWorkers = 1), and so does its assembly-failure path.

ManyQuickTestsOnSeveralWorkers_AllReachTheFileReporters runs 60 async rows with 8 workers and counts the JUnit testcases and TRX results. With the gate commented out it fails (53 of 60 JUnit cases); with it, it passes. motus run on the samples writes all 48 testcases to JUnit.

Wrong data row after a rebuild (medium). InProcessTestHost.RunAsync now looks a test up by name first, when exactly one case has that name, and falls back to the id otherwise. A [DataRow] case (or a test without rows) has a name that never changes, so it is never taken from its id under a different name. That case fails with "its data rows have changed since the tests were listed. Load the tests again to run the current ones." Rows from other data sources, such as Guid-named [DynamicData] rows, still resolve by id. DataRowAfterARebuild_IsFoundByName_NotByItsOldPlace covers this. Before the fix, the moved-up row ran and passed under the removed row's name.

Overload names (low). Overloaded test methods without data rows get their parameter types in the name: Overloaded() and Overloaded(Int32). OverloadedTests_EachRun asserts it. The PR body now also notes that a non-test file matched by a shell glob fails the run.

Runs (macOS, net8.0 and net10.0):

  • Unit tests, CI filter: all pass. Motus.Tests 1272, Motus.Cli.Tests 401 (2 or 3 skipped), Mcp 368, Recorder 105, Codegen 59, Analyzers 29, MSTest.v4 12, xUnit 8, fixtures 4 and 6.
  • Under the browser lock: Motus.Tests.Runner (85) and the Cli runner and isolation classes (45) all pass.
  • motus run on the net8.0 samples: 48 passed, 0 failed.

@TorreyBetts
TorreyBetts added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 52efd5b Oct 25, 2026
12 of 14 checks passed
@TorreyBetts
TorreyBetts deleted the fix/runner-test-isolation branch October 25, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

motus run loads test assemblies into its own load context and base directory, so a suite that hosts an ASP.NET Core app can't run

1 participant