Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating merge api, #3053

Merged
10 commits merged into from
Sep 22, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ public class CodeCoverageDataAttachmentsHandler : IDataCollectorAttachmentProces
{
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.VisualStudio.Coverage.IO";
private const string CoverageFileUtilityTypeName = "CoverageFileUtility";
private const string MergeMethodName = "MergeCoverageFilesAsync";
private const string WriteMethodName = "WriteCoverageFile";
private const string MergeMethodName = "MergeCoverageReportsAsync";
private const string CoverageMergeOperationName = "CoverageMergeOperation";

private static readonly Uri CodeCoverageDataCollectorUri = new Uri(CoverageUri);
private static Assembly CodeCoverageAssembly;
private static object ClassInstance;
private static MethodInfo MergeMethodInfo;
private static Array MergeOperationEnumValues;

public bool SupportsIncrementalProcessing => true;

Expand All @@ -39,50 +44,50 @@ public IEnumerable<Uri> GetExtensionUris()

public async Task<ICollection<AttachmentSet>> ProcessAttachmentSetsAsync(ICollection<AttachmentSet> attachments, IProgress<int> progressReporter, IMessageLogger logger, CancellationToken cancellationToken)
{
if (attachments != null && attachments.Any())
{
var coverageReportFilePaths = new List<string>();
var coverageOtherFilePaths = new List<string>();
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 attachmentSet in attachments)
{
foreach (var attachment in attachmentSet.Attachments)
{
foreach (var attachment in attachmentSet.Attachments)
if (attachment.Uri.LocalPath.EndsWith(CoverageFileExtension, StringComparison.OrdinalIgnoreCase) ||
attachment.Uri.LocalPath.EndsWith(XmlFileExtension, StringComparison.OrdinalIgnoreCase))
{
if (attachment.Uri.LocalPath.EndsWith(CoverageFileExtension, StringComparison.OrdinalIgnoreCase))
{
coverageReportFilePaths.Add(attachment.Uri.LocalPath);
}
else
{
coverageOtherFilePaths.Add(attachment.Uri.LocalPath);
}
coverageReportFilePaths.Add(attachment.Uri.LocalPath);
}
else
{
coverageOtherFilePaths.Add(attachment.Uri.LocalPath);
}
}
}

if (coverageReportFilePaths.Count > 1)
{
var mergedCoverageReportFilePath = await this.MergeCodeCoverageFilesAsync(coverageReportFilePaths, progressReporter, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(mergedCoverageReportFilePath))
{
var resultAttachmentSet = new AttachmentSet(CodeCoverageDataCollectorUri, CoverageFriendlyName);
resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(mergedCoverageReportFilePath, CoverageFriendlyName));
if (coverageReportFilePaths.Count > 1)
{
var mergedCoverageReports = await this.MergeCodeCoverageFilesAsync(coverageReportFilePaths, progressReporter, cancellationToken).ConfigureAwait(false);
var resultAttachmentSet = new AttachmentSet(CodeCoverageDataCollectorUri, CoverageFriendlyName);

foreach (var coverageOtherFilePath in coverageOtherFilePaths)
{
resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(coverageOtherFilePath, string.Empty));
}
foreach (var coverageReport in mergedCoverageReports)
{
resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(coverageReport, CoverageFriendlyName));
}

return new Collection<AttachmentSet> { resultAttachmentSet };
}
foreach (var coverageOtherFilePath in coverageOtherFilePaths)
{
resultAttachmentSet.Attachments.Add(UriDataAttachment.CreateFrom(coverageOtherFilePath, string.Empty));
}

return attachments;
return new Collection<AttachmentSet> { resultAttachmentSet };
}

return new Collection<AttachmentSet>();
return attachments;
}

private async Task<string> MergeCodeCoverageFilesAsync(IList<string> files, IProgress<int> progressReporter, CancellationToken cancellationToken)
private async Task<IList<string>> MergeCodeCoverageFilesAsync(IList<string> files, IProgress<int> progressReporter, CancellationToken cancellationToken)
{
try
{
Expand Down Expand Up @@ -110,28 +115,23 @@ private async Task<string> MergeCodeCoverageFilesAsync(IList<string> files, IPro
return null;
}

private async Task<string> MergeCodeCoverageFilesAsync(IList<string> files, CancellationToken cancellationToken)
private async Task<IList<string>> MergeCodeCoverageFilesAsync(IList<string> files, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var assemblyPath = Path.Combine(Path.GetDirectoryName(typeof(CodeCoverageDataAttachmentsHandler).GetTypeInfo().Assembly.GetAssemblyLocation()), CodeCoverageIOAssemblyName + ".dll");

// Get assembly, type and methods
Assembly assembly = new PlatformAssemblyLoadContext().LoadAssemblyFromPath(assemblyPath);
var classType = assembly.GetType($"{CodeCoverageIOAssemblyName}.{CoverageFileUtilityTypeName}");
var classInstance = Activator.CreateInstance(classType);
var mergeMethodInfo = classType?.GetMethod(MergeMethodName, new[] { typeof(IList<string>), typeof(CancellationToken) });
var writeMethodInfo = classType?.GetMethod(WriteMethodName);

// Invoke methods
var task = (Task)mergeMethodInfo.Invoke(classInstance, new object[] { files, cancellationToken });
LoadCodeCoverageAssembly();
var task = (Task)MergeMethodInfo.Invoke(ClassInstance, new object[] { files[0], files, MergeOperationEnumValues.GetValue(0), true, cancellationToken });
await task.ConfigureAwait(false);
var coverageData = task.GetType().GetProperty("Result").GetValue(task, null);
writeMethodInfo.Invoke(classInstance, new object[] { files[0], coverageData });
var mergedResults = coverageData as IList<string>;

// Delete original files and keep merged file only
foreach (var file in files.Skip(1))
foreach (var file in files)
{
if (mergedResults.Contains(file))
continue;

try
{
File.Delete(file);
Expand All @@ -142,7 +142,24 @@ private async Task<string> MergeCodeCoverageFilesAsync(IList<string> files, Canc
}
}

return files[0];
return mergedResults;
}

private void LoadCodeCoverageAssembly()
{
if (CodeCoverageAssembly != null)
return;

var assemblyPath = Path.Combine(Path.GetDirectoryName(typeof(CodeCoverageDataAttachmentsHandler).GetTypeInfo().Assembly.GetAssemblyLocation()), CodeCoverageIOAssemblyName + ".dll");
CodeCoverageAssembly = new PlatformAssemblyLoadContext().LoadAssemblyFromPath(assemblyPath);

var classType = CodeCoverageAssembly.GetType($"{CodeCoverageIOAssemblyName}.{CoverageFileUtilityTypeName}");
ClassInstance = Activator.CreateInstance(classType);

var types = CodeCoverageAssembly.GetTypes();
var mergeOperationEnum = Array.Find(types, d => d.Name == CoverageMergeOperationName);
MergeOperationEnumValues = Enum.GetValues(mergeOperationEnum);
MergeMethodInfo = classType?.GetMethod(MergeMethodName, new[] { typeof(string), typeof(IList<string>), mergeOperationEnum, typeof(bool), typeof(CancellationToken) });
}
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
namespace Microsoft.TestPlatform.Utilities.UnitTests
{
using Moq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

[TestClass]
public class CodeCoverageDataAttachmentsHandlerTests
{
private readonly Mock<IProgress<int>> mockProgressReporter;
private readonly CodeCoverageDataAttachmentsHandler coverageDataAttachmentsHandler;

public TestContext TestContext { get; set; }

internal string TestFilesDirectory => Path.Combine(TestContext.DeploymentDirectory, "TestFiles");

public CodeCoverageDataAttachmentsHandlerTests()
{
mockProgressReporter = new Mock<IProgress<int>>();
Expand Down Expand Up @@ -59,6 +63,47 @@ public async Task HandleDataCollectionAttachmentSetsShouldReturnInputIfOnly1Atta
Assert.AreEqual("file:///C:/temp/aa.coverage", resultAttachmentSets.First().Attachments.First().Uri.AbsoluteUri);
}

[TestMethod]
public async Task HandleDataCollectionAttachmentSetsShouldReturnInputIf2DifferentFormatAttachments()
{
var file1Path = Path.Combine(TestFilesDirectory, "fullcovered.cobertura.xml");
var file2Path = Path.Combine(Path.Combine(TestFilesDirectory, "fullcovered.coverage"));
var attachmentSet = new AttachmentSet(new Uri("datacollector://microsoft/CodeCoverage/2.0"), string.Empty);
attachmentSet.Attachments.Add(new UriDataAttachment(new Uri(file1Path), "coverage"));
attachmentSet.Attachments.Add(new UriDataAttachment(new Uri(file2Path), "coverage"));

Collection<AttachmentSet> attachment = new Collection<AttachmentSet> { attachmentSet };
ICollection<AttachmentSet> resultAttachmentSets = await
coverageDataAttachmentsHandler.ProcessAttachmentSetsAsync(attachment, mockProgressReporter.Object, null, CancellationToken.None);

Assert.IsNotNull(resultAttachmentSets);
Assert.IsTrue(resultAttachmentSets.Count == 1);
Assert.IsTrue(resultAttachmentSets.First().Attachments.Count == 2);
Assert.AreEqual("datacollector://microsoft/CodeCoverage/2.0", resultAttachmentSets.First().Uri.AbsoluteUri);
Assert.AreEqual("datacollector://microsoft/CodeCoverage/2.0", resultAttachmentSets.Last().Uri.AbsoluteUri);
Assert.AreEqual("file:///" + file1Path.Replace("\\", "/"), resultAttachmentSets.First().Attachments.First().Uri.AbsoluteUri);
Assert.AreEqual("file:///" + file2Path.Replace("\\", "/"), resultAttachmentSets.First().Attachments.Last().Uri.AbsoluteUri);
}

[TestMethod]
public async Task HandleDataCollectionAttachmentSetsShouldReturnInputIf2SameFormatAttachments()
{
var file1Path = Path.Combine(TestFilesDirectory, "fullcovered.cobertura.xml");
var attachmentSet = new AttachmentSet(new Uri("datacollector://microsoft/CodeCoverage/2.0"), string.Empty);
attachmentSet.Attachments.Add(new UriDataAttachment(new Uri(file1Path), "coverage"));
attachmentSet.Attachments.Add(new UriDataAttachment(new Uri(file1Path), "coverage"));

Collection<AttachmentSet> attachment = new Collection<AttachmentSet> { attachmentSet };
ICollection<AttachmentSet> resultAttachmentSets = await
coverageDataAttachmentsHandler.ProcessAttachmentSetsAsync(attachment, mockProgressReporter.Object, null, CancellationToken.None);

Assert.IsNotNull(resultAttachmentSets);
Assert.IsTrue(resultAttachmentSets.Count == 1);
Assert.IsTrue(resultAttachmentSets.First().Attachments.Count == 1);
Assert.AreEqual("datacollector://microsoft/CodeCoverage/2.0", resultAttachmentSets.First().Uri.AbsoluteUri);
Assert.AreEqual("file:///" + file1Path.Replace("\\", "/"), resultAttachmentSets.First().Attachments.First().Uri.AbsoluteUri);
}

[TestMethod]
public async Task HandleDataCollectionAttachmentSetsShouldReturnInputIfOnly1LogsAttachment()
{
Expand Down Expand Up @@ -104,7 +149,7 @@ public async Task HandleDataCollectionAttachmentSetsShouldThrowIfCancellationReq
CancellationTokenSource cts = new CancellationTokenSource();
cts.Cancel();

Collection<AttachmentSet> attachment = new Collection<AttachmentSet>
Collection<AttachmentSet> attachment = new Collection<AttachmentSet>
{
attachmentSet,
attachmentSet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TestPlatformRoot Condition="$(TestPlatformRoot) == ''">..\..\</TestPlatformRoot>
<TestProject>true</TestProject>
<IsTestProject>true</IsTestProject>
<TestPlatformRoot Condition="$(TestPlatformRoot) == ''">..\..\</TestPlatformRoot>
<TestProject>true</TestProject>
<IsTestProject>true</IsTestProject>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<Import Project="$(TestPlatformRoot)scripts/build/TestPlatform.Settings.targets" />
<PropertyGroup>
<OutputType Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">Exe</OutputType>
<TargetFrameworks>netcoreapp2.1;net451</TargetFrameworks>
<AssemblyName>Microsoft.TestPlatform.Utilities.UnitTests</AssemblyName>
<OutputType Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">Exe</OutputType>
<TargetFrameworks>netcoreapp2.1;net451</TargetFrameworks>
<AssemblyName>Microsoft.TestPlatform.Utilities.UnitTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.TestPlatform.Utilities\Microsoft.TestPlatform.Utilities.csproj" />
<ProjectReference Include="..\..\src\Microsoft.TestPlatform.Utilities\Microsoft.TestPlatform.Utilities.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeCoverage">
<Version>1.0.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.CodeCoverage">
<Version>1.0.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Coverage.IO" Version="17.0.0-beta.21465.2" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net451' ">
<Reference Include="System.Runtime" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Xml" />
<Reference Include="System.Threading.Tasks" />
<Reference Include="System.Runtime" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Xml" />
<Reference Include="System.Threading.Tasks" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="DefaultCodeCoverageConfig.xml" />
<None Update="TestFiles\**\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="DefaultCodeCoverageConfig.xml" />
</ItemGroup>
<Import Project="$(TestPlatformRoot)scripts\build\TestPlatform.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<coverage line-rate="1" branch-rate="1" complexity="8" version="1.9" timestamp="1631015306" lines-covered="6" branches-covered="1">
<sources />
<packages>
<package line-rate="1" branch-rate="1" complexity="8" name="branches">
<classes>
<class line-rate="1" branch-rate="1" complexity="8" name="Branches.SwitchConditions" filename="D:\Code\CoverletTester\Branches\SwitchConditions.cs">
<methods>
<method line-rate="1" branch-rate="1" complexity="8" name="SpecialSwitch" signature="(System.DayOfWeek)">
<lines>
<line number="12" hits="1" branch="False" />
<line number="14" hits="1" branch="False" />
<line number="16" hits="1" branch="False" />
<line number="18" hits="1" branch="False" />
<line number="20" hits="1" branch="False" />
<line number="22" hits="1" branch="False" />
</lines>
</method>
</methods>
<lines>
<line number="12" hits="1" branch="False" />
<line number="14" hits="1" branch="False" />
<line number="16" hits="1" branch="False" />
<line number="18" hits="1" branch="False" />
<line number="20" hits="1" branch="False" />
<line number="22" hits="1" branch="False" />
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<results>
<modules>
<module block_coverage="50.00" line_coverage="50.00" blocks_covered="3" blocks_not_covered="3" lines_covered="3" lines_partially_covered="0" lines_not_covered="3" name="Branches" path="Branches" id="A0010CA3957DA54FB0882F8DB3BB84E701000000">
<functions>
<function block_coverage="50.00" line_coverage="50.00" blocks_covered="3" blocks_not_covered="3" lines_covered="3" lines_partially_covered="0" lines_not_covered="3" id="0" name="SpecialSwitch(System.DayOfWeek)" namespace="Branches" type_name="SwitchConditions">
<ranges>
<range source_id="0" covered="yes" start_line="12" start_column="0" end_line="12" end_column="0" />
<range source_id="0" covered="yes" start_line="14" start_column="0" end_line="14" end_column="0" />
<range source_id="0" covered="yes" start_line="16" start_column="0" end_line="16" end_column="0" />
<range source_id="0" covered="no" start_line="18" start_column="0" end_line="18" end_column="0" />
<range source_id="0" covered="no" start_line="20" start_column="0" end_line="20" end_column="0" />
<range source_id="0" covered="no" start_line="22" start_column="0" end_line="22" end_column="0" />
</ranges>
</function>
</functions>
<source_files>
<source_file id="0" path="D:\Code\CoverletTester\Branches\SwitchConditions.cs" />
</source_files>
</module>
</modules>
</results>