Skip to content

Add --list-tests json for machine-readable test discovery output#8280

Open
Evangelink wants to merge 4 commits into
mainfrom
dev/amauryleve/list-tests-json
Open

Add --list-tests json for machine-readable test discovery output#8280
Evangelink wants to merge 4 commits into
mainfrom
dev/amauryleve/list-tests-json

Conversation

@Evangelink
Copy link
Copy Markdown
Member

Adds an optional json argument to the existing --list-tests flag so MTP can emit a single JSON document on stdout describing every discovered test. The text output and zero-arg behavior are unchanged.

Fixes #3221.
Related: dotnet/sdk#49754.

CLI surface

Command Behavior
--list-tests Unchanged (human-readable text)
--list-tests text Explicit alias for the default
--list-tests json JSON document on stdout (banner / progress / summary / per-test text suppressed; errors routed to stderr)
--list-tests <anything-else> Fails with ExitCode.InvalidCommandLine and a descriptive validation error

Schema v1

Flat array; absent fields are omitted (no null), so frameworks that don't populate TestMethodIdentifierProperty aren't broken.

{
  ""schemaVersion"": 1,
  ""tests"": [
    {
      ""uid"": ""..."",
      ""displayName"": ""..."",
      ""type"": {
        ""assemblyFullName"": ""..."",
        ""namespace"": ""..."",                  // omitted for global namespace
        ""typeName"": ""MyClass+Nested`1"",      // metadata format with arity & nesting
        ""methodName"": ""MyMethod"",
        ""methodArity"": 0,
        ""returnTypeFullName"": ""..."",
        ""parameterTypeFullNames"": [""..."", ...]
      },
      ""location"": { ""file"": ""..."", ""lineStart"": 12, ""lineEnd"": 18 },
      ""traits"":     [ { ""key"": ""Category"", ""value"": ""Integration"" }, ... ],
      ""properties"": [ { ""key"": ""ExecutionId"", ""value"": ""..."" }, ... ]
    }
  ]
}

traits and properties are both arrays of { key, value } (not JSON objects) so duplicate keys survive serialization and order is stable across runs.

Implementation notes

  • New DiscoveredTestsJsonSerializer + tiny JsonStringWriter — no dependency on System.Text.Json (not available across all targets) or Jsonite (#if !NETCOREAPP only), so the same code path runs on netstandard2.0, net462, net8.0, net9.0.
  • Always-LF line endings for stable CI golden output, U+2028/U+2029 escaped, lone surrogates replaced with U+FFFD (mirrors Utf8JsonWriter defaults).
  • TerminalOutputDevice detects JSON mode, suppresses banner / progress / per-test text / free-form output on stdout, buffers DiscoveredTestNodeStateProperty events from ConsumeAsync, and emits the JSON document once at session end from the test-host process only (controller does not double-emit).
  • Ctrl+C in JSON mode writes the cancellation message to stderr instead of corrupting the JSON document.
  • PlatformCommandLineProvider flips --list-tests arity from Zero to ZeroOrOne, validates the argument against json/text (case-insensitive), and exposes IsListTestsJsonOutput(...).

Tests

Suite Result
Microsoft.Testing.Platform.UnitTests (full, net462 / net8 / net9) 2,277 / 2,277 (159 in new/touched test classes)
MSTest.Acceptance.IntegrationTests.TestDiscoveryTests (incl. new JSON tests × 2 TFMs) 12 / 12
MSTest.Acceptance.IntegrationTests.HelpInfoTests 6 / 6
Microsoft.Testing.Platform.Acceptance.IntegrationTests.HelpInfoTests* 30 / 30
Microsoft.Testing.Platform.Acceptance.IntegrationTests.ExecutionTests 27 / 27
Microsoft.Testing.Platform.Acceptance.IntegrationTests.TrxTests 28 / 28
.\build.cmd -pack -c Debug (warnings-as-errors) succeeded, 0 warnings, 0 errors

Notes

  • No public API surface added (option key is internal CLI only).
  • PlatformResources.resx updated; all 13 XLFs regenerated via dotnet msbuild … /t:UpdateXlf.
  • No changelog entry — docs/Changelog-Platform.md is curated at release time, and v2.2.3 was just cut without an Unreleased section.

The change went through two rounds of internal expert review before opening. Highlights addressed in review:

  • Errors no longer silently swallowed in JSON mode (routed to stderr).
  • Ctrl+C no longer corrupts the JSON document (cancellation message goes to stderr).
  • PropertyBag's reverse-insertion-order traversal is reversed back to insertion order.
  • properties is an array (not object) so duplicate keys are preserved.
  • type.namespace omitted for the global namespace.
  • --list-tests text accepted as explicit alias of the default for forward compatibility.

Adds an optional json argument to the existing --list-tests flag so MTP can
emit a single JSON document on stdout describing every discovered test. The text
output and zero-arg behavior are unchanged.

CLI surface:
  --list-tests          (text, unchanged)
  --list-tests text     (explicit alias for the default)
  --list-tests json     (JSON document on stdout, banner/progress/summary/per-test
                        text suppressed; errors routed to stderr)

Schema v1 (omits absent fields, no nulls):
  { schemaVersion, tests[{ uid, displayName,
                           type{ assemblyFullName, namespace?, typeName,
                                 methodName, methodArity, returnTypeFullName,
                                 parameterTypeFullNames[] },
                           location{ file, lineStart, lineEnd },
                           traits[{key, value}],
                           properties[{key, value}] }] }

Implementation notes:
- `DiscoveredTestsJsonSerializer` + small `JsonStringWriter` (no
  `System.Text.Json`/`Jsonite` dependency, so the same code runs on
  netstandard2.0, net462, net8.0, net9.0). LF line endings; escapes U+2028 /
  U+2029 and replaces lone surrogates with U+FFFD, matching
  `Utf8JsonWriter`'s default behavior.
- `TerminalOutputDevice` detects JSON mode, suppresses banner/progress/per-test
  text/free-form data on stdout, buffers discovered TestNodes from
  `ConsumeAsync`, and emits the JSON document once at session end from the
  test-host process only. Ctrl+C in JSON mode writes a single cancellation line
  to stderr instead of corrupting the JSON document.
- `properties` and `traits` are arrays of `{key, value}` so duplicates
  survive serialization and order is stable.

Tests:
- Unit: `DiscoveredTestsJsonSerializerTests` (11) covering empty / minimal /
  type metadata / global namespace / file location / traits ordering /
  properties array / duplicate property keys / special-character escaping /
  lone-surrogate / valid surrogate pair / U+2028 U+2029.
- Unit: `PlatformCommandLineProviderTests` (4 new) for arity validation,
  accepted values (text/json case-insensitive), invalid-value message, and the
  `IsListTestsJsonOutput` helper.
- Acceptance: `MSTest.Acceptance.IntegrationTests.TestDiscoveryTests` (2 new ×
  2 TFMs) parses the JSON document end-to-end (every v1 field for Test1),
  asserts stderr is empty on success, and asserts invalid value fails with
  `ExitCode.InvalidCommandLine`.
- Updated `HelpInfoTests` (x2) and `HelpInfoAllExtensionsTests` wildcard
  expectations for the new arity (0..1) and description.

Resources updated and all 13 XLFs regenerated via `/t:UpdateXlf`.

No public API surface added.

Fixes #3221.
Related: dotnet/sdk#49754.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 16, 2026 09:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds machine-readable JSON output for --list-tests while preserving the existing text behavior.

Changes:

  • Adds json/text argument handling and validation for --list-tests.
  • Adds JSON discovery serialization and suppresses normal terminal output in JSON mode.
  • Adds unit and acceptance coverage plus updated help/resource localization files.
Show a summary per file
File Description
src/Platform/Microsoft.Testing.Platform/CommandLine/PlatformCommandLineProvider.cs Allows optional --list-tests output format and validates accepted values.
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs Buffers discovered tests and emits JSON-only stdout in JSON mode.
src/Platform/Microsoft.Testing.Platform/OutputDevice/DiscoveredTestsJsonSerializer.cs Serializes discovered test nodes into schema v1 JSON.
src/Platform/Microsoft.Testing.Platform/OutputDevice/JsonStringWriter.cs Adds a small JSON writer for cross-target serialization.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Updates CLI help text and invalid argument resource.
src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.cs Exposes the new validation resource to unit tests.
src/Platform/Microsoft.Testing.Platform/Resources/xlf/* Regenerates localized resource entries.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/CommandLine/PlatformCommandLineProviderTests.cs Covers list-tests argument validation and JSON mode detection.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/DiscoveredTestsJsonSerializerTests.cs Covers JSON schema, escaping, ordering, and optional fields.
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryTests.cs Adds acceptance coverage for JSON discovery output and invalid format.
test/IntegrationTests/*/HelpInfo*.cs Updates help/info expectations for the new option description and arity.

Copilot's findings

  • Files reviewed: 25/25 changed files
  • Comments generated: 1

Comment thread src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

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

Review Summary — PR #8280: --list-tests json

Overall this is a well-structured, well-tested feature. The custom JsonStringWriter, cross-TFM compatibility, lone-surrogate handling, banner/progress suppression, and CLI validation are all cleanly implemented. Tests cover the happy path, special character escaping, and invalid-argument rejection.

Findings

# Dimension Severity File Finding
1 Threading/Hot-reload MODERATE TerminalOutputDevice.cs:379 DisplayAfterHotReloadSessionEndAsync calls DisplayAfterSessionEndRunInternalAsync unconditionally; in a dotnet watch --list-tests json session the JSON document is emitted after every cycle and _discoveredTestsForJson is never cleared — multiple growing JSON blobs on stdout.
2 Testability/Architecture NIT TerminalOutputDevice.cs:420 Console.Error.WriteLineAsync is used directly (also in the abort callback in InitializeAsync) bypassing the injected _console. This makes error/stderr output in JSON mode un-redirectable in tests. The comment acknowledges the limitation; worth tracking.

Clean dimensions

  • Algorithmic Correctness — JSON writer logic, escaping, surrogate pair handling all correct.
  • Threading_discoveredTestsForJson write path is guarded by MTP's single-consumer-per-pump invariant; the emission path uses _asyncMonitor. Correct.
  • Security — No path traversal, no serialization of untrusted schemas, no sensitive leakage.
  • Public API — No new public API surface; IsListTestsJsonOutput is internal static. PublicAPI.Unshipped.txt not dirtied.
  • Cross-TFM — Custom JsonStringWriter avoids both System.Text.Json and Jsonite; no #if-guarded APIs needed.
  • PerformanceStringBuilder-based writer; no hot-path LINQ; PropertyBag.OfType<T> returns an array, Array.Reverse is in-place. Reasonable.
  • CLI validation — Argument validated in ValidateOptionArgumentsAsync; ZeroOrOne arity correct; case-insensitive comparison correct; resource string properly parametrised.
  • Localization — New strings in .resx; XLFs regenerated via MSBuild, not hand-edited.
  • Test quality — Unit tests cover empty, minimal, full, special-char, surrogate, and validation cases. Integration tests cover JSON output schema, invalid arg, and unchanged text mode.
  • Assertion style — Uses MSTest conventions consistently throughout.
  • Scope discipline — PR is tightly scoped to the new --list-tests json flag; no unrelated refactoring.

Generated by Expert Code Review (on open) for issue #8280 · ● 19.8M

Comment thread src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs Outdated
- Restore ExtensionResources.pl.xlf to match the updated resx (lost during
  rebase earlier; was the cause of the CI XLF out-of-date failure for the TRX
  extension).
- Help text now documents both `text` and `json` arguments (review comment
  by Copilot on PlatformResources.resx:389).
- Hot-reload: `DisplayAfterHotReloadSessionEndAsync` now early-returns in JSON
  mode so `dotnet watch --list-tests json` doesn't emit multiple
  ever-growing JSON documents (review comment by Evangelink, MODERATE).
- Extract `WriteToStandardErrorAsync` helper so the single `Console.Error`
  bypass of `IConsole` is centralized and easy to find when `IConsole`
  eventually gains stderr support (review comment by Evangelink, NIT).
- Update help/info acceptance test expectations to match the new description.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Copy link
Copy Markdown
Member Author

Pushed 884c759 to address review comments and unblock CI:

  • Hot-reload double-emission (Evangelink, MODERATE)DisplayAfterHotReloadSessionEndAsync now early-returns in JSON mode so dotnet watch --list-tests json doesn't emit multiple ever-growing JSON documents.
  • Console.Error bypass (Evangelink, NIT) — both call sites now route through a single WriteToStandardErrorAsync helper with a comment explaining it's the sole place that bypasses IConsole until IConsole gains stderr support.
  • Help text (Copilot) — the description now mentions both text and json.
  • CI XLF failureExtensionResources.pl.xlf (TRX) was reverted to its pre-Support placeholders in --report-trx-filename #8223 source during my earlier rebase (I discarded it thinking it was unrelated drift; it was actually a translation update that had to stay). Restored from origin/main.

Help/info acceptance tests updated for the new description; XLFs regenerated; .\build.cmd -pack -c Debug passes with 0 warnings, 0 errors; unit tests 159/159 in the touched classes.

…llName

MSTest's VSTestBridge (MSTestBridgedTestFramework.cs:105) intentionally emits
`assemblyFullName` and `returnTypeFullName` as empty strings, with a TODO
to populate them in the future. The acceptance test was asserting that these
fields are non-empty, which broke on every TFM as soon as the test actually ran
in CI.

Weaken the assertion to verify presence (`ValueKind == String`) rather than
non-emptiness, matching today's adapter behavior. The other field assertions
(`typeName`, `methodName`, `methodArity`, `parameterTypeFullNames`)
still pin their concrete values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 16, 2026 11:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new

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.

Provide extra options to --list-tests

3 participants