on iko/ghcjs-patch-master help sign in
Improve coverage reports (#1548)

* Provide an empty coverage report when package has no coverage - When covering a project with multiple packages, the project coverage report will produce a table of individual coverage reports for each package. The links in this table don't go anywhere when a package is uncovered (they are broken links). - By producing an empty coverage report when a package has no coverage, we fix the broken links, and provide a tad more information about the coverage state of that package. * Improve project coverage report index page - Provide a link to the union/all coverage report. - Format the reports as a list instead of a table of one column. - Provide better explanation of what each report means. * Add warning explaining modules with no coverage - Modules that have no coverage at all are simply not included in the HTML reports generated by HPC. - Add a warning to the project coverage report index page so users are aware of this limitation of HPC. * Simplify interface of coverageReport - Remove the concept of "package boundaries" from the "coverageReport" function. - The "coverageReport" is now a function of: - arbitrary checks generating tix files - arbitrary mix modules - This more closely reflects the usage of hpc, which doesn't care about package boundaries. - Use this new "coverageReport" function to simplify the "projectCoverageReport" implementation. A project coverage report now simply: - copies out constituent coverage reports. - writes out an "all coverage report" using all checks in the project and all mix modules.. - writes out a summary index page.

commit0dca71e2f3f3d63eeab01136138e0e1ceab5320d authorSamuel Evans-Powell <mail@sevanspowell.net> dateTue 19 Jul 2022, 15:40 (4 years ago) parent80082ae
3 files changed+148−117
lib/cover-project.nix+44−47 lib/cover.nix+102−69 overlays/haskell.nix+2−1
lib/cover-project.nix+44−47
@@ -10,19 +10,12 @@ coverageReports:
1010let
1111 toBashArray = arr: "(" + (lib.concatStringsSep " " arr) + ")";
1212
13− # Create table rows for a project coverage index page that look something like:
14− #
15− # | Package |
16− # |------------------|
17− # | cardano-shell |
18− # | cardano-launcher |
19− coverageTableRows = coverageReport:
13+ # Create a list element for a project coverage index page.
14+ coverageListElement = coverageReport:
2015 ''
21− <tr>
22− <td>
23− <a href="${coverageReport.passthru.name}/hpc_index.html">${coverageReport.passthru.name}</href>
24− </td>
25− </tr>
16+ <li>
17+ <a href="${coverageReport.passthru.name}/hpc_index.html">${coverageReport.passthru.name}</a>
18+ </li>
2619 '';
2720
2821 projectIndexHtml = pkgs.writeText "index.html" ''
@@ -31,23 +24,32 @@ let
3124 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
3225 </head>
3326 <body>
34− <table border="1" width="100%">
35− <tbody>
36− <tr>
37− <th>Report</th>
38− </tr>
39−
40− ${with lib; concatStringsSep "\n" (map coverageTableRows coverageReports)}
41−
42− </tbody>
43− </table>
27+ <div>
28+ WARNING: Modules with no coverage are not included in any of these reports, this is just how HPC works under the hood.
29+ </div>
30+ <div>
31+ <h2>Union Report</h2>
32+ <p>The following report shows how each module is covered by any test in the project:</p>
33+ <ul>
34+ <li>
35+ <a href="all/hpc_index.html">all</a>
36+ </li>
37+ </ul>
38+ </div>
39+ <div>
40+ <h2>Individual Reports</h2>
41+ <p>The following reports show how the tests of each package cover modules in the project:</p>
42+ <ul>
43+ ${with lib; concatStringsSep "\n" (map coverageListElement coverageReports)}
44+ </ul>
45+ </div>
4446 </body>
4547 </html>
4648 '';
4749
4850 ghc = project.pkg-set.config.ghc.package;
4951
50− libs = lib.remove null (map (r: r.library) coverageReports);
52+ libs = lib.unique (lib.concatMap (r: r.mixLibraries) coverageReports);
5153
5254 mixDirs =
5355 map
@@ -56,6 +58,17 @@ let
5658
5759 srcDirs = map (l: l.srcSubDirPath) libs;
5860
61+ allCoverageReport = haskellLib.coverageReport {
62+ name = "all";
63+ checks = lib.flatten (lib.concatMap
64+ (pkg: lib.optional (pkg ? checks) (lib.filter lib.isDerivation (lib.attrValues pkg.checks)))
65+ (lib.attrValues (haskellLib.selectProjectPackages project.hsPkgs)));
66+ mixLibraries = lib.concatMap
67+ (pkg: lib.optional (pkg.components ? library) pkg.components.library)
68+ (lib.attrValues (haskellLib.selectProjectPackages project.hsPkgs));
69+ ghc = project.pkg-set.config.ghc.package;
70+ };
71+
5972in pkgs.runCommand "project-coverage-report"
6073 ({ nativeBuildInputs = [ (ghc.buildGHC or ghc) pkgs.buildPackages.zip ];
6174 LANG = "en_US.UTF-8";
@@ -124,30 +137,14 @@ in pkgs.runCommand "project-coverage-report"
124137 cp -R $report/share/hpc/vanilla/html/* $out/share/hpc/vanilla/html/
125138 '') coverageReports)}
126139
127− if [ ''${#tixFiles[@]} -ne 0 ]; then
128− # Create tix file with test run information for all packages
129− tixFile="$out/share/hpc/vanilla/tix/all/all.tix"
130− hpcSumCmd=("hpc" "sum" "--union" "--output=$tixFile")
131− hpcSumCmd+=("''${tixFiles[@]}")
132− echo "''${hpcSumCmd[@]}"
133− eval "''${hpcSumCmd[@]}"
134−
135− # Markup a HTML coverage report for the entire project
136− cp ${projectIndexHtml} $out/share/hpc/vanilla/html/index.html
137− echo "report coverage-per-package $out/share/hpc/vanilla/html/index.html" >> $out/nix-support/hydra-build-products
138−
139− local markupOutDir="$out/share/hpc/vanilla/html/all"
140− local srcDirs=${toBashArray srcDirs}
141− local mixDirs=${toBashArray mixDirs}
142− local allMixModules=()
143−
144− mkdir $markupOutDir
145− findModules allMixModules "$out/share/hpc/vanilla/mix/" "*.mix"
140+ # Copy out "all" coverage report
141+ cp -R ${allCoverageReport}/share/hpc/vanilla/tix/all $out/share/hpc/vanilla/tix
142+ cp -R ${allCoverageReport}/share/hpc/vanilla/html/all $out/share/hpc/vanilla/html
146143
147− markup srcDirs mixDirs allMixModules "$markupOutDir" "$tixFile"
144+ # Markup a HTML coverage summary report for the entire project
145+ cp ${projectIndexHtml} $out/share/hpc/vanilla/html/index.html
148146
149− echo "report coverage $markupOutDir/hpc_index.html" >> $out/nix-support/hydra-build-products
150− ( cd $out/share/hpc/vanilla/html ; zip -r $out/share/hpc/vanilla/html.zip . )
151− echo "file zip $out/share/hpc/vanilla/html.zip" >> $out/nix-support/hydra-build-products
152− fi
147+ echo "report coverage $out/share/hpc/vanilla/html/index.html" >> $out/nix-support/hydra-build-products
148+ ( cd $out/share/hpc/vanilla/html ; zip -r $out/share/hpc/vanilla/html.zip . )
149+ echo "file zip $out/share/hpc/vanilla/html.zip" >> $out/nix-support/hydra-build-products
153150 ''
lib/cover.nix+102−69
@@ -1,18 +1,18 @@
1+# The following collects coverage information from a set of given "checks" and
2+# provides a coverage report showing how those "checks" cover a set of given
3+# "mixLibraries".
14{ stdenv, lib, haskellLib, pkgs }:
25
3−# Name of the coverage report, which should be unique
6+# Name of the coverage report, which should be unique.
47{ name
5−# Library to check coverage of
6−, library
7−# List of check derivations that generate coverage
8−, checks
9−# List of other libraries to include in the coverage report. The
10−# default value if just the derivation provided as the `library`
11−# argument. Use a larger list of libraries if you would like the tests
12−# of one local package to generate coverage for another.
13−, mixLibraries ? [library]
14−# hack for project-less projects
15−, ghc ? library.project.pkg-set.config.ghc.package
8+# List of check derivations that generate coverage.
9+, checks ? []
10+# List of libraries to include in the coverage report. If one of the above
11+# checks generates coverage for a particular library, coverage will only
12+# be included if that library is in this list.
13+, mixLibraries ? []
14+# Hack for project-less projects.
15+, ghc ? if mixLibraries == [] then null else (lib.head mixLibraries).project.pkg-set.config.ghc.package
1616}:
1717
1818let
@@ -26,7 +26,7 @@ let
2626in pkgs.runCommand (name + "-coverage-report")
2727 ({nativeBuildInputs = [ (ghc.buildGHC or ghc) pkgs.buildPackages.zip ];
2828 passthru = {
29− inherit name library checks;
29+ inherit name checks mixLibraries;
3030 };
3131 # HPC will fail if the Haskell file contains non-ASCII characters,
3232 # unless our locale is set correctly. This has been fixed, but we
@@ -70,71 +70,105 @@ in pkgs.runCommand (name + "-coverage-report")
7070 local -n tixFs=$2
7171 local outFile="$3"
7272
73− local hpcSumCmd=("hpc" "sum" "--union" "--output=$outFile")
73+ if (( "''${#tixFs[@]}" > 0 )); then
74+ local hpcSumCmd=("hpc" "sum" "--union" "--output=$outFile")
7475
75− for module in "''${includedModules[@]}"; do
76− hpcSumCmd+=("--include=$module")
77− done
76+ for module in "''${includedModules[@]}"; do
77+ hpcSumCmd+=("--include=$module")
78+ done
7879
79− for tixFile in "''${tixFs[@]}"; do
80− hpcSumCmd+=("$tixFile")
81− done
80+ for tixFile in "''${tixFs[@]}"; do
81+ hpcSumCmd+=("$tixFile")
82+ done
8283
83− echo "''${hpcSumCmd[@]}"
84− eval "''${hpcSumCmd[@]}"
84+ echo "''${hpcSumCmd[@]}"
85+ eval "''${hpcSumCmd[@]}"
86+ else
87+ # If there are no tix files we output an empty tix file so that we can
88+ # markup an empty HTML coverage report. This is preferable to failing to
89+ # output a HTML report.
90+ echo 'Tix []' > $outFile
91+ fi
8592 }
8693
8794 function findModules() {
88− local searchDir=$2
95+ local -n result=$1
96+ local -n searchDirs=$2
8997 local pattern=$3
9098
91− pushd $searchDir
92− mapfile -d $'\0' $1 < <(find ./ -type f \
93− -wholename "$pattern" -not -name "Paths*" \
94− -exec basename {} \; \
95− | sed "s/\.mix$//" \
96− | tr "\n" "\0")
97− popd
99+ for dir in "''${searchDirs[@]}"; do
100+ pushd $dir
101+ local temp=()
102+ mapfile -d $'\0' temp < <(find ./ -type f \
103+ -wholename "$pattern" -not -name "Paths*" \
104+ -exec basename {} \; \
105+ | sed "s/\.mix$//" \
106+ | tr "\n" "\0")
107+ result+=("''${temp[@]}")
108+ popd
109+ done
98110 }
99111
100− local mixDirs=${toBashArray mixDirs}
101−
102112 mkdir -p $out/nix-support
103− mkdir -p $out/share/hpc/vanilla/mix/${name}
113+ mkdir -p $out/share/hpc/vanilla/mix/
104114 mkdir -p $out/share/hpc/vanilla/tix/${name}
105115 mkdir -p $out/share/hpc/vanilla/html/${name}
106116
107− # Copy over mix files verbatim
117+ local srcDirs=${toBashArray srcDirs}
118+ local mixDirs=${toBashArray mixDirs}
119+
120+ # Copy out mix files used for this report
108121 for dir in "''${mixDirs[@]}"; do
109122 if [ -d "$dir" ]; then
110− cp -R "$dir"/* $out/share/hpc/vanilla/mix/${name}
123+ cp -R "$dir" $out/share/hpc/vanilla/mix/
111124 fi
112125 done
113126
114− local srcDirs=${toBashArray srcDirs}
115− local allMixModules=()
116− local pkgMixModules=()
117−
118− # The behaviour of stack coverage reports is to provide tix files
119− # that include coverage information for every local package, but
120− # to provide HTML reports that only include coverage info for the
121− # current package. We emulate the same behaviour here. If the user
122− # includes all local packages in the mix libraries argument, they
123− # will get a coverage report very similar to stack.
124−
125− # All mix modules
126− findModules allMixModules "$out/share/hpc/vanilla/mix/${name}" "*.mix"
127− # Only mix modules corresponding to this package
128− findModules pkgMixModules "$out/share/hpc/vanilla/mix/${name}" "*${name}*/*.mix"
129−
130− # For each test
127+ local mixModules=()
128+ # Mix modules for all packages in "mixLibraries"
129+ findModules mixModules mixDirs "*.mix"
130+
131+ # We need to make a distinction between library "exposed-modules" and
132+ # "other-modules" used in test suites:
133+ # - "exposed-modules" are addressed as "$library-$version-$hash/module"
134+ # - "other-modules" are addressed as "module"
135+ #
136+ # This complicates the code required to find the mix modules. For a given mix directory:
137+ #
138+ # mix
139+ # └── ntp-client-0.0.1
140+ # └── ntp-client-0.0.1-gYjRsBHUCaHX7ENcjHnw5
141+ # ├── Network.NTP.Client.mix
142+ # ├── Network.NTP.Client.Packet.mix
143+ # └── Network.NTP.Client.Query.mix
144+ #
145+ # Iff ntp-client uses "other-modules" in a test suite, both:
146+ # - "mix/ntp-client-0.0.1", and
147+ # - "mix/ntp-client-0.0.1/ntp-client-0.0.1-gYjRsBHUCaHX7ENcjHnw5"
148+ # need to be provided to hpc as search directories.
149+ #
150+ # I'd prefer to just exclude "other-modules", but I can't think of an easy
151+ # way to do that in bash.
152+ #
153+ # Here we expand the search dirs and modify the mix dirs accordingly:
154+ for dir in "''${mixDirs[@]}"; do
155+ local otherModulesSearchDirs=()
156+ # Simply consider any directory with a mix file as a search directory.
157+ mapfile -d $'\0' otherModulesSearchDirs < <(find $dir -type f \
158+ -wholename "*.mix" \
159+ -exec dirname {} \; \
160+ | uniq \
161+ | tr "\n" "\0")
162+ mixDirs+=("''${otherModulesSearchDirs[@]}")
163+ done
164+
131165 local tixFiles=()
132166 ${lib.concatStringsSep "\n" (builtins.map (check: ''
133167 if [ -d "${check}/share/hpc/vanilla/tix" ]; then
134168 pushd ${check}/share/hpc/vanilla/tix
135169
136170 tixFile="$(find . -iwholename "*.tix" -type f -print -quit)"
137− local newTixFile=$out/share/hpc/vanilla/tix/${name}/"$tixFile"
171+ local newTixFile=$out/share/hpc/vanilla/tix/${check.name}/"$(basename $tixFile)"
138172
139173 mkdir -p "$(dirname $newTixFile)"
140174 # Copy over the tix file verbatim
@@ -143,29 +177,28 @@ in pkgs.runCommand (name + "-coverage-report")
143177 # Add the tix file to our list
144178 tixFiles+=("$newTixFile")
145179
146− # Create a coverage report for *just that test*
147− markup srcDirs mixDirs pkgMixModules "$out/share/hpc/vanilla/html/${name}/${check.exeName}/" "$newTixFile"
180+ # Create a coverage report for *just that check* affecting any of the
181+ # "mixLibraries"
182+ markup srcDirs mixDirs mixModules "$out/share/hpc/vanilla/html/${check.name}/" "$newTixFile"
148183
149184 popd
150185 fi
151186 '') checks)
152187 }
153188
154− # Sum tix files to create a tix file with all relevant tix
155− # information and markup a HTML report from this info.
156− if (( "''${#tixFiles[@]}" > 0 )); then
157− local sumTixFile="$out/share/hpc/vanilla/tix/${name}/${name}.tix"
158− local markupOutDir="$out/share/hpc/vanilla/html/${name}"
189+ # Sum tix files to create a tix file with tix information from all tests in
190+ # the package and markup a HTML report from this info.
191+ local sumTixFile="$out/share/hpc/vanilla/tix/${name}/${name}.tix"
192+ local markupOutDir="$out/share/hpc/vanilla/html/${name}"
159193
160− # Sum all of our tix file, including modules from any local package
161− sumTix allMixModules tixFiles "$sumTixFile"
194+ # Sum all of our tix files
195+ sumTix mixModules tixFiles "$sumTixFile"
162196
163− # Markup a HTML report, included modules from only this package
164− markup srcDirs mixDirs pkgMixModules "$markupOutDir" "$sumTixFile"
197+ # Markup a HTML report
198+ markup srcDirs mixDirs mixModules "$markupOutDir" "$sumTixFile"
165199
166− # Provide a HTML zipfile and Hydra links
167− ( cd "$markupOutDir" ; zip -r $out/share/hpc/vanilla/${name}-html.zip . )
168− echo "report coverage $markupOutDir/hpc_index.html" >> $out/nix-support/hydra-build-products
169− echo "file zip $out/share/hpc/vanilla/${name}-html.zip" >> $out/nix-support/hydra-build-products
170− fi
200+ # Provide a HTML zipfile and Hydra links
201+ ( cd "$markupOutDir" ; zip -r $out/share/hpc/vanilla/${name}-html.zip . )
202+ echo "report coverage $markupOutDir/hpc_index.html" >> $out/nix-support/hydra-build-products
203+ echo "file zip $out/share/hpc/vanilla/${name}-html.zip" >> $out/nix-support/hydra-build-products
171204 ''
overlays/haskell.nix+2−1
@@ -591,8 +591,9 @@ final: prev: {
591591
592592 coverageReport = haskellLib.coverageReport (rec {
593593 name = package.identifier.name + "-" + package.identifier.version;
594− library = if components ? library then components.library else null;
594+ # Include the checks for a single package.
595595 checks = final.lib.filter (final.lib.isDerivation) (final.lib.attrValues package'.checks);
596+ # Checks from that package may provide coverage information for any library in the project.
596597 mixLibraries = final.lib.concatMap
597598 (pkg: final.lib.optional (pkg.components ? library) pkg.components.library)
598599 (final.lib.attrValues (haskellLib.selectProjectPackages project.hsPkgs));