diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 51c70e7d6..787b935f5 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed + +-Fix F# projects with `unkown` source [#1145](https://github.com/coverlet-coverage/coverlet/issues/1145) -Fix SkipAutoProps for inline assigned properties [#1139](https://github.com/coverlet-coverage/coverlet/issues/1139) -Fix partially covered throw statement [#1144](https://github.com/coverlet-coverage/coverlet/pull/1144) -Fix coverage threshold not failing when no coverage [#1115](https://github.com/coverlet-coverage/coverlet/pull/1115) diff --git a/coverlet.sln b/coverlet.sln index 17d712674..40dfd8966 100644 --- a/coverlet.sln +++ b/coverlet.sln @@ -53,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{9A8B19D4 test\Directory.Build.targets = test\Directory.Build.targets EndProjectSection EndProject +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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,10 @@ Global {F8199E19-FA9A-4559-9101-CAD7028121B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {F8199E19-FA9A-4559-9101-CAD7028121B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {F8199E19-FA9A-4559-9101-CAD7028121B4}.Release|Any CPU.Build.0 = Release|Any CPU + {1CBF6966-2A67-4D2C-8598-D174B83072F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CBF6966-2A67-4D2C-8598-D174B83072F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CBF6966-2A67-4D2C-8598-D174B83072F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CBF6966-2A67-4D2C-8598-D174B83072F4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -135,6 +141,7 @@ Global {5FF404AD-7C0B-465A-A1E9-558CDC642B0C} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {F8199E19-FA9A-4559-9101-CAD7028121B4} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {9A8B19D4-4A24-4217-AEFE-159B68F029A1} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} + {1CBF6966-2A67-4D2C-8598-D174B83072F4} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10} diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index d48036317..8a6827621 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -117,20 +117,13 @@ public bool EmbeddedPortablePdbHasLocalSource(string module, out string firstNot using (MetadataReaderProvider embeddedMetadataProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry)) { MetadataReader metadataReader = embeddedMetadataProvider.GetMetadataReader(); - foreach (DocumentHandle docHandle in metadataReader.Documents) + + var matchingResult = MatchDocumentsWithSources(metadataReader); + + if (!matchingResult.allDocumentsMatch) { - Document document = metadataReader.GetDocument(docHandle); - string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); - - // We verify all docs and return false if not all are present in local - // We could have false negative if doc is not a source - // Btw check for all possible extension could be weak approach - // We exlude from the check the autogenerated source file(i.e. source generators) - if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs")) - { - firstNotFoundDocument = docName; - return false; - } + firstNotFoundDocument = matchingResult.notFoundDocument; + return false; } } } @@ -165,20 +158,13 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc _logger.LogWarning($"{nameof(BadImageFormatException)} during MetadataReaderProvider.FromPortablePdbStream in InstrumentationHelper.PortablePdbHasLocalSource, unable to check if module has got local source."); return true; } - foreach (DocumentHandle docHandle in metadataReader.Documents) + + var matchingResult = MatchDocumentsWithSources(metadataReader); + + if (!matchingResult.allDocumentsMatch) { - Document document = metadataReader.GetDocument(docHandle); - string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); - - // We verify all docs and return false if not all are present in local - // We could have false negative if doc is not a source - // Btw check for all possible extension could be weak approach - // We exlude from the check the autogenerated source file(i.e. source generators) - if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs")) - { - firstNotFoundDocument = docName; - return false; - } + firstNotFoundDocument = matchingResult.notFoundDocument; + return false; } } } @@ -187,6 +173,27 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc return true; } + private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSources(MetadataReader metadataReader) + { + foreach (DocumentHandle docHandle in metadataReader.Documents) + { + Document document = metadataReader.GetDocument(docHandle); + string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); + Guid languageGuid = metadataReader.GetGuid(document.Language); + // We verify all docs and return false if not all are present in local + // We could have false negative if doc is not a source + // Btw check for all possible extension could be weak approach + // We exlude from the check the autogenerated source file(i.e. source generators) + // We exclude special F# construct https://github.com/coverlet-coverage/coverlet/issues/1145 + if (!_fileSystem.Exists(docName) && !docName.EndsWith(".g.cs") && + !IsUnknownModuleInFSharpAssembly(languageGuid, docName)) + { + return (false, docName); + } + } + return (true, string.Empty); + } + public void BackupOriginalModule(string module, string identifier) { var backupPath = GetBackupPath(module, identifier); @@ -443,5 +450,12 @@ private bool IsAssembly(string filePath) return false; } } + + private bool IsUnknownModuleInFSharpAssembly(Guid languageGuid, string docName) + { + // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#document-table-0x30 + return languageGuid.Equals(new Guid("ab4f38c9-b6e6-43ba-be3b-58080b2ccce3")) + && docName.EndsWith("unknown"); + } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index a97e8d89a..745009c72 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -3,6 +3,7 @@ using Xunit; using System.Collections.Generic; using System.Linq; +using Castle.Core.Internal; using Moq; using Coverlet.Core.Abstractions; @@ -29,6 +30,26 @@ public void TestGetDependenciesWithTestAssembly() Assert.True(Array.Exists(modules, m => m == module)); } + [Fact] + public void EmbeddedPortablePDPHasLocalSource_DocumentDoesNotExist_ReturnsFalse() + { + var fileSystem = new Mock {CallBase = true}; + fileSystem.Setup(x => x.Exists(It.IsAny())).Returns(false); + + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock().Object, new FileSystem())); + + Assert.False(instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument)); + Assert.False(notFoundDocument.IsNullOrEmpty()); + } + + [Fact] + public void EmbeddedPortablePDPHasLocalSource_AllDocumentsExist_ReturnsTrue() + { + Assert.True(_instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument)); + Assert.True(notFoundDocument.IsNullOrEmpty()); + } + [Fact] public void TestHasPdb() { diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 53219b293..dcdbc9499 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -513,6 +513,24 @@ public void TestInstrument_MissingModule() loggerMock.Verify(l => l.LogWarning(It.IsAny())); } + [Fact] + public void CanInstrumentFSharpAssemblyWithAnonymousRecord() + { + var loggerMock = new Mock(); + + string sample = Directory.GetFiles(Directory.GetCurrentDirectory(), "coverlet.tests.projectsample.fsharp.dll").First(); + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(sample, new Mock().Object, new FileSystem())); + + var instrumenter = new Instrumenter(sample, "_coverlet_tests_projectsample_fsharp", new CoverageParameters(), loggerMock.Object, instrumentationHelper, + new FileSystem(), new SourceRootTranslator(sample, loggerMock.Object, new FileSystem()), new CecilSymbolHelper()); + + Assert.True(instrumentationHelper.HasPdb(sample, out bool embedded)); + Assert.False(embedded); + Assert.True(instrumenter.CanInstrument()); + } + [Theory] [InlineData("NotAMatch", new string[] { }, false)] [InlineData("ExcludeFromCoverageAttribute", new string[] { }, true)] diff --git a/test/coverlet.core.tests/coverlet.core.tests.csproj b/test/coverlet.core.tests/coverlet.core.tests.csproj index 2a7adf9aa..b7cd119b3 100644 --- a/test/coverlet.core.tests/coverlet.core.tests.csproj +++ b/test/coverlet.core.tests/coverlet.core.tests.csproj @@ -24,6 +24,7 @@ + diff --git a/test/coverlet.tests.projectsample.fsharp/Library.fs b/test/coverlet.tests.projectsample.fsharp/Library.fs new file mode 100644 index 000000000..36ea79c12 --- /dev/null +++ b/test/coverlet.tests.projectsample.fsharp/Library.fs @@ -0,0 +1,4 @@ +namespace coverlet.tests.projectsample.fsharp + +module TestModule = + type Type1 = Option1 | Option2 of {| x: string |} diff --git a/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj new file mode 100644 index 000000000..7a7d09a09 --- /dev/null +++ b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj @@ -0,0 +1,13 @@ + + + + net5.0 + true + false + + + + + + +