Check nested types for exclude filter (#694)

Check nested types for exclude filter
This commit is contained in:
Marco Rossignoli
2020-01-20 15:13:48 +01:00
committed by GitHub
parent ddd734e403
commit ff879e820b
14 changed files with 523 additions and 124 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
<!-- Do not upgrade this version or we won't support old SDK -->
<PackageReference Update="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Update="NuGet.Packaging" Version="5.4.0" />
<PackageReference Update="ReportGenerator.Core" Version="4.2.15" />
<PackageReference Update="ReportGenerator.Core" Version="4.4.4" />
<!--
Do not change System.Reflection.Metadata version since we need to support VSTest DataCollectors. Goto https://www.nuget.org/packages/System.Reflection.Metadata to check versions.
We need to load assembly version 1.4.2.0 to properly work
@@ -141,6 +141,28 @@ namespace Coverlet.Core.Instrumentation
return _result;
}
// If current type or one of his parent is excluded we'll exclude it
// If I'm out every my children and every children of my children will be out
private bool IsTypeExcluded(TypeDefinition type)
{
for (TypeDefinition current = type; current != null; current = current.DeclaringType)
{
// Check exclude attribute and filters
if (current.CustomAttributes.Any(IsExcludeAttribute) || _instrumentationHelper.IsTypeExcluded(_module, current.FullName, _excludeFilters))
{
return true;
}
}
return false;
}
// Instrumenting Interlocked which is used for recording hits would cause an infinite loop.
private bool Is_System_Threading_Interlocked_CoreLib_Type(TypeDefinition type)
{
return _isCoreLibrary && type.FullName == "System.Threading.Interlocked";
}
private void InstrumentModule()
{
using (var stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.ReadWrite))
@@ -177,18 +199,15 @@ namespace Coverlet.Core.Instrumentation
foreach (TypeDefinition type in types)
{
var actualType = type.DeclaringType ?? type;
if (!actualType.CustomAttributes.Any(IsExcludeAttribute)
// Instrumenting Interlocked which is used for recording hits would cause an infinite loop.
&& (!_isCoreLibrary || actualType.FullName != "System.Threading.Interlocked")
&& !_instrumentationHelper.IsTypeExcluded(_module, actualType.FullName, _excludeFilters)
&& _instrumentationHelper.IsTypeIncluded(_module, actualType.FullName, _includeFilters)
if (
!Is_System_Threading_Interlocked_CoreLib_Type(type) &&
!IsTypeExcluded(type) &&
_instrumentationHelper.IsTypeIncluded(_module, type.FullName, _includeFilters)
)
{
if (IsSynthesizedMemberToBeExcluded(type))
{
_excludedCompilerGeneratedTypes ??= new List<string>();
_excludedCompilerGeneratedTypes.Add(type.FullName);
(_excludedCompilerGeneratedTypes ??= new List<string>()).Add(type.FullName);
}
else
{
@@ -378,8 +397,7 @@ namespace Coverlet.Core.Instrumentation
}
else
{
_excludedMethods ??= new List<(MethodDefinition, int)>();
_excludedMethods.Add((method, ordinal));
(_excludedMethods ??= new List<(MethodDefinition, int)>()).Add((method, ordinal));
}
}
@@ -78,7 +78,7 @@ namespace Coverlet.Core.Instrumentation
{
WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}'");
// Claim the current hits array and reset it to prevent double-counting scenarios.
var hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]);
int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]);
// The same module can be unloaded multiple times in the same process via different app domains.
// Use a global mutex to ensure no concurrent access.
@@ -101,8 +101,9 @@ namespace Coverlet.Core.Instrumentation
}
}
}
catch
catch (Exception ex)
{
WriteLog($"Failed to create new hits file '{HitsFilePath}'\n{ex}");
failedToCreateNewHitsFile = true;
}
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Coverlet.Core.Abstracts;
@@ -99,7 +100,7 @@ namespace Coverlet.Core.Tests
// For now we have only async Run helper
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
// we return 0 if we return something different assert fail
return 0;
@@ -138,7 +139,7 @@ namespace Coverlet.Core.Tests
{
instance.Switch(1);
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
@@ -176,7 +177,7 @@ namespace Coverlet.Core.Tests
res = ((Task<int>)instance.ConfigureAwait()).ConfigureAwait(false).GetAwaiter().GetResult();
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
@@ -227,7 +228,7 @@ namespace Coverlet.Core.Tests
instance.InvokeAnonymous_Test();
((Task<bool>)instance.InvokeAnonymousAsync_Test()).ConfigureAwait(false).GetAwaiter().GetResult();
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
@@ -273,7 +274,7 @@ namespace Coverlet.Core.Tests
{
((Task<int>)instance.Test("test")).ConfigureAwait(false).GetAwaiter().GetResult();
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
@@ -314,7 +315,7 @@ namespace Coverlet.Core.Tests
{
instance.Test();
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
@@ -344,7 +345,7 @@ namespace Coverlet.Core.Tests
{
instance.Test("test");
return Task.CompletedTask;
}, pathSerialize);
}, persistPrepareResultToFile: pathSerialize);
return 0;
@@ -361,5 +362,150 @@ namespace Coverlet.Core.Tests
File.Delete(path);
}
}
[Fact]
public void ExcludeFromCodeCoverageNextedTypes()
{
string path = Path.GetTempFileName();
try
{
RemoteExecutor.Invoke(async pathSerialize =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ExcludeFromCoverageAttrFilterClass1>(instance =>
{
Assert.Equal(42, instance.Run());
return Task.CompletedTask;
}, persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.ExcludeFromCoverage.cs")
.AssertLinesCovered(BuildConfiguration.Debug, (143, 1))
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 146, 160);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ExcludeFilteredNestedAutogeneratedTypes()
{
string path = Path.GetTempFileName();
try
{
RemoteExecutor.Invoke(async pathSerialize =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ExcludeFilterNestedAutogeneratedTypes>(instance =>
{
instance.Run();
PropertyInfo stateProp = null;
foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes())
{
if (typeof(Issue_689).FullName == type.FullName)
{
Assert.Equal(0, (stateProp = type.GetProperty("State")).GetValue(null));
break;
}
}
foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes())
{
if (typeof(EventSource_Issue_689).FullName == type.FullName)
{
type.GetMethod("RaiseEvent").Invoke(null, null);
break;
}
}
Assert.Equal(2, stateProp.GetValue(null));
return Task.CompletedTask;
},
includeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterNestedAutogeneratedTypes", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Issue_689" },
excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*NestedToFilterOut", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Uncoverlet" },
persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.ExcludeFilter.cs")
.AssertLinesCovered(BuildConfiguration.Debug, (12, 1), (13, 1), (14, 1))
.AssertLinesCovered(BuildConfiguration.Debug, (27, 1), (28, 1), (29, 1), (30, 1), (31, 1))
.AssertLinesCovered(BuildConfiguration.Debug, (39, 2), (40, 2), (41, 2), (43, 5))
.AssertLinesCovered(BuildConfiguration.Debug, (50, 1), (51, 1), (52, 1))
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 17, 21)
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 33, 36);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ExcludeFilteredTypes()
{
string path = Path.GetTempFileName();
try
{
RemoteExecutor.Invoke(async pathSerialize =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ExcludeFilterOuterTypes>(instance =>
{
Assert.Equal(42, instance.Run());
return Task.CompletedTask;
},
excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterOuterTypes" },
persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.ExcludeFilter.cs")
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 1, 62)
.AssertLinesCovered(BuildConfiguration.Debug, (66, 1), (68, 1));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ExcludeFilteredNestedTypes()
{
string path = Path.GetTempFileName();
try
{
RemoteExecutor.Invoke(async pathSerialize =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ExcludeFilterClass1>(instance =>
{
Assert.Equal(42, instance.Run());
return Task.CompletedTask;
},
excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterClass2" },
persistPrepareResultToFile: pathSerialize);
return 0;
}, path).Dispose();
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.ExcludeFilter.cs")
.AssertLinesCovered(BuildConfiguration.Debug, (73, 1))
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 75, 93);
}
finally
{
File.Delete(path);
}
}
}
}
@@ -15,6 +15,7 @@ using Coverlet.Core.Reporters;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Palmmedia.ReportGenerator.Core;
using Xunit;
using Xunit.Sdk;
namespace Coverlet.Core.Tests
@@ -28,6 +29,39 @@ namespace Coverlet.Core.Tests
static class TestInstrumentationAssert
{
public static CoverageResult GenerateReport(this CoverageResult coverageResult, [CallerMemberName]string directory = "")
{
if (coverageResult is null)
{
throw new ArgumentNullException(nameof(coverageResult));
}
TestInstrumentationHelper.GenerateHtmlReport(coverageResult, directory: directory);
return coverageResult;
}
public static bool IsPresent(this CoverageResult coverageResult, string docName)
{
if (docName is null)
{
throw new ArgumentNullException(nameof(docName));
}
foreach (InstrumenterResult instrumenterResult in coverageResult.InstrumentedResults)
{
foreach (KeyValuePair<string, Document> document in instrumenterResult.Documents)
{
if (Path.GetFileName(document.Key) == docName)
{
return true;
}
}
}
return false;
}
public static Document Document(this CoverageResult coverageResult, string docName)
{
if (docName is null)
@@ -279,7 +313,7 @@ namespace Coverlet.Core.Tests
reportFile = Path.Combine(dir.FullName, Path.ChangeExtension("report", reporter.Extension));
File.WriteAllText(reportFile, reporter.Report(coverageResult));
// i.e. reportgenerator -reports:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If\report.cobertura.xml" -targetdir:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If" -filefilters:+**\Samples\Instrumentation.cs
new Generator().GenerateReport(new ReportConfiguration(
Assert.True(new Generator().GenerateReport(new ReportConfiguration(
new[] { reportFile },
dir.FullName,
new string[0],
@@ -290,21 +324,29 @@ namespace Coverlet.Core.Tests
new string[0],
string.IsNullOrEmpty(sourceFileFilter) ? new string[0] : new[] { sourceFileFilter },
null,
null));
null)));
}
public static CoverageResult GetCoverageResult(string filePath)
{
using (var result = new FileStream(filePath, FileMode.Open))
using var result = new FileStream(filePath, FileMode.Open);
var logger = new Mock<ILogger>();
logger.Setup(l => l.LogVerbose(It.IsAny<string>())).Callback((string message) =>
{
CoveragePrepareResult coveragePrepareResultLoaded = CoveragePrepareResult.Deserialize(result);
Coverage coverage = new Coverage(coveragePrepareResultLoaded, new Mock<ILogger>().Object, DependencyInjection.Current.GetService<IInstrumentationHelper>(), new FileSystem());
return coverage.GetCoverageResult();
}
Assert.DoesNotContain("not found for module: ", message);
});
CoveragePrepareResult coveragePrepareResultLoaded = CoveragePrepareResult.Deserialize(result);
Coverage coverage = new Coverage(coveragePrepareResultLoaded, logger.Object, DependencyInjection.Current.GetService<IInstrumentationHelper>(), new FileSystem());
return coverage.GetCoverageResult();
}
async public static Task<CoveragePrepareResult> Run<T>(Func<dynamic, Task> callMethod, string persistPrepareResultToFile, bool disableRestoreModules = false)
async public static Task<CoveragePrepareResult> Run<T>(Func<dynamic, Task> callMethod, Func<string, string[]> includeFilter = null, Func<string, string[]> excludeFilter = null, string persistPrepareResultToFile = null, bool disableRestoreModules = false)
{
if (persistPrepareResultToFile is null)
{
throw new ArgumentNullException(nameof(persistPrepareResultToFile));
}
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IRetryHelper, CustomRetryHelper>();
serviceCollection.AddTransient<IProcessExitHandler, CustomProcessExitHandler>();
@@ -331,20 +373,24 @@ namespace Coverlet.Core.Tests
File.Copy(location, newPath);
File.Copy(Path.ChangeExtension(location, ".pdb"), Path.ChangeExtension(newPath, ".pdb"));
static string[] defaultFilters(string _) => Array.Empty<string>();
// Instrument module
Coverage coverage = new Coverage(newPath,
includeFilters: (includeFilter is null ? defaultFilters(fileName) : includeFilter(fileName)).Concat(
new string[]
{
$"[{Path.GetFileNameWithoutExtension(fileName)}*]{typeof(T).FullName}*"
},
}).ToArray(),
Array.Empty<string>(),
new string[]
excludeFilters: (excludeFilter is null ? defaultFilters(fileName) : excludeFilter(fileName)).Concat(new string[]
{
"[xunit.*]*",
"[coverlet.*]*"
}, Array.Empty<string>(), Array.Empty<string>(), true, false, "", false, new Logger(logFile), DependencyInjection.Current.GetService<IInstrumentationHelper>(), DependencyInjection.Current.GetService<IFileSystem>());
}).ToArray(), Array.Empty<string>(), Array.Empty<string>(), true, false, "", false, new Logger(logFile), DependencyInjection.Current.GetService<IInstrumentationHelper>(), DependencyInjection.Current.GetService<IFileSystem>());
CoveragePrepareResult prepareResult = coverage.PrepareModules();
Assert.Single(prepareResult.Results);
// Load new assembly
Assembly asm = Assembly.LoadFile(newPath);
@@ -354,13 +400,17 @@ namespace Coverlet.Core.Tests
// Flush tracker
Type tracker = asm.GetTypes().Single(n => n.FullName.Contains("Coverlet.Core.Instrumentation.Tracker"));
// For debugging purpouse
// int[] hitsArray = (int[])tracker.GetField("HitsArray").GetValue(null);
// string hitsFilePath = (string)tracker.GetField("HitsFilePath").GetValue(null);
// Void UnloadModule(System.Object, System.EventArgs)
tracker.GetTypeInfo().GetMethod("UnloadModule").Invoke(null, new object[2] { null, null });
// Persist CoveragePrepareResult
using (FileStream fs = new FileStream(persistPrepareResultToFile, FileMode.Open))
{
CoveragePrepareResult.Serialize(prepareResult).CopyTo(fs);
await CoveragePrepareResult.Serialize(prepareResult).CopyToAsync(fs);
}
return prepareResult;
@@ -23,15 +23,16 @@ namespace Coverlet.Core.Instrumentation.Tests
private readonly InstrumentationHelper _instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem());
private readonly Mock<ILogger> _mockLogger = new Mock<ILogger>();
[Fact(Skip = "To be used only validating System.Private.CoreLib instrumentation")]
[Fact]
public void TestCoreLibInstrumentation()
{
// Attention: to run this test adjust the paths and copy the IL only version of corelib
const string OriginalFilesDir = @"c:\s\tmp\Coverlet-CoreLib\Original\";
const string TestFilesDir = @"c:\s\tmp\Coverlet-CoreLib\Test\";
Directory.CreateDirectory(TestFilesDir);
// We test only on win because sample dll/pdb were build on it
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), nameof(TestCoreLibInstrumentation)));
string[] files = new[]
{
"System.Private.CoreLib.dll",
@@ -39,12 +40,54 @@ namespace Coverlet.Core.Instrumentation.Tests
};
foreach (var file in files)
File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true);
{
File.Copy(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets", file), Path.Combine(directory.FullName, file), overwrite: true);
}
Mock<FileSystem> partialMockFileSystem = new Mock<FileSystem>();
partialMockFileSystem.CallBase = true;
partialMockFileSystem.Setup(fs => fs.OpenRead(It.IsAny<string>())).Returns((string path) =>
{
if (Path.GetFileName(path) == files[1])
{
return File.OpenRead(Path.Combine(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), files[1]));
}
else
{
return File.OpenRead(path);
}
});
partialMockFileSystem.Setup(fs => fs.Exists(It.IsAny<string>())).Returns((string path) =>
{
if (Path.GetFileName(path) == files[1])
{
return File.Exists(Path.Combine(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), files[1]));
}
else
{
if (path.Contains(@":\git\runtime"))
{
return true;
}
else
{
return File.Exists(path);
}
}
});
InstrumentationHelper instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object);
Instrumenter instrumenter = new Instrumenter(Path.Combine(directory.FullName, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, _mockLogger.Object, instrumentationHelper, partialMockFileSystem.Object);
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, _mockLogger.Object, _instrumentationHelper, new FileSystem());
Assert.True(instrumenter.CanInstrument());
var result = instrumenter.Instrument();
InstrumenterResult result = instrumenter.Instrument();
Assert.NotNull(result);
Assert.Equal(1052, result.Documents.Count);
foreach ((string docName, Document _) in result.Documents)
{
Assert.False(docName.EndsWith(@"System.Private.CoreLib\src\System\Threading\Interlocked.cs"));
}
directory.Delete(true);
}
[Theory]
@@ -472,7 +515,7 @@ namespace Coverlet.Core.Instrumentation.Tests
Assert.Contains(doc.Lines.Values, l => l.Method == "System.Int32 Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr::TestLambda(System.String,System.Int32)");
Assert.DoesNotContain(doc.Lines.Values, l => l.Method == "System.Int32 Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr::TestLambda(System.String)");
Assert.DoesNotContain(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
Assert.DoesNotContain(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
instrumenterTest.Instrumenter.IsSynthesizedNameOf(l.Method, "TestLambda", 0));
Assert.DoesNotContain(doc.Lines.Values, l => l.Method == "System.Int32 Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2::TestLambda(System.String,System.Int32)");
Assert.DoesNotContain(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2/") &&
@@ -516,9 +559,9 @@ namespace Coverlet.Core.Instrumentation.Tests
var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Instrumentation.ExcludeFromCoverage.cs");
Assert.NotNull(doc);
Assert.DoesNotContain(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
Assert.DoesNotContain(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
instrumenterTest.Instrumenter.IsSynthesizedNameOf(l.Method, "TestYield", 2));
Assert.Contains(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
Assert.Contains(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/") &&
instrumenterTest.Instrumenter.IsSynthesizedNameOf(l.Method, "TestYield", 3));
Assert.Contains(doc.Lines.Values, l => l.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2/") &&
instrumenterTest.Instrumenter.IsSynthesizedNameOf(l.Method, "TestYield", 2));
@@ -1,4 +1,5 @@
using Coverlet.Core.Instrumentation;
using Coverlet.Tests.RemoteExecutor;
using System;
using System.IO;
using System.Threading;
@@ -7,111 +8,133 @@ using Xunit;
namespace Coverlet.Core.Tests.Instrumentation
{
public class ModuleTrackerTemplateTestsFixture : IDisposable
class TrackerContext : IDisposable
{
public ModuleTrackerTemplateTestsFixture()
public TrackerContext()
{
ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), nameof(ModuleTrackerTemplateTests));
ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
}
public void Dispose()
{
File.Delete(ModuleTrackerTemplate.HitsFilePath);
AppDomain.CurrentDomain.ProcessExit -= ModuleTrackerTemplate.UnloadModule;
AppDomain.CurrentDomain.DomainUnload -= ModuleTrackerTemplate.UnloadModule;
}
}
public class ModuleTrackerTemplateTests : IClassFixture<ModuleTrackerTemplateTestsFixture>, IDisposable
public class ModuleTrackerTemplateTests
{
public ModuleTrackerTemplateTests()
{
File.Delete(ModuleTrackerTemplate.HitsFilePath);
}
public void Dispose()
{
File.Delete(ModuleTrackerTemplate.HitsFilePath);
}
private static readonly Task<int> _success = Task.FromResult(0);
[Fact]
public void HitsFileCorrectlyWritten()
{
ModuleTrackerTemplate.HitsArray = new[] { 1, 2, 0, 3 };
ModuleTrackerTemplate.UnloadModule(null, null);
using var invoker = RemoteExecutor.Invoke(_ =>
{
using var ctx = new TrackerContext();
ModuleTrackerTemplate.HitsArray = new[] { 1, 2, 0, 3 };
ModuleTrackerTemplate.UnloadModule(null, null);
var expectedHitsArray = new[] { 1, 2, 0, 3 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
var expectedHitsArray = new[] { 1, 2, 0, 3 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
return _success;
});
}
[Fact]
public void HitsFileWithDifferentNumberOfEntriesCausesExceptionOnUnload()
{
WriteHitsFile(new[] { 1, 2, 3 });
ModuleTrackerTemplate.HitsArray = new[] { 1 };
Assert.Throws<InvalidOperationException>(() => ModuleTrackerTemplate.UnloadModule(null, null));
using var invoker = RemoteExecutor.Invoke(_ =>
{
using var ctx = new TrackerContext();
WriteHitsFile(new[] { 1, 2, 3 });
ModuleTrackerTemplate.HitsArray = new[] { 1 };
Assert.Throws<InvalidOperationException>(() => ModuleTrackerTemplate.UnloadModule(null, null));
return _success;
});
}
[Fact(Skip = "Failed CI Job: https://ci.appveyor.com/project/tonerdo/coverlet/builds/21145989/job/9gx5jnjs502vy1fv")]
[Fact]
public void HitsOnMultipleThreadsCorrectlyCounted()
{
ModuleTrackerTemplate.HitsArray = new[] { 0, 0, 0, 0 };
for (int i = 0; i < ModuleTrackerTemplate.HitsArray.Length; ++i)
using var invoker = RemoteExecutor.Invoke(_ =>
{
var t = new Thread(HitIndex);
t.Start(i);
}
ModuleTrackerTemplate.UnloadModule(null, null);
var expectedHitsArray = new[] { 4, 3, 2, 1 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
void HitIndex(object index)
{
var hitIndex = (int)index;
for (int i = 0; i <= hitIndex; ++i)
using var ctx = new TrackerContext();
ModuleTrackerTemplate.HitsArray = new[] { 0, 0, 0, 0 };
for (int i = 0; i < ModuleTrackerTemplate.HitsArray.Length; ++i)
{
ModuleTrackerTemplate.RecordHit(i);
var t = new Thread(HitIndex);
t.Start(i);
}
}
ModuleTrackerTemplate.UnloadModule(null, null);
var expectedHitsArray = new[] { 4, 3, 2, 1 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
static void HitIndex(object index)
{
var hitIndex = (int)index;
for (int i = 0; i <= hitIndex; ++i)
{
ModuleTrackerTemplate.RecordHit(i);
}
}
return _success;
});
}
[Fact]
public void MultipleSequentialUnloadsHaveCorrectTotalData()
{
ModuleTrackerTemplate.HitsArray = new[] { 0, 3, 2, 1 };
ModuleTrackerTemplate.UnloadModule(null, null);
ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 };
ModuleTrackerTemplate.UnloadModule(null, null);
var expectedHitsArray = new[] { 0, 4, 4, 4 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
}
[Fact]
public async void MutexBlocksMultipleWriters()
{
using (var mutex = new Mutex(
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew))
using var invoker = RemoteExecutor.Invoke(_ =>
{
Assert.True(createdNew);
using var ctx = new TrackerContext();
ModuleTrackerTemplate.HitsArray = new[] { 0, 3, 2, 1 };
ModuleTrackerTemplate.UnloadModule(null, null);
ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 };
var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null));
Assert.False(unloadTask.Wait(5));
WriteHitsFile(new[] { 0, 3, 2, 1 });
Assert.False(unloadTask.Wait(5));
mutex.ReleaseMutex();
await unloadTask;
ModuleTrackerTemplate.UnloadModule(null, null);
var expectedHitsArray = new[] { 0, 4, 4, 4 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
}
return _success;
});
}
[Fact]
public void MutexBlocksMultipleWriters()
{
using var invoker = RemoteExecutor.Invoke(async _ =>
{
using var ctx = new TrackerContext();
using (var mutex = new Mutex(
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew))
{
Assert.True(createdNew);
ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 };
var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null));
Assert.False(unloadTask.Wait(5));
WriteHitsFile(new[] { 0, 3, 2, 1 });
Assert.False(unloadTask.Wait(5));
mutex.ReleaseMutex();
await unloadTask;
var expectedHitsArray = new[] { 0, 4, 4, 4 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
}
return 0;
});
}
private void WriteHitsFile(int[] hitsArray)
@@ -0,0 +1,90 @@
// Remember to use full name because adding new using directives change line numbers
using System;
using System.Threading.Tasks;
namespace Coverlet.Core.Samples.Tests
{
public class ExcludeFilterNestedAutogeneratedTypes
{
public void Run()
{
NestedToFilterOut nested = new NestedToFilterOut();
nested.SomeMethodLambda();
nested.SomeMethodAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
public class NestedToFilterOut
{
public void SomeMethodLambda() => AppDomain.CurrentDomain.ProcessExit += (s, e) => { };
public Task<int> SomeMethodAsync() => Task.FromResult(new Random().Next());
}
}
public static class Issue_689
{
static Issue_689()
{
State = 0;
EventSource_Issue_689.Handle += (s, e) => Handler(1);
Uncoverlet.AddHandler();
}
internal static class Uncoverlet
{
internal static void AddHandler() => EventSource_Issue_689.Handle += (s, e) => Handler(2);
}
public static void Handler(int i)
{
State = i;
}
public static int State { get; set; }
}
public static class EventSource_Issue_689
{
public static event EventHandler<object> Handle;
public static void RaiseEvent()
{
Handle?.Invoke(new object(), new object());
}
}
public class ExcludeFilterOuterTypes
{
public int Run()
{
return new ExcludeFilterOuterTypes2().Run();
}
}
public class ExcludeFilterOuterTypes2
{
public int Run()
{
return 42;
}
}
public class ExcludeFilterClass1
{
public int Run() => 10 + new ExcludeFilterClass2().Run();
public class ExcludeFilterClass2
{
public int Run() => 10 + new ExcludeFilterClass3().Run();
public class ExcludeFilterClass3
{
public int Run() => 10 + new ExcludeFilterClass4().Run();
public class ExcludeFilterClass4
{
public int Run() => 12;
}
}
}
}
}
@@ -137,4 +137,25 @@ namespace Coverlet.Core.Samples.Tests
}
}
}
public class ExcludeFromCoverageAttrFilterClass1
{
public int Run() => 10 + new ExcludeFromCoverageAttrFilterClass2().Run();
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ExcludeFromCoverageAttrFilterClass2
{
public int Run() => 10 + new ExcludeFromCoverageAttrFilterClass3().Run();
public class ExcludeFromCoverageAttrFilterClass3
{
public int Run() => 10 + new ExcludeFromCoverageAttrFilterClass4().Run();
public class ExcludeFromCoverageAttrFilterClass4
{
public int Run() => 12;
}
}
}
}
}
@@ -42,6 +42,12 @@
<None Update="TestAssets\75d9f96508d74def860a568f426ea4a4.pdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestAssets\System.Private.CoreLib.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestAssets\System.Private.CoreLib.pdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
+9 -1
View File
@@ -335,7 +335,15 @@ namespace Coverlet.Integration.Tests
{
if (CleanupOnDispose)
{
Directory.Delete(ProjectRootPath, true);
try
{
Directory.Delete(ProjectRootPath, true);
}
catch (UnauthorizedAccessException)
{
// Sometimes on CI AzDo we get Access Denied on delete
// swallowed exception to not waste time
}
}
}
}
@@ -20,14 +20,9 @@ namespace Coverlet.Tests.RemoteExecutor
// 1) Add a Debug.Launch() inside lambda and attach(slow)
// 2) Temporary pass true to invoke local, it will throw because code try to replace locked files,
// but if you temporary "comment" offensive code(RestoreOriginalModule/s) you can debug all procedure and it's very very very useful
public static IRemoteInvokeHandle Invoke(Func<Task<int>> method, bool invokeLocal = false)
public static IRemoteInvokeHandle Invoke(Func<string, Task<int>> method, string arg = "", bool invokeInProcess = false)
{
return Invoke(GetMethodInfo(method), Array.Empty<string>(), invokeLocal);
}
public static IRemoteInvokeHandle Invoke(Func<string, Task<int>> method, string arg, bool invokeLocal = false)
{
if (invokeLocal)
if (invokeInProcess)
{
return new LocalInvoker(method.Invoke(arg).GetAwaiter().GetResult());
}
@@ -83,19 +78,17 @@ namespace Coverlet.Tests.RemoteExecutor
int ExitCode { get; }
}
public class LocalInvoker : IRemoteInvokeHandle
public struct LocalInvoker : IRemoteInvokeHandle
{
public int ExitCode => _result;
public readonly int ExitCode { get; }
private int _result;
public LocalInvoker(int result) => _result = result;
public LocalInvoker(int exitCode) => ExitCode = exitCode;
public void Dispose()
{
if (_result != 0)
if (ExitCode != 0)
{
throw new RemoteExecutionException($"Result '{_result}'");
throw new RemoteExecutionException($"Result '{ExitCode}'");
}
}
}