-
Notifications
You must be signed in to change notification settings - Fork 292
[MTP] Improve performance of validating command line options #5655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d88d0ce
1443d5e
85ffb88
39d6db5
78e96f8
975d2d6
86c987c
2e2ee02
07a4700
6c344de
3ba2fb0
e1369e7
780fd3b
fe102f2
cae2a1b
97c8299
63ab4f7
3f0ce4e
1cc37c3
a470f0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,40 +127,66 @@ | |
| Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider, | ||
| Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> systemOptionsByProvider) | ||
| { | ||
| IEnumerable<string> allExtensionOptions = extensionOptionsByProvider.Values.SelectMany(x => x).Select(x => x.Name).Distinct(); | ||
| IEnumerable<string> allSystemOptions = systemOptionsByProvider.Values.SelectMany(x => x).Select(x => x.Name).Distinct(); | ||
|
|
||
| IEnumerable<string> invalidReservedOptions = allSystemOptions.Intersect(allExtensionOptions); | ||
| if (invalidReservedOptions.Any()) | ||
| // Create a HashSet of all system option names for faster lookup | ||
| var systemOptionNames = new HashSet<string>(); | ||
| foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in systemOptionsByProvider) | ||
| { | ||
| var stringBuilder = new StringBuilder(); | ||
| foreach (string reservedOption in invalidReservedOptions) | ||
| foreach (CommandLineOption option in provider.Value) | ||
| { | ||
| IEnumerable<string> faultyProviderNames = extensionOptionsByProvider.Where(tuple => tuple.Value.Any(x => x.Name == reservedOption)).Select(tuple => tuple.Key.DisplayName); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsReserved, reservedOption, string.Join("', '", faultyProviderNames))); | ||
| systemOptionNames.Add(option.Name); | ||
| } | ||
| } | ||
|
|
||
| return ValidationResult.Invalid(stringBuilder.ToTrimmedString()); | ||
| StringBuilder? stringBuilder = null; | ||
| foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in extensionOptionsByProvider) | ||
| { | ||
| foreach (CommandLineOption option in provider.Value) | ||
| { | ||
| if (systemOptionNames.Contains(option.Name)) | ||
| { | ||
| stringBuilder ??= new StringBuilder(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsReserved, option.Name, provider.Key.DisplayName)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return ValidationResult.Valid(); | ||
| return stringBuilder?.Length > 0 | ||
| ? ValidationResult.Invalid(stringBuilder.ToTrimmedString()) | ||
| : ValidationResult.Valid(); | ||
| } | ||
|
|
||
| private static ValidationResult ValidateOptionsAreNotDuplicated( | ||
| Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider) | ||
| { | ||
| IEnumerable<string> duplications = extensionOptionsByProvider.Values.SelectMany(x => x) | ||
| .Select(x => x.Name) | ||
| .GroupBy(x => x) | ||
| .Where(x => x.Skip(1).Any()) | ||
| .Select(x => x.Key); | ||
| // Use a dictionary to track option names and their providers | ||
| var optionNameToProviders = new Dictionary<string, List<ICommandLineOptionsProvider>>(); | ||
| foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> kvp in extensionOptionsByProvider) | ||
| { | ||
| ICommandLineOptionsProvider provider = kvp.Key; | ||
| foreach (CommandLineOption option in kvp.Value) | ||
| { | ||
| string name = option.Name; | ||
| if (!optionNameToProviders.TryGetValue(name, out List<ICommandLineOptionsProvider>? providers)) | ||
| { | ||
| providers = new List<ICommandLineOptionsProvider>(); | ||
|
Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs
|
||
| optionNameToProviders[name] = providers; | ||
| } | ||
|
Youssef1313 marked this conversation as resolved.
|
||
|
|
||
| providers.Add(provider); | ||
| } | ||
| } | ||
|
Comment on lines
+161
to
+177
|
||
|
|
||
| // Check for duplications | ||
| StringBuilder? stringBuilder = null; | ||
| foreach (string duplicatedOption in duplications) | ||
| foreach (KeyValuePair<string, List<ICommandLineOptionsProvider>> kvp in optionNameToProviders) | ||
| { | ||
| IEnumerable<string> faultyProvidersDisplayNames = extensionOptionsByProvider.Where(tuple => tuple.Value.Any(x => x.Name == duplicatedOption)).Select(tuple => tuple.Key.DisplayName); | ||
| stringBuilder ??= new(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsDeclaredByMultipleProviders, duplicatedOption, string.Join("', '", faultyProvidersDisplayNames))); | ||
| if (kvp.Value.Count > 1) | ||
| { | ||
| string duplicatedOption = kvp.Key; | ||
| stringBuilder ??= new(); | ||
| IEnumerable<string> faultyProvidersDisplayNames = kvp.Value.Select(p => p.DisplayName); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsDeclaredByMultipleProviders, duplicatedOption, string.Join("', '", faultyProvidersDisplayNames))); | ||
| } | ||
|
Comment on lines
+161
to
+189
|
||
| } | ||
|
Comment on lines
+181
to
190
|
||
|
|
||
| return stringBuilder?.Length > 0 | ||
|
|
@@ -173,10 +199,28 @@ | |
| Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider, | ||
| Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> systemOptionsByProvider) | ||
| { | ||
| // Create a HashSet of all valid option names for faster lookup | ||
| var validOptionNames = new HashSet<string>(); | ||
| foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in extensionOptionsByProvider) | ||
| { | ||
| foreach (CommandLineOption option in provider.Value) | ||
| { | ||
| validOptionNames.Add(option.Name); | ||
| } | ||
| } | ||
|
|
||
| foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in systemOptionsByProvider) | ||
| { | ||
| foreach (CommandLineOption option in provider.Value) | ||
| { | ||
| validOptionNames.Add(option.Name); | ||
| } | ||
| } | ||
|
|
||
| StringBuilder? stringBuilder = null; | ||
| foreach (CommandLineParseOption optionRecord in parseResult.Options) | ||
| { | ||
| if (!extensionOptionsByProvider.Union(systemOptionsByProvider).Any(tuple => tuple.Value.Any(x => x.Name == optionRecord.Name))) | ||
| if (!validOptionNames.Contains(optionRecord.Name)) | ||
| { | ||
| stringBuilder ??= new(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineUnknownOption, optionRecord.Name)); | ||
|
|
@@ -192,7 +236,7 @@ | |
| CommandLineParseResult parseResult, | ||
| Dictionary<string, (ICommandLineOptionsProvider Provider, CommandLineOption Option)> providerAndOptionByOptionName) | ||
| { | ||
| StringBuilder stringBuilder = new(); | ||
| StringBuilder? stringBuilder = null; | ||
| foreach (IGrouping<string, CommandLineParseOption> groupedOptions in parseResult.Options.GroupBy(x => x.Name)) | ||
| { | ||
| // getting the arguments count for an option. | ||
|
|
@@ -207,19 +251,22 @@ | |
|
|
||
| if (arity > option.Arity.Max && option.Arity.Max == 0) | ||
| { | ||
| stringBuilder ??= new(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsNoArguments, optionName, provider.DisplayName, provider.Uid)); | ||
| } | ||
| else if (arity < option.Arity.Min) | ||
| { | ||
| stringBuilder ??= new(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsAtLeastArguments, optionName, provider.DisplayName, provider.Uid, option.Arity.Min)); | ||
| } | ||
| else if (arity > option.Arity.Max) | ||
| { | ||
| stringBuilder ??= new(); | ||
| stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsAtMostArguments, optionName, provider.DisplayName, provider.Uid, option.Arity.Max)); | ||
| } | ||
| } | ||
|
|
||
| return stringBuilder.Length > 0 | ||
| return stringBuilder?.Length > 0 | ||
| ? ValidationResult.Invalid(stringBuilder.ToTrimmedString()) | ||
| : ValidationResult.Valid(); | ||
| } | ||
|
|
@@ -280,7 +327,23 @@ | |
| } | ||
|
|
||
| private static string ToTrimmedString(this StringBuilder stringBuilder) | ||
| #pragma warning disable RS0030 // Do not use banned APIs | ||
| => stringBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray()); | ||
| #pragma warning restore RS0030 // Do not use banned APIs | ||
| { | ||
| // Use a more efficient approach to trim without creating unnecessary intermediate strings | ||
| string result = stringBuilder.ToString(); | ||
| int end = result.Length; | ||
|
|
||
| // Find the last non-whitespace char | ||
| while (end > 0) | ||
| { | ||
| char c = result[end - 1]; | ||
| if (c is not ('\r' or '\n')) | ||
|
Comment on lines
+331
to
+339
|
||
| { | ||
| break; | ||
| } | ||
|
Youssef1313 marked this conversation as resolved.
Youssef1313 marked this conversation as resolved.
|
||
|
|
||
| end--; | ||
| } | ||
|
|
||
| return end == result.Length ? result : result.Substring(0, end); | ||
| } | ||
|
Comment on lines
329
to
+348
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -231,6 +231,91 @@ public async Task ParseAndValidateAsync_UnknownOption_ReturnsFalse() | |||||||||
| Assert.AreEqual("Unknown option '--x'", result.ErrorMessage); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| [TestMethod] | ||||||||||
| public async Task ParseAndValidateAsync_MultipleUnknownOptions_ReportsAll() | ||||||||||
| { | ||||||||||
| // Arrange | ||||||||||
| string[] args = ["--x", "--y"]; | ||||||||||
| CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment()); | ||||||||||
|
|
||||||||||
| ICommandLineOptionsProvider[] extensionCommandLineProvider = | ||||||||||
| [ | ||||||||||
| new ExtensionCommandLineProviderMockUnknownOption() | ||||||||||
| ]; | ||||||||||
|
|
||||||||||
| // Act | ||||||||||
| ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders, | ||||||||||
| extensionCommandLineProvider, new Mock<ICommandLineOptions>().Object); | ||||||||||
|
|
||||||||||
| // Assert | ||||||||||
| Assert.IsFalse(result.IsValid); | ||||||||||
| Assert.Contains("Unknown option '--x'", result.ErrorMessage); | ||||||||||
| Assert.Contains("Unknown option '--y'", result.ErrorMessage); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| [TestMethod] | ||||||||||
| public async Task ParseAndValidateAsync_MultipleReservedOptionsFromDifferentProviders_ReturnsFalse() | ||||||||||
| { | ||||||||||
| // Arrange | ||||||||||
| string[] args = []; | ||||||||||
| CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment()); | ||||||||||
| ICommandLineOptionsProvider[] extensionCommandLineProvider = | ||||||||||
| [ | ||||||||||
| new ExtensionCommandLineProviderMockReservedOptions(), | ||||||||||
| new ExtensionCommandLineProviderMockWithNamedOption("help", "Provider2") | ||||||||||
| ]; | ||||||||||
|
|
||||||||||
| // Act | ||||||||||
| ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders, | ||||||||||
| extensionCommandLineProvider, new Mock<ICommandLineOptions>().Object); | ||||||||||
|
|
||||||||||
| // Assert | ||||||||||
| Assert.IsFalse(result.IsValid); | ||||||||||
| Assert.Contains("Option '--help' is reserved and cannot be used by providers: 'help'", result.ErrorMessage); | ||||||||||
|
||||||||||
| Assert.Contains("Option '--help' is reserved and cannot be used by providers: 'help'", result.ErrorMessage); | |
| Assert.Contains("Option '--help' is reserved and cannot be used by providers:", result.ErrorMessage); | |
| Assert.Contains("'help'", result.ErrorMessage); | |
| Assert.Contains("'Provider2'", result.ErrorMessage); |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a test extension provider mock, hard-coding Uid to PlatformCommandLineProvider and describing it as 'Built-in' is misleading and can make diagnostics harder to interpret if Uid/description ever appear in validation output. Consider setting Uid to something unique to the mock (or derived from displayName) and updating the description to reflect that it’s a test/extension provider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidateExtensionOptionsDoNotContainReservedOptions now emits one error line per (provider, option) and only includes the current provider’s DisplayName. Previously it emitted one line per reserved option and included all offending providers, so this is a behavior/error-message regression (and will get worse if the resource string is corrected to use the provider placeholder). Consider aggregating by option name (e.g., option -> list of providers) and formatting a single message per reserved option with the complete provider list.