| File: Processors\RunSpecificTestsArgumentProcessor.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.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; 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.Interfaces; using Microsoft.VisualStudio.TestPlatform.Common.Utilities; using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.ArtifactProcessing; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine; using Microsoft.VisualStudio.TestPlatform.Utilities; using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources; namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors; internal class RunSpecificTestsArgumentProcessor : IArgumentProcessor { public const string CommandName = "/Tests"; private Lazy<IArgumentProcessorCapabilities>? _metadata; private Lazy<IArgumentExecutor>? _executor; private readonly IRunSettingsProvider _runSettingsProvider; private readonly CommandLineOptions _commandLineOptions; private readonly ITestRequestManager _testRequestManager; public RunSpecificTestsArgumentProcessor(CommandLineOptions commandLineOptions, IRunSettingsProvider runSettingsProvider, ITestRequestManager testRequestManager) { _commandLineOptions = commandLineOptions; _runSettingsProvider = runSettingsProvider; _testRequestManager = testRequestManager; } public Lazy<IArgumentProcessorCapabilities> Metadata => _metadata ??= new Lazy<IArgumentProcessorCapabilities>(() => new RunSpecificTestsArgumentProcessorCapabilities()); public Lazy<IArgumentExecutor>? Executor { get => _executor ??= new Lazy<IArgumentExecutor>(() => new RunSpecificTestsArgumentExecutor( _commandLineOptions, _runSettingsProvider, _testRequestManager, new ArtifactProcessingManager(_commandLineOptions.TestSessionCorrelationId), ConsoleOutput.Instance)); set => _executor = value; } } internal class RunSpecificTestsArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities { public override string CommandName => RunSpecificTestsArgumentProcessor.CommandName; public override bool IsAction => true; public override bool AllowMultiple => false; public override string HelpContentResourceName => CommandLineResources.RunSpecificTestsHelp; public override HelpContentPriority HelpPriority => HelpContentPriority.RunSpecificTestsArgumentProcessorHelpPriority; public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.Normal; } internal class RunSpecificTestsArgumentExecutor : IArgumentExecutor { public const char SplitDelimiter = ','; public const char EscapeDelimiter = '\\'; /// <summary> /// Used for getting sources. /// </summary> private readonly CommandLineOptions _commandLineOptions; /// <summary> /// The instance of testPlatforms /// </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> /// Given Collection of strings for filtering test cases /// </summary> private Collection<string>? _selectedTestNames; /// <summary> /// Used for tracking the total no. of tests discovered from the given sources. /// </summary> private long _discoveredTestCount; /// <summary> /// Collection of test cases that match at least one of the given search strings /// </summary> private readonly Collection<TestCase> _selectedTestCases = new(); /// <summary> /// Effective run settings applicable to test run after inferring the multi-targeting settings. /// </summary> private string? _effectiveRunSettings; /// <summary> /// List of filters that have not yet been discovered /// </summary> HashSet<string> _undiscoveredFilters = new(); /// <summary> /// Registers for discovery events during discovery /// </summary> private readonly ITestDiscoveryEventsRegistrar _discoveryEventsRegistrar; /// <summary> /// Registers and Unregisters for test run events before and after test run /// </summary> private readonly ITestRunEventsRegistrar _testRunEventsRegistrar; /// <summary> /// Default constructor. /// </summary> public RunSpecificTestsArgumentExecutor( CommandLineOptions options, IRunSettingsProvider runSettingsProvider, ITestRequestManager testRequestManager, IArtifactProcessingManager artifactProcessingManager, IOutput output) { ValidateArg.NotNull(options, nameof(options)); ValidateArg.NotNull(testRequestManager, nameof(testRequestManager)); _commandLineOptions = options; _testRequestManager = testRequestManager; _runSettingsManager = runSettingsProvider; Output = output; _discoveryEventsRegistrar = new DiscoveryEventsRegistrar(DiscoveryRequest_OnDiscoveredTests); _testRunEventsRegistrar = new TestRunRequestEventsRegistrar(Output, _commandLineOptions, artifactProcessingManager, _runSettingsManager); } #region IArgumentProcessor /// <summary> /// Splits given the search strings and adds to selectTestNamesCollection. /// </summary> /// <param name="argument"></param> [MemberNotNull(nameof(_selectedTestNames))] public void Initialize(string? argument) { if (!argument.IsNullOrWhiteSpace()) { _selectedTestNames = new Collection<string>( argument.Tokenize(SplitDelimiter, EscapeDelimiter) .Where(x => !StringUtils.IsNullOrWhiteSpace(x)) .Select(s => s.Trim()).ToList()); } if (_selectedTestNames == null || _selectedTestNames.Count <= 0) { throw new CommandLineException(CommandLineResources.SpecificTestsRequired); } // by default all filters are not discovered on launch _undiscoveredFilters = new HashSet<string>(_selectedTestNames); } /// <summary> /// Execute specific tests that match any of the given strings. /// </summary> /// <returns></returns> public ArgumentProcessorResult Execute() { TPDebug.Assert(Output != null); TPDebug.Assert(_commandLineOptions != null); TPDebug.Assert(_testRequestManager != null); TPDebug.Assert(!StringUtils.IsNullOrWhiteSpace(_runSettingsManager.ActiveRunSettings?.SettingsXml)); if (!_commandLineOptions.Sources.Any()) { throw new CommandLineException(CommandLineResources.MissingTestSourceFile); } _effectiveRunSettings = _runSettingsManager.ActiveRunSettings.SettingsXml; // Discover tests from sources and filter on every discovery reported. DiscoverTestsAndSelectSpecified(_commandLineOptions.Sources); // Now that tests are discovered and filtered, we run only those selected tests. ExecuteSelectedTests(); bool treatNoTestsAsError = RunSettingsUtilities.GetTreatNoTestsAsError(_effectiveRunSettings); return treatNoTestsAsError && _selectedTestCases.Count == 0 ? ArgumentProcessorResult.Fail : ArgumentProcessorResult.Success; } #endregion /// <summary> /// Discovers tests from the given sources and selects only specified tests. /// </summary> /// <param name="sources"> Test source assemblies paths. </param> private void DiscoverTestsAndSelectSpecified(IEnumerable<string> sources) { if (Environment.GetEnvironmentVariable("VSTEST_MSBUILD_NOLOGO") != "1") { Output.WriteLine(CommandLineResources.StartingDiscovery, OutputLevel.Information); } if (!StringUtils.IsNullOrEmpty(EqtTrace.LogFile)) { Output.Information(false, CommandLineResources.VstestDiagLogOutputPath, EqtTrace.LogFile); } _testRequestManager.DiscoverTests( new DiscoveryRequestPayload() { Sources = sources, RunSettings = _effectiveRunSettings }, _discoveryEventsRegistrar, Constants.DefaultProtocolConfig); } /// <summary> /// Executes the selected tests /// </summary> private void ExecuteSelectedTests() { if (_selectedTestCases.Count > 0) { if (_undiscoveredFilters.Count != 0) { string missingFilters = string.Join(", ", _undiscoveredFilters); string warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.SomeTestsUnavailableAfterFiltering, _discoveredTestCount, missingFilters); Output.Warning(false, warningMessage); } // for command line keep alive is always false. bool keepAlive = false; EqtTrace.Verbose("RunSpecificTestsArgumentProcessor:Execute: Test run is queued."); var runRequestPayload = new TestRunRequestPayload() { TestCases = _selectedTestCases.ToList(), RunSettings = _effectiveRunSettings, KeepAlive = keepAlive, TestPlatformOptions = new TestPlatformOptions() { TestCaseFilter = _commandLineOptions.TestCaseFilterValue } }; _testRequestManager.RunTests(runRequestPayload, null, _testRunEventsRegistrar, Constants.DefaultProtocolConfig); } else { string warningMessage; if (_discoveredTestCount > 0) { // No tests that matched any of the given strings. warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.NoTestsAvailableAfterFiltering, _discoveredTestCount, string.Join(", ", _selectedTestNames!)); } else { // No tests were discovered from the given sources. warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.NoTestsAvailableInSources, string.Join(", ", _commandLineOptions.Sources)); if (!_commandLineOptions.TestAdapterPathsSet) { warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.StringFormatToJoinTwoStrings, warningMessage, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound); } } Output.Warning(false, warningMessage); } } /// <summary> /// Filter discovered tests and find matching tests from given search strings. /// Any name of the test that can match multiple strings will be added only once. /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private void DiscoveryRequest_OnDiscoveredTests(object? sender, DiscoveredTestsEventArgs args) { TPDebug.Assert(_selectedTestNames != null, "Initialize should have been called"); TPDebug.Assert(args.DiscoveredTestCases != null); _discoveredTestCount += args.DiscoveredTestCases.Count(); foreach (var testCase in args.DiscoveredTestCases!) { foreach (var nameCriteria in _selectedTestNames) { if (testCase.FullyQualifiedName.IndexOf(nameCriteria, StringComparison.OrdinalIgnoreCase) != -1) { _selectedTestCases.Add(testCase); // If a testcase matched then a filter matched - so remove the filter from not found list _undiscoveredFilters.Remove(nameCriteria); break; } } } } private class DiscoveryEventsRegistrar : ITestDiscoveryEventsRegistrar { private readonly EventHandler<DiscoveredTestsEventArgs> _discoveredTestsHandler; public DiscoveryEventsRegistrar(EventHandler<DiscoveredTestsEventArgs> discoveredTestsHandler) { _discoveredTestsHandler = discoveredTestsHandler; } public void LogWarning(string message) { ConsoleLogger.RaiseTestRunWarning(message); } public void RegisterDiscoveryEvents(IDiscoveryRequest discoveryRequest) { discoveryRequest.OnDiscoveredTests += _discoveredTestsHandler; } public void UnregisterDiscoveryEvents(IDiscoveryRequest discoveryRequest) { discoveryRequest.OnDiscoveredTests -= _discoveredTestsHandler; } } private class TestRunRequestEventsRegistrar : ITestRunEventsRegistrar { private readonly IOutput _output; private readonly CommandLineOptions _commandLineOptions; private readonly IArtifactProcessingManager _artifactProcessingManager; private readonly IRunSettingsProvider _runSettingsProvider; public TestRunRequestEventsRegistrar(IOutput output, CommandLineOptions commandLineOptions, IArtifactProcessingManager artifactProcessingManager, IRunSettingsProvider runSettingsProvider) { _output = output; _commandLineOptions = commandLineOptions; _artifactProcessingManager = artifactProcessingManager; _runSettingsProvider = runSettingsProvider; } public void LogWarning(string message) { ConsoleLogger.RaiseTestRunWarning(message); } public void RegisterTestRunEvents(ITestRunRequest testRunRequest) { testRunRequest.OnRunCompletion += TestRunRequest_OnRunCompletion; } public void UnregisterTestRunEvents(ITestRunRequest testRunRequest) { testRunRequest.OnRunCompletion -= TestRunRequest_OnRunCompletion; } /// <summary> /// Handles the TestRunRequest complete event /// </summary> /// <param name="sender"></param> /// <param name="e">RunCompletion args</param> private void TestRunRequest_OnRunCompletion(object? sender, TestRunCompleteEventArgs e) { // If run is not aborted/canceled then check the count of executed tests. // we need to check if there are any tests executed - to try show some help info to user to check for installed vsix extensions if (!e.IsAborted && !e.IsCanceled) { var testsFoundInAnySource = e.TestRunStatistics != null && (e.TestRunStatistics.ExecutedTests > 0); // Indicate the user to use testadapterpath command if there are no tests found if (!testsFoundInAnySource && !_commandLineOptions.TestAdapterPathsSet && _commandLineOptions.TestCaseFilterValue == null) { _output.Warning(false, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound); } } // Collect tests session artifacts for post processing if (_commandLineOptions.ArtifactProcessingMode == ArtifactProcessingMode.Collect) { var settingsXml = _runSettingsProvider.ActiveRunSettings?.SettingsXml; TPDebug.Assert(settingsXml is not null, "RunSettingsProvider.ActiveRunSettings.SettingsXml is null"); _artifactProcessingManager.CollectArtifacts(e, settingsXml); } } } }