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);