// 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