| File: CodeCoverageDataAttachmentsHandler.cs | Web Access |
| Project: src\vstest\src\Microsoft.TestPlatform.Utilities\Microsoft.TestPlatform.Utilities.csproj (Microsoft.TestPlatform.Utilities) |
// 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.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Xml; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; namespace Microsoft.VisualStudio.TestPlatform.Utilities; public class CodeCoverageDataAttachmentsHandler : IDataCollectorAttachmentProcessor { private const string CoverageUri = "datacollector://microsoft/CodeCoverage/2.0"; private const string CoverageFileExtension = ".coverage"; private const string XmlFileExtension = ".xml"; private const string CoverageFriendlyName = "Code Coverage"; private const string CodeCoverageIoAssemblyName = "Microsoft.CodeCoverage.IO"; private const string CoverageFileUtilityTypeName = "CoverageFileUtility"; private const string MergeMethodName = "MergeCoverageReportsAsync"; private const string CoverageMergeOperationName = "CoverageMergeOperation"; private static readonly Uri CodeCoverageDataCollectorUri = new(CoverageUri); private static Assembly? s_codeCoverageAssembly; private static object? s_classInstance; private static MethodInfo? s_mergeMethodInfo; private static Array? s_mergeOperationEnumValues; public bool SupportsIncrementalProcessing => true; public IEnumerable<Uri>? GetExtensionUris() { yield return CodeCoverageDataCollectorUri; } public async Task<ICollection<AttachmentSet>> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection<AttachmentSet>? attachments, IProgress<int> progressReporter, IMessageLogger? logger, CancellationToken cancellationToken) { if (attachments?.Any() != true) return new Collection<AttachmentSet>(); var coverageReportFilePaths = new List<string>(); var coverageOtherFilePaths = new List<string>(); foreach (var attachmentSet in attachments) { foreach (var attachment in attachmentSet.Attachments) { if (attachment.Uri.LocalPath.EndsWith(CoverageFileExtension, StringComparison.OrdinalIgnoreCase) || attachment.Uri.LocalPath.EndsWith(XmlFileExtension, StringComparison.OrdinalIgnoreCase)) { coverageReportFilePaths.Add(attachment.Uri.LocalPath); } else { coverageOtherFilePaths.Add(attachment.Uri.LocalPath); } } } if (coverageReportFilePaths.Count > 1) { var resultAttachmentSet = new AttachmentSet(CodeCoverageDataCollectorUri, CoverageFriendlyName); var mergedCoverageReports = await MergeCodeCoverageFilesAsync(coverageReportFilePaths, progressReporter, cancellationToken).ConfigureAwait(false); if (mergedCoverageReports is not null) { foreach (var coverageReport in mergedCoverageReports) { resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(coverageReport, CoverageFriendlyName)); } } foreach (var coverageOtherFilePath in coverageOtherFilePaths) { resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(coverageOtherFilePath, string.Empty)); } return new Collection<AttachmentSet> { resultAttachmentSet }; } return attachments; } private static async Task<IList<string>?> MergeCodeCoverageFilesAsync(IList<string> files, IProgress<int> progressReporter, CancellationToken cancellationToken) { try { // Warning: Don't remove this method call. // // We took a dependency on Coverage.CoreLib.Net. In the unlikely case it cannot be // resolved, this method call will throw an exception that will be caught and // absorbed here. var result = await MergeCodeCoverageFilesAsync(files, cancellationToken).ConfigureAwait(false); progressReporter?.Report(100); return result; } catch (OperationCanceledException) { // Occurs due to cancellation, ok to re-throw. throw; } catch (Exception ex) { EqtTrace.Error( "CodeCoverageDataCollectorAttachmentsHandler: Failed to load datacollector. Error: {0}", ex.ToString()); } return null; } private static async Task<IList<string>?> MergeCodeCoverageFilesAsync(IList<string> files, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // Invoke methods LoadCodeCoverageAssembly(); TPDebug.Assert(s_mergeOperationEnumValues != null); var task = (Task)s_mergeMethodInfo.Invoke(s_classInstance, [files[0], files, s_mergeOperationEnumValues.GetValue(0)!, true, cancellationToken])!; await task.ConfigureAwait(false); if (task.GetType().GetProperty("Result")!.GetValue(task, null) is not IList<string> mergedResults) { EqtTrace.Error("CodeCoverageDataCollectorAttachmentsHandler: Failed to merge code coverage files."); return files; } // Delete original files and keep merged file only foreach (var file in files) { if (mergedResults.Contains(file)) continue; try { File.Delete(file); } catch (Exception ex) { EqtTrace.Error($"CodeCoverageDataCollectorAttachmentsHandler: Failed to remove {file}. Error: {ex}"); } } return mergedResults; } [MemberNotNull(nameof(s_codeCoverageAssembly), nameof(s_classInstance), nameof(s_mergeOperationEnumValues), nameof(s_mergeMethodInfo))] private static void LoadCodeCoverageAssembly() { if (s_codeCoverageAssembly != null) { TPDebug.Assert(s_classInstance != null && s_mergeOperationEnumValues != null && s_mergeMethodInfo != null); return; } var dataAttachmentAssemblyLocation = typeof(CodeCoverageDataAttachmentsHandler).Assembly.GetAssemblyLocation()!; var assemblyPath = Path.Combine(Path.GetDirectoryName(dataAttachmentAssemblyLocation)!, CodeCoverageIoAssemblyName + ".dll"); s_codeCoverageAssembly = new PlatformAssemblyLoadContext().LoadAssemblyFromPath(assemblyPath); var classType = s_codeCoverageAssembly.GetType($"{CodeCoverageIoAssemblyName}.{CoverageFileUtilityTypeName}")!; s_classInstance = Activator.CreateInstance(classType)!; var types = s_codeCoverageAssembly.GetTypes(); var mergeOperationEnum = Array.Find(types, d => d.Name == CoverageMergeOperationName)!; s_mergeOperationEnumValues = Enum.GetValues(mergeOperationEnum); s_mergeMethodInfo = classType.GetMethod(MergeMethodName, [typeof(string), typeof(IList<string>), mergeOperationEnum, typeof(bool), typeof(CancellationToken)])!; } }