Skip to content

Commit 0c85839

Browse files
Fix deterministic build+source link bug (#895)
Fix deterministic build+source link bug
1 parent 7c12e15 commit 0c85839

File tree

9 files changed

+122
-31
lines changed

9 files changed

+122
-31
lines changed

Documentation/Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Fixed
10-
-Attribute exclusion does not work if attribute name does not end with "Attribute" [883](https://github.com/coverlet-coverage/coverlet/issues/883) by https://github.com/bddckr
10+
-Attribute exclusion does not work if attribute name does not end with "Attribute" [884](https://github.com/coverlet-coverage/coverlet/pull/884) by https://github.com/bddckr
11+
-Fix deterministic build+source link bug [895](https://github.com/coverlet-coverage/coverlet/pull/895)
1112

1213
## Release date 2020-05-30
1314
### Packages
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
namespace Coverlet.Core.Abstractions
1+
using System.Collections.Generic;
2+
using Coverlet.Core.Helpers;
3+
4+
namespace Coverlet.Core.Abstractions
25
{
36
internal interface ISourceRootTranslator
47
{
58
string ResolveFilePath(string originalFileName);
9+
IReadOnlyList<SourceRootMapping> ResolvePathRoot(string pathRoot);
610
}
711
}

src/coverlet.core/Coverage.cs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using Coverlet.Core.Abstractions;
6+
using Coverlet.Core.Helpers;
67
using Coverlet.Core.Instrumentation;
78

89
using Newtonsoft.Json;
@@ -77,7 +78,11 @@ public Coverage(string module,
7778
_results = new List<InstrumenterResult>();
7879
}
7980

80-
public Coverage(CoveragePrepareResult prepareResult, ILogger logger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem)
81+
public Coverage(CoveragePrepareResult prepareResult,
82+
ILogger logger,
83+
IInstrumentationHelper instrumentationHelper,
84+
IFileSystem fileSystem,
85+
ISourceRootTranslator sourceRootTranslator)
8186
{
8287
_identifier = prepareResult.Identifier;
8388
_module = prepareResult.Module;
@@ -87,6 +92,7 @@ public Coverage(CoveragePrepareResult prepareResult, ILogger logger, IInstrument
8792
_logger = logger;
8893
_instrumentationHelper = instrumentationHelper;
8994
_fileSystem = fileSystem;
95+
_sourceRootTranslator = sourceRootTranslator;
9096
}
9197

9298
public CoveragePrepareResult PrepareModules()
@@ -429,29 +435,33 @@ private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments,
429435
string key = sourceLinkDocument.Key;
430436
if (Path.GetFileName(key) != "*") continue;
431437

432-
string directoryDocument = Path.GetDirectoryName(document);
433-
string sourceLinkRoot = Path.GetDirectoryName(key);
434-
string relativePath = "";
435-
436-
// if document is on repo root we skip relative path calculation
437-
if (directoryDocument != sourceLinkRoot)
438+
IReadOnlyList<SourceRootMapping> rootMapping = _sourceRootTranslator.ResolvePathRoot(key.Substring(0, key.Length - 1));
439+
foreach (string keyMapping in rootMapping is null ? new List<string>() { key } : new List<string>(rootMapping.Select(m => m.OriginalPath)))
438440
{
439-
if (!directoryDocument.StartsWith(sourceLinkRoot + Path.DirectorySeparatorChar))
440-
continue;
441+
string directoryDocument = Path.GetDirectoryName(document);
442+
string sourceLinkRoot = Path.GetDirectoryName(keyMapping);
443+
string relativePath = "";
441444

442-
relativePath = directoryDocument.Substring(sourceLinkRoot.Length + 1);
443-
}
445+
// if document is on repo root we skip relative path calculation
446+
if (directoryDocument != sourceLinkRoot)
447+
{
448+
if (!directoryDocument.StartsWith(sourceLinkRoot + Path.DirectorySeparatorChar))
449+
continue;
444450

445-
if (relativePathOfBestMatch.Length == 0)
446-
{
447-
keyWithBestMatch = sourceLinkDocument.Key;
448-
relativePathOfBestMatch = relativePath;
449-
}
451+
relativePath = directoryDocument.Substring(sourceLinkRoot.Length + 1);
452+
}
450453

451-
if (relativePath.Length < relativePathOfBestMatch.Length)
452-
{
453-
keyWithBestMatch = sourceLinkDocument.Key;
454-
relativePathOfBestMatch = relativePath;
454+
if (relativePathOfBestMatch.Length == 0)
455+
{
456+
keyWithBestMatch = sourceLinkDocument.Key;
457+
relativePathOfBestMatch = relativePath;
458+
}
459+
460+
if (relativePath.Length < relativePathOfBestMatch.Length)
461+
{
462+
keyWithBestMatch = sourceLinkDocument.Key;
463+
relativePathOfBestMatch = relativePath;
464+
}
455465
}
456466
}
457467

src/coverlet.core/Helpers/SourceRootTranslator.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal class SourceRootTranslator : ISourceRootTranslator
1919
private readonly IFileSystem _fileSystem;
2020
private readonly Dictionary<string, List<SourceRootMapping>> _sourceRootMapping;
2121
private const string MappingFileName = "CoverletSourceRootsMapping";
22-
private Dictionary<string, string> _resolutionCache;
22+
private Dictionary<string, string> _resolutionCacheFiles;
2323

2424
public SourceRootTranslator(ILogger logger, IFileSystem fileSystem)
2525
{
@@ -76,11 +76,16 @@ private Dictionary<string, List<SourceRootMapping>> LoadSourceRootMapping(string
7676
return mapping;
7777
}
7878

79+
public IReadOnlyList<SourceRootMapping> ResolvePathRoot(string pathRoot)
80+
{
81+
return _sourceRootMapping.TryGetValue(pathRoot, out List<SourceRootMapping> sourceRootMapping) ? sourceRootMapping.AsReadOnly() : null;
82+
}
83+
7984
public string ResolveFilePath(string originalFileName)
8085
{
81-
if (_resolutionCache != null && _resolutionCache.ContainsKey(originalFileName))
86+
if (_resolutionCacheFiles != null && _resolutionCacheFiles.ContainsKey(originalFileName))
8287
{
83-
return _resolutionCache[originalFileName];
88+
return _resolutionCacheFiles[originalFileName];
8489
}
8590

8691
foreach (KeyValuePair<string, List<SourceRootMapping>> mapping in _sourceRootMapping)
@@ -92,7 +97,7 @@ public string ResolveFilePath(string originalFileName)
9297
string pathToCheck;
9398
if (_fileSystem.Exists(pathToCheck = Path.GetFullPath(originalFileName.Replace(mapping.Key, srm.OriginalPath))))
9499
{
95-
(_resolutionCache ??= new Dictionary<string, string>()).Add(originalFileName, pathToCheck);
100+
(_resolutionCacheFiles ??= new Dictionary<string, string>()).Add(originalFileName, pathToCheck);
96101
_logger.LogVerbose($"Mapping resolved: '{originalFileName}' -> '{pathToCheck}'");
97102
return pathToCheck;
98103
}

src/coverlet.msbuild.tasks/CoverageResultTask.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public override bool Execute()
9696
// Task.Log is teared down after a task and thus the new MSBuildLogger must be passed to the InstrumentationHelper
9797
// https://github.com/microsoft/msbuild/issues/5153
9898
instrumentationHelper.SetLogger(_logger);
99-
coverage = new Coverage(CoveragePrepareResult.Deserialize(instrumenterStateStream), this._logger, ServiceProvider.GetService<IInstrumentationHelper>(), fileSystem);
99+
coverage = new Coverage(CoveragePrepareResult.Deserialize(instrumenterStateStream), this._logger, ServiceProvider.GetService<IInstrumentationHelper>(), fileSystem, ServiceProvider.GetService<ISourceRootTranslator>());
100100
}
101101

102102
try

test/coverlet.core.tests/Coverage/InstrumenterHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static CoverageResult GetCoverageResult(string filePath)
6363
});
6464
_processWideContainer.GetRequiredService<IInstrumentationHelper>().SetLogger(logger.Object);
6565
CoveragePrepareResult coveragePrepareResultLoaded = CoveragePrepareResult.Deserialize(result);
66-
Coverage coverage = new Coverage(coveragePrepareResultLoaded, logger.Object, _processWideContainer.GetService<IInstrumentationHelper>(), new FileSystem());
66+
Coverage coverage = new Coverage(coveragePrepareResultLoaded, logger.Object, _processWideContainer.GetService<IInstrumentationHelper>(), new FileSystem(), new SourceRootTranslator(new Mock<ILogger>().Object, new FileSystem()));
6767
return coverage.GetCoverageResult();
6868
}
6969

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ public void Translate_Success()
2828
Assert.Equal(@"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb", translator.ResolveFilePath(fileToTranslate));
2929
}
3030

31+
[ConditionalFact]
32+
[SkipOnOS(OS.Linux, "Windows path format only")]
33+
[SkipOnOS(OS.MacOS, "Windows path format only")]
34+
public void TranslatePathRoot_Success()
35+
{
36+
Mock<ILogger> logger = new Mock<ILogger>();
37+
Mock<IFileSystem> fileSystem = new Mock<IFileSystem>();
38+
fileSystem.Setup(f => f.Exists(It.IsAny<string>())).Returns((string p) =>
39+
{
40+
if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping") return true;
41+
return false;
42+
});
43+
fileSystem.Setup(f => f.ReadAllLines(It.IsAny<string>())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest"));
44+
SourceRootTranslator translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object);
45+
Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath);
46+
}
47+
3148
[Fact]
3249
public void Translate_EmptyFile()
3350
{

test/coverlet.integration.tests/BaseTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ private protected void AddCoverletCollectosRef(string projectPath)
200200
xml.Save(csproj);
201201
}
202202

203-
private protected string AddCollectorRunsettingsFile(string projectPath, string includeFilter = "[coverletsamplelib.integration.template]*DeepThought")
203+
private protected string AddCollectorRunsettingsFile(string projectPath, string includeFilter = "[coverletsamplelib.integration.template]*DeepThought", bool sourceLink = false)
204204
{
205205
string runSettings =
206206
$@"<?xml version=""1.0"" encoding=""utf-8"" ?>
@@ -211,6 +211,7 @@ private protected string AddCollectorRunsettingsFile(string projectPath, string
211211
<Configuration>
212212
<Format>json,cobertura</Format>
213213
<Include>{includeFilter}</Include>
214+
<UseSourceLink>{(sourceLink ? "true" : "false")}</UseSourceLink>
214215
<!-- We need to include test assembly because test and code to cover are in same template project -->
215216
<IncludeTestAssembly>true</IncludeTestAssembly>
216217
</Configuration>

test/coverlet.integration.tests/DeterministicBuild.cs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void Msbuild()
6666
string sourceRootMappingFilePath = Path.Combine(_testProjectPath, "bin", _buildConfiguration, _testProjectTfm!, "CoverletSourceRootsMapping");
6767
Assert.True(File.Exists(sourceRootMappingFilePath), sourceRootMappingFilePath);
6868
Assert.True(!string.IsNullOrEmpty(File.ReadAllText(sourceRootMappingFilePath)), "Empty CoverletSourceRootsMapping file");
69-
Assert.Equal(2, File.ReadAllLines(sourceRootMappingFilePath).Length);
69+
Assert.Contains("=/_/", File.ReadAllText(sourceRootMappingFilePath));
7070

7171
DotnetCli($"test -c {_buildConfiguration} --no-build /p:CollectCoverage=true /p:Include=\"[coverletsample.integration.determisticbuild]*DeepThought\" /p:IncludeTestAssembly=true", out standardOutput, out _, _testProjectPath);
7272
Assert.Contains("Test Run Successful.", standardOutput);
@@ -80,6 +80,30 @@ public void Msbuild()
8080
RunCommand("git", "clean -fdx", out _, out _, _testProjectPath);
8181
}
8282

83+
[Fact]
84+
public void Msbuild_SourceLink()
85+
{
86+
CreateDeterministicTestPropsFile();
87+
DotnetCli($"build -c {_buildConfiguration} /p:DeterministicSourcePaths=true", out string standardOutput, out string _, _testProjectPath);
88+
Assert.Contains("Build succeeded.", standardOutput);
89+
string sourceRootMappingFilePath = Path.Combine(_testProjectPath, "bin", _buildConfiguration, _testProjectTfm!, "CoverletSourceRootsMapping");
90+
Assert.True(File.Exists(sourceRootMappingFilePath), sourceRootMappingFilePath);
91+
Assert.True(!string.IsNullOrEmpty(File.ReadAllText(sourceRootMappingFilePath)), "Empty CoverletSourceRootsMapping file");
92+
Assert.Contains("=/_/", File.ReadAllText(sourceRootMappingFilePath));
93+
94+
DotnetCli($"test -c {_buildConfiguration} --no-build /p:CollectCoverage=true /p:UseSourceLink=true /p:Include=\"[coverletsample.integration.determisticbuild]*DeepThought\" /p:IncludeTestAssembly=true", out standardOutput, out _, _testProjectPath);
95+
Assert.Contains("Test Run Successful.", standardOutput);
96+
Assert.Contains("| coverletsample.integration.determisticbuild | 100% | 100% | 100% |", standardOutput);
97+
Assert.True(File.Exists(Path.Combine(_testProjectPath, "coverage.json")));
98+
Assert.Contains("raw.githubusercontent.com", File.ReadAllText(Path.Combine(_testProjectPath, "coverage.json")));
99+
AssertCoverage(standardOutput);
100+
101+
// Process exits hang on clean seem that process doesn't close, maybe some mbuild node reuse? btw manually tested
102+
// DotnetCli("clean", out standardOutput, out standardError, _fixture.TestProjectPath);
103+
// Assert.False(File.Exists(sourceRootMappingFilePath));
104+
RunCommand("git", "clean -fdx", out _, out _, _testProjectPath);
105+
}
106+
83107
[Fact]
84108
public void Collectors()
85109
{
@@ -89,7 +113,7 @@ public void Collectors()
89113
string sourceRootMappingFilePath = Path.Combine(_testProjectPath, "bin", GetAssemblyBuildConfiguration().ToString(), _testProjectTfm!, "CoverletSourceRootsMapping");
90114
Assert.True(File.Exists(sourceRootMappingFilePath), sourceRootMappingFilePath);
91115
Assert.NotEmpty(File.ReadAllText(sourceRootMappingFilePath));
92-
Assert.Equal(2, File.ReadAllLines(sourceRootMappingFilePath).Length);
116+
Assert.Contains("=/_/", File.ReadAllText(sourceRootMappingFilePath));
93117

94118
string runSettingsPath = AddCollectorRunsettingsFile(_testProjectPath, "[coverletsample.integration.determisticbuild]*DeepThought");
95119
Assert.True(DotnetCli($"test -c {_buildConfiguration} --no-build \"{_testProjectPath}\" --collect:\"XPlat Code Coverage\" --settings \"{runSettingsPath}\" --diag:{Path.Combine(_testProjectPath, "log.txt")}", out standardOutput, out _), standardOutput);
@@ -108,6 +132,35 @@ public void Collectors()
108132
RunCommand("git", "clean -fdx", out _, out _, _testProjectPath);
109133
}
110134

135+
[Fact]
136+
public void Collectors_SourceLink()
137+
{
138+
CreateDeterministicTestPropsFile();
139+
DotnetCli($"build -c {_buildConfiguration} /p:DeterministicSourcePaths=true", out string standardOutput, out string _, _testProjectPath);
140+
Assert.Contains("Build succeeded.", standardOutput);
141+
string sourceRootMappingFilePath = Path.Combine(_testProjectPath, "bin", GetAssemblyBuildConfiguration().ToString(), _testProjectTfm!, "CoverletSourceRootsMapping");
142+
Assert.True(File.Exists(sourceRootMappingFilePath), sourceRootMappingFilePath);
143+
Assert.NotEmpty(File.ReadAllText(sourceRootMappingFilePath));
144+
Assert.Contains("=/_/", File.ReadAllText(sourceRootMappingFilePath));
145+
146+
string runSettingsPath = AddCollectorRunsettingsFile(_testProjectPath, "[coverletsample.integration.determisticbuild]*DeepThought", sourceLink: true);
147+
Assert.True(DotnetCli($"test -c {_buildConfiguration} --no-build \"{_testProjectPath}\" --collect:\"XPlat Code Coverage\" --settings \"{runSettingsPath}\" --diag:{Path.Combine(_testProjectPath, "log.txt")}", out standardOutput, out _), standardOutput);
148+
Assert.Contains("Test Run Successful.", standardOutput);
149+
AssertCoverage(standardOutput);
150+
Assert.Contains("raw.githubusercontent.com", File.ReadAllText(Directory.GetFiles(_testProjectPath, "coverage.cobertura.xml", SearchOption.AllDirectories).Single()));
151+
152+
// Check out/in process collectors injection
153+
string dataCollectorLogContent = File.ReadAllText(Directory.GetFiles(_testProjectPath, "log.datacollector.*.txt").Single());
154+
Assert.Contains("[coverlet]Initializing CoverletCoverageDataCollector with configuration:", dataCollectorLogContent);
155+
Assert.Contains("[coverlet]Initialize CoverletInProcDataCollector", File.ReadAllText(Directory.GetFiles(_testProjectPath, "log.host.*.txt").Single()));
156+
Assert.Contains("[coverlet]Mapping resolved", dataCollectorLogContent);
157+
158+
// Process exits hang on clean seem that process doesn't close, maybe some mbuild node reuse? btw manually tested
159+
// DotnetCli("clean", out standardOutput, out standardError, _fixture.TestProjectPath);
160+
// Assert.False(File.Exists(sourceRootMappingFilePath));
161+
RunCommand("git", "clean -fdx", out _, out _, _testProjectPath);
162+
}
163+
111164
public void Dispose()
112165
{
113166
File.Delete(Path.Combine(_testProjectPath, PropsFileName));

0 commit comments

Comments
 (0)