Support for deterministic builds is available only in the msbuild
(/p:CollectCoverage=true
) and collectors
(--collect:"XPlat Code Coverage"
) drivers. Deterministic builds are important because they enable verification that the resulting binary was built from the specified sources. For more information on how to enable deterministic builds, I recommend you to take a look at Claire Novotny's guide.
From a coverage perspective, deterministic builds create some challenges because coverage tools usually need access to complete source file metadata (ie. local path) during instrumentation and report generation. These files are reported inside the .pdb
files, where debugging information is stored.
In local (non-CI) builds, metadata emitted to pdbs are not "deterministic", which means that source files are reported with their full paths. For example, when we build the same project on different machines we'll have different paths emitted inside pdbs, hence, builds are "non deterministic".
As explained above, to improve the level of security of generated artifacts (for instance, DLLs inside the NuGet package), we need to apply some signature (signing with certificate) and validate before usage to avoid possible security issues like tampering.
Finally, thanks to deterministic CI builds (with the ContinuousIntegrationBuild
property set to true
) plus signature we can validate artifacts and be sure that the binary was built from specific sources (because there is no hard-coded variable metadata, like paths from different build machines).
Coverlet supports also deterministic reports(for now only for cobertura coverage format).
If you include DeterministicReport
parameters for msbuild
and collectors
integrations resulting report will be like:
<?xml version="1.0" encoding="utf-8"?>
<coverage line-rate="0.8571" branch-rate="0.5" version="1.9" timestamp="1612702997" lines-covered="6" lines-valid="7" branches-covered="1" branches-valid="2">
<sources />
<packages>
<package name="MyLibrary" line-rate="0.8571" branch-rate="0.5" complexity="3">
<classes>
<class name="MyLibrary.Hello" filename="/_/MyLibrary/Hello.cs" line-rate="0.8571" branch-rate="0.5" complexity="3">
<methods>
...
As you can see we have empty <sources />
element and the filename
start with well known deterministic fragment /_/...
Deterministic build is supported without any workaround since version 3.1.100 of .NET Core SDK
At the moment, deterministic build works thanks to the Roslyn compiler emitting deterministic metadata if DeterministicSourcePaths
is enabled. Take a look here for more information.
To allow Coverlet to correctly do its work, we need to provide information to translate deterministic paths to real local paths for every project referenced by the test project. The current workaround is to add at the root of your repo a Directory.Build.targets
with a custom target
that supports Coverlet resolution algorithm.
<!-- This target must be imported into Directory.Build.targets -->
<!-- Workaround. Remove once we're on 3.1.300+
https://github.com/dotnet/sourcelink/issues/572 -->
<Project>
<PropertyGroup>
<TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
</ItemGroup>
<ItemGroup>
<SourceRoot Include="$([MSBuild]::EnsureTrailingSlash($(NuGetPackageRoot)))" Condition="'$(NuGetPackageRoot)' != ''" />
</ItemGroup>
<Target Name="CoverletGetPathMap"
DependsOnTargets="InitializeSourceRootMappedPaths"
Returns="@(_LocalTopLevelSourceRoot)"
Condition="'$(DeterministicSourcePaths)' == 'true'">
<ItemGroup>
<_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
</ItemGroup>
</Target>
</Project>
If you already have a Directory.Build.targets
file on your repo root you can simply copy DeterministicBuild.targets
(which can be found at the root of this repo) next to yours and import it in your targets file. This target will be used by Coverlet to generate, at build time, a file that contains mapping translation information, the file is named CoverletSourceRootsMapping
and will be in the output folder of your project.
You can follow our step-by-step sample
Feel free to file an issue in case you run into any trouble!