// 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 && !WIN_UI #if NETFRAMEWORK using System.Security; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; #if NETFRAMEWORK using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; #endif namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; internal sealed class DeploymentUtility : DeploymentUtilityBase { public DeploymentUtility() : base() { } public DeploymentUtility(DeploymentItemUtility deploymentItemUtility, AssemblyUtility assemblyUtility, FileUtility fileUtility) : base(deploymentItemUtility, assemblyUtility, fileUtility) { } public override void AddDeploymentItemsBasedOnMsTestSetting(string testSourceHandler, IList deploymentItems, List warnings) { #if NETFRAMEWORK if (MSTestSettingsProvider.Settings.DeployTestSourceDependencies) { PlatformServiceProvider.Instance.AdapterTraceLogger.Info("Adding the references and satellite assemblies to the deployment items list"); // Get the referenced assemblies. ProcessNewStorage(testSourceHandler, deploymentItems, warnings); // Get the satellite assemblies IEnumerable satelliteItems = GetSatellites(deploymentItems, testSourceHandler, warnings); foreach (DeploymentItem satelliteItem in satelliteItems) { DeploymentItemUtility.AddDeploymentItem(deploymentItems, satelliteItem); } } else { PlatformServiceProvider.Instance.AdapterTraceLogger.Info("Adding the test source directory to the deployment items list"); DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(Path.GetDirectoryName(testSourceHandler))); } #else // It should add items from bin\debug but since deployment items in netcore are run from bin\debug only, so no need to implement it #endif } #if !NETCOREAPP private static int ProcessId { get { int processId = field; if (processId == 0) { field = processId = GetProcessId(); // Assume that process Id zero is invalid for user processes. It holds for all mainstream operating systems. Debug.Assert(processId != 0, "processId is expected to be non-zero."); } return processId; static int GetProcessId() { using var process = Process.GetCurrentProcess(); return process.Id; } } } #else private static int ProcessId => Environment.ProcessId; #endif /// /// Get root deployment directory. /// /// The base directory. /// Root deployment directory. public override string GetRootDeploymentDirectory(string baseDirectory) { string dateTimeSuffix = $"{DateTime.Now.ToString("yyyyMMddTHHmmss", DateTimeFormatInfo.InvariantInfo)}_{ProcessId}"; string directoryName = string.Format(CultureInfo.InvariantCulture, Resource.TestRunName, DeploymentFolderPrefix, #if NETFRAMEWORK Environment.UserName, #else Environment.GetEnvironmentVariable("USERNAME") ?? Environment.GetEnvironmentVariable("USER"), #endif dateTimeSuffix); directoryName = FileUtility.ReplaceInvalidFileNameCharacters(directoryName); return FileUtility.GetNextIterationDirectoryName(baseDirectory, directoryName); } protected override void AddDependenciesOfDeploymentItem(string deploymentItemFile, IList filesToDeploy, IList warnings) { #if NETFRAMEWORK var dependencies = new List(); AddDependencies(deploymentItemFile, null, dependencies, warnings); foreach (DeploymentItem dependencyItem in dependencies) { DebugEx.Assert(Path.IsPathRooted(dependencyItem.SourcePath), "Path of the dependency " + dependencyItem.SourcePath + " is not rooted."); // Add dependencies to filesToDeploy. filesToDeploy.Add(dependencyItem.SourcePath); } #else // Its implemented only in full framework project as dependent files are not fetched in netcore. #endif } #if NETFRAMEWORK [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] public void ProcessNewStorage(string testSourceHandler, IList deploymentItems, IList warnings) { // Add deployment items and process .config files only for storages we have not processed before. if (!DeploymentItemUtility.IsValidDeploymentItem(testSourceHandler, string.Empty, out string? errorMessage)) { warnings.Add(errorMessage); return; } DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(testSourceHandler, string.Empty, DeploymentItemOriginType.TestStorage)); // Deploy .config file if exists, only for assemblies, i.e. DLL and EXE. // First check .config, then if not found check for App.Config // and deploy AppConfig to .config. if (AssemblyUtility.IsAssemblyExtension(Path.GetExtension(testSourceHandler))) { string? configFile = AddTestSourceConfigFileIfExists(testSourceHandler, deploymentItems); // Deal with test dependencies: update dependencyDeploymentItems and missingDependentAssemblies. try { // We look for dependent assemblies only for DLL and EXE's. AddDependencies(testSourceHandler, configFile, deploymentItems, warnings); } catch (Exception e) { string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorFailedToDeployDependencies, testSourceHandler, e); warnings.Add(warning); } } } public IEnumerable GetSatellites(IEnumerable deploymentItems, string testSourceHandler, IList warnings) { List satellites = []; foreach (DeploymentItem item in deploymentItems) { // We do not care about deployment items which are directories because in that case we deploy all files underneath anyway. string? path = null; try { path = GetFullPathToDeploymentItemSource(item.SourcePath, testSourceHandler); path = Path.GetFullPath(path); if (StringEx.IsNullOrEmpty(path) || !AssemblyUtility.IsAssemblyExtension(Path.GetExtension(path)) || !FileUtility.DoesFileExist(path) || !AssemblyUtility.IsAssembly(path)) { continue; } } catch (Exception ex) when (ex is ArgumentException or SecurityException or IOException or NotSupportedException) { // IOException covers PathTooLongException. if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DeploymentManager.GetSatellites: {0}", ex); } } // Note: now Path operations with itemPath should not result in any exceptions. // path is already canonicalized. // If we cannot access satellite due to security, etc, we report warning. try { string itemDir = Path.GetDirectoryName(path); List itemSatellites = AssemblyUtility.GetSatelliteAssemblies(path!); foreach (string satellite in itemSatellites) { DebugEx.Assert(!StringEx.IsNullOrEmpty(satellite), "DeploymentManager.DoDeployment: got empty satellite!"); DebugEx.Assert( satellite.StartsWith(itemDir, StringComparison.OrdinalIgnoreCase), "DeploymentManager.DoDeployment: Got satellite that does not start with original item path"); string satelliteDir = Path.GetDirectoryName(satellite); string localeDir = itemDir.Length > satelliteDir.Length ? string.Empty : satelliteDir.Substring(itemDir.Length + 1); string relativeOutputDir = Path.Combine(item.RelativeOutputDirectory, localeDir); // Now finally add the item! DeploymentItem satelliteItem = new(satellite, relativeOutputDir, DeploymentItemOriginType.Satellite); DeploymentItemUtility.AddDeploymentItem(satellites, satelliteItem); } } catch (Exception ex) when (ex is ArgumentException or SecurityException or IOException) { // IOException covers PathTooLongException. if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DeploymentManager.GetSatellites: {0}", ex); } string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorGettingSatellite, item, ex.GetType(), ex.GetExceptionMessage()); warnings.Add(warning); } } return satellites; } /// /// Process test storage and add dependent assemblies to dependencyDeploymentItems. /// /// The test source. /// The config file. /// Deployment items. /// Warnings. private void AddDependencies(string testSourceHandler, string? configFile, IList deploymentItems, IList warnings) { DebugEx.Assert(!StringEx.IsNullOrEmpty(testSourceHandler), "testSourceHandler should not be null or empty."); // config file can be null. DebugEx.Assert(deploymentItems != null, "deploymentItems should not be null."); DebugEx.Assert(Path.IsPathRooted(testSourceHandler), "path should be rooted."); var sw = Stopwatch.StartNew(); // Note: if this is not an assembly we simply return empty array, also: // we do recursive search and report missing. IReadOnlyList references = AssemblyUtility.GetFullPathToDependentAssemblies(testSourceHandler, configFile, out IList? warningList); foreach (string warning in warningList) { warnings.Add(warning); } if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Source:{0} has following references", testSourceHandler); PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Resolving dependencies took {0} ms", sw.ElapsedMilliseconds); } foreach (string reference in references) { DeploymentItem deploymentItem = new(reference, string.Empty, DeploymentItemOriginType.Dependency); DeploymentItemUtility.AddDeploymentItem(deploymentItems, deploymentItem); if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Reference:{0} ", reference); } } } #endif } #endif