Skip to content

Commit 8e478bc

Browse files
authored
Implement code coverage for the SDK (#245)
1 parent 0d4e325 commit 8e478bc

File tree

11 files changed

+664
-36
lines changed

11 files changed

+664
-36
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Code Coverage Report
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
coverage:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout PR branch
16+
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
17+
with:
18+
path: pr
19+
20+
- name: Checkout main branch
21+
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
22+
with:
23+
ref: main
24+
path: main
25+
26+
- name: Install Nix
27+
uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
28+
with:
29+
extra_nix_config: |
30+
extra-substituters = https://cache.garnix.io
31+
extra-trusted-public-keys = cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=
32+
33+
- name: Build coverage report for main
34+
run: |
35+
cd main
36+
nix build --impure '.#coverage-ghc910' --print-build-logs || echo "Main branch coverage build failed, will show PR coverage only"
37+
if [ -L result ]; then
38+
cp result/share/hpc/coverage.md ../main-coverage.md || echo "No main coverage found"
39+
fi
40+
41+
- name: Build coverage report for PR
42+
run: |
43+
cd pr
44+
nix build --impure '.#coverage-ghc910' --print-build-logs
45+
46+
- name: Extract PR coverage markdown
47+
run: |
48+
cp pr/result/share/hpc/coverage.md pr-coverage.md
49+
50+
- name: Generate coverage diff
51+
run: |
52+
cat > coverage-report.md <<'EOF'
53+
# 📊 Code Coverage Report
54+
55+
## Current PR Coverage
56+
57+
EOF
58+
59+
cat pr-coverage.md | tail -n +2 >> coverage-report.md
60+
61+
if [ -f main-coverage.md ]; then
62+
echo "" >> coverage-report.md
63+
echo "---" >> coverage-report.md
64+
echo "" >> coverage-report.md
65+
echo "## 📈 Coverage Comparison vs. Main" >> coverage-report.md
66+
echo "" >> coverage-report.md
67+
68+
# Extract overall percentages
69+
PR_PCT=$(grep "Overall Coverage" pr-coverage.md | grep -oE "[0-9]+\.[0-9]+" | head -1)
70+
MAIN_PCT=$(grep "Overall Coverage" main-coverage.md | grep -oE "[0-9]+\.[0-9]+" | head -1)
71+
72+
if [ -n "$PR_PCT" ] && [ -n "$MAIN_PCT" ]; then
73+
DIFF=$(echo "$PR_PCT - $MAIN_PCT" | bc)
74+
if (( $(echo "$DIFF > 0" | bc -l) )); then
75+
echo "✅ **Coverage increased by ${DIFF}%** (${MAIN_PCT}% → ${PR_PCT}%)" >> coverage-report.md
76+
elif (( $(echo "$DIFF < 0" | bc -l) )); then
77+
echo "⚠️ **Coverage decreased by ${DIFF#-}%** (${MAIN_PCT}% → ${PR_PCT}%)" >> coverage-report.md
78+
else
79+
echo "➡️ **Coverage unchanged** at ${PR_PCT}%" >> coverage-report.md
80+
fi
81+
fi
82+
83+
echo "" >> coverage-report.md
84+
echo "<details>" >> coverage-report.md
85+
echo "<summary>Main Branch Coverage (for comparison)</summary>" >> coverage-report.md
86+
echo "" >> coverage-report.md
87+
cat main-coverage.md | tail -n +2 >> coverage-report.md
88+
echo "</details>" >> coverage-report.md
89+
fi
90+
91+
- name: Upload HTML report as artifact
92+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
93+
with:
94+
name: coverage-html-report
95+
path: pr/result/share/hpc/html/
96+
retention-days: 14
97+
98+
- name: Comment coverage report on PR
99+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
100+
with:
101+
recreate: true
102+
path: coverage-report.md

flake.nix

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,22 @@
3636
packages = flakeUtils.forAllSystems (
3737
{pkgs, ...}: let
3838
haskellUtils = import ./nix/utils/haskell.nix pkgs;
39+
inherit (import ./nix/utils/matrix.nix) ghcVersions;
40+
41+
# Generate coverage packages for each GHC version
42+
coveragePackages = builtins.listToAttrs (
43+
builtins.map (ghcVersion: {
44+
name = "coverage-${ghcVersion}";
45+
value = import ./nix/packages/coverage.nix {
46+
inherit pkgs ghcVersion;
47+
haskellPackages = pkgs.haskell.packages.${ghcVersion};
48+
};
49+
})
50+
ghcVersions
51+
);
3952
in
4053
haskellUtils.localPackageMatrix
54+
// coveragePackages
4155
// {
4256
temporal-bridge = pkgs.temporal_bridge;
4357
temporal-test-server = pkgs.temporal-test-server;

nix/overlays/haskell/coverage.nix

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
lib,
3+
haskell,
4+
...
5+
}: _hfinal: hprev: let
6+
inherit (haskell.lib.compose) doCoverage;
7+
# Disable haddock documentation
8+
disableHaddock = drv: drv.overrideAttrs (_: {doHaddock = false;});
9+
# Apply coverage and disable haddocks
10+
coverageNoHaddock = drv: disableHaddock (doCoverage drv);
11+
in {
12+
# Only enable coverage for temporal-sdk (the main SDK package)
13+
# Disable haddocks for all packages to speed up builds
14+
temporal-api-protos = disableHaddock hprev.temporal-api-protos;
15+
temporal-sdk-core = disableHaddock hprev.temporal-sdk-core;
16+
temporal-sdk = coverageNoHaddock hprev.temporal-sdk;
17+
temporal-sdk-codec-server = disableHaddock hprev.temporal-sdk-codec-server;
18+
temporal-codec-encryption = disableHaddock hprev.temporal-codec-encryption;
19+
temporal-sdk-optimal-codec = disableHaddock hprev.temporal-sdk-optimal-codec;
20+
}
Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
11
{
22
lib,
33
haskell,
4-
stdenv,
5-
darwin,
64
temporal-cli,
75
temporal-test-server,
86
...
9-
}:
10-
hfinal: hprev:
11-
let
7+
}: hfinal: _hprev: let
128
inherit (haskell.lib.compose) addTestToolDepends enableCabalFlag;
13-
in
14-
{
15-
temporal-api-protos = hfinal.callCabal2nix "temporal-api-protos" ../../../protos { };
9+
in {
10+
temporal-api-protos = hfinal.callCabal2nix "temporal-api-protos" ../../../protos {};
1611

17-
temporal-sdk-core = lib.pipe (hfinal.callCabal2nix "temporal-sdk-core" ../../../core { }) [
12+
temporal-sdk-core = lib.pipe (hfinal.callCabal2nix "temporal-sdk-core" ../../../core {}) [
1813
(enableCabalFlag "external_lib")
1914
];
2015

21-
temporal-sdk = lib.pipe (hfinal.callCabal2nix "temporal-sdk" ../../../sdk { }) [
16+
temporal-sdk = lib.pipe (hfinal.callCabal2nix "temporal-sdk" ../../../sdk {}) [
2217
(addTestToolDepends [
2318
temporal-cli
2419
temporal-test-server
2520
])
2621
(
2722
drv:
28-
drv.overrideAttrs (_: {
29-
__darwinAllowLocalNetworking = true;
30-
})
23+
drv.overrideAttrs (_: {
24+
__darwinAllowLocalNetworking = true;
25+
})
3126
)
3227
];
3328

3429
temporal-sdk-codec-server =
3530
hfinal.callCabal2nix "temporal-sdk-codec-server" ../../../codec-server
36-
{ };
31+
{};
3732

3833
temporal-codec-encryption =
3934
hfinal.callCabal2nix "temporal-codec-encryption" ../../../codec-encryption
40-
{ };
35+
{};
4136

4237
temporal-sdk-optimal-codec =
4338
hfinal.callCabal2nix "temporal-sdk-optimal-codec" ../../../optimal-codec
44-
{ };
39+
{};
40+
41+
tix-to-markdown =
42+
hfinal.callCabal2nix "tix-to-markdown" ../../../tools/tix-to-markdown
43+
{};
4544
}

nix/packages/coverage.nix

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
{
2+
pkgs,
3+
haskellPackages,
4+
ghcVersion,
5+
}: let
6+
# Apply coverage overlay to get packages with coverage enabled
7+
hpkgs = haskellPackages.extend (import ../overlays/haskell/coverage.nix pkgs);
8+
9+
# The main package we're generating coverage for
10+
sdkPackage = hpkgs.temporal-sdk;
11+
12+
# Tool to convert .tix files to markdown
13+
tixToMarkdown = hpkgs.tix-to-markdown;
14+
in
15+
pkgs.stdenv.mkDerivation {
16+
name = "temporal-sdk-coverage-${ghcVersion}";
17+
18+
buildInputs = [
19+
hpkgs.ghc
20+
pkgs.haskellPackages.hpc
21+
tixToMarkdown
22+
];
23+
24+
phases = ["buildPhase" "installPhase"];
25+
26+
buildPhase = ''
27+
echo "Building coverage report for temporal-sdk..."
28+
echo "SDK package: ${sdkPackage}"
29+
30+
# Create directories
31+
mkdir -p coverage
32+
33+
# The doCoverage function already generates HTML coverage reports
34+
# We just need to copy them from the package
35+
if [ -d "${sdkPackage}/share/hpc/vanilla/html" ]; then
36+
echo "Found pre-generated HPC HTML report"
37+
cp -rv "${sdkPackage}/share/hpc/vanilla/html" coverage/
38+
39+
# Make the directory writable so we can create symlinks
40+
chmod -R u+w coverage/html
41+
42+
# Create index.html symlink to hpc_index.html for easier access
43+
if [ -f "coverage/html/hpc_index.html" ]; then
44+
ln -sf hpc_index.html coverage/html/index.html
45+
fi
46+
47+
echo "Coverage HTML report copied successfully!"
48+
else
49+
echo "Warning: No coverage HTML found in ${sdkPackage}"
50+
mkdir -p coverage/html
51+
cat > coverage/html/index.html <<EOF
52+
<!DOCTYPE html>
53+
<html>
54+
<head><title>No Coverage Data</title></head>
55+
<body>
56+
<h1>No Coverage Data Available</h1>
57+
<p>No coverage report was found in the package.</p>
58+
<p>Package checked: ${sdkPackage.pname}</p>
59+
</body>
60+
</html>
61+
EOF
62+
fi
63+
64+
# Also copy .tix files for reference
65+
if [ -d "${sdkPackage}/share/hpc/vanilla/tix" ]; then
66+
echo "Copying .tix files..."
67+
mkdir -p coverage/tix
68+
cp -v "${sdkPackage}"/share/hpc/vanilla/tix/*.tix coverage/tix/ 2>&1 || true
69+
fi
70+
71+
# Find the mix directory - it's in a versioned library path
72+
tix_file="${sdkPackage}/share/hpc/vanilla/tix/temporal-sdk-tests.tix"
73+
mix_dir=$(find "${sdkPackage}/lib" -type d -name "mix" -path "*/extra-compilation-artifacts/hpc/*/mix" | head -1)
74+
75+
if [ -f "$tix_file" ] && [ -d "$mix_dir" ]; then
76+
echo "Found .tix file: $tix_file"
77+
echo "Found .mix directory: $mix_dir"
78+
79+
echo "Generating text coverage summary..."
80+
${hpkgs.ghc}/bin/hpc report \
81+
--hpcdir="$mix_dir" \
82+
"$tix_file" > coverage/report.txt 2>&1 || echo "Note: Could not generate text report"
83+
84+
if [ -f coverage/report.txt ] && [ -s coverage/report.txt ]; then
85+
echo "Coverage summary:"
86+
cat coverage/report.txt
87+
fi
88+
89+
# Generate markdown coverage report using tix-to-markdown
90+
# Set locale to UTF-8 to support emoji characters
91+
echo "Generating markdown coverage report..."
92+
export LC_ALL=C.UTF-8
93+
${tixToMarkdown}/bin/tix-to-markdown --hpcdir "$mix_dir" "$tix_file" > coverage/coverage.md 2>&1 || echo "Note: Could not generate markdown report"
94+
95+
if [ -f coverage/coverage.md ] && [ -s coverage/coverage.md ]; then
96+
echo "Markdown coverage report generated successfully!"
97+
echo "---"
98+
cat coverage/coverage.md
99+
echo "---"
100+
fi
101+
else
102+
echo "Warning: Could not find .tix file or .mix directory"
103+
echo " .tix file: $tix_file (exists: $([ -f "$tix_file" ] && echo "yes" || echo "no"))"
104+
echo " .mix dir: $mix_dir (exists: $([ -d "$mix_dir" ] && echo "yes" || echo "no"))"
105+
fi
106+
'';
107+
108+
installPhase = ''
109+
mkdir -p $out/share/hpc
110+
mkdir -p $out/bin
111+
112+
# Copy coverage data
113+
cp -r coverage/html $out/share/hpc/
114+
[ -f coverage/report.txt ] && cp coverage/report.txt $out/share/hpc/ || true
115+
[ -f coverage/coverage.md ] && cp coverage/coverage.md $out/share/hpc/ || true
116+
[ -d coverage/tix ] && cp -r coverage/tix $out/share/hpc/ || true
117+
118+
# Create a convenience script to view the report
119+
cat > $out/bin/view-coverage <<EOF
120+
#!/usr/bin/env bash
121+
set -e
122+
123+
report_path="$out/share/hpc/html/index.html"
124+
125+
if [ ! -f "\$report_path" ]; then
126+
echo "Error: Coverage report not found at \$report_path"
127+
exit 1
128+
fi
129+
130+
echo "Opening coverage report..."
131+
echo "Report location: \$report_path"
132+
133+
# Try to open in browser (works on macOS and most Linux)
134+
if command -v open >/dev/null 2>&1; then
135+
open "\$report_path"
136+
elif command -v xdg-open >/dev/null 2>&1; then
137+
xdg-open "\$report_path"
138+
else
139+
echo "Please open the following file in your browser:"
140+
echo "\$report_path"
141+
fi
142+
EOF
143+
chmod +x $out/bin/view-coverage
144+
145+
# Create a script to show the text report
146+
cat > $out/bin/show-coverage-report <<EOF
147+
#!/usr/bin/env bash
148+
if [ -f "$out/share/hpc/report.txt" ]; then
149+
cat "$out/share/hpc/report.txt"
150+
else
151+
echo "No coverage report found."
152+
fi
153+
EOF
154+
chmod +x $out/bin/show-coverage-report
155+
156+
echo "Coverage report installed to $out/share/hpc/html/index.html"
157+
'';
158+
159+
meta = with pkgs.lib; {
160+
description = "Code coverage report for temporal-sdk Haskell packages";
161+
platforms = platforms.unix;
162+
};
163+
}

nix/packages/tix-to-markdown.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{haskellPackages}:
2+
haskellPackages.callCabal2nix "tix-to-markdown" ../../tools/tix-to-markdown {}

0 commit comments

Comments
 (0)