Skip to content

Commit 3a3cb5a

Browse files
authored
Merge pull request #209 from pjanotti/corelib.support
Add System.Private.CoreLib support
2 parents 291ff7d + e808d16 commit 3a3cb5a

File tree

5 files changed

+163
-14
lines changed

5 files changed

+163
-14
lines changed

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ private void InstrumentModule()
6666
{
6767
resolver.AddSearchDirectory(Path.GetDirectoryName(_module));
6868
var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver };
69+
if (Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib")
70+
{
71+
parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider();
72+
}
6973

7074
using (var module = ModuleDefinition.ReadModule(stream, parameters))
7175
{
@@ -108,7 +112,7 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
108112
foreach (FieldDefinition fieldDef in moduleTrackerTemplate.Fields)
109113
{
110114
var fieldClone = new FieldDefinition(fieldDef.Name, fieldDef.Attributes, fieldDef.FieldType);
111-
fieldClone.FieldType = module.ImportReference(fieldClone.FieldType);
115+
fieldClone.FieldType = module.ImportReference(fieldDef.FieldType);
112116

113117
_customTrackerTypeDef.Fields.Add(fieldClone);
114118

@@ -170,7 +174,14 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
170174
}
171175

172176
foreach (var handler in methodDef.Body.ExceptionHandlers)
177+
{
178+
if (handler.CatchType != null)
179+
{
180+
handler.CatchType = module.ImportReference(handler.CatchType);
181+
}
182+
173183
methodOnCustomType.Body.ExceptionHandlers.Add(handler);
184+
}
174185

175186
_customTrackerTypeDef.Methods.Add(methodOnCustomType);
176187
}
@@ -406,5 +417,72 @@ private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method)
406417
return null;
407418
}
408419
}
420+
421+
/// <summary>
422+
/// A custom importer created specifically to allow the instrumentation of System.Private.CoreLib by
423+
/// removing the external references to netstandard that are generated when instrumenting a typical
424+
/// assembly.
425+
/// </summary>
426+
private class CoreLibMetadataImporterProvider : IMetadataImporterProvider
427+
{
428+
public IMetadataImporter GetMetadataImporter(ModuleDefinition module)
429+
{
430+
return new CoreLibMetadataImporter(module);
431+
}
432+
433+
private class CoreLibMetadataImporter : IMetadataImporter
434+
{
435+
private readonly ModuleDefinition module;
436+
private readonly DefaultMetadataImporter defaultMetadataImporter;
437+
438+
public CoreLibMetadataImporter(ModuleDefinition module)
439+
{
440+
this.module = module;
441+
this.defaultMetadataImporter = new DefaultMetadataImporter(module);
442+
}
443+
444+
public AssemblyNameReference ImportReference(AssemblyNameReference reference)
445+
{
446+
return this.defaultMetadataImporter.ImportReference(reference);
447+
}
448+
449+
public TypeReference ImportReference(TypeReference type, IGenericParameterProvider context)
450+
{
451+
var importedRef = this.defaultMetadataImporter.ImportReference(type, context);
452+
importedRef.GetElementType().Scope = module.TypeSystem.CoreLibrary;
453+
return importedRef;
454+
}
455+
456+
public FieldReference ImportReference(FieldReference field, IGenericParameterProvider context)
457+
{
458+
var importedRef = this.defaultMetadataImporter.ImportReference(field, context);
459+
importedRef.FieldType.GetElementType().Scope = module.TypeSystem.CoreLibrary;
460+
return importedRef;
461+
}
462+
463+
public MethodReference ImportReference(MethodReference method, IGenericParameterProvider context)
464+
{
465+
var importedRef = this.defaultMetadataImporter.ImportReference(method, context);
466+
importedRef.DeclaringType.GetElementType().Scope = module.TypeSystem.CoreLibrary;
467+
468+
foreach (var parameter in importedRef.Parameters)
469+
{
470+
if (parameter.ParameterType.Scope == module.TypeSystem.CoreLibrary)
471+
{
472+
continue;
473+
}
474+
475+
parameter.ParameterType.GetElementType().Scope = module.TypeSystem.CoreLibrary;
476+
}
477+
478+
if (importedRef.ReturnType.Scope != module.TypeSystem.CoreLibrary)
479+
{
480+
importedRef.ReturnType.GetElementType().Scope = module.TypeSystem.CoreLibrary;
481+
}
482+
483+
return importedRef;
484+
}
485+
}
486+
}
409487
}
410488
}

src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,37 @@ public static class ModuleTrackerTemplate
2020
public static string HitsFilePath;
2121
public static int[] HitsArray;
2222

23+
// Special case when instrumenting CoreLib, the thread static below prevents infinite loop in CoreLib
24+
// while allowing the tracker template to call any of its types and functions.
25+
[ThreadStatic]
26+
private static bool t_isTracking;
27+
2328
[ThreadStatic]
2429
private static int[] t_threadHits;
2530

2631
private static List<int[]> _threads;
2732

2833
static ModuleTrackerTemplate()
2934
{
35+
t_isTracking = true;
3036
_threads = new List<int[]>(2 * Environment.ProcessorCount);
3137

3238
AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule);
3339
AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule);
40+
t_isTracking = false;
3441
// At the end of the instrumentation of a module, the instrumenter needs to add code here
3542
// to initialize the static fields according to the values derived from the instrumentation of
3643
// the module.
3744
}
3845

3946
public static void RecordHit(int hitLocationIndex)
4047
{
48+
if (t_isTracking)
49+
return;
50+
4151
if (t_threadHits == null)
4252
{
53+
t_isTracking = true;
4354
lock (_threads)
4455
{
4556
if (t_threadHits == null)
@@ -48,13 +59,16 @@ public static void RecordHit(int hitLocationIndex)
4859
_threads.Add(t_threadHits);
4960
}
5061
}
62+
t_isTracking = false;
5163
}
5264

5365
++t_threadHits[hitLocationIndex];
5466
}
5567

5668
public static void UnloadModule(object sender, EventArgs e)
5769
{
70+
t_isTracking = true;
71+
5872
// Update the global hits array from data from all the threads
5973
lock (_threads)
6074
{
@@ -76,10 +90,10 @@ public static void UnloadModule(object sender, EventArgs e)
7690
if (!createdNew)
7791
mutex.WaitOne();
7892

79-
if (!File.Exists(HitsFilePath))
93+
bool failedToCreateNewHitsFile = false;
94+
try
8095
{
81-
// File not created yet, just write it
82-
using (var fs = new FileStream(HitsFilePath, FileMode.Create))
96+
using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew))
8397
using (var bw = new BinaryWriter(fs))
8498
{
8599
bw.Write(HitsArray.Length);
@@ -89,18 +103,23 @@ public static void UnloadModule(object sender, EventArgs e)
89103
}
90104
}
91105
}
92-
else
106+
catch
107+
{
108+
failedToCreateNewHitsFile = true;
109+
}
110+
111+
if (failedToCreateNewHitsFile)
93112
{
94113
// Update the number of hits by adding value on disk with the ones on memory.
95114
// This path should be triggered only in the case of multiple AppDomain unloads.
96-
using (var fs = File.Open(HitsFilePath, FileMode.Open))
115+
using (var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
97116
using (var br = new BinaryReader(fs))
98117
using (var bw = new BinaryWriter(fs))
99118
{
100119
int hitsLength = br.ReadInt32();
101120
if (hitsLength != HitsArray.Length)
102121
{
103-
throw new InvalidDataException(
122+
throw new InvalidOperationException(
104123
$"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {HitsArray.Length}");
105124
}
106125

@@ -116,6 +135,10 @@ public static void UnloadModule(object sender, EventArgs e)
116135
// Prevent any double counting scenario, i.e.: UnloadModule called twice (not sure if this can happen in practice ...)
117136
// Only an issue if DomainUnload and ProcessExit can both happens, perhaps can be removed...
118137
Array.Clear(HitsArray, 0, HitsArray.Length);
138+
139+
// On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file
140+
// this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll.
141+
mutex.ReleaseMutex();
119142
}
120143
}
121144
}

test/coverlet.core.performancetest/PerformanceTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ namespace coverlet.core.performancetest
1414
/// </summary>
1515
public class PerformanceTest
1616
{
17-
[Theory(/*Skip = "Only enabled when explicitly testing performance."*/)]
18-
// [InlineData(150)]
17+
[Theory]
1918
[InlineData(20_000)]
2019
public void TestPerformance(int iterations)
2120
{

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@ namespace Coverlet.Core.Instrumentation.Tests
99
{
1010
public class InstrumenterTests
1111
{
12+
[Fact(Skip = "To be used only validating System.Private.CoreLib instrumentation")]
13+
public void TestCoreLibInstrumentation()
14+
{
15+
// Attention: to run this test adjust the paths and copy the IL only version of corelib
16+
const string OriginalFilesDir = @"c:\s\tmp\Coverlet-CoreLib\Original\";
17+
const string TestFilesDir = @"c:\s\tmp\Coverlet-CoreLib\Test\";
18+
19+
Directory.CreateDirectory(TestFilesDir);
20+
21+
string[] files = new[]
22+
{
23+
"System.Private.CoreLib.dll",
24+
"System.Private.CoreLib.pdb"
25+
};
26+
27+
foreach (var file in files)
28+
File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true);
29+
30+
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
31+
Assert.True(instrumenter.CanInstrument());
32+
var result = instrumenter.Instrument();
33+
Assert.NotNull(result);
34+
}
35+
1236
[Fact]
1337
public void TestInstrument()
1438
{
@@ -22,6 +46,19 @@ public void TestInstrument()
2246
instrumenterTest.Directory.Delete(true);
2347
}
2448

49+
[Fact]
50+
public void TestInstrumentCoreLib()
51+
{
52+
var instrumenterTest = CreateInstrumentor(fakeCoreLibModule: true);
53+
54+
var result = instrumenterTest.Instrumenter.Instrument();
55+
56+
Assert.Equal(Path.GetFileNameWithoutExtension(instrumenterTest.Module), result.Module);
57+
Assert.Equal(instrumenterTest.Module, result.ModulePath);
58+
59+
instrumenterTest.Directory.Delete(true);
60+
}
61+
2562
[Theory]
2663
[InlineData(typeof(ClassExcludedByCodeAnalysisCodeCoverageAttr))]
2764
[InlineData(typeof(ClassExcludedByCoverletCodeCoverageAttr))]
@@ -39,18 +76,30 @@ public void TestInstrument_ClassesWithExcludeAttributeAreExcluded(Type excludedT
3976
instrumenterTest.Directory.Delete(true);
4077
}
4178

42-
private InstrumenterTest CreateInstrumentor()
79+
private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false)
4380
{
4481
string module = GetType().Assembly.Location;
4582
string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
4683
string identifier = Guid.NewGuid().ToString();
4784

4885
DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), identifier));
4986

50-
File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true);
51-
File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true);
87+
string destModule, destPdb;
88+
if (fakeCoreLibModule)
89+
{
90+
destModule = "System.Private.CoreLib.dll";
91+
destPdb = "System.Private.CoreLib.pdb";
92+
}
93+
else
94+
{
95+
destModule = Path.GetFileName(module);
96+
destPdb = Path.GetFileName(pdb);
97+
}
98+
99+
File.Copy(module, Path.Combine(directory.FullName, destModule), true);
100+
File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true);
52101

53-
module = Path.Combine(directory.FullName, Path.GetFileName(module));
102+
module = Path.Combine(directory.FullName, destModule);
54103
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
55104
return new InstrumenterTest
56105
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void HitsFileWithDifferentNumberOfEntriesCausesExceptionOnUnload()
4949
{
5050
WriteHitsFile(new[] { 1, 2, 3 });
5151
ModuleTrackerTemplate.HitsArray = new[] { 1 };
52-
Assert.Throws<InvalidDataException>(() => ModuleTrackerTemplate.UnloadModule(null, null));
52+
Assert.Throws<InvalidOperationException>(() => ModuleTrackerTemplate.UnloadModule(null, null));
5353
}
5454

5555
[Fact]

0 commit comments

Comments
 (0)