Skip to content

Add log to tracker #553

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

Merged
merged 13 commits into from
Sep 23, 2019
11 changes: 11 additions & 0 deletions Documentation/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,14 @@ System.Diagnostics.Debugger.Launch();
```

**Every time you update code and rebuild new package remember to remove local nuget cache(`RMDIR "C:\Users\[winUser]\.nuget\packages\coverlet.collector" /S /Q`) otherwise you'll load old collector code because the package version wasn't changed**

## Enable injected tracker log

Coverlet works thanks to ModuleTracker that is injected during instrumentation for every covered module.
This piece of code run as a part of tests and doesn't have any connection with coverlet.
We can collect logs from trackers through an enviroment variable
```
set COVERLET_ENABLETRACKERLOG=1
```
When enabled tracking event will be collected in log file near to module location.
File name will be something like `moduleName.dll_tracker.txt`
1 change: 1 addition & 0 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ private void CalculateCoverage()
}

_instrumentationHelper.DeleteHitsFile(result.HitsFilePath);
_logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted");
}
}

Expand Down
117 changes: 74 additions & 43 deletions src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Threading;

namespace Coverlet.Core.Instrumentation
Expand All @@ -19,6 +20,7 @@ public static class ModuleTrackerTemplate
public static string HitsFilePath;
public static int[] HitsArray;
public static bool SingleHit;
private static bool _enableLog = int.TryParse(Environment.GetEnvironmentVariable("COVERLET_ENABLETRACKERLOG"), out int result) ? result == 1 : false;

static ModuleTrackerTemplate()
{
Expand Down Expand Up @@ -72,64 +74,93 @@ public static void RecordSingleHit(int hitLocationIndex)

public static void UnloadModule(object sender, EventArgs e)
{
// Claim the current hits array and reset it to prevent double-counting scenarios.
var 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.
using (var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew))
try
{
if (!createdNew)
mutex.WaitOne();
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]);

bool failedToCreateNewHitsFile = false;
try
// 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.
using (var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew))
{
using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew))
using (var bw = new BinaryWriter(fs))
WriteLog($"Flushing hit file '{HitsFilePath}'");
if (!createdNew)
mutex.WaitOne();

bool failedToCreateNewHitsFile = false;
try
{
bw.Write(hitsArray.Length);
foreach (int hitCount in hitsArray)
using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew))
using (var bw = new BinaryWriter(fs))
{
bw.Write(hitCount);
bw.Write(hitsArray.Length);
foreach (int hitCount in hitsArray)
{
bw.Write(hitCount);
}
}
}
}
catch
{
failedToCreateNewHitsFile = true;
}
catch
{
failedToCreateNewHitsFile = true;
}

if (failedToCreateNewHitsFile)
{
// Update the number of hits by adding value on disk with the ones on memory.
// This path should be triggered only in the case of multiple AppDomain unloads.
using (var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
using (var br = new BinaryReader(fs))
using (var bw = new BinaryWriter(fs))
if (failedToCreateNewHitsFile)
{
int hitsLength = br.ReadInt32();
if (hitsLength != hitsArray.Length)
// Update the number of hits by adding value on disk with the ones on memory.
// This path should be triggered only in the case of multiple AppDomain unloads.
using (var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
using (var br = new BinaryReader(fs))
using (var bw = new BinaryWriter(fs))
{
throw new InvalidOperationException(
$"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}");
}
int hitsLength = br.ReadInt32();
if (hitsLength != hitsArray.Length)
{
throw new InvalidOperationException(
$"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}");
}

for (int i = 0; i < hitsLength; ++i)
{
int oldHitCount = br.ReadInt32();
bw.Seek(-sizeof(int), SeekOrigin.Current);
if (SingleHit)
bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0);
else
bw.Write(hitsArray[i] + oldHitCount);
for (int i = 0; i < hitsLength; ++i)
{
int oldHitCount = br.ReadInt32();
bw.Seek(-sizeof(int), SeekOrigin.Current);
if (SingleHit)
bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0);
else
bw.Write(hitsArray[i] + oldHitCount);
}
}
}

// 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
// this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll.
mutex.ReleaseMutex();
WriteLog($"Hit file '{HitsFilePath}' flushed, size {new FileInfo(HitsFilePath).Length}");
}
}
catch (Exception ex)
{
WriteLog(ex.ToString());
throw;
}
}

// 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
// this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll.
mutex.ReleaseMutex();
private static void WriteLog(string logText)
{
if (_enableLog)
{
try
{
// We don't set path as global var to keep benign possible errors inside try/catch
// I'm not sure that location will be ok in every scenario
string location = Assembly.GetExecutingAssembly().Location;
File.AppendAllText(Path.Combine(Path.GetDirectoryName(location), Path.GetFileName(location) + "_tracker.txt"), $"[{DateTime.UtcNow} {Thread.CurrentThread.ManagedThreadId}]{logText}{Environment.NewLine}");
}
catch
{
// do nothing if log fail
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ public void TestInstrument_ExcludedFilesHelper(string[] excludeFilterHelper, Val
}
}

[Fact]
[Fact(Skip = "Temporary disabled because flaky on CI, we cannot use coverlet.core.dll/pdb as test lib")]
public void SkipEmbeddedPpdbWithoutLocalSource()
{
string xunitDll = Directory.GetFiles(Directory.GetCurrentDirectory(), "xunit.*.dll").First();
Expand All @@ -364,7 +364,7 @@ public void SkipEmbeddedPpdbWithoutLocalSource()
loggerMock.VerifyNoOtherCalls();
}

[Fact]
[Fact(Skip = "Temporary disabled because flaky on CI, we cannot use coverlet.core.dll/pdb as test lib")]
public void SkipPpdbWithoutLocalSource()
{
Mock<IFileSystem> mockFileSystem = new Mock<IFileSystem>();
Expand Down