diff --git a/TestPlatform.sln b/TestPlatform.sln
index fde613978a..0871578e83 100644
--- a/TestPlatform.sln
+++ b/TestPlatform.sln
@@ -170,6 +170,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpMinitool.x86", "src\Dat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AttachVS", "src\AttachVS\AttachVS.csproj", "{8238A052-D626-49EB-A011-51DC6D0DBA30}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", "{6CE2F530-582B-4695-A209-41065E103426}"
+ ProjectSection(SolutionItems) = preProject
+ playground\README.md = playground\README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestPlatform.Playground", "playground\TestPlatform.Playground\TestPlatform.Playground.csproj", "{545A88D3-1AE2-4D39-9B7C-C691768AD17F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTest1", "playground\MSTest1\MSTest1.csproj", "{57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Microsoft.TestPlatform.Execution.Shared\Microsoft.TestPlatform.Execution.Shared.projitems*{10b6ade1-f808-4612-801d-4452f5b52242}*SharedItemsImports = 5
@@ -835,6 +844,30 @@ Global
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x64.Build.0 = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.ActiveCfg = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.Build.0 = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|x64.Build.0 = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Debug|x86.Build.0 = Debug|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|x64.ActiveCfg = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|x64.Build.0 = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|x86.ActiveCfg = Release|Any CPU
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F}.Release|x86.Build.0 = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|x64.Build.0 = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Debug|x86.Build.0 = Debug|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|x64.ActiveCfg = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|x64.Build.0 = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|x86.ActiveCfg = Release|Any CPU
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -907,6 +940,8 @@ Global
{33A20B85-7024-4112-B1E7-00CD0E4A9F96} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
{2C88C923-3D7A-4492-9241-7A489750CAB7} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
{8238A052-D626-49EB-A011-51DC6D0DBA30} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
+ {545A88D3-1AE2-4D39-9B7C-C691768AD17F} = {6CE2F530-582B-4695-A209-41065E103426}
+ {57A61A09-10AD-44BE-8DF4-A6FD108F7DF7} = {6CE2F530-582B-4695-A209-41065E103426}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0541B30C-FF51-4E28-B172-83F5F3934BCD}
diff --git a/playground/MSTest1/MSTest1.csproj b/playground/MSTest1/MSTest1.csproj
new file mode 100644
index 0000000000..e952c0fa11
--- /dev/null
+++ b/playground/MSTest1/MSTest1.csproj
@@ -0,0 +1,30 @@
+
+
+ ..\..\
+ true
+
+
+
+
+ $(TargetFrameworks);net472
+
+ $(TargetFrameworks);net451
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/MSTest1/UnitTest1.cs b/playground/MSTest1/UnitTest1.cs
new file mode 100644
index 0000000000..3c93d8ebca
--- /dev/null
+++ b/playground/MSTest1/UnitTest1.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace MSTest1
+{
+ [TestClass]
+ public class UnitTest1
+ {
+ [TestMethod]
+ public void TestMethod1()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/README.md b/playground/README.md
new file mode 100644
index 0000000000..c774287f37
--- /dev/null
+++ b/playground/README.md
@@ -0,0 +1,17 @@
+# Playground
+
+This Plaground directory contains projects to aid interactive debugging of test platform. TestPlatform is normally built
+as a set of distinct pieces and then assembled in the artifacts folder. This forces rebuilding using build.cmd to try out
+changes. The TestPlatform.Playground project builds a simpler version of TestPlatform to avoid always rebuilding via
+build.cmd, offering a tighther development loop.
+
+The project references TranslationLayer, vstest.console, TestHostProvider, testhost and MSTest1 projects, to make sure
+we build all the dependencies of that are used to run tests via VSTestConsoleWrapper. It then copies the components from
+their original build locations, to $(TargetDir)\vstest.console directory, and it's subfolders to create an executable
+copy of TestPlatform that is similar to what we ship.
+
+The copying might trigger only on re-build, if you see outdated dependencies, Rebuild this project instead of just Build.
+
+Use this as playground for your debugging of end-to-end scenarios, it will automatically attach vstest.console and teshost
+sub-processes. It won't stop at entry-point automatically, don't forget to set your breakpoints, or remove VSTEST_DEBUG_NOBP
+from the environment variables of this project.
\ No newline at end of file
diff --git a/playground/TestPlatform.Playground/Program.cs b/playground/TestPlatform.Playground/Program.cs
new file mode 100644
index 0000000000..52a745a7d6
--- /dev/null
+++ b/playground/TestPlatform.Playground/Program.cs
@@ -0,0 +1,131 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.TestPlatform.VsTestConsole.TranslationLayer;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+namespace TestPlatform.Playground
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+ // This project references TranslationLayer, vstest.console, TestHostProvider, testhost and MSTest1 projects, to make sure
+ // we build all the dependencies of that are used to run tests via VSTestConsoleWrapper. It then copies the components from
+ // their original build locations, to $(TargetDir)\vstest.console directory, and it's subfolders to create an executable
+ // copy of TestPlatform that is similar to what we ship.
+ //
+ // The copying might trigger only on re-build, if you see outdated dependencies, Rebuild this project instead of just Build.
+ //
+ // Use this as playground for your debugging of end-to-end scenarios, it will automatically attach vstest.console and teshost
+ // sub-processes. It won't stop at entry-point automatically, don't forget to set your breakpoints, or remove VSTEST_DEBUG_NOBP
+ // from the environment variables of this project.
+
+ var thisAssemblyPath = Assembly.GetEntryAssembly().Location;
+ var here = Path.GetDirectoryName(thisAssemblyPath);
+ var playground = Path.GetFullPath(Path.Combine(here, "..", "..", "..", ".."));
+
+ var console = Path.Combine(here, "vstest.console", "vstest.console.exe");
+ var consoleOptions = new ConsoleParameters
+ {
+ LogFilePath = Path.Combine(here, "logs", "log.txt"),
+ TraceLevel = TraceLevel.Verbose,
+ };
+
+ var r = new VsTestConsoleWrapper(console, consoleOptions);
+
+ var sourceSettings = @"
+
+
+ true
+
+
+ ";
+ var sources = new[] {
+ Path.Combine(playground, "MSTest1", "bin", "Debug", "net472", "MSTest1.dll")
+ };
+
+ var options = new TestPlatformOptions();
+ r.RunTestsWithCustomTestHost(sources, sourceSettings, options, new TestRunHandler(), new DebuggerTestHostLauncher());
+ }
+
+ public class TestRunHandler : ITestRunEventsHandler
+ {
+
+ public TestRunHandler()
+ {
+ }
+
+ public void HandleLogMessage(TestMessageLevel level, string message)
+ {
+ Console.WriteLine($"[{level.ToString().ToUpper()}]: {message}");
+ }
+
+ public void HandleRawMessage(string rawMessage)
+ {
+ Console.WriteLine($"[MESSAGE]: { rawMessage}");
+ }
+
+ public void HandleTestRunComplete(TestRunCompleteEventArgs testRunCompleteArgs, TestRunChangedEventArgs lastChunkArgs, ICollection runContextAttachments, ICollection executorUris)
+ {
+ Console.WriteLine($"[COMPLETE]: err: { testRunCompleteArgs.Error }, lastChunk: {WriteTests(lastChunkArgs?.NewTestResults)}");
+ }
+
+ public void HandleTestRunStatsChange(TestRunChangedEventArgs testRunChangedArgs)
+ {
+ Console.WriteLine($"[PROGRESS - NEW RESULTS]: {WriteTests(testRunChangedArgs.NewTestResults)}");
+ }
+
+ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessStartInfo)
+ {
+ throw new NotImplementedException();
+ }
+
+ private string WriteTests(IEnumerable testResults)
+ {
+ return WriteTests(testResults?.Select(t => t.TestCase));
+ }
+
+ private string WriteTests(IEnumerable testCases)
+ {
+ return testCases == null ? null : "\t" + string.Join("\n\t", testCases.Select(r => r.DisplayName));
+ }
+ }
+
+ internal class DebuggerTestHostLauncher : ITestHostLauncher2
+ {
+ public bool IsDebug => true;
+
+ public bool AttachDebuggerToProcess(int pid)
+ {
+ return true;
+ }
+
+ public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo)
+ {
+ return 1;
+ }
+
+ public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo, CancellationToken cancellationToken)
+ {
+ return 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/TestPlatform.Playground/Properties/launchSettings.json b/playground/TestPlatform.Playground/Properties/launchSettings.json
new file mode 100644
index 0000000000..c688670d9c
--- /dev/null
+++ b/playground/TestPlatform.Playground/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "TestPlatform.Playground": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "VSTEST_CONNECTION_TIMEOUT": "999",
+ "VSTEST_DEBUG_NOBP": "1",
+ "VSTEST_RUNNER_DEBUG_ATTACHVS": "1",
+ "VSTEST_HOST_DEBUG_ATTACHVS": "1",
+ "VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS": "1"
+ }
+ }
+ }
+}
diff --git a/playground/TestPlatform.Playground/TestPlatform.Playground.csproj b/playground/TestPlatform.Playground/TestPlatform.Playground.csproj
new file mode 100644
index 0000000000..d73746f16e
--- /dev/null
+++ b/playground/TestPlatform.Playground/TestPlatform.Playground.csproj
@@ -0,0 +1,45 @@
+
+
+ ..\..\
+ true
+
+
+ $(MSBuildWarningsAsMessages);MSB3270;MSB3276
+
+
+
+
+ Exe
+ $(TargetFrameworks);net472
+
+ $(TargetFrameworks);net451
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.TestPlatform.Client/TestPlatform.cs b/src/Microsoft.TestPlatform.Client/TestPlatform.cs
index bb7c62320d..5a2c928315 100644
--- a/src/Microsoft.TestPlatform.Client/TestPlatform.cs
+++ b/src/Microsoft.TestPlatform.Client/TestPlatform.cs
@@ -26,6 +26,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Client
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;
+
using ClientResources = Resources.Resources;
///
@@ -89,15 +90,7 @@ public IDiscoveryRequest CreateDiscoveryRequest(
throw new ArgumentNullException(nameof(discoveryCriteria));
}
- // Update cache with Extension folder's files.
- this.AddExtensionAssemblies(discoveryCriteria.RunSettings);
-
- // Update extension assemblies from source when design mode is false.
- var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(discoveryCriteria.RunSettings);
- if (!runConfiguration.DesignMode)
- {
- this.AddExtensionAssembliesFromSource(discoveryCriteria.Sources);
- }
+ PopulateExtensions(discoveryCriteria.RunSettings, discoveryCriteria.Sources);
// Initialize loggers.
var loggerManager = this.TestEngine.GetLoggerManager(requestData);
@@ -125,15 +118,8 @@ public ITestRunRequest CreateTestRunRequest(
throw new ArgumentNullException(nameof(testRunCriteria));
}
- this.AddExtensionAssemblies(testRunCriteria.TestRunSettings);
-
- var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(testRunCriteria.TestRunSettings);
-
- // Update extension assemblies from source when design mode is false.
- if (!runConfiguration.DesignMode)
- {
- this.AddExtensionAssembliesFromSource(testRunCriteria);
- }
+ var sources = GetSources(testRunCriteria);
+ PopulateExtensions(testRunCriteria.TestRunSettings, sources);
// Initialize loggers.
var loggerManager = this.TestEngine.GetLoggerManager(requestData);
@@ -156,6 +142,21 @@ public ITestRunRequest CreateTestRunRequest(
return new TestRunRequest(requestData, testRunCriteria, executionManager, loggerManager);
}
+ private void PopulateExtensions(string runSettings, IEnumerable sources)
+ {
+ var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(runSettings);
+ var strategy = runConfiguration.TestAdapterLoadingStrategy;
+
+ // Update cache with Extension folder's files.
+ this.AddExtensionAssemblies(runSettings, strategy);
+
+ // Update extension assemblies from source when design mode is false.
+ if (!runConfiguration.DesignMode)
+ {
+ this.AddExtensionAssembliesFromSource(sources, strategy);
+ }
+ }
+
///
public bool StartTestSession(
IRequestData requestData,
@@ -167,9 +168,11 @@ public bool StartTestSession(
throw new ArgumentNullException(nameof(testSessionCriteria));
}
- this.AddExtensionAssemblies(testSessionCriteria.RunSettings);
-
var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(testSessionCriteria.RunSettings);
+ var strategy = runConfiguration.TestAdapterLoadingStrategy;
+
+ this.AddExtensionAssemblies(testSessionCriteria.RunSettings, strategy);
+
if (!runConfiguration.DesignMode)
{
return false;
@@ -222,13 +225,7 @@ private void ThrowExceptionIfTestHostManagerIsNull(
}
}
- ///
- /// Updates the test adapter paths provided through run settings to be used by the test
- /// service.
- ///
- ///
- /// The run settings.
- private void AddExtensionAssemblies(string runSettings)
+ private void AddExtensionAssemblies(string runSettings, TestAdapterLoadingStrategy adapterLoadingStrategy)
{
IEnumerable customTestAdaptersPaths = RunSettingsUtilities.GetTestAdaptersPaths(runSettings);
@@ -236,71 +233,45 @@ private void AddExtensionAssemblies(string runSettings)
{
foreach (string customTestAdaptersPath in customTestAdaptersPaths)
{
- var adapterPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(customTestAdaptersPath));
- if (!Directory.Exists(adapterPath))
- {
- if (EqtTrace.IsWarningEnabled)
- {
- EqtTrace.Warning(string.Format("AdapterPath Not Found:", adapterPath));
- }
+ var extensionAssemblies = ExpandTestAdapterPaths(customTestAdaptersPath, this.fileHelper, adapterLoadingStrategy);
- continue;
- }
-
- var extensionAssemblies = new List(
- this.fileHelper.EnumerateFiles(
- adapterPath,
- SearchOption.AllDirectories,
- TestPlatformConstants.TestAdapterEndsWithPattern,
- TestPlatformConstants.TestLoggerEndsWithPattern,
- TestPlatformConstants.DataCollectorEndsWithPattern,
- TestPlatformConstants.RunTimeEndsWithPattern));
-
- if (extensionAssemblies.Count > 0)
+ if (extensionAssemblies.Any())
{
this.UpdateExtensions(extensionAssemblies, skipExtensionFilters: false);
}
+
}
}
}
///
- /// Updates the extension assemblies from source directory.
+ /// Updates the test logger paths from source directory.
///
///
- /// The test run criteria.
- private void AddExtensionAssembliesFromSource(TestRunCriteria testRunCriteria)
+ /// The list of sources.
+ private void AddExtensionAssembliesFromSource(IEnumerable sources, TestAdapterLoadingStrategy strategy)
{
- IEnumerable sources = testRunCriteria.Sources;
- if (testRunCriteria.HasSpecificTests)
+ // Skip discovery unless we're using the default behavior, or NextToSource is specified.
+ if (strategy != TestAdapterLoadingStrategy.Default
+ && (strategy & TestAdapterLoadingStrategy.NextToSource) != TestAdapterLoadingStrategy.NextToSource)
{
- // If the test execution is with a test filter, group them by sources.
- sources = testRunCriteria.Tests.Select(tc => tc.Source).Distinct();
+ return;
}
- AddExtensionAssembliesFromSource(sources);
- }
-
- ///
- /// Updates the test logger paths from source directory.
- ///
- ///
- /// The list of sources.
- private void AddExtensionAssembliesFromSource(IEnumerable sources)
- {
// Currently we support discovering loggers only from Source directory.
var loggersToUpdate = new List();
foreach (var source in sources)
{
var sourceDirectory = Path.GetDirectoryName(source);
- if (!string.IsNullOrEmpty(sourceDirectory)
- && this.fileHelper.DirectoryExists(sourceDirectory))
+ if (!string.IsNullOrEmpty(sourceDirectory) && this.fileHelper.DirectoryExists(sourceDirectory))
{
+ var searchOption = GetSearchOption(strategy, SearchOption.TopDirectoryOnly);
+
loggersToUpdate.AddRange(
this.fileHelper.EnumerateFiles(
sourceDirectory,
- SearchOption.TopDirectoryOnly,
+ searchOption,
TestPlatformConstants.TestLoggerEndsWithPattern));
}
}
@@ -318,22 +289,134 @@ private void AddExtensionAssembliesFromSource(IEnumerable sources)
///
private static void AddExtensionAssembliesFromExtensionDirectory()
{
+ // This method runs before adapter initialization path, ideally we should replace this mechanism
+ // this is currently required because we need TestHostProvider to be able to resolve.
+ var runSettings = RunSettingsManager.Instance.ActiveRunSettings.SettingsXml;
+ var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(runSettings);
+ var strategy = runConfiguration.TestAdapterLoadingStrategy;
+
var fileHelper = new FileHelper();
- var extensionsFolder = Path.Combine(
- Path.GetDirectoryName(
- typeof(TestPlatform).GetTypeInfo().Assembly.GetAssemblyLocation()),
- "Extensions");
+ var defaultExtensionPaths = Enumerable.Empty();
+ // Explicit adapter loading
+ if ((strategy & TestAdapterLoadingStrategy.Explicit) == TestAdapterLoadingStrategy.Explicit)
+ {
+ defaultExtensionPaths = RunSettingsUtilities.GetTestAdaptersPaths(runSettings)
+ .SelectMany(path => ExpandTestAdapterPaths(path, fileHelper, strategy))
+ .Union(defaultExtensionPaths);
+ }
+
+ var extensionsFolder = Path.Combine(Path.GetDirectoryName(typeof(TestPlatform).GetTypeInfo().Assembly.GetAssemblyLocation()), "Extensions");
if (fileHelper.DirectoryExists(extensionsFolder))
{
- var defaultExtensionPaths = fileHelper.EnumerateFiles(
- extensionsFolder,
- SearchOption.TopDirectoryOnly,
- ".dll",
- ".exe");
+ // Load default runtime providers
+ if ((strategy & TestAdapterLoadingStrategy.DefaultRuntimeProviders) == TestAdapterLoadingStrategy.DefaultRuntimeProviders)
+ {
+ defaultExtensionPaths = fileHelper
+ .EnumerateFiles(extensionsFolder, SearchOption.TopDirectoryOnly, TestPlatformConstants.RunTimeEndsWithPattern)
+ .Union(defaultExtensionPaths);
+ }
+
+ // Default extension loader
+ if (strategy == TestAdapterLoadingStrategy.Default
+ || (strategy & TestAdapterLoadingStrategy.ExtensionsDirectory) == TestAdapterLoadingStrategy.ExtensionsDirectory)
+ {
+ defaultExtensionPaths = fileHelper
+ .EnumerateFiles(extensionsFolder, SearchOption.TopDirectoryOnly, ".dll", ".exe")
+ .Union(defaultExtensionPaths);
+ }
+ }
+
+ TestPluginCache.Instance.DefaultExtensionPaths = defaultExtensionPaths.Distinct();
+ }
+
+ private static SearchOption GetSearchOption(TestAdapterLoadingStrategy strategy, SearchOption defaultStrategyOption) {
+ if (strategy == TestAdapterLoadingStrategy.Default) {
+ return defaultStrategyOption;
+ }
+
+ var searchOption = SearchOption.TopDirectoryOnly;
+ if ((strategy & TestAdapterLoadingStrategy.Recursive) == TestAdapterLoadingStrategy.Recursive)
+ {
+ searchOption = SearchOption.AllDirectories;
+ }
+
+ return searchOption;
+ }
- TestPluginCache.Instance.DefaultExtensionPaths = defaultExtensionPaths;
+ private static IEnumerable ExpandTestAdapterPaths(string path, IFileHelper fileHelper, TestAdapterLoadingStrategy strategy)
+ {
+ var adapterPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(path));
+
+ // Default behavior is to only accept directories!
+ if (strategy == TestAdapterLoadingStrategy.Default)
+ {
+ return ExpandAdaptersWithDefaultStrategy(adapterPath, fileHelper);
+ }
+
+ var adapters = ExpandAdaptersWithExplicitStrategy(adapterPath, fileHelper, strategy);
+
+ return adapters.Distinct();
+ }
+
+ private static IEnumerable ExpandAdaptersWithExplicitStrategy(string path, IFileHelper fileHelper, TestAdapterLoadingStrategy strategy)
+ {
+ if ((strategy & TestAdapterLoadingStrategy.Explicit) != TestAdapterLoadingStrategy.Explicit)
+ {
+ return Enumerable.Empty();
}
+
+ if (fileHelper.Exists(path))
+ {
+ return new[] { path };
+ }
+ else if (fileHelper.DirectoryExists(path))
+ {
+ var searchOption = GetSearchOption(strategy, SearchOption.TopDirectoryOnly);
+
+ var adapterPaths = fileHelper.EnumerateFiles(
+ path,
+ searchOption,
+ TestPlatformConstants.TestAdapterEndsWithPattern,
+ TestPlatformConstants.TestLoggerEndsWithPattern,
+ TestPlatformConstants.DataCollectorEndsWithPattern,
+ TestPlatformConstants.RunTimeEndsWithPattern);
+
+ return adapterPaths;
+ }
+
+ EqtTrace.Warning(string.Format("AdapterPath Not Found:", path));
+ return Enumerable.Empty();
+ }
+
+ private static IEnumerable ExpandAdaptersWithDefaultStrategy(string path, IFileHelper fileHelper)
+ {
+ if (!fileHelper.DirectoryExists(path))
+ {
+ EqtTrace.Warning(string.Format("AdapterPath Not Found:", path));
+
+ return Enumerable.Empty();
+ }
+
+ return fileHelper.EnumerateFiles(
+ path,
+ SearchOption.AllDirectories,
+ TestPlatformConstants.TestAdapterEndsWithPattern,
+ TestPlatformConstants.TestLoggerEndsWithPattern,
+ TestPlatformConstants.DataCollectorEndsWithPattern,
+ TestPlatformConstants.RunTimeEndsWithPattern);
+ }
+
+ private static IEnumerable GetSources(TestRunCriteria testRunCriteria)
+ {
+ IEnumerable sources = testRunCriteria.Sources;
+ if (testRunCriteria.HasSpecificTests)
+ {
+ // If the test execution is with a test filter, group them by sources.
+ sources = testRunCriteria.Tests.Select(tc => tc.Source).Distinct();
+ }
+
+ return sources;
}
}
}
diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExtensionManager.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExtensionManager.cs
index 7bf7bb2d74..133409c6b8 100644
--- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExtensionManager.cs
+++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExtensionManager.cs
@@ -126,7 +126,6 @@ public LazyExtension TryGetTestExtension(Uri extensionUri
///
/// The URI of the test extension to be looked up.
/// The test extension or null if one was not found.
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Case insensitiveness needs to be supported.")]
public LazyExtension TryGetTestExtension(string extensionUri)
{
ValidateArg.NotNull(extensionUri, nameof(extensionUri));
diff --git a/src/Microsoft.TestPlatform.Common/Utilities/RunSettingsUtilities.cs b/src/Microsoft.TestPlatform.Common/Utilities/RunSettingsUtilities.cs
index 28f037c4c9..9d8fe81ec8 100644
--- a/src/Microsoft.TestPlatform.Common/Utilities/RunSettingsUtilities.cs
+++ b/src/Microsoft.TestPlatform.Common/Utilities/RunSettingsUtilities.cs
@@ -175,7 +175,6 @@ private static bool GetTreatNoTestsAsError(RunConfiguration runConfiguration)
/// Gets the test adapters path from the run configuration
///
/// Test run settings
- /// True to return null, if adapter paths is not set.
/// Test adapters paths
public static IEnumerable GetTestAdaptersPaths(string runSettings)
{
@@ -193,5 +192,16 @@ public static IEnumerable GetTestAdaptersPaths(string runSettings)
return testAdaptersPaths;
}
+ ///
+ /// Gets the test adapter loading strategy
+ ///
+ /// Test run settings
+ /// Test adapter loading strategy
+ internal static TestAdapterLoadingStrategy GetLoadingStrategy(string runSettings) {
+ var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(runSettings);
+
+ return runConfiguration?.TestAdapterLoadingStrategy ?? TestAdapterLoadingStrategy.Default;
+ }
+
}
}
diff --git a/src/Microsoft.TestPlatform.ObjectModel/Friends.cs b/src/Microsoft.TestPlatform.ObjectModel/Friends.cs
index 7e2123cd6d..63d5c25c54 100644
--- a/src/Microsoft.TestPlatform.ObjectModel/Friends.cs
+++ b/src/Microsoft.TestPlatform.ObjectModel/Friends.cs
@@ -8,6 +8,8 @@
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.TestPlatform.Extensions.MSPhoneAdapter, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("datacollector, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.TestPlatform.Common, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("vstest.console, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("Microsoft.VisualStudio.TestPlatform.Client, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("Microsoft.TestPlatform.ObjectModel.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("datacollector.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
diff --git a/src/Microsoft.TestPlatform.ObjectModel/RunSettings/RunConfiguration.cs b/src/Microsoft.TestPlatform.ObjectModel/RunSettings/RunConfiguration.cs
index bee3c87e8d..66dc0916d2 100644
--- a/src/Microsoft.TestPlatform.ObjectModel/RunSettings/RunConfiguration.cs
+++ b/src/Microsoft.TestPlatform.ObjectModel/RunSettings/RunConfiguration.cs
@@ -385,6 +385,11 @@ public string TestAdaptersPaths
}
}
+ ///
+ /// Gets or sets the test adapter loading strategy.
+ ///
+ internal TestAdapterLoadingStrategy TestAdapterLoadingStrategy { get; set; }
+
///
/// Gets or sets the execution thread apartment state.
///
@@ -571,6 +576,13 @@ public override XmlElement ToXml()
root.AppendChild(testAdaptersPaths);
}
+ if(this.TestAdapterLoadingStrategy != TestAdapterLoadingStrategy.Default)
+ {
+ XmlElement adapterLoadingStrategy = doc.CreateElement("TestAdapterLoadingStrategy");
+ adapterLoadingStrategy.InnerXml = this.TestAdapterLoadingStrategy.ToString();
+ root.AppendChild(adapterLoadingStrategy);
+ }
+
XmlElement treatTestAdapterErrorsAsWarnings = doc.CreateElement("TreatTestAdapterErrorsAsWarnings");
treatTestAdapterErrorsAsWarnings.InnerXml = this.TreatTestAdapterErrorsAsWarnings.ToString();
root.AppendChild(treatTestAdapterErrorsAsWarnings);
@@ -838,6 +850,19 @@ public static RunConfiguration FromXml(XmlReader reader)
runConfiguration.TestAdaptersPaths = reader.ReadElementContentAsString();
break;
+ case "TestAdapterLoadingStrategy":
+ XmlRunSettingsUtilities.ThrowOnHasAttributes(reader);
+ value = reader.ReadElementContentAsString();
+ if (Enum.TryParse(value, out var loadingStrategy)) {
+ runConfiguration.TestAdapterLoadingStrategy = loadingStrategy;
+ }
+ else {
+ throw new SettingsException(string.Format(CultureInfo.CurrentCulture,
+ Resources.Resources.InvalidSettingsIncorrectValue, Constants.RunConfigurationSettingsName, value, elementName));
+ }
+
+ break;
+
case "TreatTestAdapterErrorsAsWarnings":
XmlRunSettingsUtilities.ThrowOnHasAttributes(reader);
bool treatTestAdapterErrorsAsWarnings = false;
@@ -925,6 +950,7 @@ public static RunConfiguration FromXml(XmlReader reader)
XmlRunSettingsUtilities.ThrowOnHasAttributes(reader);
runConfiguration.DotnetHostPath = reader.ReadElementContentAsString();
break;
+
case "TreatNoTestsAsError":
XmlRunSettingsUtilities.ThrowOnHasAttributes(reader);
string treatNoTestsAsErrorValueString = reader.ReadElementContentAsString();
diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestAdapterLoadingStrategy.cs b/src/Microsoft.TestPlatform.ObjectModel/TestAdapterLoadingStrategy.cs
new file mode 100644
index 0000000000..a99a6ac105
--- /dev/null
+++ b/src/Microsoft.TestPlatform.ObjectModel/TestAdapterLoadingStrategy.cs
@@ -0,0 +1,47 @@
+// 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;
+
+namespace Microsoft.VisualStudio.TestPlatform.ObjectModel
+{
+ ///
+ /// Represents a loading strategy
+ ///
+ [Flags]
+ internal enum TestAdapterLoadingStrategy
+ {
+ ///
+ /// A strategy not defined, Test Platfrom will load adapters normally.
+ ///
+ Default = 0b0000_0000_0000_0000,
+
+ ///
+ /// Test Plarform will only load adapters specified by /TestAdapterPath (or RunConfiguration.TestAdaptersPaths node).
+ /// If a specific adapter path is provided, adapter will be loaded; if a directory path is provided adapters directly in that folder will be loaded.
+ /// If no adapter path is specified, test run will fail.
+ /// This will imply /InIsolation switch and force the tests to be run in an isolated process.
+ ///
+ Explicit = 0b0000_0000_0000_0001,
+
+ ///
+ /// Load adapters next to source.
+ ///
+ NextToSource = 0b0000_0000_0000_0010,
+
+ ///
+ /// Default runtime providers inside Extensions folder will be included
+ ///
+ DefaultRuntimeProviders = 0b0000_0000_0000_0100,
+
+ ///
+ /// Load adapters inside Extensions folder.
+ ///
+ ExtensionsDirectory = 0b0000_0000_0000_1000,
+
+ ///
+ /// Directory wide searches will be recursive, this is required to be with with NextToSource or Explicit.
+ ///
+ Recursive = 0b0001_0000_0000_0000,
+ }
+}
diff --git a/src/vstest.console/CommandLine/CommandLineOptions.cs b/src/vstest.console/CommandLine/CommandLineOptions.cs
index 4edd13929b..923b7b8127 100644
--- a/src/vstest.console/CommandLine/CommandLineOptions.cs
+++ b/src/vstest.console/CommandLine/CommandLineOptions.cs
@@ -132,7 +132,12 @@ public IEnumerable Sources
///
/// Path to the custom test adapters.
///
- public string TestAdapterPath { get; set; }
+ public string[] TestAdapterPath { get; set; }
+
+ ///
+ /// Test adapter loading strategy.
+ ///
+ public TestAdapterLoadingStrategy TestAdapterLoadingStrategy { get; set; }
///
/// Process Id of the process which launched vstest runner
@@ -304,6 +309,8 @@ public void AddSource(string source)
this.sources = this.sources.Union(matchingFiles).ToList();
}
+ public bool TestAdapterPathsSet => (TestAdapterPath?.Length ?? 0) != 0;
+
#endregion
#region Internal Methods
diff --git a/src/vstest.console/CommandLine/Executor.cs b/src/vstest.console/CommandLine/Executor.cs
index 4357c0af3a..7c0e9d0045 100644
--- a/src/vstest.console/CommandLine/Executor.cs
+++ b/src/vstest.console/CommandLine/Executor.cs
@@ -210,7 +210,14 @@ private int GetArgumentProcessors(string[] args, out List pr
// Examples: processors to enable loggers that are statically configured, and to start logging,
// should always be executed.
var processorsToAlwaysExecute = processorFactory.GetArgumentProcessorsToAlwaysExecute();
- processors.AddRange(processorsToAlwaysExecute);
+ foreach (var processor in processorsToAlwaysExecute)
+ {
+ if (processors.Any(i => i.Metadata.Value.CommandName == processor.Metadata.Value.CommandName)) {
+ continue;
+ }
+
+ processors.Add(ArgumentProcessorFactory.WrapLazyProcessorToInitializeOnInstantiation(processor));
+ }
// Initialize Runsettings with defaults
RunSettingsManager.Instance.AddDefaultRunSettings();
diff --git a/src/vstest.console/Processors/RunSpecificTestsArgumentProcessor.cs b/src/vstest.console/Processors/RunSpecificTestsArgumentProcessor.cs
index 96e3e44223..1916b86b47 100644
--- a/src/vstest.console/Processors/RunSpecificTestsArgumentProcessor.cs
+++ b/src/vstest.console/Processors/RunSpecificTestsArgumentProcessor.cs
@@ -283,7 +283,7 @@ private void ExecuteSelectedTests()
// No tests were discovered from the given sources.
warningMessage = string.Format(CultureInfo.CurrentUICulture, CommandLineResources.NoTestsAvailableInSources, string.Join(", ", this.commandLineOptions.Sources));
- if (string.IsNullOrEmpty(this.commandLineOptions.TestAdapterPath))
+ if (!commandLineOptions.TestAdapterPathsSet)
{
warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.StringFormatToJoinTwoStrings, warningMessage, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound);
}
@@ -385,7 +385,7 @@ private void TestRunRequest_OnRunCompletion(object sender, TestRunCompleteEventA
var testsFoundInAnySource = (e.TestRunStatistics == null) ? false : (e.TestRunStatistics.ExecutedTests > 0);
// Indicate the user to use testadapterpath command if there are no tests found
- if (!testsFoundInAnySource && string.IsNullOrEmpty(CommandLineOptions.Instance.TestAdapterPath) && this.commandLineOptions.TestCaseFilterValue == null)
+ if (!testsFoundInAnySource && !CommandLineOptions.Instance.TestAdapterPathsSet && this.commandLineOptions.TestCaseFilterValue == null)
{
this.output.Warning(false, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound);
}
diff --git a/src/vstest.console/Processors/RunTestsArgumentProcessor.cs b/src/vstest.console/Processors/RunTestsArgumentProcessor.cs
index 4e624019f2..a52c8e9619 100644
--- a/src/vstest.console/Processors/RunTestsArgumentProcessor.cs
+++ b/src/vstest.console/Processors/RunTestsArgumentProcessor.cs
@@ -257,7 +257,7 @@ private void TestRunRequest_OnRunCompletion(object sender, TestRunCompleteEventA
var testsFoundInAnySource = (e.TestRunStatistics == null) ? false : (e.TestRunStatistics.ExecutedTests > 0);
// Indicate the user to use test adapter path command if there are no tests found
- if (!testsFoundInAnySource && string.IsNullOrEmpty(CommandLineOptions.Instance.TestAdapterPath) && this.commandLineOptions.TestCaseFilterValue == null)
+ if (!testsFoundInAnySource && !CommandLineOptions.Instance.TestAdapterPathsSet && this.commandLineOptions.TestCaseFilterValue == null)
{
this.output.Warning(false, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound);
}
diff --git a/src/vstest.console/Processors/TestAdapterLoadingStrategyArgumentProcessor.cs b/src/vstest.console/Processors/TestAdapterLoadingStrategyArgumentProcessor.cs
new file mode 100644
index 0000000000..f265584dd1
--- /dev/null
+++ b/src/vstest.console/Processors/TestAdapterLoadingStrategyArgumentProcessor.cs
@@ -0,0 +1,256 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+
+ using Microsoft.VisualStudio.TestPlatform.Common;
+ using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
+ using Microsoft.VisualStudio.TestPlatform.Common.Utilities;
+ using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+ using Microsoft.VisualStudio.TestPlatform.Utilities;
+ using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
+ using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;
+
+ using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources;
+
+ ///
+ /// Allows the user to specify a order of loading custom adapters from.
+ ///
+ internal class TestAdapterLoadingStrategyArgumentProcessor : IArgumentProcessor
+ {
+ #region Constants
+
+ ///
+ /// The name of the command line argument that the TestAdapterLoadingStrategyArgumentProcessor handles.
+ ///
+ public const string CommandName = "/TestAdapterLoadingStrategy";
+
+ #endregion
+
+ private Lazy metadata;
+
+ private Lazy executor;
+
+ ///
+ /// Gets the metadata.
+ ///
+ public Lazy Metadata
+ {
+ get
+ {
+ if (this.metadata == null)
+ {
+ this.metadata = new Lazy(() => new TestAdapterLoadingStrategyArgumentProcessorCapabilities());
+ }
+
+ return this.metadata;
+ }
+ }
+
+ ///
+ /// Gets or sets the executor.
+ ///
+ public Lazy Executor
+ {
+ get
+ {
+ if (this.executor == null)
+ {
+ this.executor = new Lazy(() => new TestAdapterLoadingStrategyArgumentExecutor(CommandLineOptions.Instance, RunSettingsManager.Instance, ConsoleOutput.Instance, new FileHelper()));
+ }
+
+ return this.executor;
+ }
+
+ set
+ {
+ this.executor = value;
+ }
+ }
+ }
+
+ ///
+ /// The argument capabilities.
+ ///
+ internal class TestAdapterLoadingStrategyArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities
+ {
+ public override string CommandName => TestAdapterLoadingStrategyArgumentProcessor.CommandName;
+
+ public override bool AllowMultiple => false;
+
+ public override bool IsAction => false;
+
+ public override bool AlwaysExecute => true;
+
+ public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.TestAdapterLoadingStrategy;
+
+ public override string HelpContentResourceName => CommandLineResources.TestAdapterLoadingStrategyHelp;
+
+ public override HelpContentPriority HelpPriority => HelpContentPriority.TestAdapterLoadingStrategyArgumentProcessorHelpPriority;
+ }
+
+ ///
+ /// The argument executor.
+ ///
+ internal class TestAdapterLoadingStrategyArgumentExecutor : IArgumentExecutor
+ {
+ #region Fields
+ ///
+ /// Used for getting sources.
+ ///
+ private CommandLineOptions commandLineOptions;
+
+ ///
+ /// Run settings provider.
+ ///
+ private IRunSettingsProvider runSettingsManager;
+
+ ///
+ /// Used for sending output.
+ ///
+ private IOutput output;
+
+ ///
+ /// For file related operation
+ ///
+ private IFileHelper fileHelper;
+
+ #endregion
+
+ public const string DefaultStrategy = "Default";
+ public const string ExplicitStrategy = "Explicit";
+
+ ///
+ /// Default constructor.
+ ///
+ /// The options.
+ /// The test platform
+ public TestAdapterLoadingStrategyArgumentExecutor(CommandLineOptions options, IRunSettingsProvider runSettingsManager, IOutput output, IFileHelper fileHelper)
+ {
+ Contract.Requires(options != null);
+
+ this.commandLineOptions = options;
+ this.runSettingsManager = runSettingsManager;
+ this.output = output;
+ this.fileHelper = fileHelper;
+ }
+
+ #region IArgumentExecutor
+
+ ///
+ /// Initializes with the argument that was provided with the command.
+ ///
+ /// Argument that was provided with the command.
+ public void Initialize(string argument)
+ {
+ var strategy = TestAdapterLoadingStrategy.Default;
+
+ if (!string.IsNullOrEmpty(argument) && !Enum.TryParse(argument, out strategy))
+ {
+ throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterLoadingStrategyValueInvalid, argument));
+ }
+
+ if (strategy == TestAdapterLoadingStrategy.Recursive) {
+ throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterLoadingStrategyValueInvalidRecursive, $"{nameof(TestAdapterLoadingStrategy.Explicit)}, {nameof(TestAdapterLoadingStrategy.NextToSource)}"));
+ }
+
+ if (string.IsNullOrWhiteSpace(argument))
+ {
+ InitializeDefaultStrategy();
+ return;
+ }
+
+ InitializeStrategy(strategy);
+ }
+
+ private void InitializeDefaultStrategy()
+ {
+ ValidateTestAdapterPaths(TestAdapterLoadingStrategy.Default);
+
+ SetStrategy(TestAdapterLoadingStrategy.Default);
+ }
+
+ private void InitializeStrategy(TestAdapterLoadingStrategy strategy)
+ {
+ ValidateTestAdapterPaths(strategy);
+
+ if (!commandLineOptions.TestAdapterPathsSet && (strategy & TestAdapterLoadingStrategy.Explicit) == TestAdapterLoadingStrategy.Explicit)
+ {
+ throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterPathValueRequiredWhenStrategyXIsUsed, ExplicitStrategy));
+ }
+
+ SetStrategy(strategy);
+ }
+
+ private void ForceIsolation()
+ {
+ if (this.commandLineOptions.InIsolation)
+ {
+ return;
+ }
+
+ this.commandLineOptions.InIsolation = true;
+ this.runSettingsManager.UpdateRunSettingsNode(InIsolationArgumentExecutor.RunSettingsPath, "true");
+ }
+
+ private void ValidateTestAdapterPaths(TestAdapterLoadingStrategy strategy)
+ {
+ var testAdapterPaths = commandLineOptions.TestAdapterPath ?? new string[0];
+ if (!commandLineOptions.TestAdapterPathsSet)
+ {
+ testAdapterPaths = TestAdapterPathArgumentExecutor.SplitPaths(this.runSettingsManager.QueryRunSettingsNode("RunConfiguration.TestAdaptersPaths")).Union(testAdapterPaths).Distinct().ToArray();
+ }
+
+ for (var i = 0; i < testAdapterPaths.Length; i++)
+ {
+ var adapterPath = testAdapterPaths[i];
+ var testAdapterPath = this.fileHelper.GetFullPath(Environment.ExpandEnvironmentVariables(adapterPath));
+
+ if (strategy == TestAdapterLoadingStrategy.Default)
+ {
+ if (!this.fileHelper.DirectoryExists(testAdapterPath))
+ {
+ throw new CommandLineException(
+ string.Format(CultureInfo.CurrentCulture, CommandLineResources.InvalidTestAdapterPathCommand, adapterPath, CommandLineResources.TestAdapterPathDoesNotExist)
+ );
+ }
+ }
+
+ testAdapterPaths[i] = testAdapterPath;
+ }
+
+ this.runSettingsManager.UpdateRunSettingsNode("RunConfiguration.TestAdaptersPaths", string.Join(";", testAdapterPaths));
+ }
+
+ private void SetStrategy(TestAdapterLoadingStrategy strategy)
+ {
+ var adapterStrategy = strategy.ToString();
+
+ commandLineOptions.TestAdapterLoadingStrategy = strategy;
+ this.runSettingsManager.UpdateRunSettingsNode("RunConfiguration.TestAdapterLoadingStrategy", adapterStrategy);
+ if ((strategy & TestAdapterLoadingStrategy.Explicit) == TestAdapterLoadingStrategy.Explicit)
+ {
+ ForceIsolation();
+ }
+ }
+
+ ///
+ /// Executes the argument processor.
+ ///
+ /// The .
+ public ArgumentProcessorResult Execute()
+ {
+ // Nothing to do since we updated the parameter during initialize parameter
+ return ArgumentProcessorResult.Success;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/vstest.console/Processors/TestAdapterPathArgumentProcessor.cs b/src/vstest.console/Processors/TestAdapterPathArgumentProcessor.cs
index 120544e1c8..07fee0fdfe 100644
--- a/src/vstest.console/Processors/TestAdapterPathArgumentProcessor.cs
+++ b/src/vstest.console/Processors/TestAdapterPathArgumentProcessor.cs
@@ -123,7 +123,7 @@ internal class TestAdapterPathArgumentExecutor : IArgumentExecutor
///
/// Separators for multiple paths in argument.
///
- private readonly char[] argumentSeparators = new[] { ';' };
+ private static readonly char[] argumentSeparators = new[] { ';' };
#endregion
@@ -154,60 +154,33 @@ public TestAdapterPathArgumentExecutor(CommandLineOptions options, IRunSettingsP
/// Argument that was provided with the command.
public void Initialize(string argument)
{
- string invalidAdapterPathArgument = argument;
-
if (string.IsNullOrWhiteSpace(argument))
{
throw new CommandLineException(
string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterPathValueRequired));
}
- string customAdaptersPath;
-
- try
- {
- var testAdapterPaths = new List();
- var testAdapterFullPaths = new List();
-
- // VSTS task add double quotes around TestAdapterpath. For example if user has given TestAdapter path C:\temp,
- // Then VSTS task will add TestAdapterPath as "/TestAdapterPath:\"C:\Temp\"".
- // Remove leading and trailing ' " ' chars...
- argument = argument.Trim().Trim(new char[] { '\"' });
+ string[] customAdaptersPath;
- // Get test adapter paths from RunSettings.
- var testAdapterPathsInRunSettings = this.runSettingsManager.QueryRunSettingsNode("RunConfiguration.TestAdaptersPaths");
+ var testAdapterPaths = new List();
- if (!string.IsNullOrWhiteSpace(testAdapterPathsInRunSettings))
- {
- testAdapterPaths.AddRange(SplitPaths(testAdapterPathsInRunSettings));
- }
+ // VSTS task add double quotes around TestAdapterpath. For example if user has given TestAdapter path C:\temp,
+ // Then VSTS task will add TestAdapterPath as "/TestAdapterPath:\"C:\Temp\"".
+ // Remove leading and trailing ' " ' chars...
+ argument = argument.Trim().Trim(new char[] { '\"' });
- testAdapterPaths.AddRange(SplitPaths(argument));
-
- foreach (var testadapterPath in testAdapterPaths)
- {
- // TestAdaptersPaths could contain environment variables
- var testAdapterFullPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(testadapterPath));
+ // Get test adapter paths from RunSettings.
+ var testAdapterPathsInRunSettings = this.runSettingsManager.QueryRunSettingsNode("RunConfiguration.TestAdaptersPaths");
- if (!this.fileHelper.DirectoryExists(testAdapterFullPath))
- {
- invalidAdapterPathArgument = testadapterPath;
- throw new DirectoryNotFoundException(CommandLineResources.TestAdapterPathDoesNotExist);
- }
-
- testAdapterFullPaths.Add(testAdapterFullPath);
- }
-
- customAdaptersPath = string.Join(";", testAdapterFullPaths.Distinct().ToArray());
-
- this.runSettingsManager.UpdateRunSettingsNode("RunConfiguration.TestAdaptersPaths", customAdaptersPath);
- }
- catch (Exception e)
+ if (!string.IsNullOrWhiteSpace(testAdapterPathsInRunSettings))
{
- throw new CommandLineException(
- string.Format(CultureInfo.CurrentCulture, CommandLineResources.InvalidTestAdapterPathCommand, invalidAdapterPathArgument, e.Message));
+ testAdapterPaths.AddRange(SplitPaths(testAdapterPathsInRunSettings));
}
+ testAdapterPaths.AddRange(SplitPaths(argument));
+ customAdaptersPath = testAdapterPaths.Distinct().ToArray();
+
+ this.runSettingsManager.UpdateRunSettingsNode("RunConfiguration.TestAdaptersPaths", string.Join(";", customAdaptersPath));
this.commandLineOptions.TestAdapterPath = customAdaptersPath;
}
@@ -216,7 +189,7 @@ public void Initialize(string argument)
///
/// Source paths joined by semicolons.
/// Paths.
- private string[] SplitPaths(string paths)
+ public static string[] SplitPaths(string paths)
{
if (string.IsNullOrWhiteSpace(paths))
{
diff --git a/src/vstest.console/Processors/Utilities/ArgumentProcessorFactory.cs b/src/vstest.console/Processors/Utilities/ArgumentProcessorFactory.cs
index 33b9729c6e..0a87380b91 100644
--- a/src/vstest.console/Processors/Utilities/ArgumentProcessorFactory.cs
+++ b/src/vstest.console/Processors/Utilities/ArgumentProcessorFactory.cs
@@ -202,11 +202,10 @@ public IArgumentProcessor CreateDefaultActionArgumentProcessor()
/// Gets the argument processors that are tagged as special and to be always executed.
/// The Lazy's that are returned will initialize the underlying argument processor when first accessed.
///
- /// The argument processors that are tagged as special and to be always executed.
+ /// The argument processors that are tagged as to be always executed.
public IEnumerable GetArgumentProcessorsToAlwaysExecute()
{
- return SpecialCommandToProcessorMap.Values
- .Where(lazyProcessor => lazyProcessor.Metadata.Value.IsSpecialCommand && lazyProcessor.Metadata.Value.AlwaysExecute);
+ return argumentProcessors.Where(lazyProcessor => lazyProcessor.Metadata.Value.AlwaysExecute);
}
#endregion
@@ -220,6 +219,7 @@ public IEnumerable GetArgumentProcessorsToAlwaysExecute()
new RunTestsArgumentProcessor(),
new RunSpecificTestsArgumentProcessor(),
new TestAdapterPathArgumentProcessor(),
+ new TestAdapterLoadingStrategyArgumentProcessor(),
new TestCaseFilterArgumentProcessor(),
new ParentProcessIdArgumentProcessor(),
new PortArgumentProcessor(),
@@ -287,7 +287,7 @@ private void BuildCommandMaps()
/// The lazy processor.
/// The argument with which the real processor should be initialized.
/// The decorated lazy processor.
- private static IArgumentProcessor WrapLazyProcessorToInitializeOnInstantiation(
+ public static IArgumentProcessor WrapLazyProcessorToInitializeOnInstantiation(
IArgumentProcessor processor,
string initArg = null)
{
diff --git a/src/vstest.console/Processors/Utilities/ArgumentProcessorPriority.cs b/src/vstest.console/Processors/Utilities/ArgumentProcessorPriority.cs
index eb14b21b87..3a3616304e 100644
--- a/src/vstest.console/Processors/Utilities/ArgumentProcessorPriority.cs
+++ b/src/vstest.console/Processors/Utilities/ArgumentProcessorPriority.cs
@@ -43,10 +43,18 @@ internal enum ArgumentProcessorPriority
///
/// Priority of TestAdapterPathArgumentProcessor.
+ ///
/// The priority of TestAdapterPath processor is more than the logger because logger initialization
/// loads the extensions which are incomplete if custom test adapter is enabled
///
- TestAdapterPath = 10,
+ TestAdapterPath = 9,
+
+ ///
+ /// Priority of TestAdapterLoadingStrategyArgumentProcessor.
+ ///
+ /// This needs to be higher than most of other arguments, because it affects where we look for test adapters.
+ ///
+ TestAdapterLoadingStrategy = 10,
///
/// Priority of processors that needs to update runsettings.
diff --git a/src/vstest.console/Processors/Utilities/HelpContentPriority.cs b/src/vstest.console/Processors/Utilities/HelpContentPriority.cs
index ecd57d18de..27917088a4 100644
--- a/src/vstest.console/Processors/Utilities/HelpContentPriority.cs
+++ b/src/vstest.console/Processors/Utilities/HelpContentPriority.cs
@@ -22,6 +22,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors
/// --ListTests
/// --Parallel
/// --TestAdapterPath
+ /// --TestAdapterLoadingStrategy
///
/// Diagnose/Report
/// --Diag
@@ -33,7 +34,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors
/// --Port
///
/// Help
- /// -Help
+ /// -�Help
///
internal enum HelpContentPriority
{
@@ -97,6 +98,11 @@ internal enum HelpContentPriority
///
TestAdapterPathArgumentProcessorHelpPriority,
+ ///
+ /// TestAdapterLoadingStrategyArgumentProcessor Help
+ ///
+ TestAdapterLoadingStrategyArgumentProcessorHelpPriority,
+
///
/// EnableDiagArgumentProcessor Help
///
diff --git a/src/vstest.console/Resources/Resources.Designer.cs b/src/vstest.console/Resources/Resources.Designer.cs
index eda555c483..46a8a88ca5 100644
--- a/src/vstest.console/Resources/Resources.Designer.cs
+++ b/src/vstest.console/Resources/Resources.Designer.cs
@@ -1507,6 +1507,39 @@ internal static string SwitchToNoIsolation {
}
}
+ ///
+ /// Looks up a localized string similar to --TestAdapterLoadingStrategy|/TestAdapterLoadingStrategy:<strategy>
+ /// This affects adapter loading behavior.
+ ///
+ /// Currently supported behaviours:
+ /// - Explicit: Test Plarform will only load adapters specified by /TestAdapterPath (or RunConfiguration.TestAdaptersPaths node).
+ /// If a specific adapter path is provided, adapter will be loaded; if a directory path is provided adapters directly in that folder will be loaded, unless Recursive option is also specified.
+ /// [rest of string was truncated]";.
+ ///
+ internal static string TestAdapterLoadingStrategyHelp {
+ get {
+ return ResourceManager.GetString("TestAdapterLoadingStrategyHelp", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Specified value ({0}) for /TestAdapterLoadingStrategy is invalid!.
+ ///
+ internal static string TestAdapterLoadingStrategyValueInvalid {
+ get {
+ return ResourceManager.GetString("TestAdapterLoadingStrategyValueInvalid", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to "Recursive" adapter loading strategy is cannot be used by itself. Please specify at least one of: {0}.
+ ///
+ internal static string TestAdapterLoadingStrategyValueInvalidRecursive {
+ get {
+ return ResourceManager.GetString("TestAdapterLoadingStrategyValueInvalidRecursive", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The custom test adapter search path provided was not found, provide a valid path and try again..
///
@@ -1537,6 +1570,15 @@ internal static string TestAdapterPathValueRequired {
}
}
+ ///
+ /// Looks up a localized string similar to The /TestAdapterPath parameter needs to be provided when "{0}" test adapter loading strategy is specified!.
+ ///
+ internal static string TestAdapterPathValueRequiredWhenStrategyXIsUsed {
+ get {
+ return ResourceManager.GetString("TestAdapterPathValueRequiredWhenStrategyXIsUsed", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to --TestCaseFilter|/TestCaseFilter:<Expression>
/// Run tests that match the given expression.
diff --git a/src/vstest.console/Resources/Resources.resx b/src/vstest.console/Resources/Resources.resx
index d80d990139..654a14e7d3 100644
--- a/src/vstest.console/Resources/Resources.resx
+++ b/src/vstest.console/Resources/Resources.resx
@@ -560,6 +560,44 @@
The /TestAdapterPath parameter requires a value, which is path of a location containing custom test adapters. Example: /TestAdapterPath:c:\MyCustomAdapters
+
+ --TestAdapterLoadingStrategy|/TestAdapterLoadingStrategy:<strategy>
+ This affects adapter loading behavior.
+
+ Currently supported behaviours:
+ - Explicit: Test Plarform will only load adapters specified by /TestAdapterPath (or RunConfiguration.TestAdaptersPaths node).
+ If a specific adapter path is provided, adapter will be loaded; if a directory path is provided adapters directly in that folder will be loaded, unless Recursive option is also specified.
+ If no adapter path is specified, test run will fail.
+ This will imply /InIsolation switch and force the tests to be run in an isolated process.
+
+ - Default: Test Platfrom will load adapters is if this argument has not beed specified.
+ It will pick up extensions from next to source, provided aditional adapter paths and from the default directory.
+
+ - DefaultRuntimeProviders: Load default runtime providers shipped with Test Platform.
+ If this is not specified when "Explicit" option is set, a test host provider need to be specified explicitly.
+
+ - ExtensionsDirectory: Load adapters inside Extensions folder.
+
+ - NextToSource: Load adapters next to source.
+
+ - Recursive: Recursively search folders when loading adapters. This requires "Explicit" or "NextToSource" to be specified too.
+
+ Do not translate "Default", "DefaultRuntimeProviders", "ExtensionsDirectory", "NextToSource" or "Recursive".
+
+
+ Specified value ({0}) for /TestAdapterLoadingStrategy is invalid!
+
+
+ Recursive adapter loading strategy is cannot be used by itself. Please combine with one or more of: {0}
+
+ - Do not translate "Recursive",
+ - {0} is the strategy names, seperated by comma: for example "Explicit, NextToSource"
+
+
+
+ The /TestAdapterPath parameter needs to be provided when "{0}" test adapter loading strategy is specified!
+ {0} is the strategy name, "Explicit" for example.
+
--TestCaseFilter|/TestCaseFilter:<Expression>
Run tests that match the given expression.
diff --git a/src/vstest.console/Resources/xlf/Resources.cs.xlf b/src/vstest.console/Resources/xlf/Resources.cs.xlf
index 064a9ba62c..02b004475f 100644
--- a/src/vstest.console/Resources/xlf/Resources.cs.xlf
+++ b/src/vstest.console/Resources/xlf/Resources.cs.xlf
@@ -1108,6 +1108,60 @@
Testovací běh se přerušil s chybou {0}.
+
+
+ --TestAdapterLoadingStrategy|/TestAdapterLoadingStrategy:<strategy>
+ This affects adapter loading behavior.
+
+ Currently supported behaviours:
+ - Explicit: Test Plarform will only load adapters specified by /TestAdapterPath (or RunConfiguration.TestAdaptersPaths node).
+ If a specific adapter path is provided, adapter will be loaded; if a directory path is provided adapters directly in that folder will be loaded.
+ If no adapter path is specified, test run will fail.
+ This will imply /InIsolation switch and force the tests to be run in an isolated process.
+
+ - Default: Test Platfrom will load adapters is if this argument has not beed specified.
+ It will pick up extensions from next to source, provided aditional adapter paths and from the default directory.
+
+ Do not translate "Default", "DefaultRuntimeProviders", "ExtensionsDirectory", "NextToSource" or "Recursive".
+
+
+
+ Specified value for /TestAdapterLoadingStrategy is invalid!
+
+
+
+
+ The /TestAdapterPath parameter needs to be provided when "{0}" test adapter loading strategy is specified!
+ {0} is the strategy name, "Explicit" for example.
+
+
+
+ "Recursive" adapter loading strategy is cannot be used by itself. Please specify at least one of: {0}
+
+ - Do not translate "Recursive",
+ - {0} is the strategy names, seperated by comma: for example "Explicit, NextToSource"
+
+