Skip to content

fix: re-enable RPC tests and modernize harness (#5540)#5632

Merged
thomhurst merged 1 commit intomainfrom
fix/rpc-tests-issue-5540
Apr 19, 2026
Merged

fix: re-enable RPC tests and modernize harness (#5540)#5632
thomhurst merged 1 commit intomainfrom
fix/rpc-tests-issue-5540

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Root cause fix: RunTestsRequest was serializing the node filter as testCases, but Microsoft.Testing.Platform reads it from tests — so the server silently ignored the filter and ran the full catalogue on every RunTests call. Renamed the JSON property to match the protocol wire format.
  • Pipeline TFM: RunRpcTestsModule targeted net8.0 while TUnit.RpcTests.csproj only targets net10.0, leaving the suite skipped. Aligned the testable framework list to net10.0.
  • Modernization: extracted TestHostSession (IAsyncDisposable) and TestProjectBuilds (per-framework shared build cache), parameterized the three scenarios across net8.0/net10.0 via MethodDataSource, removed dead abstractions (IProcessHandle/ProcessHandle, custom LogLevel enum, ConsoleRpcListener, RunRequest), switched to Microsoft.Extensions.Logging.LogLevel with a string-enum converter, and tightened TestNode deserialization via JsonExtensionData.

Closes #5540.

Test plan

  • Discovery_ReturnsFullTestCatalogue passes on net8.0 and net10.0
  • RunTests_WithUidFilter_ExecutesOnlySelectedTests passes on net8.0 and net10.0 — confirms server honors the filter
  • RunTests_WithSkippedTest_ReportsSkippedState passes on net8.0 and net10.0
  • Full local run: 6/6 green in ~2m22s
  • dotnet build TUnit.RpcTests clean (0 warnings, 0 errors)

Restore the previously-disabled RPC test suite and fix the underlying
failures. The MTP server was silently ignoring the client's test filter
because the RunTestsRequest wire field was serialized as `testCases`, but
Microsoft.Testing.Platform reads it from `tests`. This caused every
`RunTests` call to execute the full catalogue instead of the selected
nodes. Renamed the JSON property to match the protocol.

Pipeline was also targeting `net8.0` while the csproj only targets
`net10.0`, so the suite never ran. Aligned the testable framework list.

While re-enabling, modernized the harness: extracted TestHostSession
(IAsyncDisposable) and TestProjectBuilds (shared per-framework build
cache), parameterized tests across net8.0/net10.0 via MethodDataSource,
dropped dead abstractions (IProcessHandle/ProcessHandle, custom LogLevel,
ConsoleRpcListener, RunRequest), and switched to
Microsoft.Extensions.Logging.LogLevel with a string-enum converter.
@codacy-production
Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
ErrorProne 1 medium

View in Codacy

🟢 Metrics 9 complexity

Metric Results
Complexity 9

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR is a solid fix — the root cause ("testCases""tests" wire-format mismatch) is clear and correct, and the refactoring around it is well-motivated.

Core Fix ✅

RunTestsRequest.cs — The JSON property rename from testCases to tests is the crux of the fix. The protocol was silently ignoring the filter because the server read a field the client wasn't sending under that name. Correct.

RunRpcTestsModule.cs — Re-enabling net10.0 to match the project target is correct. No concerns here.


Architecture: TestHostSession

Extracting session lifecycle into an IAsyncDisposable is the right abstraction. The private constructor + static factory (StartAsync) pattern is idiomatic for types that require async initialization. Good.

One issue in DisposeAsync: Client.Dispose() almost certainly disposes _tcpClient internally (since TestingPlatformClient still holds a reference to it), and then _tcpClient.Dispose() is called a second time. TcpClient tolerates double-dispose, but it is a code smell. Consider having TestingPlatformClient.Dispose() only dispose the JsonRpc/stream, and let TestHostSession own _tcpClient disposal exclusively. Similarly, _rpc.Dispose() is called after Client.Dispose() — if TestingPlatformClient disposes the JsonRpc it receives, that is another double-dispose.


TestProjectBuilds.EnsureBuiltAsync — Stale-failure risk

public static Task EnsureBuiltAsync(string framework, CancellationToken cancellationToken)
    => _builds.GetOrAdd(framework, tfm => BuildAsync(tfm, cancellationToken));

If the first caller is cancelled (or the build fails transiently), the faulted/cancelled Task is permanently cached in _builds. Every subsequent caller for that TFM will immediately receive the same bad task instead of retrying. In a flaky CI environment this could permanently poison the test run. A safer pattern:

public static Task EnsureBuiltAsync(string framework, CancellationToken cancellationToken)
{
    if (_builds.TryGetValue(framework, out var existing) && !existing.IsFaulted && !existing.IsCanceled)
        return existing;
    var fresh = BuildAsync(framework, cancellationToken);
    _builds[framework] = fresh;
    return fresh;
}

There is also a subtle race: GetOrAdd is not atomic — two threads may both call BuildAsync for the same TFM simultaneously. For a test helper this is probably harmless (both builds succeed), but worth noting.


Frameworks() Return Type

public static IEnumerable<Func<string>> Frameworks()
{
    yield return () => "net8.0";
    yield return () => "net10.0";
}

The test methods accept string framework, not Func<string>. If TUnit's MethodDataSource transparently invokes the Func<T> wrapper to materialize the argument, this works — but it is not obvious from reading the code why wrapping in Func<T> is needed here. If TUnit supports IEnumerable<string> directly from a MethodDataSource, prefer that for clarity. If the Func<string> wrapper is load-bearing (e.g., deferred evaluation), a comment explaining why would help future readers.


ExitAsync Silent Swallow

catch
{
    // Best effort — connection may already be gone
}

Swallowing all exceptions unconditionally can hide bugs during development. Since this is called in DisposeAsync, consider at least logging to Console.Error or Trace so it is visible in test output without being fatal.


TestNode.JsonExtensionData

Using [JsonExtensionData] to round-trip unknown fields back to the server when constructing RunTestsRequest is exactly right. This avoids the common trap of stripping server-generated fields (traits, location info, etc.) that the server expects to see back on runTests.


Dead Code Removal ✅

Removing IProcessHandle/ProcessHandle (which had a NotImplementedException in StopAsync and no-ops everywhere else), ConsoleRpcListener, the duplicate LogLevel enum, and RunRequest are all correct.


Minor: Magic number

await Assert.That(discovered).Count().IsGreaterThanOrEqualTo(4000);

This will fail if the test project's test count drops below 4000. Consider a named constant or a comment explaining where this threshold comes from.


Summary: The fix is correct and the refactoring is a clear improvement. The two issues worth addressing before merge are the stale-task caching in TestProjectBuilds (can permanently break a CI run after a transient failure) and the potential double-dispose in TestHostSession. Everything else is minor.

@thomhurst thomhurst merged commit 1237ee3 into main Apr 19, 2026
14 of 15 checks passed
@thomhurst thomhurst deleted the fix/rpc-tests-issue-5540 branch April 19, 2026 19:35
This was referenced Apr 20, 2026
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 21, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.35.2 to
1.37.10.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.37.10

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.10 -->

## What's Changed
### Other Changes
* docs(test-filters): add migration callout for --filter →
--treenode-filter by @​johnkattenhorn in
thomhurst/TUnit#5628
* fix: re-enable RPC tests and modernize harness (#​5540) by @​thomhurst
in thomhurst/TUnit#5632
* fix(mocks): propagate [Obsolete] and null-forgiving raise dispatch
(#​5626) by @​JohnVerheij in
thomhurst/TUnit#5631
* ci: use setup-dotnet built-in NuGet cache by @​thomhurst in
thomhurst/TUnit#5635
* feat(playwright): propagate W3C trace context into browser contexts by
@​thomhurst in thomhurst/TUnit#5636
### Dependencies
* chore(deps): update tunit to 1.37.0 by @​thomhurst in
thomhurst/TUnit#5625

## New Contributors
* @​johnkattenhorn made their first contribution in
thomhurst/TUnit#5628
* @​JohnVerheij made their first contribution in
thomhurst/TUnit#5631

**Full Changelog**:
thomhurst/TUnit@v1.37.0...v1.37.10

## 1.37.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.0 -->

## What's Changed
### Other Changes
* fix: stabilize flaky tests across analyzer, OTel, and engine suites by
@​thomhurst in thomhurst/TUnit#5609
* perf: engine hot-path allocation wins (#​5528 B) by @​thomhurst in
thomhurst/TUnit#5610
* feat(analyzers): detect collection IsEqualTo reference equality
(TUnitAssertions0016) by @​thomhurst in
thomhurst/TUnit#5615
* perf: consolidate test dedup + hook register guards (#​5528 A) by
@​thomhurst in thomhurst/TUnit#5612
* perf: engine discovery/init path cleanup (#​5528 C) by @​thomhurst in
thomhurst/TUnit#5611
* fix(assertions): render collection contents in IsEqualTo failure
messages (#​5613 B) by @​thomhurst in
thomhurst/TUnit#5619
* feat(analyzers): code-fix for TUnit0015 to insert CancellationToken
(#​5613 D) by @​thomhurst in
thomhurst/TUnit#5621
* fix(assertions): add Task reference forwarders on
AsyncDelegateAssertion by @​thomhurst in
thomhurst/TUnit#5618
* test(asp-net): fix race in FactoryMethodOrderTests by @​thomhurst in
thomhurst/TUnit#5623
* feat(analyzers): code-fix for TUnit0049 to insert [MatrixDataSource]
(#​5613 C) by @​thomhurst in
thomhurst/TUnit#5620
* fix(pipeline): isolate AOT publish outputs to stop clobbering pack
DLLs (#​5622) by @​thomhurst in
thomhurst/TUnit#5624
### Dependencies
* chore(deps): update tunit to 1.36.0 by @​thomhurst in
thomhurst/TUnit#5608
* chore(deps): update modularpipelines to 3.2.8 by @​thomhurst in
thomhurst/TUnit#5614


**Full Changelog**:
thomhurst/TUnit@v1.36.0...v1.37.0

## 1.36.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.36.0 -->

## What's Changed
### Other Changes
* fix: don't render test's own trace as Linked Trace in HTML report by
@​thomhurst in thomhurst/TUnit#5580
* fix(docs): benchmark index links 404 by @​thomhurst in
thomhurst/TUnit#5587
* docs: replace repeated benchmark link suffix with per-test
descriptions by @​thomhurst in
thomhurst/TUnit#5588
* docs: clearer distributed tracing setup and troubleshooting by
@​thomhurst in thomhurst/TUnit#5597
* fix: auto-suppress ExecutionContext flow for hosted services (#​5589)
by @​thomhurst in thomhurst/TUnit#5598
* feat: auto-align DistributedContextPropagator to W3C by @​thomhurst in
thomhurst/TUnit#5599
* feat: TUnit0064 analyzer + code fix for direct WebApplicationFactory
inheritance by @​thomhurst in
thomhurst/TUnit#5601
* feat: auto-propagate test trace context through IHttpClientFactory by
@​thomhurst in thomhurst/TUnit#5603
* feat: TUnit.OpenTelemetry zero-config tracing package by @​thomhurst
in thomhurst/TUnit#5602
* fix: restore [Obsolete] members removed in v1.27 (#​5539) by
@​thomhurst in thomhurst/TUnit#5605
* feat: generalize OTLP receiver for use outside TUnit.Aspire by
@​thomhurst in thomhurst/TUnit#5606
* feat: auto-configure OpenTelemetry in TestWebApplicationFactory SUT by
@​thomhurst in thomhurst/TUnit#5607
### Dependencies
* chore(deps): update tunit to 1.35.2 by @​thomhurst in
thomhurst/TUnit#5581
* chore(deps): update dependency typescript to ~6.0.3 by @​thomhurst in
thomhurst/TUnit#5582
* chore(deps): update dependency coverlet.collector to v10 by
@​thomhurst in thomhurst/TUnit#5600


**Full Changelog**:
thomhurst/TUnit@v1.35.2...v1.36.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.35.2...v1.37.10).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.35.2&new-version=1.37.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Apr 21, 2026
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.

Fix and re-enable RPC tests in CI pipeline

1 participant