Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink
, '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

Read CLI options from testconfig.json via IConfiguration (#6349) - #8664

Merged
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c
Jun 3, 2026
Merged

Read CLI options from testconfig.json via IConfiguration (#6349)#8664
Amaury Levé (Evangelink) merged 8 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/unify-config-option-c

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Resolves part of #6349: let users specify CLI options inside testconfig.json by routing ICommandLineOptions through IConfiguration.

Approach

  1. New CommandLineConfigurationSource (Order = 0) wraps the parsed CLI into an IConfigurationProvider that flattens each option under commandLineOptions:<name> (zero-arity) or commandLineOptions:<name>:<index> (arg-bearing).
  2. The CLI source is registered in TestHostBuilder.CommonServicesbefore the JSON source, so CLI keeps highest precedence.
  3. AggregatedConfiguration.TryGetCommandLineOptionFromProviders does a provider-aware lookup: it walks providers in registration order, and the first provider with any data for that option wins outright (no per-key cross-provider merging). This avoids cases where IConfiguration[key] accidentally returns the JSON :0 while the CLI set the zero-arity bare key, or vice versa.
  4. The public extension helpers (IsCommandLineOptionSet, TryGetCommandLineOptionArguments) delegate to that method when given an AggregatedConfiguration, and CommandLineHandler becomes a thin facade that does the same. End result: every consumer of ICommandLineOptions transparently sees JSON-sourced options.
  5. JsonConfigurationProvider exposes a typed, schema-validated enumeration of commandLineOptions:* entries. CommandLineOptionsValidator runs three extra passes over those entries (unknown-option, arity, per-arg validation) so testconfig.json typos surface during startup instead of crashing inside option handlers.
  6. DisplayBannerIfEnabledAsync reads --no-banner from the unified ICommandLineOptions, so banner suppression honors testconfig.json as well as the CLI.

What works

  • testconfig.json keys under commandLineOptions are surfaced everywhere ICommandLineOptions is consumed.
  • CLI still wins over JSON for the same option.
  • --results-directory defined in JSON is honored (was previously ignored - the legacy GetResultsDirectoryCore path was bypassing JSON).
  • --no-banner defined in JSON suppresses the banner.
  • Invalid JSON entries (unknown option, wrong arity, bad arg) fail at startup with a clear error referencing testconfig.json, instead of crashing later. Concrete cases now caught:
    • --timeout: true (was: IndexOutOfRangeException in args[0])
    • --exit-on-process-exit: "abc" (was: FormatException in int.Parse(args[0]))
    • typos like "timeoutt": "30s" (was: silently ignored)
  • Back-compat ctor on CommandLineHandler is kept so external code keeps compiling.

Rough edges still on the table

These are not addressed in this PR:

  1. Bootstrap readers.--diagnostic* and --config-file are read off parseResultbeforeIConfiguration is built. Honoring them from JSON requires either a two-phase config build or rewriting those bootstrap paths.
  2. Scalar bool ambiguity. For arg-bearing options, JSON "true" / "false" is ambiguous with the zero-arity presence marker. Callers must use the array form ("foo": ["true"]) for non-boolean string values that happen to be "true" / "false". No code enforces this today.
  3. API visibility. New helpers are kept internal for now. If we ship this we need to decide whether they become public surface.

Tests

  • CommandLineConfigurationProviderTests covers the source/provider flattening + the provider-aware invariants (including ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end).
  • CommandLineConfigurationExtensionsTests covers the helper-level and handler-end-to-end behavior.
  • JsonCommandLineOptionsTests (new) covers the JSON enumeration schema, the three new validator passes, disabled entries, shadowing, and case-insensitivity.
  • Full UT project green on net8.0 / net9.0 / net462.

…rosoft#6349)
Introduces a CLI-backed IConfigurationSource (Order=0) so values parsed from the command line, env vars, and testconfig.json all flow through the same IConfiguration. CommandLineHandler becomes a facade over IConfiguration when one is supplied so every existing ICommandLineOptions consumer transparently sees JSON-sourced options.
This is a prototype to highlight rough edges:
- CommandLineOptionsValidator still walks parseResult.Options only (arity / per-option / unknown-option detection skips JSON-only options); TODOs added in place.
- Bootstrap-time readers (TestApplication diagnostic plumbing, --config-file, --no-banner) read parseResult before IConfiguration is built.
- IConfiguration only exposes string?; multi-value reads walk indexed keys (commandLineOptions:<name>:<index>) which the JSON parser already produces for arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cation
Review-round fixes:
- Add AggregatedConfiguration.TryGetCommandLineOptionFromProviders that resolves command-line options at the option granularity by walking providers in registration order. The first provider with any data for the option wins outright, so a CLI zero-arity flag is no longer silently merged with JSON indexed arguments.
- Route ConfigurationExtensions.IsCommandLineOptionSet / TryGetCommandLineOptionArguments through the new provider-aware path when the IConfiguration is an AggregatedConfiguration; keep the merged-view fallback for test mocks.
- Update AggregatedConfiguration.GetResultsDirectoryCore to consult the unified command-line view first so JSON-supplied results-directory is honored.
- Strengthen tests: rewrite the precedence test to use a shared storage key (a JSON array) so a flipped Order would actually fail it; add ProviderAwareResolution_CliZeroArityShadowsJsonIndexedArgs end-to-end test that locks in the new behavior through the CommandLineHandler facade.
- Expand validator TODOs with the concrete runtime crash sites surfaced during review (timeout args[0], exit-on-process-exit int.Parse).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…directory
Review-round-2 fixes:
- AggregatedConfiguration.GetResultsDirectoryCore now checks for the
CommandLineConfigurationProvider before going through the unified path.
Without it (legacy hand-built AggregatedConfiguration), the parseResult
fallback runs first so a custom provider cannot silently demote CLI
precedence for --results-directory.
- Document the provider contract for the commandLineOptions section directly
on TryGetCommandLineOptionFromProviders: TryGet returning true with a null
value is treated as absent at this provider, and indexed entries must be
contiguous from :0.
- Add focused in-memory provider tests that lock the provider-aware
invariants without requiring a JSON file:
* FirstProviderWithDataShadowsLaterProvidersForSameOption
* ExplicitDisableAtFirstProviderShortCircuits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 28, 2026 14:57

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 prototype routes command-line options through the platform IConfiguration model so testconfig.json can provide values consumed by existing ICommandLineOptions users, while preserving CLI precedence.

Changes:

  • Adds a CLI-backed configuration source/provider under commandLineOptions:*.
  • Adds provider-aware command-line option lookup helpers on AggregatedConfiguration/ConfigurationExtensions.
  • Wires CommandLineHandler to use the unified configuration view and adds unit coverage for precedence and lookup behavior.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.csRegisters the CLI configuration source and passes built configuration into command-line handling.
src/Platform/Microsoft.Testing.Platform/Configurations/PlatformConfigurationConstants.csAdds the commandLineOptions section constant.
src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.csAdds unified command-line option lookup helpers.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationSource.csAdds the configuration source for parsed CLI options.
src/Platform/Microsoft.Testing.Platform/Configurations/CommandLineConfigurationProvider.csFlattens parsed CLI options into configuration keys.
src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.csAdds provider-aware option resolution and uses it for results-directory.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.csDocuments current validation gaps for JSON-sourced options.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineManager.csPasses configuration into CommandLineHandler.
src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineHandler.csDelegates option reads to configuration when available.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationProviderTests.csAdds tests for CLI provider flattening and precedence behavior.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/CommandLineConfigurationExtensionsTests.csAdds tests for helper and handler behavior over configuration-backed options.

Copilot's findings

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

@EvangelinkAmaury Levé (Evangelink) changed the title [Prototype] Option C: read CLI options from testconfig.json via IConfiguration (#6349)Read CLI options from testconfig.json via IConfiguration (#6349)Jun 2, 2026
CopilotAI review requested due to automatic review settings June 2, 2026 21:45
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from 1eaacbf to dad27c1CompareJune 2, 2026 21:45

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.

Copilot's findings

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Round-2 review summary

All four round-1 fixes are correctly applied and the build is clean (0 warnings, 0 errors; 1034 pass / 0 fail). Two minor correctness observations and three test-hygiene NITs left inline. Nothing blocking.

Fix validation

FixStatus
Case-insensitive duplicate detection (3 dicts + ToDictionary)✅ Applied correctly. One remaining edge case at validator.cs:62 — see inline.
FormatException catch around EnumerateJsonCommandLineOptions✅ Mirrors the standard failure path, no double-banner risk. One asymmetry re HasTool — see inline.
[DoesNotReturn] on the two throw helpers✅ Both helpers carry the attribute.
Rewritten XML doc on TryGetCommandLineOptionArguments✅ No more stale "validation on parseResult only" wording.

New test adequacy

TestVerdict
EnumerateCommandLineOptions_SectionNameCaseInsensitive_Honored✅ Strong assertions (option name + argument value).
Validator_EmptyOptionNameInJson_FailsWithJsonPrefix⚠️ Only checks "testconfig.json" substring — wouldn't catch a resource string change. Advisory.
Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully⚠️ Test comment misrepresents pre-fix behavior; assertion only checks IsValid == false. See inline.
Validator_JsonSparseIndexedEntry_DefensiveSchemaRejection⚠️ Test name doesn't match what's tested. See inline.

Public API hygiene

PublicAPI.Unshipped.txt only has #nullable enable — all new types (JsonCommandLineOptionEntry, CommandLineConfigurationSource, CommandLineConfigurationProvider) and methods (EnumerateJsonCommandLineOptions, TryGetCommandLineOptionArguments, IsCommandLineOptionSet) are correctly internal.
JsonCommandLineOptionEntry uses plain get; properties — no init accessors on any new API.
✅ New internalCommandLineHandler constructor accepting IConfiguration? preserves source/binary compatibility (the existing public constructor delegates with configuration: null).

Summary table

#DimensionVerdict
1Algorithmic Correctness🟡 2 LOW (system+system dictionary, FormatException + HasTool)
13Test Completeness & Coverage⚪ 1 NIT (weaker substring assertions on 3 new tests)
16Naming & Conventions⚪ 1 NIT (Validator_JsonSparseIndexedEntry_* name)
17Documentation Accuracy⚪ 1 NIT (test comment misrepresents pre-fix behavior)

✅ 17/21 dimensions clean.

Threading & Concurrency, Security & IPC, Public API & Binary Compatibility, Performance & Allocations, Cross-TFM Compatibility, Resource & IDisposable, Defensive Coding at Boundaries, Localization, Test Isolation, Assertion Quality, Flakiness Patterns, Data-Driven Coverage, Code Structure, Analyzer Quality (N/A), IPC Wire Compatibility (N/A), Build Infrastructure, Scope Discipline — all clean.

Noted for follow-up (out-of-scope for this PR)

  • Env-var injection of commandLineOptions:* keys: now that IsCommandLineOptionSet/TryGetCommandLineOptionArguments route through IConfiguration, an env var spelled commandLineOptions__timeout=30s (with __: normalization in EnvironmentVariablesConfigurationProvider) will silently shadow JSON values and be invisible to CommandLineOptionsValidator (which only sees CLI + JSON entries). This is an existing platform concern that the unification only exposes — not introduced here — but worth tracking.
  • Behavioral break for extension authors who intentionally registered case-differing option names (e.g. "Timeout" for one provider, "timeout" for another). They were previously accepted as distinct (silently mis-treated as one by every downstream case-insensitive lookup); they now fail validation up front. This is the correct fix, but worth a CHANGELOG entry if not already planned.

@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from dad27c1 to bcb7973CompareJune 3, 2026 00:23
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Round 3 review — bcb797345

21/21 dimensions clean — no blocking, medium, or low findings.

Round-2 fixes validation

Fix #1ValidateOptionsAreNotDuplicated extended to cover system providers: CORRECT and complete.

I traced every collision shape through the new pass and confirmed no legitimate existing behavior changes:

  • Extension-vs-extension (case-differing). Previously crashed with ArgumentException at the providerAndOptionByOptionName.ToDictionary(..., StringComparer.OrdinalIgnoreCase) lookup on line 60-62. Now caught with the friendly "declared by multiple" error. ✅
  • Extension-vs-system (any casing). Already caught earlier by ValidateExtensionOptionsDoNotContainReservedOptions (line 107-152), which uses OrdinalIgnoreCase and returns early. The new Concat-over-both-dictionaries iteration is reachable in theory for this case but practically pre-empted. ✅
  • System-vs-system (case-differing). Previously crashed at the same ToDictionary lookup. Now caught with the friendly error. ✅ (Pinned by the new Validator_DuplicateOptionNamesAcrossSystemAndSystem_FailsGracefully test.)

I considered whether two providers could legitimately register the same option name (e.g. a system provider exposing a public alias also added by an extension). The contract is that each ICommandLineOptionsProvider.GetCommandLineOptions() returns options it owns, and the pre-existing ValidateExtensionOptionsDoNotContainReservedOptions pass already treats extension-vs-system collisions as errors (any casing). So no legitimate scenario is regressed.

Minor cosmetic observation (NIT, not blocking): line 60 still uses LINQ Union over the two dictionaries, while the new code at line 165 uses Concat. Both produce the same result here (provider keys are reference-equal across the two source dictionaries, and there are no duplicates by reference), but slight stylistic inconsistency. Not worth a follow-up.


Fix #2FormatException exception filter + rich error display: CORRECT and complete.

  • The when (!loggingState.CommandLineParseResult.HasTool) exception filter is standard C# 6+ and works on all four TFMs.
  • The silent-degrade-to-empty-list path for tools is consistent with the existing pipeline convention: validation errors are already gated by !HasTool on line 230, so tools that have a structurally-broken commandLineOptions section already wouldn't see validation surface the issue. The FormatException fallback mirrors this.
  • I verified HasTool semantics: ParseResult.HasTool => ToolName is not null. --help and --info are regular options handled via IsHelpInvoked / IsInfoInvoked on CommandLineHandler (lines 240-247), not tools. So --help and --info go through the normal !HasTool path and do surface FormatException with the rich InvalidCommandLineArguments header.
  • Only server-mode tools (those that set ToolName via the tool entry, e.g. --server) silently degrade. Even for these, the rest of testconfig.json (e.g. results-directory, custom IConfiguration[key] lookups) remains accessible because the throw fires only during the typed-schema enumeration of the commandLineOptions section — direct provider.TryGet calls bypass it.
  • The rich-error formatting (StringBuilder + PlatformResources.InvalidCommandLineArguments header + "\t- {ex.Message}" + TrimEnd()) matches CommandLineOptionsValidator (line 23-28). Environment.NewLine is correctly avoided (confirmed via grep). StringBuilder resolves via the global System.Text using in Directory.Build.props.
  • DisplayBannerIfEnabledAsync call inside the catch (line 205) safely reads --no-banner via commandLineOptions.IsOptionSet, which routes through provider.TryGet on the already-flattened key-value store, not through EnumerateCommandLineOptions. No re-throw risk.

Fresh-pass net-new findings

Nothing blocking, medium, or low. Three honest NIT-level observations, all noted-for-follow-up only:

  1. NIT (style)CommandLineOptionsValidator.cs line 60 uses Union where Concat would be slightly clearer and marginally cheaper (the new code at line 165 already uses Concat).
  2. NIT (test redundancy)Validator_JsonEntryWithTooFewArguments_FailsArityCheck (line 454) overlaps with Validator_JsonArityTooFew_Fails (line 200); only the arity shape differs (ArgumentArity(2,2) vs ExactlyOne). Defensible as defensive coverage of a non-trivial arity.
  3. NIT (docs precision) — The comment on Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully (lines 410-415) cites line 62, which will become stale on future edits to the validator. Consider replacing with a symbolic reference ("the OrdinalIgnoreCaseToDictionary lookup").

Other dimensions checked clean

  • Public API & init accessors — All new types/members are internal. No PublicAPI.Unshipped.txt churn. ✅
  • Cross-TFM — Exception filters (C# 6+), StringBuilder, CultureInfo, OrdinalIgnoreCase all available on net462 / netstandard2.0 / net8.0 / net9.0. ✅
  • Localization — Two new resx strings (JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage, JsonCommandLineOptionsValidationErrorPrefix) with <comment> metadata. .xlf deltas look auto-generated (stub-per-resource pattern, +10 per language file). No manual xlf edits. ✅
  • Threading — All affected code paths run on the single-threaded host startup pipeline. No new shared mutable state. ✅
  • IPC wire compatibility — N/A; no serialized type changes. ✅
  • Resource management — N/A; no new disposables. ✅
  • Defensive coding — Trust boundary (user testconfig.json) correctly guarded; internal invariants (e.g. sparse-indexed defensive guard in JsonConfigurationProvider) correctly preserved. ✅
  • Test isolation — Tests use isolated TestProvider instances per [TestMethod]; no shared static mutable state. ✅
  • Flakiness — No Thread.Sleep, no wall-clock assertions, no hard-coded ports in new tests. ✅

Verdict

Ready to merge. Round-2 fixes are correct, complete, and free of new bugs. The three NIT observations above are stylistic/documentation polish and do not affect users. Items explicitly deferred in the PR description (scalar-bool ambiguity, --diagnostic*/--config-file bootstrap reads, IsCommandLineOptionSet API visibility) remain out of scope for this PR as agreed.

This is a follow-up to the unified ICommandLineOptions/IConfiguration
work that addresses two gaps surfaced during review.
* Validator gap. CommandLineOptionsValidator walks parseResult.Options
only. Options set exclusively via testconfig.json bypass arity,
per-option, and unknown-option validation, so typos can crash deep
inside option handlers (e.g. --timeout IndexOutOfRange,
--exit-on-process-exit FormatException).
JsonConfigurationProvider now exposes a typed, schema-validated
enumeration of commandLineOptions entries (scalar / true / false /
scalar-array, anything else fails fast with FormatException).
CommandLineOptionsValidator runs three extra passes over those
entries: unknown-option detection, arity check, and per-arg
validation - so testconfig.json typos surface during startup
instead of crashing later. Option-name dictionaries now use
OrdinalIgnoreCase to match JSON's case-insensitive storage.
* --no-banner read. DisplayBannerIfEnabledAsync used to read off the
raw parseResult, so noBanner: true in testconfig.json was ignored.
It now takes the unified ICommandLineOptions and honors both
sources.
Adds JsonCommandLineOptionsTests covering enumeration schema,
validator passes (unknown / arity / per-arg), disabled entries,
shadowing, case-insensitivity, and the round-trip through
ConfigurationManager.
Deferred (truly architectural): --diagnostic* / --config-file
bootstrap reads, scalar-bool ambiguity, IsCommandLineOptionSet API
visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/unify-config-option-c branch from bcb7973 to 75c8179CompareJune 3, 2026 01:03
CopilotAI review requested due to automatic review settings June 3, 2026 01:03

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.

Copilot's findings

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

Comment threadsrc/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx Outdated
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 3, 2026 07:57
Copilotand others added 2 commits June 3, 2026 12:54
…ccuracy
- Strip section prefix from fullKey when formatting
JsonCommandLineOptionsEntryMustBeScalarOrArrayErrorMessage so the
'{0}' placeholder renders as the entry name relative to the section
('foo' or 'foo:0') instead of the redundant 'commandLineOptions:foo'.
Update the resx <comment> to match the new contract and regenerate xlf.
- Add a Assert.DoesNotContain pin so EnumerateCommandLineOptions_NestedObject_IsRejected
catches accidental regression to the prefixed rendering.
- Rewrite the Validator_DuplicateOptionNamesDifferingByCase_FailsGracefully
comment to accurately describe the pre-fix behavior (silent acceptance,
not raw ArgumentException), per @Evangelink's Round-2 NIT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 13:04

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.

Copilot's findings

  • Files reviewed: 29/29 changed files
  • Comments generated: 2

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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.

Copilot's findings

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

Sign up for freeto 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.

2 participants

@Evangelink