// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#if !WINDOWS_UWP
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
///
/// The MSTest settings.
///
#pragma warning disable CA1852 // Seal internal types - Inherited in test
internal class MSTestAdapterSettings
{
///
/// Initializes a new instance of the class.
///
public MSTestAdapterSettings()
{
DeleteDeploymentDirectoryAfterTestRunIsComplete = true;
DeploymentEnabled = true;
DeployTestSourceDependencies = true;
SearchDirectories = [];
Configuration = null;
}
///
/// Gets a value indicating whether deployment is enable or not.
///
public bool DeploymentEnabled { get; private set; }
///
/// Gets a value indicating whether deployment directory has to be deleted after test run.
///
public bool DeleteDeploymentDirectoryAfterTestRunIsComplete { get; private set; }
///
/// Gets a value indicating whether the test source references are to deployed.
///
public bool DeployTestSourceDependencies { get; private set; }
///
/// Gets list of paths recursive or non recursive paths.
///
protected List SearchDirectories { get; private set; }
internal static IConfiguration? Configuration { get; set; }
///
/// Convert the parameter xml to TestSettings.
///
/// Reader to load the settings from.
/// An instance of the class.
public static MSTestAdapterSettings ToSettings(XmlReader reader)
{
if (reader is null)
{
throw new ArgumentNullException(nameof(reader));
}
// Expected format of the xml is: -
//
//
// true
// true
// true
//
//
//
// ...// by default includeSubDirectories is false
//
//
MSTestAdapterSettings settings = MSTestSettingsProvider.Settings;
if (!reader.IsEmptyElement)
{
reader.Read();
while (reader.NodeType == XmlNodeType.Element)
{
bool result;
string elementName = reader.Name.ToUpperInvariant();
switch (elementName)
{
case "ASSEMBLYRESOLUTION":
{
settings.ReadAssemblyResolutionPath(reader);
break;
}
case "DEPLOYMENTENABLED":
{
if (bool.TryParse(reader.ReadInnerXml(), out result))
{
settings.DeploymentEnabled = result;
}
break;
}
case "DEPLOYTESTSOURCEDEPENDENCIES":
{
if (bool.TryParse(reader.ReadInnerXml(), out result))
{
settings.DeployTestSourceDependencies = result;
}
break;
}
case "DELETEDEPLOYMENTDIRECTORYAFTERTESTRUNISCOMPLETE":
{
if (bool.TryParse(reader.ReadInnerXml(), out result))
{
settings.DeleteDeploymentDirectoryAfterTestRunIsComplete = result;
}
break;
}
default:
{
reader.Skip();
break;
}
}
}
}
return settings;
}
private static void ParseBooleanSetting(IConfiguration configuration, string key, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
if (bool.TryParse(value, out bool result))
{
setSetting(result);
}
}
internal static MSTestAdapterSettings ToSettings(IConfiguration configuration)
{
// Expected format of the json is: -
//
// "mstest" : {
// "deployment" : {
// "enabled": true / false,
// "deployTestSourceDependencies": true / false,
// "deleteDeploymentDirectoryAfterTestRunIsComplete": true / false
// },
// ... remaining settings
// }
MSTestAdapterSettings settings = MSTestSettingsProvider.Settings;
Configuration = configuration;
ParseBooleanSetting(configuration, "deployment:enabled", value => settings.DeploymentEnabled = value);
ParseBooleanSetting(configuration, "deployment:deployTestSourceDependencies", value => settings.DeployTestSourceDependencies = value);
ParseBooleanSetting(configuration, "deployment:deleteDeploymentDirectoryAfterTestRunIsComplete", value => settings.DeleteDeploymentDirectoryAfterTestRunIsComplete = value);
settings.ReadAssemblyResolutionPath(configuration);
return settings;
}
///
/// Determines if the AppDomain are disabled based of the runsettings.
///
/// The XML runsettings content.
/// true if AppDomains are disabled; false otherwise.
public static bool IsAppDomainCreationDisabled(string? settingsXml)
{
// Expected format of the json is: -
// "mstest" : {
// "execution": {
// "disableAppDomain": true,
// }
// }
if (StringEx.IsNullOrEmpty(settingsXml) && Configuration is null)
{
return false;
}
bool disableAppDomain = true;
// HACK: When running VSTest, and VSTest didn't create TestHostAppDomain (default behavior), we must be enabling appdomain in MSTest.
// Otherwise, we will not merge app.config properly, nor we will have correct BaseDirectory of current domain.
// This detects if we run in testhost.*.exe or in vstest.console.exe.This covers all: running with vstest.console.exe because there we can run in both modes, running with dotnet test or VS, because there we can run only in testhost(in isolation).
#if NETFRAMEWORK
if (AppDomain.CurrentDomain.Id == 1 &&
(AppDomain.CurrentDomain.FriendlyName.StartsWith("testhost.", StringComparison.Ordinal) ||
AppDomain.CurrentDomain.FriendlyName.StartsWith("vstest.console.", StringComparison.Ordinal)) &&
AppDomain.CurrentDomain.FriendlyName.EndsWith(".exe", StringComparison.Ordinal))
{
disableAppDomain = false;
}
#endif
if (!StringEx.IsNullOrEmpty(settingsXml))
{
StringReader stringReader = new(settingsXml);
var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings);
var xmlDoc = new XmlDocument() { XmlResolver = null };
xmlDoc.Load(reader);
if (bool.TryParse(xmlDoc["RunSettings"]?["RunConfiguration"]?["DisableAppDomain"]?.InnerText, out bool result))
{
disableAppDomain = result;
}
}
string? isAppDomainDisabled = Configuration?["mstest:execution:disableAppDomain"];
if (!StringEx.IsNullOrEmpty(isAppDomainDisabled) &&
bool.TryParse(isAppDomainDisabled, out bool resultFromConfiguration))
{
disableAppDomain = resultFromConfiguration;
}
return disableAppDomain;
}
///
/// Returns the array of path with recursive property true/false from comma separated paths
/// for ex: paths = c:\a\b;e:\balh\foo;%SystemDrive%\SomeDirectory and recursive = true
/// it will return an list {{c:\a\b, true}, {e:\balh\foo, true}, {c:\somedirectory, true}}.
///
/// the base directory for relative path.
/// RecursiveDirectoryPath information.
public List GetDirectoryListWithRecursiveProperty(string baseDirectory)
{
List directoriesList = [];
foreach (RecursiveDirectoryPath recPath in SearchDirectories)
{
// If path has environment variable, then resolve it
string? directoryPath = ResolveEnvironmentVariableAndReturnFullPathIfExist(recPath.DirectoryPath, baseDirectory);
if (!StringEx.IsNullOrEmpty(directoryPath))
{
directoriesList.Add(new RecursiveDirectoryPath(directoryPath, recPath.IncludeSubDirectories));
}
}
return directoriesList;
}
///
/// Gets the full path and expands any environment variables contained in the path.
///
/// The path to be expanded.
/// The base directory for the path which is not rooted path.
/// The expanded path.
internal string? ResolveEnvironmentVariableAndReturnFullPathIfExist(string? path, string? baseDirectory)
{
// Trim beginning and trailing white space from the path.
path = path?.Trim(' ', '\t');
if (StringEx.IsNullOrEmpty(path))
{
return null;
}
string? warningMessage = null;
// Expand any environment variables in the path.
path = ExpandEnvironmentVariables(path);
// If the path is a relative path, expand it relative to the base directory
if (!Path.IsPathRooted(path))
{
if (!StringEx.IsNullOrEmpty(baseDirectory))
{
path = Path.Combine(baseDirectory, path);
}
else
{
warningMessage = $"The Directory: {path}, has following problem: This is not an absolute path. A base directory should be provided for this to be used as a relative path.";
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Warning(warningMessage);
}
return null;
}
}
try
{
// Get the full path.
// This will cleanup the path converting any "..\" to the appropriate value
// and convert any alternative directory separators to "\"
path = Path.GetFullPath(path);
}
catch (Exception e)
{
warningMessage = e.Message;
}
if (!StringEx.IsNullOrEmpty(warningMessage))
{
warningMessage = $"The Directory: {path}, has following problem: {warningMessage}";
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Warning(warningMessage);
}
return null;
}
if (DoesDirectoryExist(path))
{
return path;
}
// generate warning that path does not exist.
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Warning($"The Directory: {path}, does not exist.");
}
return null;
}
///
/// Verifies if a directory exists.
///
/// path.
/// True if directory exists.
/// Only present for unit testing scenarios.
protected virtual bool DoesDirectoryExist(string path) => Directory.Exists(path);
///
/// Expands any environment variables in the path provided.
///
/// path.
/// expanded string.
/// Only present for unit testing scenarios.
protected virtual string ExpandEnvironmentVariables(string path) => Environment.ExpandEnvironmentVariables(path);
private void ReadAssemblyResolutionPath(XmlReader reader)
{
if (reader is null)
{
throw new ArgumentNullException(nameof(reader));
}
// Expected format of the xml is: -
//
//
//
//
// ...// by default includeSubDirectories is false
//
bool empty = reader.IsEmptyElement;
reader.Read();
if (!empty)
{
while (reader.NodeType == XmlNodeType.Element)
{
if (string.Equals("Directory", reader.Name, StringComparison.OrdinalIgnoreCase))
{
string? recursiveAttribute = reader.GetAttribute("includeSubDirectories");
// read the path specified
string? path = reader.GetAttribute("path");
if (!StringEx.IsNullOrEmpty(path))
{
// Do we have to look in sub directory for dependent dll.
bool includeSubDirectories = string.Equals(recursiveAttribute, "true", StringComparison.OrdinalIgnoreCase);
SearchDirectories.Add(new RecursiveDirectoryPath(path, includeSubDirectories));
}
}
else
{
string message = string.Format(CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlElement, reader.Name, "AssemblyResolution");
throw new AdapterSettingsException(message);
}
// Move to the next element under tag AssemblyResolution
reader.Read();
}
}
// go to the end of the element.
reader.ReadEndElement();
}
private void ReadAssemblyResolutionPath(IConfiguration configuration)
{
// Expected format of the json is: -
//
// "mstest" : {
// "assemblyResolution" : [
// { "path" : "..." , includeSubDirectories: "true" } ,
// { "path" : "..." , includeSubDirectories: "true" } ,
// { "path" : "..." , includeSubDirectories: "true" } ,
// ...
// ]
// ... remaining settings
// }
int index = 0;
while (configuration[$"mstest:assemblyResolution:{index}:path"] is string path)
{
if (!StringEx.IsNullOrEmpty(path))
{
// Default includeSubDirectories to false if not provided
bool includeSubDirectories = false;
ParseBooleanSetting(configuration, $"assemblyResolution:{index++}:includeSubDirectories", value => includeSubDirectories = value);
SearchDirectories.Add(new RecursiveDirectoryPath(path, includeSubDirectories));
}
}
}
}
#endif