Skip to content

Commit

Permalink
Merge pull request #2 from tonerdo/master
Browse files Browse the repository at this point in the history
Update fork
  • Loading branch information
daveMueller authored Sep 5, 2019
2 parents a6e0687 + 75520dd commit 84c0aff
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 102 deletions.
35 changes: 11 additions & 24 deletions Documentation/VSTestIntegration.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
# Coverlet Integration with VSTest

## Motivation

The cross platform solution for Code Coverage generation for .NET Core projects (in a consistent manner).
The asks for Code Coverage support for .NET Core on Linux is the most commented issue on vstest repo:
https://github.com/Microsoft/vstest/issues/981#issuecomment-320311552

## Summary

What would integrating Coverlet with Microsoft Test Platform mean:

1. Coverlet based coverage solution is available as a data collector that does the instrumentation of the necessary modules before test execution and appropriate restore after.
2. Authoring - When new test projects are created (dotnet mstest), references to the data collector package are added by default. This reduces adoption friction for customers.
3. Test execution - Today Coverlet is invoked as a msbuild target and as such only works with dotnet test (and requires a csproj). With the data collector becoming available, coverage can also be collected when tests are run on built binaries (dotnet vstest)

## Proposed Solution

### Scenarios to support

The following table summarizes the support that needs to be added for a seamless code coverage collection for .NET Core on both Linux and Windows platforms:

| Entry point | How will code coverage be enabled? | Syntax |
|-------------|------------------------------------|----------------------------------------------------------------------|
|dotnet test CLI | Through a switch to condition data collection | `dotnet test --collect:"XPlat Code Coverage"` |
|dotnet vstest CLI | Through a switch to condition data collection | `dotnet vstest --collect:"XPlat Code Coverage"` |

NB. If you're using `dotnet vstest` you MUST `publish` your test project before i.e.
```bash
C:\project
dotnet publish
...
vstest -> C:\project\bin\Debug\netcoreapp3.0\testdll.dll
vstest -> C:\project\bin\Debug\netcoreapp3.0\publish\
...
dotnet vstest C:\project\bin\Debug\netcoreapp3.0\publish\testdll.dll --collect:"XPlat Code Coverage"
```

### Coverlet Options Supported with VSTest

#### Default
Expand Down Expand Up @@ -75,11 +67,6 @@ This runsettings file can easily be provided using command line option as given

2. `dotnet vstest --settings coverletArgs.runsettings`


#### Scope of Enhancement

Currently, advanced options are supported via runsettings. Providing support through additional command line arguments in vstest can be taken up separately.

## Implementation Details

The proposed solution is implemented with the help of [datacollectors](https://github.com/Microsoft/vstest-docs/blob/master/docs/extensions/datacollector.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,6 @@ private string[] ParseExcludeFilters(XmlElement configurationElement)
}
}

// if we've only one element mean that we only added CoverletConstants.DefaultExcludeFilter
// so add default exclusions
if (excludeFilters.Count == 1)
{
excludeFilters.Add("[xunit*]*");
}

return excludeFilters.ToArray();
}

Expand Down
6 changes: 0 additions & 6 deletions src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ static int Main(string[] args)
logger.Level = verbosity.ParsedValue;
}

// We add default exclusion filter if no specified
if (excludeFilters.Values.Count == 0)
{
excludeFilters.Values.Add("[xunit*]*");
}

Coverage coverage = new Coverage(module.Value,
includeFilters.Values.ToArray(),
includeDirectories.Values.ToArray(),
Expand Down
3 changes: 3 additions & 0 deletions src/coverlet.core/Instrumentation/InstrumenterResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;

namespace Coverlet.Core.Instrumentation
Expand All @@ -17,6 +18,7 @@ public class Line
public int Hits;
}

[DebuggerDisplay("Line = {Number} Offset = {Offset} EndOffset = {EndOffset} Path = {Path} Ordinal = {Ordinal} Hits = {Hits}")]
[DataContract]
public class Branch : Line
{
Expand All @@ -30,6 +32,7 @@ public class Branch : Line
public uint Ordinal;
}

[DebuggerDisplay("line = {Line} Ordinal = {Ordinal}")]
// Implements IEquatable because is used by dictionary key https://docs.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=netcore-2.2#remarks
[DataContract]
public class BranchKey : IEquatable<BranchKey>
Expand Down
6 changes: 0 additions & 6 deletions src/coverlet.msbuild.tasks/InstrumentationTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,6 @@ public override bool Execute()
var excludedSourceFiles = _excludeByFile?.Split(',');
var excludeAttributes = _excludeByAttribute?.Split(',');

// We add default exclusion filter if no specified
if (excludeFilters is null || excludeFilters.Length == 0)
{
excludeFilters = new string[] { "[xunit*]*" };
}

Coverage coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _includeTestAssembly, _singleHit, _mergeWith, _useSourceLink, _logger);
CoveragePrepareResult prepareResult = coverage.PrepareModules();
InstrumenterState = new TaskItem(System.IO.Path.GetTempFileName());
Expand Down
28 changes: 0 additions & 28 deletions test/coverlet.collector.tests/CoverletSettingsParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,5 @@ private void CreateCoverletNodes(XmlDocument doc, XmlElement configElement, stri
node.InnerText = nodeValue;
configElement.AppendChild(node);
}

[Fact]
public void ParseShouldSkipXunitModulesIfEmptyExclude()
{
var testModules = new List<string> { "abc.dll" };

CoverletSettings coverletSettings = _coverletSettingsParser.Parse(null, testModules);

Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]);
Assert.Equal("[xunit*]*", coverletSettings.ExcludeFilters[1]);
Assert.Equal(2, coverletSettings.ExcludeFilters.Length);
}

[Fact]
public void ParseShouldNotSkipXunitModulesIfNotEmptyExclude()
{
var testModules = new List<string> { "abc.dll" };
var doc = new XmlDocument();
var configElement = doc.CreateElement("Configuration");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName, "[coverlet.*.tests?]*");

CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules);

Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]);
Assert.Equal("[coverlet.*.tests?]*", coverletSettings.ExcludeFilters[1]);
Assert.Equal(2, coverletSettings.ExcludeFilters.Length);
Assert.DoesNotContain("[xunit*]*", coverletSettings.ExcludeFilters);
}
}
}
36 changes: 33 additions & 3 deletions test/coverlet.core.tests/CoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void TestCoverageWithTestAssembly()
}

[Fact]
public void Condition_If()
public void SelectionStatements_If()
{
// We need to pass file name to remote process where it save instrumentation result
// Similar to msbuild input/output
Expand All @@ -73,7 +73,7 @@ public void Condition_If()
RemoteExecutor.Invoke(async pathSerialize =>
{
// Run load and call a delegate passing class as dynamic to simplify method call
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Conditions>(instance =>
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<SelectionStatements>(instance =>
{
// We call method to trigger coverage hits
instance.If(true);
Expand All @@ -91,7 +91,7 @@ public void Condition_If()
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);

// Asserts on doc/lines/branches
result.Document("Instrumentation.cs")
result.Document("Instrumentation.SelectionStatements.cs")
// (line, hits)
.AssertLinesCovered((11, 1), (15, 0))
// (line,ordinal,hits)
Expand All @@ -106,5 +106,35 @@ public void Condition_If()
File.Delete(path);
}
}

[Fact]
public void SelectionStatements_Switch()
{
string path = Path.GetTempFileName();
try
{
RemoteExecutor.Invoke(async pathSerialize =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<SelectionStatements>(instance =>
{
instance.Switch(1);
return Task.CompletedTask;
}, pathSerialize);
return 0;
}, path).Dispose();

CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);

result.Document("Instrumentation.SelectionStatements.cs")
.AssertLinesCovered(BuildConfiguration.Release, (24, 1), (26, 0), (28, 0))
.AssertBranchesCovered(BuildConfiguration.Release, (24, 1, 1))
.AssertLinesCovered(BuildConfiguration.Debug, (20, 1), (21, 1), (24, 1), (30, 1))
.AssertBranchesCovered(BuildConfiguration.Debug, (21, 0, 0), (21, 1, 1), (21, 2, 0), (21, 3, 0));
}
finally
{
File.Delete(path);
}
}
}
}
32 changes: 23 additions & 9 deletions test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ public void TestCoreLibInstrumentation()
Assert.NotNull(result);
}

[Fact]
public void TestInstrument()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestInstrument(bool singleHit)
{
var instrumenterTest = CreateInstrumentor();
var instrumenterTest = CreateInstrumentor(singleHit: singleHit);

var result = instrumenterTest.Instrumenter.Instrument();

Expand All @@ -55,10 +57,12 @@ public void TestInstrument()
instrumenterTest.Directory.Delete(true);
}

[Fact]
public void TestInstrumentCoreLib()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestInstrumentCoreLib(bool singleHit)
{
var instrumenterTest = CreateInstrumentor(fakeCoreLibModule: true);
var instrumenterTest = CreateInstrumentor(fakeCoreLibModule: true, singleHit: singleHit);

var result = instrumenterTest.Instrumenter.Instrument();

Expand Down Expand Up @@ -144,7 +148,7 @@ public void TestInstrument_ClassesWithPropertyWithCustomExcludeAttributeAreExclu
instrumenterTest.Directory.Delete(true);
}

private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, string[] attributesToIgnore = null)
private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, string[] attributesToIgnore = null, string[] excludedFiles = null, bool singleHit = false)
{
string module = GetType().Assembly.Location;
string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
Expand All @@ -168,7 +172,8 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, stri
File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true);

module = Path.Combine(directory.FullName, destModule);
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore, false, new Mock<ILogger>().Object);
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), excludedFiles, attributesToIgnore, singleHit, new Mock<ILogger>().Object);

return new InstrumenterTest
{
Instrumenter = instrumenter,
Expand Down Expand Up @@ -253,5 +258,14 @@ public void SkipEmbeddedPpdbWithoutLocalSource()
loggerMock.VerifyNoOtherCalls();
}

[Fact]
public void TestInstrument_MissingModule()
{
var loggerMock = new Mock<ILogger>();
var instrumenter = new Instrumenter("test", "_test_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, loggerMock.Object);
Assert.False(instrumenter.CanInstrument());
loggerMock.Verify(l => l.LogWarning(It.IsAny<string>()));
}

}
}
}
48 changes: 48 additions & 0 deletions test/coverlet.core.tests/InstrumenterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@

namespace Coverlet.Core.Tests
{
[Flags]
public enum BuildConfiguration
{
Debug = 1,
Release = 2
}

public static class TestInstrumentationAssert
{
public static Document Document(this CoverageResult coverageResult, string docName)
Expand All @@ -42,12 +49,24 @@ public static Document Document(this CoverageResult coverageResult, string docNa
}

public static Document AssertBranchesCovered(this Document document, params (int line, int ordinal, int hits)[] lines)
{
return AssertBranchesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines);
}

public static Document AssertBranchesCovered(this Document document, BuildConfiguration configuration, params (int line, int ordinal, int hits)[] lines)
{
if (document is null)
{
throw new ArgumentNullException(nameof(document));
}

BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration();

if ((buildConfiguration & configuration) != buildConfiguration)
{
return document;
}

List<string> branchesToCover = new List<string>(lines.Select(b => $"[line {b.line} ordinal {b.ordinal}]"));
foreach (KeyValuePair<BranchKey, Branch> branch in document.Branches)
{
Expand Down Expand Up @@ -77,12 +96,24 @@ public static Document AssertBranchesCovered(this Document document, params (int
}

public static Document AssertLinesCovered(this Document document, params (int line, int hits)[] lines)
{
return AssertLinesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines);
}

public static Document AssertLinesCovered(this Document document, BuildConfiguration configuration, params (int line, int hits)[] lines)
{
if (document is null)
{
throw new ArgumentNullException(nameof(document));
}

BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration();

if ((buildConfiguration & configuration) != buildConfiguration)
{
return document;
}

List<int> linesToCover = new List<int>(lines.Select(l => l.line));
foreach (KeyValuePair<int, Line> line in document.Lines)
{
Expand All @@ -106,6 +137,23 @@ public static Document AssertLinesCovered(this Document document, params (int li

return document;
}

private static BuildConfiguration GetAssemblyBuildConfiguration()
{
var configurationAttribute = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configurationAttribute.Configuration.Equals("Debug", StringComparison.InvariantCultureIgnoreCase))
{
return Tests.BuildConfiguration.Debug;
}
else if (configurationAttribute.Configuration.Equals("Release", StringComparison.InvariantCultureIgnoreCase))
{
return Tests.BuildConfiguration.Release;
}
else
{
throw new NotSupportedException($"Build configuration '{configurationAttribute.Configuration}' not supported");
}
}
}

public static class TestInstrumentationHelper
Expand Down
Loading

0 comments on commit 84c0aff

Please sign in to comment.