Skip to content

Commit ac0e0fa

Browse files
Allow standalone coverlet usage for integration/end-to-end tests using .NET tool driver (#991)
Allow standalone coverlet usage for integration/end-to-end tests using .NET tool driver
1 parent 3fe2d90 commit ac0e0fa

File tree

11 files changed

+98
-32
lines changed

11 files changed

+98
-32
lines changed

Documentation/GlobalTool.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ coverlet --help
99
The current options are (output of `coverlet --help`):
1010

1111
```bash
12-
Cross platform .NET Core code coverage tool 1.0.0.0
12+
Cross platform .NET Core code coverage tool 3.0.0.0
1313

1414
Usage: coverlet [arguments] [options]
1515

1616
Arguments:
17-
<ASSEMBLY> Path to the test assembly.
17+
<ASSEMBLY|DIRECTORY> Path to the test assembly or application directory.
1818

1919
Options:
2020
-h|--help Show help information
@@ -23,21 +23,21 @@ Options:
2323
-a|--targetargs Arguments to be passed to the test runner.
2424
-o|--output Output of the generated coverage report
2525
-v|--verbosity Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed.
26-
-f|--format Format of the generated coverage report[multiple value].
26+
-f|--format Format of the generated coverage report.
2727
--threshold Exits with error if the coverage % is below value.
28-
--threshold-type Coverage type to apply the threshold to[multiple value].
28+
--threshold-type Coverage type to apply the threshold to.
2929
--threshold-stat Coverage statistic used to enforce the threshold value.
30-
--exclude Filter expressions to exclude specific modules and types[multiple value].
31-
--include Filter expressions to include specific modules and types[multiple value].
32-
--include-directory Include directories containing additional assemblies to be instrumented[multiple value].
33-
--exclude-by-file Glob patterns specifying source files to exclude[multiple value].
34-
--exclude-by-attribute Attributes to exclude from code coverage[multiple value].
30+
--exclude Filter expressions to exclude specific modules and types.
31+
--include Filter expressions to include only specific modules and types.
32+
--exclude-by-file Glob patterns specifying source files to exclude.
33+
--include-directory Include directories containing additional assemblies to be instrumented.
34+
--exclude-by-attribute Attributes to exclude from code coverage.
3535
--include-test-assembly Specifies whether to report code coverage of the test assembly.
36-
--single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location.
36+
--single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location
37+
--skipautoprops Neither track nor record auto-implemented properties.
3738
--merge-with Path to existing coverage result to merge.
3839
--use-source-link Specifies whether to use SourceLink URIs in place of file system paths.
39-
--skipautoprops Neither track nor record auto-implemented properties.
40-
--does-not-return-attribute Attributes that mark methods that do not return[multiple value].
40+
--does-not-return-attribute Attributes that mark methods that do not return.
4141
```
4242
4343
NB. For a [multiple value] options you have to specify values multiple times i.e.
@@ -60,6 +60,20 @@ After the above command is run, a `coverage.json` file containing the results wi
6060
6161
_Note: The `--no-build` flag is specified so that the `/path/to/test-assembly.dll` isn't rebuilt_
6262
63+
## Code Coverage for integration tests and end-to-end tests.
64+
65+
Sometimes, there are tests that doesn't use regular unit test frameworks like xunit. You may find yourself in a situation where your tests are driven by a custom executable/script, which when run, could do anything from making API calls to driving Selenium.
66+
67+
As an example, suppose you have a folder `/integrationtest` which contains said executable (lets call it `runner.exe`) and everything it needs to successfully execute. You can use our tool to startup the executable and gather live coverage:
68+
69+
```bash
70+
coverlet "/integrationtest" --target "/application/runner.exe"
71+
```
72+
73+
Coverlet will first instrument all .NET assemblies within the `integrationtests` folder, after which it will execute `runner.exe`. Finally, at shutdown of your `runner.exe`, it will generate the coverage report. You can use all parameters available to customize the report generation. Coverage results will be generated once `runner.exe` exits. You can use all parameters available to customize the report generation.
74+
75+
_Note: Today, Coverlet relies on `AppDomain.CurrentDomain.ProcessExit` and `AppDomain.CurrentDomain.DomainUnload` to record hits to the filesystem, as a result, you need to ensure a graceful process shutdown. Forcefully, killing the process will result in an incomplete coverage report._
76+
6377
## Coverage Output
6478
6579
Coverlet can generate coverage results in multiple formats, which is specified using the `--format` or `-f` options. For example, the following command emits coverage results in the `opencover` format instead of `json`:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Coverlet can be used through three different *drivers*
2929

3030
* VSTest engine integration
3131
* MSBuild task integration
32-
* As a .NET Global tool
32+
* As a .NET Global tool (supports standalone integration tests)
3333

3434
Coverlet supports only SDK-style projects https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2019
3535

src/coverlet.console/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ static int Main(string[] args)
4444
app.VersionOption("-v|--version", GetAssemblyVersion());
4545
int exitCode = (int)CommandExitCodes.Success;
4646

47-
CommandArgument module = app.Argument("<ASSEMBLY>", "Path to the test assembly.");
47+
CommandArgument moduleOrAppDirectory = app.Argument("<ASSEMBLY|DIRECTORY>", "Path to the test assembly or application directory.");
4848
CommandOption target = app.Option("-t|--target", "Path to the test runner application.", CommandOptionType.SingleValue);
4949
CommandOption targs = app.Option("-a|--targetargs", "Arguments to be passed to the test runner.", CommandOptionType.SingleValue);
5050
CommandOption output = app.Option("-o|--output", "Output of the generated coverage report", CommandOptionType.SingleValue);
@@ -67,8 +67,8 @@ static int Main(string[] args)
6767

6868
app.OnExecute(() =>
6969
{
70-
if (string.IsNullOrEmpty(module.Value) || string.IsNullOrWhiteSpace(module.Value))
71-
throw new CommandParsingException(app, "No test assembly specified.");
70+
if (string.IsNullOrEmpty(moduleOrAppDirectory.Value) || string.IsNullOrWhiteSpace(moduleOrAppDirectory.Value))
71+
throw new CommandParsingException(app, "No test assembly or application directory specified.");
7272

7373
if (!target.HasValue())
7474
throw new CommandParsingException(app, "Target must be specified.");
@@ -94,7 +94,7 @@ static int Main(string[] args)
9494
DoesNotReturnAttributes = doesNotReturnAttributes.Values.ToArray()
9595
};
9696

97-
Coverage coverage = new Coverage(module.Value,
97+
Coverage coverage = new Coverage(moduleOrAppDirectory.Value,
9898
parameters,
9999
logger,
100100
serviceProvider.GetRequiredService<IInstrumentationHelper>(),

src/coverlet.core/Coverage.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class CoverageParameters
2929

3030
internal class Coverage
3131
{
32-
private string _module;
32+
private string _moduleOrAppDirectory;
3333
private string _identifier;
3434
private string[] _includeFilters;
3535
private string[] _includeDirectories;
@@ -54,15 +54,15 @@ public string Identifier
5454
get { return _identifier; }
5555
}
5656

57-
public Coverage(string module,
57+
public Coverage(string moduleOrDirectory,
5858
CoverageParameters parameters,
5959
ILogger logger,
6060
IInstrumentationHelper instrumentationHelper,
6161
IFileSystem fileSystem,
6262
ISourceRootTranslator sourceRootTranslator,
6363
ICecilSymbolHelper cecilSymbolHelper)
6464
{
65-
_module = module;
65+
_moduleOrAppDirectory = moduleOrDirectory;
6666
_includeFilters = parameters.IncludeFilters;
6767
_includeDirectories = parameters.IncludeDirectories ?? Array.Empty<string>();
6868
_excludeFilters = parameters.ExcludeFilters;
@@ -91,7 +91,7 @@ public Coverage(CoveragePrepareResult prepareResult,
9191
ISourceRootTranslator sourceRootTranslator)
9292
{
9393
_identifier = prepareResult.Identifier;
94-
_module = prepareResult.Module;
94+
_moduleOrAppDirectory = prepareResult.ModuleOrDirectory;
9595
_mergeWith = prepareResult.MergeWith;
9696
_useSourceLink = prepareResult.UseSourceLink;
9797
_results = new List<InstrumenterResult>(prepareResult.Results);
@@ -103,7 +103,7 @@ public Coverage(CoveragePrepareResult prepareResult,
103103

104104
public CoveragePrepareResult PrepareModules()
105105
{
106-
string[] modules = _instrumentationHelper.GetCoverableModules(_module, _includeDirectories, _includeTestAssembly);
106+
string[] modules = _instrumentationHelper.GetCoverableModules(_moduleOrAppDirectory, _includeDirectories, _includeTestAssembly);
107107

108108
Array.ForEach(_excludeFilters ?? Array.Empty<string>(), filter => _logger.LogVerbose($"Excluded module filter '{filter}'"));
109109
Array.ForEach(_includeFilters ?? Array.Empty<string>(), filter => _logger.LogVerbose($"Included module filter '{filter}'"));
@@ -161,7 +161,7 @@ public CoveragePrepareResult PrepareModules()
161161
return new CoveragePrepareResult()
162162
{
163163
Identifier = _identifier,
164-
Module = _module,
164+
ModuleOrDirectory = _moduleOrAppDirectory,
165165
MergeWith = _mergeWith,
166166
UseSourceLink = _useSourceLink,
167167
Results = _results.ToArray()

src/coverlet.core/CoveragePrepareResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class CoveragePrepareResult
1313
[DataMember]
1414
public string Identifier { get; set; }
1515
[DataMember]
16-
public string Module { get; set; }
16+
public string ModuleOrDirectory { get; set; }
1717
[DataMember]
1818
public string MergeWith { get; set; }
1919
[DataMember]

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ public InstrumentationHelper(IProcessExitHandler processExitHandler, IRetryHelpe
3131
_sourceRootTranslator = sourceRootTranslator;
3232
}
3333

34-
public string[] GetCoverableModules(string module, string[] directories, bool includeTestAssembly)
34+
public string[] GetCoverableModules(string moduleOrAppDirectory, string[] directories, bool includeTestAssembly)
3535
{
3636
Debug.Assert(directories != null);
37+
Debug.Assert(moduleOrAppDirectory != null);
38+
39+
bool isAppDirectory = !File.Exists(moduleOrAppDirectory) && Directory.Exists(moduleOrAppDirectory);
40+
string moduleDirectory = isAppDirectory ? moduleOrAppDirectory : Path.GetDirectoryName(moduleOrAppDirectory);
3741

38-
string moduleDirectory = Path.GetDirectoryName(module);
3942
if (moduleDirectory == string.Empty)
4043
{
4144
moduleDirectory = Directory.GetCurrentDirectory();
@@ -67,8 +70,8 @@ public string[] GetCoverableModules(string module, string[] directories, bool in
6770
// The module's name must be unique.
6871
var uniqueModules = new HashSet<string>();
6972

70-
if (!includeTestAssembly)
71-
uniqueModules.Add(Path.GetFileName(module));
73+
if (!includeTestAssembly && !isAppDirectory)
74+
uniqueModules.Add(Path.GetFileName(moduleOrAppDirectory));
7275

7376
return dirs.SelectMany(d => Directory.EnumerateFiles(d))
7477
.Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m)))

test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,13 @@ public void TestIsTypeExcludedAndIncludedWithMatchingAndMismatchingFilter(string
208208
public void TestIncludeDirectories()
209209
{
210210
string module = typeof(InstrumentationHelperTests).Assembly.Location;
211-
212211
DirectoryInfo newDir = Directory.CreateDirectory("TestIncludeDirectories");
212+
newDir.Delete(true);
213+
newDir.Create();
213214
DirectoryInfo newDir2 = Directory.CreateDirectory("TestIncludeDirectories2");
215+
newDir2.Delete(true);
216+
newDir2.Create();
217+
214218
File.Copy(module, Path.Combine(newDir.FullName, Path.GetFileName(module)));
215219
module = Path.Combine(newDir.FullName, Path.GetFileName(module));
216220
File.Copy("coverlet.msbuild.tasks.dll", Path.Combine(newDir.FullName, "coverlet.msbuild.tasks.dll"));
@@ -220,11 +224,23 @@ public void TestIncludeDirectories()
220224
Assert.Single(currentDirModules);
221225
Assert.Equal("coverlet.msbuild.tasks.dll", Path.GetFileName(currentDirModules[0]));
222226

223-
var moreThanOneDirectory = _instrumentationHelper.GetCoverableModules(module, new string[] { newDir2.FullName }, false);
227+
var moreThanOneDirectory = _instrumentationHelper
228+
.GetCoverableModules(module, new string[] { newDir2.FullName }, false)
229+
.OrderBy(f => f).ToArray();
230+
224231
Assert.Equal(2, moreThanOneDirectory.Length);
225232
Assert.Equal("coverlet.msbuild.tasks.dll", Path.GetFileName(moreThanOneDirectory[0]));
226233
Assert.Equal("coverlet.core.dll", Path.GetFileName(moreThanOneDirectory[1]));
227234

235+
var moreThanOneDirectoryPlusTestAssembly = _instrumentationHelper
236+
.GetCoverableModules(module, new string[] { newDir2.FullName }, true)
237+
.OrderBy(f => f).ToArray();
238+
239+
Assert.Equal(3, moreThanOneDirectoryPlusTestAssembly.Length);
240+
Assert.Equal("coverlet.core.tests.dll", Path.GetFileName(moreThanOneDirectoryPlusTestAssembly[0]));
241+
Assert.Equal("coverlet.msbuild.tasks.dll", Path.GetFileName(moreThanOneDirectoryPlusTestAssembly[1]));
242+
Assert.Equal("coverlet.core.dll", Path.GetFileName(moreThanOneDirectoryPlusTestAssembly[2]));
243+
228244
newDir.Delete(true);
229245
newDir2.Delete(true);
230246
}

test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void CoveragePrepareResult_SerializationRoundTrip()
2929
CoveragePrepareResult cpr = new CoveragePrepareResult();
3030
cpr.Identifier = "Identifier";
3131
cpr.MergeWith = "MergeWith";
32-
cpr.Module = "Module";
32+
cpr.ModuleOrDirectory = "Module";
3333
cpr.UseSourceLink = true;
3434

3535
InstrumenterResult ir = new InstrumenterResult();
@@ -99,7 +99,7 @@ public void CoveragePrepareResult_SerializationRoundTrip()
9999

100100
Assert.Equal(cpr.Identifier, roundTrip.Identifier);
101101
Assert.Equal(cpr.MergeWith, roundTrip.MergeWith);
102-
Assert.Equal(cpr.Module, roundTrip.Module);
102+
Assert.Equal(cpr.ModuleOrDirectory, roundTrip.ModuleOrDirectory);
103103
Assert.Equal(cpr.UseSourceLink, roundTrip.UseSourceLink);
104104

105105
for (int i = 0; i < cpr.Results.Length; i++)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
using Coverlet.Integration.Template;
4+
5+
namespace HelloWorld
6+
{
7+
class Program
8+
{
9+
static void Main(string[] args)
10+
{
11+
DeepThought dt = new DeepThought();
12+
dt.AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything();
13+
Console.WriteLine("Hello World!");
14+
}
15+
}
16+
}

test/coverlet.integration.template/coverlet.integration.template.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<IsPackable>false</IsPackable>
66
<AssemblyName>coverletsamplelib.integration.template</AssemblyName>
77
<IsTestProject>false</IsTestProject>
8+
<OutputType>Exe</OutputType>
9+
<GenerateProgramFile>false</GenerateProgramFile>
810
</PropertyGroup>
911

1012
<ItemGroup>

0 commit comments

Comments
 (0)