Skip to content

Commit 581053b

Browse files
padelsbachejohnstown
authored andcommitted
CI: add code coverage workflow, misc script updates
1 parent 17461f2 commit 581053b

5 files changed

Lines changed: 220 additions & 4 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
name: Code Coverage
2+
3+
on:
4+
push:
5+
branches: [ 'master', 'main', 'release/**' ]
6+
pull_request:
7+
branches: [ '*' ]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
build_wolfssl:
16+
name: Build wolfSSL
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 15
19+
steps:
20+
- name: Checkout wolfSSL
21+
uses: actions/checkout@v6
22+
with:
23+
repository: wolfssl/wolfssl
24+
path: wolfssl
25+
26+
# Match the sshd-test workflow so the cert and ML-DSA paths are built
27+
# and measured rather than compiled out.
28+
- name: Build wolfSSL
29+
working-directory: ./wolfssl
30+
run: |
31+
./autogen.sh
32+
./configure --enable-all --enable-mldsa
33+
make -j$(nproc)
34+
sudo make install
35+
sudo ldconfig
36+
37+
- name: tar build-dir
38+
run: tar -zcf wolfssl-install.tgz /usr/local/lib/libwolfssl* /usr/local/include/wolfssl
39+
40+
- name: Upload built lib
41+
uses: actions/upload-artifact@v7
42+
with:
43+
name: wolfssl-coverage
44+
path: wolfssl-install.tgz
45+
retention-days: 5
46+
47+
# Use clang to report line, branch, function and MC/DC coverage in one run.
48+
coverage:
49+
name: Coverage
50+
runs-on: ubuntu-latest
51+
timeout-minutes: 45
52+
needs: build_wolfssl
53+
steps:
54+
- name: Checkout wolfSSH
55+
uses: actions/checkout@v6
56+
57+
# clang 18 is the min: -fcoverage-mcdc does not exist before it.
58+
- name: Install clang and LLVM coverage tools
59+
run: |
60+
sudo apt-get update
61+
sudo apt-get install -y clang-18 llvm-18 libclang-rt-18-dev
62+
63+
- name: Download wolfSSL
64+
uses: actions/download-artifact@v8
65+
with:
66+
name: wolfssl-coverage
67+
68+
- name: Install wolfSSL
69+
run: |
70+
sudo tar -xzf wolfssl-install.tgz -C /
71+
sudo ldconfig
72+
73+
# -O0 keeps line and branch attribution honest; atomic counters are
74+
# required because several tests drive client and server on separate
75+
# threads, and the default non-atomic updates lose increments.
76+
- name: Build wolfSSH
77+
run: |
78+
./autogen.sh
79+
./configure --enable-all --enable-ossh-certs CC=clang-18 \
80+
CPPFLAGS="-DMAX_PATH_SZ=120" \
81+
CFLAGS="-fprofile-instr-generate -fcoverage-mapping -fcoverage-mcdc -fprofile-update=atomic -O0 -g" \
82+
LDFLAGS="-fprofile-instr-generate"
83+
make -j$(nproc)
84+
85+
# %p in the pattern keeps forked servers from overwriting the raw
86+
# profile of the client that spawned them.
87+
- name: Run tests
88+
run: |
89+
mkdir -p prof
90+
LLVM_PROFILE_FILE="$PWD/prof/%p-%m.profraw" \
91+
timeout -k 30 1200 make check
92+
93+
# 'make check' does not execute wolfsshd, so run it separately
94+
- name: Run wolfSSHd tests
95+
working-directory: ./apps/wolfsshd/test
96+
run: |
97+
prof="$GITHUB_WORKSPACE/prof/%p-%m.profraw"
98+
sudo LLVM_PROFILE_FILE="$prof" SSHD_ENV="LLVM_PROFILE_FILE=$prof" \
99+
./run_all_sshd_tests.sh
100+
sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE/prof"
101+
102+
- name: Report coverage
103+
run: |
104+
llvm-profdata-18 merge -sparse prof/*.profraw -o wolfssh.profdata
105+
# llvm-cov takes one binary positionally and the rest via -object.
106+
# Programs linking the shared library are libtool wrapper scripts, so
107+
# take the real binary from .libs when one is there. The apps are
108+
# optional, so skip whatever this configuration did not build.
109+
first=""
110+
args=()
111+
for t in tests/*.test apps/wolfssh/wolfssh apps/wolfsshd/wolfsshd \
112+
apps/wolfsshd/test/test_configuration; do
113+
[ -e "$t" ] || continue
114+
real="$(dirname "$t")/.libs/$(basename "$t")"
115+
[ -x "$real" ] || real="$t"
116+
if [ -z "$first" ]; then
117+
first="$real"
118+
else
119+
args+=(-object "$real")
120+
fi
121+
done
122+
if [ -z "$first" ]; then
123+
echo "no instrumented binaries found"
124+
exit 1
125+
fi
126+
ignore='(tests|examples)/.*|apps/wolfsshd/test/.*'
127+
ignore="$ignore"'|.*/include/wolfssl/.*|.*/wolfssh/.*\.h'
128+
llvm-cov-18 report "$first" "${args[@]}" \
129+
-instr-profile=wolfssh.profdata \
130+
--show-mcdc-summary \
131+
--ignore-filename-regex="$ignore" | tee coverage-report.txt
132+
llvm-cov-18 show "$first" "${args[@]}" \
133+
-instr-profile=wolfssh.profdata \
134+
--show-mcdc --format=html --output-dir=coverage-html \
135+
--ignore-filename-regex="$ignore"
136+
# lcov text for any external dashboard that consumes it.
137+
llvm-cov-18 export "$first" "${args[@]}" \
138+
-instr-profile=wolfssh.profdata \
139+
--format=lcov \
140+
--ignore-filename-regex="$ignore" > coverage.lcov
141+
{
142+
echo '### Coverage'
143+
echo '```'
144+
cat coverage-report.txt
145+
echo '```'
146+
} >> "$GITHUB_STEP_SUMMARY"
147+
148+
- name: Upload coverage report
149+
uses: actions/upload-artifact@v7
150+
with:
151+
name: coverage-report
152+
path: |
153+
coverage-report.txt
154+
coverage.lcov
155+
coverage-html/
156+
retention-days: 30
157+
158+
- name: Show test logs on failure
159+
if: failure()
160+
run: |
161+
echo "=== test-suite.log ==="
162+
cat test-suite.log || true
163+
for f in tests/*.log scripts/*.log; do
164+
[ -f "$f" ] || continue
165+
echo ""
166+
echo "=== $f ==="
167+
cat "$f"
168+
done
169+
170+
- name: Upload failure logs
171+
if: failure()
172+
uses: actions/upload-artifact@v7
173+
with:
174+
name: wolfssh-coverage-logs
175+
path: |
176+
test-suite.log
177+
tests/*.log
178+
scripts/*.log
179+
config.log
180+
retention-days: 5

apps/wolfssh-options.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ int main(void)
131131
printf("FPKI\n");
132132
#endif
133133

134+
/* certman.c's FPKI certificate profile enforcement. Separate from FPKI
135+
* above. */
136+
#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_FPKI)
137+
printf("FPKI_PROFILE\n");
138+
#endif
139+
134140
/* PQC Options */
135141
#ifndef WOLFSSH_NO_MLDSA
136142
printf("MLDSA\n");

apps/wolfsshd/test/sshd_x509_test.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
# Not named PWD: the shell rewrites that variable on every cd, so a saved
66
# copy would not survive the cd to the repository root below.
77
TESTDIR=`pwd`
8+
. ./wolfssh_options.sh
9+
10+
# No FPKI profiles exist in keys/, so skip this test which would fail.
11+
# Drop this skip once conforming certificates are added.
12+
if wolfssh_has FPKI_PROFILE; then
13+
echo "wolfSSHd enforces FPKI profiles; test certs meet none, skipping"
14+
exit 77
15+
fi
16+
817
cd ../../..
918

1019
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then

apps/wolfsshd/test/sshd_x509_upn_fail.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ if ! wolfssh_has FPKI; then
1717
exit 77
1818
fi
1919

20+
# No FPKI profiles exist in keys/, so skip this test which would fail.
21+
# Drop this skip once conforming certificates are added.
22+
if wolfssh_has FPKI_PROFILE; then
23+
echo "wolfSSHd enforces FPKI profiles; UPN check not reached, skipping"
24+
exit 77
25+
fi
26+
2027
# Count existing rejection lines first so a stale match left in the appended
2128
# log (start_sshd.sh uses 'wolfsshd -E ./log.txt', which never truncates) is
2229
# not mistaken for this run's rejection.

apps/wolfsshd/test/start_sshd.sh

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,24 @@ EOF
9393
sudo env $SSHD_ENV "$SSHD_BIN" -d -E ./log.txt -f "$CONFIG"
9494

9595
# The PID of the started sshd is the one present now that was not there
96-
# before. The daemon can still die after sudo returns, so guard the same
97-
# way and let the caller's empty-PID check report it.
98-
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
99-
PID=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") | sed -n 's/^> *//p' | head -n1`
96+
# before. wolfSSHd forks twice while daemonizing, so for a moment its two
97+
# short lived parents are listed as well; wait for the new pids to settle
98+
# on the single survivor. Recording a parent instead would leave
99+
# stop_wolfsshd killing a pid that is already gone while the real daemon
100+
# keeps the port. The daemon can also die after sudo returns, so leave PID
101+
# empty in that case and let the caller's empty-PID check report it.
102+
PID=""
103+
for i in $(seq 1 50); do
104+
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
105+
NEW=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") \
106+
| sed -n 's/^> *//p'`
107+
NEW_COUNT=`printf '%s\n' $NEW | grep -c .` || NEW_COUNT=0
108+
if [ "$NEW_COUNT" -eq 1 ]; then
109+
PID="$NEW"
110+
break
111+
fi
112+
sleep 0.1
113+
done
100114
printf "SSHD running on PID $PID\n"
101115
}
102116

0 commit comments

Comments
 (0)