| File: Processors\ListFullyQualifiedTestsArgumentProcessor.cs | Web Access |
| Project: src\vstest\src\vstest.console\vstest.console.csproj (vstest.console) |
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.VisualStudio.TestPlatform.Client.RequestHelper; using Microsoft.VisualStudio.TestPlatform.CommandLine.Internal; using Microsoft.VisualStudio.TestPlatform.Common; using Microsoft.VisualStudio.TestPlatform.Common.Filtering; using Microsoft.VisualStudio.TestPlatform.Common.Interfaces; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Microsoft.VisualStudio.TestPlatform.Utilities; using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources; namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors; /// <summary> /// Argument Executor for the "--ListFullyQualifiedTests|/ListFullyQualifiedTests" command line argument. /// </summary> internal class ListFullyQualifiedTestsArgumentProcessor : IArgumentProcessor { /// <summary> /// The name of the command line argument that the ListFullyQualifiedTestsArgumentProcessor handles. /// </summary> public const string CommandName = "/ListFullyQualifiedTests"; private Lazy<IArgumentProcessorCapabilities>? _metadata; private Lazy<IArgumentExecutor>? _executor; private readonly IRunSettingsProvider _runSettingsProvider; private readonly CommandLineOptions _commandLineOptions; private readonly ITestRequestManager _testRequestManager; public ListFullyQualifiedTestsArgumentProcessor(CommandLineOptions commandLineOptions, IRunSettingsProvider runSettingsProvider, ITestRequestManager testRequestManager) { _commandLineOptions = commandLineOptions; _runSettingsProvider = runSettingsProvider; _testRequestManager = testRequestManager; } /// <summary> /// Gets the metadata. /// </summary> public Lazy<IArgumentProcessorCapabilities> Metadata => _metadata ??= new Lazy<IArgumentProcessorCapabilities>(() => new ListFullyQualifiedTestsArgumentProcessorCapabilities()); /// <summary> /// Gets or sets the executor. /// </summary> public Lazy<IArgumentExecutor>? Executor { get => _executor ??= new Lazy<IArgumentExecutor>(() => new ListFullyQualifiedTestsArgumentExecutor( _commandLineOptions, _runSettingsProvider, _testRequestManager)); set => _executor = value; } } internal class ListFullyQualifiedTestsArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities { public override string CommandName => ListFullyQualifiedTestsArgumentProcessor.CommandName; public override bool AllowMultiple => false; public override bool IsAction => true; public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.Normal; } /// <summary> /// Argument Executor for the "/ListTests" command line argument. /// </summary> internal class ListFullyQualifiedTestsArgumentExecutor : IArgumentExecutor { /// <summary> /// Used for getting sources. /// </summary> private readonly CommandLineOptions _commandLineOptions; /// <summary> /// Used for getting tests. /// </summary> private readonly ITestRequestManager _testRequestManager; /// <summary> /// Used for sending output. /// </summary> internal IOutput Output; /// <summary> /// RunSettingsManager to get currently active run settings. /// </summary> private readonly IRunSettingsProvider _runSettingsManager; /// <summary> /// Registers for discovery events during discovery /// </summary> private readonly ITestDiscoveryEventsRegistrar _discoveryEventsRegistrar; /// <summary> /// List to store the discovered tests /// </summary> private readonly List<string> _discoveredTests = new(); /// <summary> /// Default constructor. /// </summary> /// <param name="options"> /// The options. /// </param> /// <param name="runSettingsProvider">Runsettings provider.</param> /// <param name="testRequestManager">Test request manager.</param> public ListFullyQualifiedTestsArgumentExecutor( CommandLineOptions options, IRunSettingsProvider runSettingsProvider, ITestRequestManager testRequestManager) : this(options, runSettingsProvider, testRequestManager, ConsoleOutput.Instance) { } /// <summary> /// Default constructor. /// </summary> /// <param name="options"> /// The options. /// </param> /// <param name="runSettingsProvider">Runsettings provider.</param> /// <param name="testRequestManager">Test request manager.</param> /// <param name="output">Output such as console.</param> internal ListFullyQualifiedTestsArgumentExecutor( CommandLineOptions options, IRunSettingsProvider runSettingsProvider, ITestRequestManager testRequestManager, IOutput output) { ValidateArg.NotNull(options, nameof(options)); _commandLineOptions = options; Output = output; _testRequestManager = testRequestManager; _runSettingsManager = runSettingsProvider; _discoveryEventsRegistrar = new DiscoveryEventsRegistrar(_discoveredTests, _commandLineOptions); } #region IArgumentExecutor /// <summary> /// Initializes with the argument that was provided with the command. /// </summary> /// <param name="argument">Argument that was provided with the command.</param> public void Initialize(string? argument) { if (!argument.IsNullOrWhiteSpace()) { _commandLineOptions.AddSource(argument); } } /// <summary> /// Lists out the available discoverers. /// </summary> public ArgumentProcessorResult Execute() { TPDebug.Assert(Output != null); TPDebug.Assert(_commandLineOptions != null); TPDebug.Assert(!StringUtils.IsNullOrWhiteSpace(_runSettingsManager?.ActiveRunSettings?.SettingsXml)); if (!_commandLineOptions.Sources.Any()) { throw new CommandLineException(CommandLineResources.MissingTestSourceFile); } if (!EqtTrace.LogFile.IsNullOrEmpty()) { Output.Information(false, CommandLineResources.VstestDiagLogOutputPath, EqtTrace.LogFile); } var runSettings = _runSettingsManager.ActiveRunSettings.SettingsXml; _testRequestManager.DiscoverTests( new DiscoveryRequestPayload { Sources = _commandLineOptions.Sources, RunSettings = runSettings }, _discoveryEventsRegistrar, Constants.DefaultProtocolConfig); if (_commandLineOptions.ListTestsTargetPath.IsNullOrEmpty()) { // This string does not need to go to Resources. Reason - only internal consumption throw new CommandLineException("Target Path should be specified for listing FQDN tests!"); } File.WriteAllLines(_commandLineOptions.ListTestsTargetPath, _discoveredTests); return ArgumentProcessorResult.Success; } #endregion private class DiscoveryEventsRegistrar : ITestDiscoveryEventsRegistrar { private readonly List<string> _discoveredTests; private readonly CommandLineOptions _options; public DiscoveryEventsRegistrar(List<string> discoveredTests, CommandLineOptions cmdOptions) { _discoveredTests = discoveredTests; _options = cmdOptions; } public void LogWarning(string message) { ConsoleLogger.RaiseTestRunWarning(message); } public void RegisterDiscoveryEvents(IDiscoveryRequest discoveryRequest) { discoveryRequest.OnDiscoveredTests += DiscoveryRequest_OnDiscoveredTests; } public void UnregisterDiscoveryEvents(IDiscoveryRequest discoveryRequest) { discoveryRequest.OnDiscoveredTests -= DiscoveryRequest_OnDiscoveredTests; } private void DiscoveryRequest_OnDiscoveredTests(object? sender, DiscoveredTestsEventArgs args) { if (args == null) { throw new TestPlatformException("DiscoveredTestsEventArgs cannot be null."); } // Initializing the test case filter here because the filter value is read late. TestCaseFilter.Initialize(_options.TestCaseFilterValue); var discoveredTests = args.DiscoveredTestCases!.ToList(); var filteredTests = TestCaseFilter.FilterTests(discoveredTests).ToList(); // remove any duplicate tests filteredTests = filteredTests.Select(test => test.FullyQualifiedName) .Distinct() .Select(fqdn => filteredTests.First(test => test.FullyQualifiedName == fqdn)) .ToList(); _discoveredTests.AddRange(filteredTests.Select(test => test.FullyQualifiedName)); } } private static class TestCaseFilter { private static TestCaseFilterExpression? s_filterExpression; private const string TestCategory = "TestCategory"; private const string Category = "Category"; private const string Traits = "Traits"; public static void Initialize(string? filterString) { ValidateFilter(filterString); } /// <summary> /// Filter tests /// </summary> public static IEnumerable<TestCase> FilterTests(IEnumerable<TestCase> testCases) { EqtTrace.Verbose("TestCaseFilter.FilterTests : Test Filtering invoked."); List<TestCase> filteredList; try { filteredList = GetFilteredTestCases(testCases); } catch (Exception ex) { EqtTrace.Error("TestCaseFilter.FilterTests : Exception during filtering : {0}", ex.ToString()); throw; } return filteredList; } private static void ValidateFilter(string? filterString) { if (filterString.IsNullOrEmpty()) { s_filterExpression = null; return; } var filterWrapper = new FilterExpressionWrapper(filterString); if (filterWrapper.ParseError != null) { var fe = new FormatException($"Invalid Test Case Filter: {filterString}"); EqtTrace.Error("TestCaseFilter.ValidateFilter : Filtering failed with exception : " + fe.Message); throw fe; } s_filterExpression = new TestCaseFilterExpression(filterWrapper); } /// <summary> /// get list of test cases that satisfy the filter criteria /// </summary> private static List<TestCase> GetFilteredTestCases(IEnumerable<TestCase> testCases) { var filteredList = new List<TestCase>(); if (s_filterExpression == null) { filteredList = testCases.ToList(); return filteredList; } foreach (var testCase in testCases) { var traitDictionary = GetTestPropertiesInTraitDictionary(testCase);// Dictionary with trait key to value mapping traitDictionary = GetTraitsInTraitDictionary(traitDictionary, testCase.Traits); // Skip test if not fitting filter criteria. if (!s_filterExpression.MatchTestCase(testCase, p => PropertyValueProvider(p, traitDictionary))) { continue; } filteredList.Add(testCase); } return filteredList; } /// <summary> /// fetch the test properties on this test method as traits and populate a trait dictionary /// </summary> private static Dictionary<string, List<string>> GetTestPropertiesInTraitDictionary(TestCase testCase) { var traitDictionary = new Dictionary<string, List<string>>(); foreach (var testProperty in testCase.Properties) { string testPropertyKey = testProperty.Label; if (testPropertyKey.Equals(Traits)) { // skip the "Traits" property. traits to be set separately continue; } var testPropertyValue = testCase.GetPropertyValue(testProperty); if (testPropertyKey.Equals(TestCategory)) { if (testPropertyValue is string[] testPropertyValueArray) { var testPropertyValueList = new List<string>(testPropertyValueArray); traitDictionary.Add(testPropertyKey, testPropertyValueList); continue; } } //always return value as a list of string if (testPropertyValue != null) { var multiValue = new List<string> { testPropertyValue.ToString()! }; traitDictionary.Add(testPropertyKey, multiValue); } } return traitDictionary; } /// <summary> /// fetch the traits on this test method and populate a trait dictionary /// </summary> private static Dictionary<string, List<string>> GetTraitsInTraitDictionary(Dictionary<string, List<string>> traitDictionary, TraitCollection traits) { foreach (var trait in traits) { var newTraitValueList = new List<string> { trait.Value }; if (!traitDictionary.TryGetValue(trait.Name, out List<string>? currentTraitValue)) { // if the current trait's key is not already present, add the current trait key-value pair traitDictionary.Add(trait.Name, newTraitValueList); } else { if (null == currentTraitValue) { // if the current trait's value is null, replace the previous value with the current value traitDictionary[trait.Name] = newTraitValueList; } else { // if the current trait's key is already present and is not null, append current value to the previous value list List<string> traitValueList = currentTraitValue; traitValueList.Add(trait.Value); } } } //This is hack for NUnit, XUnit to understand test category -> This method is called only for NUnit/XUnit if (!traitDictionary.ContainsKey(TestCategory) && traitDictionary.ContainsKey(Category)) { traitDictionary.TryGetValue(Category, out var categoryValue); traitDictionary.Add(TestCategory, categoryValue!); } return traitDictionary; } /// <summary> /// Provides value for property name 'propertyName' as used in filter. /// </summary> private static string[]? PropertyValueProvider(string propertyName, Dictionary<string, List<string>> traitDictionary) { traitDictionary.TryGetValue(propertyName, out List<string>? propertyValueList); if (propertyValueList != null) { var propertyValueArray = propertyValueList.ToArray(); return propertyValueArray; } return null; } } }