diff --git a/src/Microsoft.TestPlatform.Common/Interfaces/Engine/IDataCollectorAttachmentsProcessorsFactory.cs b/src/Microsoft.TestPlatform.Common/Interfaces/Engine/IDataCollectorAttachmentsProcessorsFactory.cs index 771b310d47..3dcd299b28 100644 --- a/src/Microsoft.TestPlatform.Common/Interfaces/Engine/IDataCollectorAttachmentsProcessorsFactory.cs +++ b/src/Microsoft.TestPlatform.Common/Interfaces/Engine/IDataCollectorAttachmentsProcessorsFactory.cs @@ -27,7 +27,7 @@ internal interface IDataCollectorAttachmentsProcessorsFactory /// /// Registered data collector attachment processor /// -internal class DataCollectorAttachmentProcessor +internal class DataCollectorAttachmentProcessor : IDisposable { /// /// Data collector FriendlyName @@ -44,4 +44,9 @@ public DataCollectorAttachmentProcessor(string friendlyName, IDataCollectorAttac FriendlyName = string.IsNullOrEmpty(friendlyName) ? throw new ArgumentException("Invalid FriendlyName", nameof(friendlyName)) : friendlyName; DataCollectorAttachmentProcessorInstance = dataCollectorAttachmentProcessor; } + + public void Dispose() + { + (DataCollectorAttachmentProcessorInstance as IDisposable)?.Dispose(); + } } diff --git a/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs b/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs index a9cc94e7b4..ae8a17e400 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs +++ b/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs @@ -21,7 +21,6 @@ static FeatureFlag() { FeatureFlags.Add(ARTIFACTS_POSTPROCESSING, true); FeatureFlags.Add(ARTIFACTS_POSTPROCESSING_SDK_KEEP_OLD_UX, false); - FeatureFlags.Add(FORCE_DATACOLLECTORS_ATTACHMENTPROCESSORS, false); } // Added for artifact porst-processing, it enable/disable the post processing. @@ -33,9 +32,6 @@ static FeatureFlag() // Added in 17.2-preview 7.0-preview public static string ARTIFACTS_POSTPROCESSING_SDK_KEEP_OLD_UX = VSTEST_FEATURE + "_" + "ARTIFACTS_POSTPROCESSING_SDK_KEEP_OLD_UX"; - // Temporary used to allow tests to work - public static string FORCE_DATACOLLECTORS_ATTACHMENTPROCESSORS = VSTEST_FEATURE + "_" + "FORCE_DATACOLLECTORS_ATTACHMENTPROCESSORS"; - // For now we're checking env var. // We could add it also to some section inside the runsettings. public bool IsEnabled(string featureName) => diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomain.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomain.cs new file mode 100644 index 0000000000..940ce8b000 --- /dev/null +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomain.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.TestRunAttachmentsProcessing; + +/// +/// This class is a proxy implementation of IDataCollectorAttachmentProcessor. +/// We cannot load extension directly inside the runner in design mode because we're locking files +/// and in some scenario build or publish can fail. +/// +/// DataCollectorAttachmentProcessorAppDomain creates DataCollectorAttachmentProcessorRemoteWrapper in a +/// custom domain. +/// +/// IDataCollectorAttachmentProcessor needs to communicate back some information like, report percentage state +/// of the processing, send messages through the IMessageLogger etc...so we have a full duplex communication. +/// +/// For this reason we use an anonymous pipe to "listen" to the events from the real implementation and we forward +/// the information to the caller. +/// +internal class DataCollectorAttachmentProcessorAppDomain : IDataCollectorAttachmentProcessor, IDisposable +{ + private readonly string _pipeShutdownMessagePrefix = Guid.NewGuid().ToString(); + private readonly DataCollectorAttachmentProcessorRemoteWrapper _wrapper; + private readonly InvokedDataCollector _invokedDataCollector; + private readonly AppDomain _appDomain; + private readonly IMessageLogger? _dataCollectorAttachmentsProcessorsLogger; + private readonly Task _pipeServerReadTask; + private readonly AnonymousPipeClientStream _pipeClientStream; + + public bool LoadSucceded { get; private set; } + public string? AssemblyQualifiedName => _wrapper.AssemblyQualifiedName; + public string? FriendlyName => _wrapper.FriendlyName; + private IMessageLogger? _processAttachmentSetsLogger; + private IProgress? _progressReporter; + + public DataCollectorAttachmentProcessorAppDomain(InvokedDataCollector invokedDataCollector!!, IMessageLogger dataCollectorAttachmentsProcessorsLogger) + { + _invokedDataCollector = invokedDataCollector; + _appDomain = AppDomain.CreateDomain(invokedDataCollector.Uri.ToString()); + _dataCollectorAttachmentsProcessorsLogger = dataCollectorAttachmentsProcessorsLogger; + _wrapper = (DataCollectorAttachmentProcessorRemoteWrapper)_appDomain.CreateInstanceFromAndUnwrap( + typeof(DataCollectorAttachmentProcessorRemoteWrapper).Assembly.Location, + typeof(DataCollectorAttachmentProcessorRemoteWrapper).FullName, + false, + BindingFlags.Default, + null, + new[] { _pipeShutdownMessagePrefix }, + null, + null); + + _pipeClientStream = new AnonymousPipeClientStream(PipeDirection.In, _wrapper.GetClientHandleAsString()); + _pipeServerReadTask = Task.Run(() => PipeReaderTask()); + + EqtTrace.Verbose($"DataCollectorAttachmentProcessorAppDomain.ctor: AppDomain '{_appDomain.FriendlyName}' created to host assembly '{invokedDataCollector.FilePath}'"); + + InitExtension(); + } + + private void InitExtension() + { + try + { + LoadSucceded = _wrapper.LoadExtension(_invokedDataCollector.FilePath, _invokedDataCollector.Uri); + EqtTrace.Verbose($"DataCollectorAttachmentProcessorAppDomain.ctor: Extension '{_invokedDataCollector.Uri}' loaded. LoadSucceded: {LoadSucceded} AssemblyQualifiedName: '{AssemblyQualifiedName}' HasAttachmentProcessor: '{HasAttachmentProcessor}' FriendlyName: '{FriendlyName}'"); + } + catch (Exception ex) + { + EqtTrace.Error($"DataCollectorAttachmentProcessorAppDomain.InitExtension: Exception during extension initialization\n{ex}"); + } + } + + private void PipeReaderTask() + { + try + { + using StreamReader sr = new(_pipeClientStream, Encoding.Default, false, 1024, true); + while (_pipeClientStream?.IsConnected == true) + { + try + { + string messagePayload = sr.ReadLine().Replace("\0", Environment.NewLine); + + if (messagePayload.StartsWith(_pipeShutdownMessagePrefix)) + { + EqtTrace.Info($"DataCollectorAttachmentProcessorAppDomain.PipeReaderTask: Shutdown message received, message: {messagePayload}"); + return; + } + + string prefix = messagePayload.Substring(0, messagePayload.IndexOf('|')); + string message = messagePayload.Substring(messagePayload.IndexOf('|') + 1); + + switch (prefix) + { + case AppDomainPipeMessagePrefix.EqtTraceError: EqtTrace.Error(message); break; + case AppDomainPipeMessagePrefix.EqtTraceInfo: EqtTrace.Info(message); break; + case AppDomainPipeMessagePrefix.LoadExtensionTestMessageLevelInformational: + case AppDomainPipeMessagePrefix.LoadExtensionTestMessageLevelWarning: + case AppDomainPipeMessagePrefix.LoadExtensionTestMessageLevelError: + _dataCollectorAttachmentsProcessorsLogger? + .SendMessage((TestMessageLevel)Enum.Parse(typeof(TestMessageLevel), prefix.Substring(prefix.LastIndexOf('.') + 1), false), message); + break; + case AppDomainPipeMessagePrefix.ProcessAttachmentTestMessageLevelInformational: + case AppDomainPipeMessagePrefix.ProcessAttachmentTestMessageLevelWarning: + case AppDomainPipeMessagePrefix.ProcessAttachmentTestMessageLevelError: + _processAttachmentSetsLogger? + .SendMessage((TestMessageLevel)Enum.Parse(typeof(TestMessageLevel), prefix.Substring(prefix.LastIndexOf('.') + 1), false), message); + break; + case AppDomainPipeMessagePrefix.Report: + _progressReporter?.Report(int.Parse(message)); + break; + default: + EqtTrace.Error($"DataCollectorAttachmentProcessorAppDomain:PipeReaderTask: Unknown message: {message}"); + break; + } + } + catch (Exception ex) + { + EqtTrace.Error($"DataCollectorAttachmentProcessorAppDomain.PipeReaderTask: Exception during the pipe reading, Pipe connected: {_pipeClientStream.IsConnected}\n{ex}"); + } + } + + EqtTrace.Info($"DataCollectorAttachmentProcessorAppDomain.PipeReaderTask: Exiting from the pipe read loop."); + } + catch (Exception ex) + { + EqtTrace.Error($"DataCollectorAttachmentProcessorAppDomain.PipeReaderTask: Exception on stream reader for the pipe reading\n{ex}"); + } + } + + public bool HasAttachmentProcessor => _wrapper.HasAttachmentProcessor; + + public bool SupportsIncrementalProcessing => _wrapper.SupportsIncrementalProcessing; + + public IEnumerable? GetExtensionUris() => _wrapper?.GetExtensionUris(); + + public async Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) + { + // We register the cancellation and we call cancel inside the AppDomain + cancellationToken.Register(() => _wrapper.CancelProcessAttachment()); + _processAttachmentSetsLogger = logger; + _progressReporter = progressReporter; + return JsonDataSerializer.Instance.Deserialize(await Task.Run(() => _wrapper.ProcessAttachment(configurationElement.OuterXml, JsonDataSerializer.Instance.Serialize(attachments.ToArray()))).ConfigureAwait(false)); + } + + public void Dispose() + { + _wrapper.Dispose(); + + string appDomainName = _appDomain.FriendlyName; + AppDomain.Unload(_appDomain); + EqtTrace.Verbose($"DataCollectorAttachmentProcessorAppDomain.Dispose: Unloaded AppDomain '{appDomainName}'"); + + if (_pipeServerReadTask?.Wait(TimeSpan.FromSeconds(30)) == false) + { + EqtTrace.Error($"DataCollectorAttachmentProcessorAppDomain.Dispose: PipeReaderTask timeout expired"); + } + + // We don't need to close the pipe handle because we're communicating with an in-process pipe and the same handle is closed by AppDomain.Unload(_appDomain); + // Disposing here will fail for invalid handle during the release but we call it to avoid the GC cleanup inside the finalizer thread + // where it fails the same. + // + // We could also suppress the finalizers + // GC.SuppressFinalize(_pipeClientStream); + // GC.SuppressFinalize(_pipeClientStream.SafePipeHandle); + // but doing so mean relying to an implementation detail, + // for instance if some changes are done and some other object finalizer will be added; + // this will run on .NET Framework and it's unexpected but we prefer to rely on the documented semantic: + // "if I call dispose no finalizers will be called for unmanaged resources hold by this object". + try + { + _pipeClientStream?.Dispose(); + } + catch + { } + } +} + +internal static class AppDomainPipeMessagePrefix +{ + public const string EqtTraceError = "EqtTrace.Error"; + public const string EqtTraceInfo = "EqtTrace.Info"; + public const string Report = "Report"; + public const string LoadExtensionTestMessageLevelInformational = "LoadExtension.TestMessageLevel.Informational"; + public const string LoadExtensionTestMessageLevelWarning = "LoadExtension.TestMessageLevel.Warning"; + public const string LoadExtensionTestMessageLevelError = "LoadExtension.TestMessageLevel.Error"; + public const string ProcessAttachmentTestMessageLevelInformational = "ProcessAttachment.TestMessageLevel.Informational"; + public const string ProcessAttachmentTestMessageLevelWarning = "ProcessAttachment.TestMessageLevel.Warning"; + public const string ProcessAttachmentTestMessageLevelError = "ProcessAttachment.TestMessageLevel.Error"; +} + +#endif diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorWrapper.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorWrapper.cs new file mode 100644 index 0000000000..df3bda0244 --- /dev/null +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentProcessorWrapper.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +using Microsoft.VisualStudio.TestPlatform.Common.DataCollector; +using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.TestRunAttachmentsProcessing; + +/// +/// This class is the "container" for the real IDataCollectorAttachmentProcessor implementation. +/// It tries to load the extension and it receives calls from the DataCollectorAttachmentProcessorAppDomain that +/// acts as a proxy for the main AppDomain(the runner one). +/// +internal class DataCollectorAttachmentProcessorRemoteWrapper : MarshalByRefObject +{ + private readonly AnonymousPipeServerStream _pipeServerStream = new(PipeDirection.Out, HandleInheritability.None); + private readonly object _pipeClientLock = new(); + private readonly string _pipeShutdownMessagePrefix; + + private IDataCollectorAttachmentProcessor? _dataCollectorAttachmentProcessorInstance; + + private CancellationTokenSource? _processAttachmentCts; + + public string? AssemblyQualifiedName { get; private set; } + + public string? FriendlyName { get; private set; } + + public bool LoadSucceded { get; private set; } + + public bool HasAttachmentProcessor { get; private set; } + + public DataCollectorAttachmentProcessorRemoteWrapper(string pipeShutdownMessagePrefix!!) + { + _pipeShutdownMessagePrefix = pipeShutdownMessagePrefix; + } + + public string GetClientHandleAsString() => _pipeServerStream.GetClientHandleAsString(); + + public bool SupportsIncrementalProcessing => _dataCollectorAttachmentProcessorInstance?.SupportsIncrementalProcessing == true; + + public Uri[]? GetExtensionUris() => _dataCollectorAttachmentProcessorInstance?.GetExtensionUris()?.ToArray(); + + public string ProcessAttachment( + string configurationElement, + string attachments) + { + var doc = new XmlDocument(); + doc.LoadXml(configurationElement); + AttachmentSet[] attachmentSets = JsonDataSerializer.Instance.Deserialize(attachments); + SynchronousProgress progress = new(Report); + _processAttachmentCts = new CancellationTokenSource(); + + ICollection attachmentsResult = + Task.Run(async () => await _dataCollectorAttachmentProcessorInstance!.ProcessAttachmentSetsAsync( + doc.DocumentElement, + attachmentSets, + progress, + new MessageLogger(this, nameof(ProcessAttachment)), + _processAttachmentCts.Token)) + // We cannot marshal Task so we need to block the thread until the end of the processing + .ConfigureAwait(false).GetAwaiter().GetResult(); + + return JsonDataSerializer.Instance.Serialize(attachmentsResult.ToArray()); + } + + public void CancelProcessAttachment() => _processAttachmentCts?.Cancel(); + + public bool LoadExtension(string filePath, Uri dataCollectorUri) + { + var dataCollectorExtensionManager = DataCollectorExtensionManager.Create(filePath, true, new MessageLogger(this, nameof(LoadExtension))); + var dataCollectorExtension = dataCollectorExtensionManager.TryGetTestExtension(dataCollectorUri); + if (dataCollectorExtension is null || dataCollectorExtension?.Metadata.HasAttachmentProcessor == false) + { + TraceInfo($"DataCollectorAttachmentsProcessorsFactory: DataCollectorExtension not found for uri '{dataCollectorUri}'"); + return false; + } + + Type attachmentProcessorType = ((DataCollectorConfig)dataCollectorExtension!.TestPluginInfo).AttachmentsProcessorType; + try + { + _dataCollectorAttachmentProcessorInstance = TestPluginManager.CreateTestExtension(attachmentProcessorType); + AssemblyQualifiedName = attachmentProcessorType.AssemblyQualifiedName; + FriendlyName = dataCollectorExtension.Metadata.FriendlyName; + LoadSucceded = true; + HasAttachmentProcessor = true; + TraceInfo($"DataCollectorAttachmentProcessorWrapper.LoadExtension: Creation of collector attachment processor '{attachmentProcessorType.AssemblyQualifiedName}' from file '{filePath}' succeded"); + return true; + } + catch (Exception ex) + { + TraceError($"DataCollectorAttachmentProcessorWrapper.LoadExtension: Failed during the creation of data collector attachment processor '{attachmentProcessorType.AssemblyQualifiedName}'\n{ex}"); + SendMessage(nameof(LoadExtension), TestMessageLevel.Error, $"DataCollectorAttachmentProcessorWrapper.LoadExtension: Failed during the creation of data collector attachment processor '{attachmentProcessorType.AssemblyQualifiedName}'\n{ex}"); + } + + return false; + } + + private void TraceError(string message) => Trace(AppDomainPipeMessagePrefix.EqtTraceError, message); + + private void TraceInfo(string message) => Trace(AppDomainPipeMessagePrefix.EqtTraceInfo, message); + + private void Trace(string traceType, string message) + { + lock (_pipeClientLock) + { + WriteToPipe($"{traceType}|{message}"); + } + } + + private void Report(int value) + { + lock (_pipeClientLock) + { + WriteToPipe($"{AppDomainPipeMessagePrefix.Report}|{value}"); + } + } + + private void SendMessage(string origin, TestMessageLevel messageLevel, string message) + { + lock (_pipeClientLock) + { + WriteToPipe($"{origin}.TestMessageLevel.{messageLevel}|{message}"); + } + } + + private void WriteToPipe(string message) + { + using StreamWriter sw = new(_pipeServerStream, Encoding.Default, 1024, true); + sw.AutoFlush = true; + // We want to keep the protocol very simple and text message oriented. + // On the read side we do ReadLine() to simplify the parsing and + // for this reason we remove the \n to null terminator and we'll aggregate on client side. + sw.WriteLine(message.Replace(Environment.NewLine, "\0").Replace("\n", "\0")); + _pipeServerStream.Flush(); + _pipeServerStream.WaitForPipeDrain(); + } + + class MessageLogger : IMessageLogger + { + private readonly string _name; + private readonly DataCollectorAttachmentProcessorRemoteWrapper _wrapper; + + public MessageLogger(DataCollectorAttachmentProcessorRemoteWrapper wrapper!!, string name!!) + { + _wrapper = wrapper; + _name = name; + } + + public void SendMessage(TestMessageLevel testMessageLevel, string message) + => _wrapper.SendMessage(_name, testMessageLevel, message); + } + + class SynchronousProgress : IProgress + { + private readonly Action _report; + + public SynchronousProgress(Action report!!) => _report = report; + + public void Report(int value) => _report(value); + } + + public void Dispose() + { + _processAttachmentCts?.Dispose(); + // Send shutdown message to gracefully close the client. + WriteToPipe($"{_pipeShutdownMessagePrefix}_{AppDomain.CurrentDomain.FriendlyName}"); + _pipeServerStream.Dispose(); + (_dataCollectorAttachmentProcessorInstance as IDisposable)?.Dispose(); + } +} + +#endif diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentsProcessorsFactory.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentsProcessorsFactory.cs index 7955bab950..55fd6984bf 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentsProcessorsFactory.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/DataCollectorAttachmentsProcessorsFactory.cs @@ -4,7 +4,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; using Microsoft.VisualStudio.TestPlatform.Common.DataCollector; using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework; @@ -29,31 +35,58 @@ public DataCollectorAttachmentProcessor[] Create(InvokedDataCollector[] invokedD { IDictionary> datacollectorsAttachmentsProcessors = new Dictionary>(); - - // Temporary disabled in design mode. - // We have an issue when the collector is found inside bin folder/subfolder of the user in case of VS. - // Usually collector are loaded from nuget package or visual studio special folders, but if a user for some reason run `dotnet publish` - // and after run the datacollector with the attachment processor we're loading 'published' version and no more nuget one. - // This led to file locking that prevents further `dotnet publish` and maybe build. - if (!RunSettingsHelper.Instance.IsDesignMode || FeatureFlag.Instance.IsEnabled(FeatureFlag.FORCE_DATACOLLECTORS_ATTACHMENTPROCESSORS)) + if (invokedDataCollectors?.Length > 0) { - if (invokedDataCollectors?.Length > 0) + // We order files by filename descending so in case of the same collector from the same nuget but with different versions, we'll run the newer version. + // i.e. C:\Users\xxx\.nuget\packages\coverlet.collector + // /3.0.2 + // /3.0.3 + // /3.1.0 + foreach (var invokedDataCollector in invokedDataCollectors.OrderByDescending(d => d.FilePath)) { - // We order files by filename descending so in case of the same collector from the same nuget but with different versions, we'll run the newer version. - // i.e. C:\Users\xxx\.nuget\packages\coverlet.collector - // /3.0.2 - // /3.0.3 - // /3.1.0 - foreach (var invokedDataCollector in invokedDataCollectors.OrderByDescending(d => d.FilePath)) + // We'll merge using only one AQN in case of more "same processors" in different assembly. + if (!invokedDataCollector.HasAttachmentProcessor) { - // We'll merge using only one AQN in case of more "same processors" in different assembly. - if (!invokedDataCollector.HasAttachmentProcessor) - { - continue; - } + continue; + } - EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Analyzing data collector attachment processor Uri: {invokedDataCollector.Uri} AssemblyQualifiedName: {invokedDataCollector.AssemblyQualifiedName} FilePath: {invokedDataCollector.FilePath} HasAttachmentProcessor: {invokedDataCollector.HasAttachmentProcessor}"); + EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Analyzing data collector attachment processor Uri: {invokedDataCollector.Uri} AssemblyQualifiedName: {invokedDataCollector.AssemblyQualifiedName} FilePath: {invokedDataCollector.FilePath} HasAttachmentProcessor: {invokedDataCollector.HasAttachmentProcessor}"); +#if NETFRAMEWORK + // If we're in design mode we need to load the extension inside a different AppDomain to avoid to lock extension file containers. + if (RunSettingsHelper.Instance.IsDesignMode) + { + try + { + var wrapper = new DataCollectorAttachmentProcessorAppDomain(invokedDataCollector, logger); + if (wrapper.LoadSucceded && wrapper.HasAttachmentProcessor) + { + if (!datacollectorsAttachmentsProcessors.ContainsKey(wrapper.AssemblyQualifiedName)) + { + datacollectorsAttachmentsProcessors.Add(wrapper.AssemblyQualifiedName, new Tuple(wrapper.FriendlyName, wrapper)); + EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Collector attachment processor '{wrapper.AssemblyQualifiedName}' from file '{invokedDataCollector.FilePath}' added to the 'run list'"); + } + else + { + // If we already registered same IDataCollectorAttachmentProcessor we need to unload the unused AppDomain. + EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Unloading unused AppDomain for '{wrapper.FriendlyName}'"); + wrapper.Dispose(); + } + } + else + { + EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: DataCollectorExtension not found for uri '{invokedDataCollector.Uri}'"); + } + } + catch (Exception ex) + { + EqtTrace.Error($"DataCollectorAttachmentsProcessorsFactory: Failed during the creation of data collector attachment processor '{invokedDataCollector.AssemblyQualifiedName}'\n{ex}"); + logger?.SendMessage(TestMessageLevel.Error, $"DataCollectorAttachmentsProcessorsFactory: Failed during the creation of data collector attachment processor '{invokedDataCollector.AssemblyQualifiedName}'\n{ex}"); + } + } + else + { +#endif // We cache extension locally by file path var dataCollectorExtensionManager = DataCollectorExtensionManagerCache.GetOrAdd(invokedDataCollector.FilePath, DataCollectorExtensionManager.Create(invokedDataCollector.FilePath, true, TestSessionMessageLogger.Instance)); var dataCollectorExtension = dataCollectorExtensionManager.TryGetTestExtension(invokedDataCollector.Uri); @@ -72,7 +105,7 @@ public DataCollectorAttachmentProcessor[] Create(InvokedDataCollector[] invokedD logger?.SendMessage(TestMessageLevel.Error, $"DataCollectorAttachmentsProcessorsFactory: Failed during the creation of data collector attachment processor '{attachmentProcessorType.AssemblyQualifiedName}'\n{ex}"); } - if (dataCollectorAttachmentProcessorInstance != null && !datacollectorsAttachmentsProcessors.ContainsKey(attachmentProcessorType.AssemblyQualifiedName)) + if (dataCollectorAttachmentProcessorInstance is not null && !datacollectorsAttachmentsProcessors.ContainsKey(attachmentProcessorType.AssemblyQualifiedName)) { datacollectorsAttachmentsProcessors.Add(attachmentProcessorType.AssemblyQualifiedName, new Tuple(dataCollectorExtension.Metadata.FriendlyName, dataCollectorAttachmentProcessorInstance)); EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Collector attachment processor '{attachmentProcessorType.AssemblyQualifiedName}' from file '{invokedDataCollector.FilePath}' added to the 'run list'"); @@ -82,7 +115,9 @@ public DataCollectorAttachmentProcessor[] Create(InvokedDataCollector[] invokedD { EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: DataCollectorExtension not found for uri '{invokedDataCollector.Uri}'"); } +#if NETFRAMEWORK } +#endif } } @@ -97,7 +132,7 @@ public DataCollectorAttachmentProcessor[] Create(InvokedDataCollector[] invokedD var finalDatacollectorsAttachmentsProcessors = new List(); foreach (var attachementProcessor in datacollectorsAttachmentsProcessors) { - EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Valid data collector attachment processor found: '{attachementProcessor.Value.Item2.GetType().AssemblyQualifiedName}'"); + EqtTrace.Info($"DataCollectorAttachmentsProcessorsFactory: Valid data collector attachment processor found: '{attachementProcessor.Key}'"); finalDatacollectorsAttachmentsProcessors.Add(new DataCollectorAttachmentProcessor(attachementProcessor.Value.Item1, attachementProcessor.Value.Item2)); } diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/TestRunAttachmentsProcessingManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/TestRunAttachmentsProcessingManager.cs index 1437fed841..231885cf79 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/TestRunAttachmentsProcessingManager.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/AttachmentsProcessing/TestRunAttachmentsProcessingManager.cs @@ -106,7 +106,9 @@ private async Task> ProcessAttachmentsAsync(string run var dataCollectorAttachmentsProcessors = _dataCollectorAttachmentsProcessorsFactory.Create(invokedDataCollector?.ToArray(), logger); for (int i = 0; i < dataCollectorAttachmentsProcessors.Length; i++) { - var dataCollectorAttachmentsProcessor = dataCollectorAttachmentsProcessors[i]; + // We need to dispose the DataCollectorAttachmentProcessor to unload the AppDomain for net451 + using DataCollectorAttachmentProcessor dataCollectorAttachmentsProcessor = dataCollectorAttachmentsProcessors[i]; + int attachmentsHandlerIndex = i + 1; if (!dataCollectorAttachmentsProcessor.DataCollectorAttachmentProcessorInstance.SupportsIncrementalProcessing) @@ -147,7 +149,7 @@ private async Task> ProcessAttachmentsAsync(string run configuration = collectorConfiguration.Configuration; } - EqtTrace.Info($"TestRunAttachmentsProcessingManager: Invocation of data collector attachment processor '{dataCollectorAttachmentsProcessor.DataCollectorAttachmentProcessorInstance.GetType().AssemblyQualifiedName}' with configuration '{(configuration == null ? "null" : configuration.OuterXml)}'"); + EqtTrace.Info($"TestRunAttachmentsProcessingManager: Invocation of data collector attachment processor AssemblyQualifiedName: '{dataCollectorAttachmentsProcessor.DataCollectorAttachmentProcessorInstance.GetType().AssemblyQualifiedName}' FriendlyName: '{dataCollectorAttachmentsProcessor.FriendlyName}' with configuration '{(configuration == null ? "null" : configuration.OuterXml)}'"); ICollection processedAttachments = await dataCollectorAttachmentsProcessor.DataCollectorAttachmentProcessorInstance.ProcessAttachmentSetsAsync( configuration, new Collection(attachmentsToBeProcessed), diff --git a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CodeCoverageTests.cs b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CodeCoverageTests.cs index e11224712c..8a9cb88f71 100644 --- a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CodeCoverageTests.cs +++ b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CodeCoverageTests.cs @@ -33,13 +33,6 @@ public class CodeCoverageTests : CodeCoverageAcceptanceTestBase private RunEventHandler _runEventHandler; private TestRunAttachmentsProcessingEventHandler _testRunAttachmentsProcessingEventHandler; - static CodeCoverageTests() - { -#pragma warning disable RS0030 // Do not used banned APIs - We need it temporary - Environment.SetEnvironmentVariable("VSTEST_FEATURE_FORCE_DATACOLLECTORS_ATTACHMENTPROCESSORS", "1"); -#pragma warning restore RS0030 // Do not used banned APIs - We need it temporary - } - private void Setup() { _vstestConsoleWrapper = GetVsTestConsoleWrapper(out _tempDirectory); diff --git a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/DataCollectorAttachmentProcessor.cs b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/DataCollectorAttachmentProcessor.cs new file mode 100644 index 0000000000..16708093ba --- /dev/null +++ b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/DataCollectorAttachmentProcessor.cs @@ -0,0 +1,128 @@ +// 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.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +using Microsoft.TestPlatform.TestUtilities; +using Microsoft.TestPlatform.VsTestConsole.TranslationLayer.Interfaces; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.TestPlatform.AcceptanceTests.TranslationLayerTests; + +[TestClass] +[TestCategory("Windows-Review")] +public class DataCollectorAttachmentProcessor : AcceptanceTestBase +{ + private readonly IVsTestConsoleWrapper _vstestConsoleWrapper; + private readonly RunEventHandler _runEventHandler; + private readonly TestRunAttachmentsProcessingEventHandler _testRunAttachmentsProcessingEventHandler; + + public DataCollectorAttachmentProcessor() + { + _vstestConsoleWrapper = GetVsTestConsoleWrapper(); + _runEventHandler = new RunEventHandler(); + _testRunAttachmentsProcessingEventHandler = new TestRunAttachmentsProcessingEventHandler(); + } + + [TestCleanup] + public void Cleanup() + { + _vstestConsoleWrapper?.EndSession(); + } + + [TestMethod] + [NetFullTargetFrameworkDataSource] + [NetCoreTargetFrameworkDataSource] + public async Task AttachmentProcessorDataCollector_ExtensionFileNotLocked(RunnerInfo runnerInfo) + { + // arrange + SetTestEnvironment(_testEnvironment, runnerInfo); + var originalExtensionsPath = Path.Combine( + _testEnvironment.TestAssetsPath, + Path.GetFileNameWithoutExtension("AttachmentProcessorDataCollector"), + "bin", + IntegrationTestEnvironment.BuildConfiguration, + "netstandard2.0"); + + string extensionPath = Path.Combine(TempDirectory.Path, "AttachmentProcessorDataCollector"); + Directory.CreateDirectory(extensionPath); + TempDirectory.CopyDirectory(new DirectoryInfo(originalExtensionsPath), new DirectoryInfo(extensionPath)); + + string runSettings = GetRunsettingsFilePath(TempDirectory.Path); + XElement runSettingsXml = XElement.Load(runSettings); + runSettingsXml.Add(new XElement("RunConfiguration", new XElement("TestAdaptersPaths", extensionPath))); + // Set datacollector parameters + runSettingsXml!.Element("DataCollectionRunSettings")! + .Element("DataCollectors")! + .Element("DataCollector")! + .Add(new XElement("Configuration", new XElement("MergeFile", "MergedFile.txt"))); + runSettingsXml.Save(runSettings); + + // act + _vstestConsoleWrapper.RunTests(GetTestAssemblies(), File.ReadAllText(runSettings), new TestPlatformOptions(), _runEventHandler); + _vstestConsoleWrapper.RunTests(GetTestAssemblies(), File.ReadAllText(runSettings), new TestPlatformOptions(), _runEventHandler); + await _vstestConsoleWrapper.ProcessTestRunAttachmentsAsync(_runEventHandler.Attachments, _runEventHandler.InvokedDataCollectors, File.ReadAllText(runSettings), true, false, _testRunAttachmentsProcessingEventHandler, CancellationToken.None); + + // assert + // Extension path is not locked, we can remove it. + Directory.Delete(extensionPath, true); + + // Ensure we ran the extension. + using var logFile = new FileStream(Path.Combine(TempDirectory.Path, "log.txt"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var streamReader = new StreamReader(logFile); + string logFileContent = streamReader.ReadToEnd(); + Assert.IsTrue(Regex.IsMatch(logFileContent, $@"DataCollectorAttachmentsProcessorsFactory: Collector attachment processor 'AttachmentProcessorDataCollector\.SampleDataCollectorAttachmentProcessor, AttachmentProcessorDataCollector, Version=.*, Culture=neutral, PublicKeyToken=null' from file '{extensionPath.Replace(@"\", @"\\")}\\AttachmentProcessorDataCollector.dll' added to the 'run list'")); + Assert.IsTrue(Regex.IsMatch(logFileContent, @"Invocation of data collector attachment processor AssemblyQualifiedName: 'Microsoft\.VisualStudio\.TestPlatform\.CrossPlatEngine\.TestRunAttachmentsProcessing\.DataCollectorAttachmentProcessorAppDomain, Microsoft\.TestPlatform\.CrossPlatEngine, Version=.*, Culture=neutral, PublicKeyToken=.*' FriendlyName: 'SampleDataCollector'")); + } + + private static string GetRunsettingsFilePath(string resultsDir) + { + var runsettingsPath = Path.Combine(resultsDir, "test_" + Guid.NewGuid() + ".runsettings"); + var dataCollectionAttributes = new Dictionary + { + { "friendlyName", "SampleDataCollector" }, + { "uri", "my://sample/datacollector" } + }; + + CreateDataCollectionRunSettingsFile(runsettingsPath, dataCollectionAttributes); + return runsettingsPath; + } + + private static void CreateDataCollectionRunSettingsFile(string destinationRunsettingsPath, Dictionary dataCollectionAttributes) + { + var doc = new XmlDocument(); + var xmlDeclaration = doc.CreateNode(XmlNodeType.XmlDeclaration, string.Empty, string.Empty); + + doc.AppendChild(xmlDeclaration); + var runSettingsNode = doc.CreateElement(Constants.RunSettingsName); + doc.AppendChild(runSettingsNode); + var dcConfigNode = doc.CreateElement(Constants.DataCollectionRunSettingsName); + runSettingsNode.AppendChild(dcConfigNode); + var dataCollectorsNode = doc.CreateElement(Constants.DataCollectorsSettingName); + dcConfigNode.AppendChild(dataCollectorsNode); + var dataCollectorNode = doc.CreateElement(Constants.DataCollectorSettingName); + dataCollectorsNode.AppendChild(dataCollectorNode); + + foreach (var kvp in dataCollectionAttributes) + { + dataCollectorNode.SetAttribute(kvp.Key, kvp.Value); + } + + using var stream = new FileHelper().GetStream(destinationRunsettingsPath, FileMode.Create); + doc.Save(stream); + } + + private IList GetTestAssemblies() + => new List { "SimpleTestProject.dll", "SimpleTestProject2.dll" }.Select(p => GetAssetFullPath(p)).ToList(); +} diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomainTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomainTests.cs new file mode 100644 index 0000000000..1e044b0f48 --- /dev/null +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/AttachmentsProcessing/DataCollectorAttachmentProcessorAppDomainTests.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.TestRunAttachmentsProcessing; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +namespace Microsoft.TestPlatform.CrossPlatEngine.UnitTests.DataCollectorAttachmentProcessorAppDomainTests; + +[TestClass] +public class DataCollectorAttachmentProcessorAppDomainTests +{ + private readonly Mock _loggerMock = new(); + internal static string SomeState = "deafultState"; + + [TestMethod] + public async Task DataCollectorAttachmentProcessorAppDomain_ShouldBeIsolated() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSample"), "AppDomainSample", typeof(AppDomainSampleDataCollector).AssemblyQualifiedName, typeof(AppDomainSampleDataCollector).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSample"), string.Empty); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml(""); + + // act + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + Assert.IsTrue(dcap.LoadSucceded); + await dcap.ProcessAttachmentSetsAsync(doc.DocumentElement, attachments, new Progress((int report) => { }), _loggerMock.Object, CancellationToken.None); + + //Assert + // If the processor runs in another AppDomain the static state is not shared and should not change. + Assert.AreEqual("deafultState", SomeState); + } + + [TestMethod] + public async Task DataCollectorAttachmentProcessorAppDomain_ShouldCancel() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSample"), "AppDomainSample", typeof(AppDomainSampleDataCollector).AssemblyQualifiedName, typeof(AppDomainSampleDataCollector).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSample"), string.Empty); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml("5000"); + CancellationTokenSource cts = new(); + + // act + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + Assert.IsTrue(dcap.LoadSucceded); + + Task runProcessing = dcap.ProcessAttachmentSetsAsync(doc.DocumentElement, attachments, new Progress((int report) => cts.Cancel()), _loggerMock.Object, cts.Token); + + //assert + await Assert.ThrowsExceptionAsync(async () => await runProcessing); + } + + [TestMethod] + public async Task DataCollectorAttachmentProcessorAppDomain_ShouldReturnCorrectAttachments() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSample"), "AppDomainSample", typeof(AppDomainSampleDataCollector).AssemblyQualifiedName, typeof(AppDomainSampleDataCollector).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSample"), "AppDomainSample"); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml(""); + + // act + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + Assert.IsTrue(dcap.LoadSucceded); + + var attachmentsResult = await dcap.ProcessAttachmentSetsAsync(doc.DocumentElement, attachments, new Progress(), _loggerMock.Object, CancellationToken.None); + + // assert + // We return same instance but we're marshaling so we expected different pointers + Assert.AreNotSame(attachmentSet, attachmentsResult); + + Assert.AreEqual(attachmentSet.DisplayName, attachmentsResult.First().DisplayName); + Assert.AreEqual(attachmentSet.Uri, attachmentsResult.First().Uri); + Assert.AreEqual(attachmentSet.Attachments.Count, attachmentsResult.Count); + Assert.AreEqual(attachmentSet.Attachments[0].Description, attachmentsResult.First().Attachments[0].Description); + Assert.AreEqual(attachmentSet.Attachments[0].Uri, attachmentsResult.First().Attachments[0].Uri); + Assert.AreEqual(attachmentSet.Attachments[0].Uri, attachmentsResult.First().Attachments[0].Uri); + } + + [TestMethod] + public async Task DataCollectorAttachmentProcessorAppDomain_ShouldReportProgressCorrectly() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSample"), "AppDomainSample", typeof(AppDomainSampleDataCollector).AssemblyQualifiedName, typeof(AppDomainSampleDataCollector).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSample"), "AppDomainSample"); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml(""); + + // act + var progress = new CustomProgress(); + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + Assert.IsTrue(dcap.LoadSucceded); + + var attachmentsResult = await dcap.ProcessAttachmentSetsAsync( + doc.DocumentElement, + attachments, + progress, + _loggerMock.Object, + CancellationToken.None); + + // assert + progress.CountdownEvent.Wait(new CancellationTokenSource(10000).Token); + Assert.AreEqual(10, progress.Progress[0]); + Assert.AreEqual(50, progress.Progress[1]); + Assert.AreEqual(100, progress.Progress[2]); + } + + [TestMethod] + public async Task DataCollectorAttachmentProcessorAppDomain_ShouldLogCorrectly() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSample"), "AppDomainSample", typeof(AppDomainSampleDataCollector).AssemblyQualifiedName, typeof(AppDomainSampleDataCollector).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSample"), "AppDomainSample"); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml(""); + CountdownEvent countdownEvent = new(3); + List> messages = new(); + _loggerMock.Setup(x => x.SendMessage(It.IsAny(), It.IsAny())).Callback((TestMessageLevel messageLevel, string message) + => + { + countdownEvent.Signal(); + messages.Add(new Tuple(messageLevel, message)); + }); + + // act + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + Assert.IsTrue(dcap.LoadSucceded); + + var attachmentsResult = await dcap.ProcessAttachmentSetsAsync(doc.DocumentElement, attachments, new Progress(), _loggerMock.Object, CancellationToken.None); + + // assert + countdownEvent.Wait(new CancellationTokenSource(10000).Token); + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(TestMessageLevel.Informational, messages[0].Item1); + Assert.AreEqual("Info", messages[0].Item2); + Assert.AreEqual(TestMessageLevel.Warning, messages[1].Item1); + Assert.AreEqual("Warning", messages[1].Item2); + Assert.AreEqual(TestMessageLevel.Error, messages[2].Item1); + Assert.AreEqual($"line1{Environment.NewLine}line2{Environment.NewLine}line3", messages[2].Item2); + } + + [TestMethod] + public void DataCollectorAttachmentProcessorAppDomain_ShouldReportFailureDuringExtensionCreation() + { + // arrange + var invokedDataCollector = new InvokedDataCollector(new Uri("datacollector://AppDomainSampleFailure"), "AppDomainSampleFailure", typeof(AppDomainSampleDataCollectorFailure).AssemblyQualifiedName, typeof(AppDomainSampleDataCollectorFailure).Assembly.Location, true); + var attachmentSet = new AttachmentSet(new Uri("datacollector://AppDomainSampleFailure"), "AppDomainSampleFailure"); + attachmentSet.Attachments.Add(new UriDataAttachment(new Uri("C:\\temp\\sample"), "sample")); + Collection attachments = new() { attachmentSet }; + var doc = new XmlDocument(); + doc.LoadXml(""); + using ManualResetEventSlim errorReportEvent = new(); + _loggerMock.Setup(x => x.SendMessage(It.IsAny(), It.IsAny())).Callback((TestMessageLevel messageLevel, string message) + => + { + if (messageLevel == TestMessageLevel.Error) + { + Assert.IsTrue(message.Contains("System.Exception: Failed to create the extension")); + errorReportEvent.Set(); + } + }); + + // act + using DataCollectorAttachmentProcessorAppDomain dcap = new(invokedDataCollector, _loggerMock.Object); + + //assert + errorReportEvent.Wait(new CancellationTokenSource(10000).Token); + Assert.IsFalse(dcap.LoadSucceded); + } + + [DataCollectorFriendlyName("AppDomainSample")] + [DataCollectorTypeUri("datacollector://AppDomainSample")] + [DataCollectorAttachmentProcessor(typeof(AppDomainDataCollectorAttachmentProcessor))] + public class AppDomainSampleDataCollector : DataCollector + { + public override void Initialize( + XmlElement configurationElement, + DataCollectionEvents events, + DataCollectionSink dataSink, + DataCollectionLogger logger, + DataCollectionEnvironmentContext environmentContext) + { + + } + } + + public class AppDomainDataCollectorAttachmentProcessor : IDataCollectorAttachmentProcessor + { + public bool SupportsIncrementalProcessing => false; + + public IEnumerable GetExtensionUris() => new[] { new Uri("datacollector://AppDomainSample") }; + + public async Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) + { + SomeState = "Updated shared state"; + + var timeout = configurationElement.InnerText; + if (!string.IsNullOrEmpty(timeout)) + { + progressReporter.Report(100); + + DateTime expire = DateTime.UtcNow + TimeSpan.FromMilliseconds(int.Parse(timeout)); + while (true) + { + if (DateTime.UtcNow > expire) + { + cancellationToken.ThrowIfCancellationRequested(); + } + +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods + await Task.Delay(1000); +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods + } + } + + progressReporter.Report(10); + progressReporter.Report(50); + progressReporter.Report(100); + + logger.SendMessage(TestMessageLevel.Informational, "Info"); + logger.SendMessage(TestMessageLevel.Warning, "Warning"); + logger.SendMessage(TestMessageLevel.Error, $"line1{Environment.NewLine}line2\nline3"); + + return attachments; + } + } + + [DataCollectorFriendlyName("AppDomainSampleFailure")] + [DataCollectorTypeUri("datacollector://AppDomainSampleFailure")] + [DataCollectorAttachmentProcessor(typeof(AppDomainDataCollectorAttachmentProcessorFailure))] + public class AppDomainSampleDataCollectorFailure : DataCollector + { + public override void Initialize( + XmlElement configurationElement, + DataCollectionEvents events, + DataCollectionSink dataSink, + DataCollectionLogger logger, + DataCollectionEnvironmentContext environmentContext) + { + + } + } + + public class AppDomainDataCollectorAttachmentProcessorFailure : IDataCollectorAttachmentProcessor + { + public AppDomainDataCollectorAttachmentProcessorFailure() + { + throw new Exception("Failed to create the extension"); + } + + public bool SupportsIncrementalProcessing => false; + + public IEnumerable GetExtensionUris() => throw new NotImplementedException(); + + public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) + => throw new NotImplementedException(); + } + + public class CustomProgress : IProgress + { + public List Progress { get; set; } = new List(); + public CountdownEvent CountdownEvent { get; set; } = new CountdownEvent(3); + + public void Report(int value) + { + Progress.Add(value); + CountdownEvent.Signal(); + } + } +} + +#endif diff --git a/test/Microsoft.TestPlatform.TestUtilities/IntegrationTestBase.cs b/test/Microsoft.TestPlatform.TestUtilities/IntegrationTestBase.cs index abbbbf0967..90d658badf 100644 --- a/test/Microsoft.TestPlatform.TestUtilities/IntegrationTestBase.cs +++ b/test/Microsoft.TestPlatform.TestUtilities/IntegrationTestBase.cs @@ -522,11 +522,26 @@ protected virtual string SetVSTestConsoleDLLPathInArgs(string args) /// /// Returns the VsTestConsole Wrapper. /// - /// + public IVsTestConsoleWrapper GetVsTestConsoleWrapper() + { + return GetVsTestConsoleWrapper(TempDirectory); + } + + /// + /// Returns the VsTestConsole Wrapper. + /// public IVsTestConsoleWrapper GetVsTestConsoleWrapper(out TempDirectory logFileDir) { logFileDir = new TempDirectory(); + return GetVsTestConsoleWrapper(logFileDir); + } + /// + /// Returns the VsTestConsole Wrapper. + /// + /// + public IVsTestConsoleWrapper GetVsTestConsoleWrapper(TempDirectory logFileDir) + { if (!Directory.Exists(logFileDir.Path)) { Directory.CreateDirectory(logFileDir.Path);