Skip to content

Commit db09e8b

Browse files
authored
Skip fsharp construct with unknown source (#1165)
Skip fsharp construct with `unknown` source
1 parent 32df97e commit db09e8b

File tree

8 files changed

+106
-26
lines changed

8 files changed

+106
-26
lines changed

Documentation/Changelog.md

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

99
### Fixed
10+
11+
-Fix F# projects with `unkown` source [#1145](https://github.com/coverlet-coverage/coverlet/issues/1145)
1012
-Fix SkipAutoProps for inline assigned properties [#1139](https://github.com/coverlet-coverage/coverlet/issues/1139)
1113
-Fix partially covered throw statement [#1144](https://github.com/coverlet-coverage/coverlet/pull/1144)
1214
-Fix coverage threshold not failing when no coverage [#1115](https://github.com/coverlet-coverage/coverlet/pull/1115)

coverlet.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{9A8B19D4
5353
test\Directory.Build.targets = test\Directory.Build.targets
5454
EndProjectSection
5555
EndProject
56+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "coverlet.tests.projectsample.fsharp", "test\coverlet.tests.projectsample.fsharp\coverlet.tests.projectsample.fsharp.fsproj", "{1CBF6966-2A67-4D2C-8598-D174B83072F4}"
57+
EndProject
5658
Global
5759
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5860
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,10 @@ Global
115117
{F8199E19-FA9A-4559-9101-CAD7028121B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
116118
{F8199E19-FA9A-4559-9101-CAD7028121B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
117119
{F8199E19-FA9A-4559-9101-CAD7028121B4}.Release|Any CPU.Build.0 = Release|Any CPU
120+
{1CBF6966-2A67-4D2C-8598-D174B83072F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
121+
{1CBF6966-2A67-4D2C-8598-D174B83072F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
122+
{1CBF6966-2A67-4D2C-8598-D174B83072F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
123+
{1CBF6966-2A67-4D2C-8598-D174B83072F4}.Release|Any CPU.Build.0 = Release|Any CPU
118124
EndGlobalSection
119125
GlobalSection(SolutionProperties) = preSolution
120126
HideSolutionNode = FALSE
@@ -135,6 +141,7 @@ Global
135141
{5FF404AD-7C0B-465A-A1E9-558CDC642B0C} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
136142
{F8199E19-FA9A-4559-9101-CAD7028121B4} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
137143
{9A8B19D4-4A24-4217-AEFE-159B68F029A1} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
144+
{1CBF6966-2A67-4D2C-8598-D174B83072F4} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
138145
EndGlobalSection
139146
GlobalSection(ExtensibilityGlobals) = postSolution
140147
SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,13 @@ public bool EmbeddedPortablePdbHasLocalSource(string module, out string firstNot
117117
using (MetadataReaderProvider embeddedMetadataProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry))
118118
{
119119
MetadataReader metadataReader = embeddedMetadataProvider.GetMetadataReader();
120-
foreach (DocumentHandle docHandle in metadataReader.Documents)
120+
121+
var matchingResult = MatchDocumentsWithSources(metadataReader);
122+
123+
if (!matchingResult.allDocumentsMatch)
121124
{
122-
Document document = metadataReader.GetDocument(docHandle);
123-
string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name));
124-
125-
// We verify all docs and return false if not all are present in local
126-
// We could have false negative if doc is not a source
127-
// Btw check for all possible extension could be weak approach
128-
// We exlude from the check the autogenerated source file(i.e. source generators)
129-
if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs"))
130-
{
131-
firstNotFoundDocument = docName;
132-
return false;
133-
}
125+
firstNotFoundDocument = matchingResult.notFoundDocument;
126+
return false;
134127
}
135128
}
136129
}
@@ -165,20 +158,13 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc
165158
_logger.LogWarning($"{nameof(BadImageFormatException)} during MetadataReaderProvider.FromPortablePdbStream in InstrumentationHelper.PortablePdbHasLocalSource, unable to check if module has got local source.");
166159
return true;
167160
}
168-
foreach (DocumentHandle docHandle in metadataReader.Documents)
161+
162+
var matchingResult = MatchDocumentsWithSources(metadataReader);
163+
164+
if (!matchingResult.allDocumentsMatch)
169165
{
170-
Document document = metadataReader.GetDocument(docHandle);
171-
string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name));
172-
173-
// We verify all docs and return false if not all are present in local
174-
// We could have false negative if doc is not a source
175-
// Btw check for all possible extension could be weak approach
176-
// We exlude from the check the autogenerated source file(i.e. source generators)
177-
if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs"))
178-
{
179-
firstNotFoundDocument = docName;
180-
return false;
181-
}
166+
firstNotFoundDocument = matchingResult.notFoundDocument;
167+
return false;
182168
}
183169
}
184170
}
@@ -187,6 +173,27 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc
187173
return true;
188174
}
189175

176+
private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSources(MetadataReader metadataReader)
177+
{
178+
foreach (DocumentHandle docHandle in metadataReader.Documents)
179+
{
180+
Document document = metadataReader.GetDocument(docHandle);
181+
string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name));
182+
Guid languageGuid = metadataReader.GetGuid(document.Language);
183+
// We verify all docs and return false if not all are present in local
184+
// We could have false negative if doc is not a source
185+
// Btw check for all possible extension could be weak approach
186+
// We exlude from the check the autogenerated source file(i.e. source generators)
187+
// We exclude special F# construct https://github.com/coverlet-coverage/coverlet/issues/1145
188+
if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs") &&
189+
!IsUnknownModuleInFSharpAssembly(languageGuid, docName))
190+
{
191+
return (false, docName);
192+
}
193+
}
194+
return (true, string.Empty);
195+
}
196+
190197
public void BackupOriginalModule(string module, string identifier)
191198
{
192199
var backupPath = GetBackupPath(module, identifier);
@@ -443,5 +450,12 @@ private bool IsAssembly(string filePath)
443450
return false;
444451
}
445452
}
453+
454+
private bool IsUnknownModuleInFSharpAssembly(Guid languageGuid, string docName)
455+
{
456+
// https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#document-table-0x30
457+
return languageGuid.Equals(new Guid("ab4f38c9-b6e6-43ba-be3b-58080b2ccce3"))
458+
&& docName.EndsWith("unknown");
459+
}
446460
}
447461
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Xunit;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using Castle.Core.Internal;
67
using Moq;
78
using Coverlet.Core.Abstractions;
89

@@ -29,6 +30,26 @@ public void TestGetDependenciesWithTestAssembly()
2930
Assert.True(Array.Exists(modules, m => m == module));
3031
}
3132

33+
[Fact]
34+
public void EmbeddedPortablePDPHasLocalSource_DocumentDoesNotExist_ReturnsFalse()
35+
{
36+
var fileSystem = new Mock<FileSystem> {CallBase = true};
37+
fileSystem.Setup(x => x.Exists(It.IsAny<string>())).Returns(false);
38+
39+
InstrumentationHelper instrumentationHelper =
40+
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock<ILogger>().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock<ILogger>().Object, new FileSystem()));
41+
42+
Assert.False(instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument));
43+
Assert.False(notFoundDocument.IsNullOrEmpty());
44+
}
45+
46+
[Fact]
47+
public void EmbeddedPortablePDPHasLocalSource_AllDocumentsExist_ReturnsTrue()
48+
{
49+
Assert.True(_instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument));
50+
Assert.True(notFoundDocument.IsNullOrEmpty());
51+
}
52+
3253
[Fact]
3354
public void TestHasPdb()
3455
{

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,24 @@ public void TestInstrument_MissingModule()
513513
loggerMock.Verify(l => l.LogWarning(It.IsAny<string>()));
514514
}
515515

516+
[Fact]
517+
public void CanInstrumentFSharpAssemblyWithAnonymousRecord()
518+
{
519+
var loggerMock = new Mock<ILogger>();
520+
521+
string sample = Directory.GetFiles(Directory.GetCurrentDirectory(), "coverlet.tests.projectsample.fsharp.dll").First();
522+
InstrumentationHelper instrumentationHelper =
523+
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock<ILogger>().Object,
524+
new SourceRootTranslator(sample, new Mock<ILogger>().Object, new FileSystem()));
525+
526+
var instrumenter = new Instrumenter(sample, "_coverlet_tests_projectsample_fsharp", new CoverageParameters(), loggerMock.Object, instrumentationHelper,
527+
new FileSystem(), new SourceRootTranslator(sample, loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
528+
529+
Assert.True(instrumentationHelper.HasPdb(sample, out bool embedded));
530+
Assert.False(embedded);
531+
Assert.True(instrumenter.CanInstrument());
532+
}
533+
516534
[Theory]
517535
[InlineData("NotAMatch", new string[] { }, false)]
518536
[InlineData("ExcludeFromCoverageAttribute", new string[] { }, true)]

test/coverlet.core.tests/coverlet.core.tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<ProjectReference Include="$(RepoRoot)src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj" />
2525
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.projectsample.empty\coverlet.tests.projectsample.empty.csproj" />
2626
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.projectsample.excludedbyattribute\coverlet.tests.projectsample.excludedbyattribute.csproj" />
27+
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.projectsample.fsharp\coverlet.tests.projectsample.fsharp.fsproj" />
2728
<ProjectReference Include="$(RepoRoot)test\coverlet.core.tests.samples.netstandard\coverlet.core.tests.samples.netstandard.csproj" />
2829
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.xunit.extensions\coverlet.tests.xunit.extensions.csproj" />
2930
</ItemGroup>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace coverlet.tests.projectsample.fsharp
2+
3+
module TestModule =
4+
type Type1 = Option1 | Option2 of {| x: string |}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
6+
<IsTestProject>false</IsTestProject>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Compile Include="Library.fs" />
11+
</ItemGroup>
12+
13+
</Project>

0 commit comments

Comments
 (0)