CI: add code coverage workflow, misc script updates

pull/1189/head
Paul Adelsbach 2026-08-20 12:37:41 -07:00 committed by John Safranek
parent 17461f2271
commit 581053bcf6
5 changed files with 220 additions and 4 deletions

View File

@ -0,0 +1,180 @@
name: Code Coverage
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_wolfssl:
name: Build wolfSSL
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v6
with:
repository: wolfssl/wolfssl
path: wolfssl
# Match the sshd-test workflow so the cert and ML-DSA paths are built
# and measured rather than compiled out.
- name: Build wolfSSL
working-directory: ./wolfssl
run: |
./autogen.sh
./configure --enable-all --enable-mldsa
make -j$(nproc)
sudo make install
sudo ldconfig
- name: tar build-dir
run: tar -zcf wolfssl-install.tgz /usr/local/lib/libwolfssl* /usr/local/include/wolfssl
- name: Upload built lib
uses: actions/upload-artifact@v7
with:
name: wolfssl-coverage
path: wolfssl-install.tgz
retention-days: 5
# Use clang to report line, branch, function and MC/DC coverage in one run.
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 45
needs: build_wolfssl
steps:
- name: Checkout wolfSSH
uses: actions/checkout@v6
# clang 18 is the min: -fcoverage-mcdc does not exist before it.
- name: Install clang and LLVM coverage tools
run: |
sudo apt-get update
sudo apt-get install -y clang-18 llvm-18 libclang-rt-18-dev
- name: Download wolfSSL
uses: actions/download-artifact@v8
with:
name: wolfssl-coverage
- name: Install wolfSSL
run: |
sudo tar -xzf wolfssl-install.tgz -C /
sudo ldconfig
# -O0 keeps line and branch attribution honest; atomic counters are
# required because several tests drive client and server on separate
# threads, and the default non-atomic updates lose increments.
- name: Build wolfSSH
run: |
./autogen.sh
./configure --enable-all --enable-ossh-certs CC=clang-18 \
CPPFLAGS="-DMAX_PATH_SZ=120" \
CFLAGS="-fprofile-instr-generate -fcoverage-mapping -fcoverage-mcdc -fprofile-update=atomic -O0 -g" \
LDFLAGS="-fprofile-instr-generate"
make -j$(nproc)
# %p in the pattern keeps forked servers from overwriting the raw
# profile of the client that spawned them.
- name: Run tests
run: |
mkdir -p prof
LLVM_PROFILE_FILE="$PWD/prof/%p-%m.profraw" \
timeout -k 30 1200 make check
# 'make check' does not execute wolfsshd, so run it separately
- name: Run wolfSSHd tests
working-directory: ./apps/wolfsshd/test
run: |
prof="$GITHUB_WORKSPACE/prof/%p-%m.profraw"
sudo LLVM_PROFILE_FILE="$prof" SSHD_ENV="LLVM_PROFILE_FILE=$prof" \
./run_all_sshd_tests.sh
sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE/prof"
- name: Report coverage
run: |
llvm-profdata-18 merge -sparse prof/*.profraw -o wolfssh.profdata
# llvm-cov takes one binary positionally and the rest via -object.
# Programs linking the shared library are libtool wrapper scripts, so
# take the real binary from .libs when one is there. The apps are
# optional, so skip whatever this configuration did not build.
first=""
args=()
for t in tests/*.test apps/wolfssh/wolfssh apps/wolfsshd/wolfsshd \
apps/wolfsshd/test/test_configuration; do
[ -e "$t" ] || continue
real="$(dirname "$t")/.libs/$(basename "$t")"
[ -x "$real" ] || real="$t"
if [ -z "$first" ]; then
first="$real"
else
args+=(-object "$real")
fi
done
if [ -z "$first" ]; then
echo "no instrumented binaries found"
exit 1
fi
ignore='(tests|examples)/.*|apps/wolfsshd/test/.*'
ignore="$ignore"'|.*/include/wolfssl/.*|.*/wolfssh/.*\.h'
llvm-cov-18 report "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--show-mcdc-summary \
--ignore-filename-regex="$ignore" | tee coverage-report.txt
llvm-cov-18 show "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--show-mcdc --format=html --output-dir=coverage-html \
--ignore-filename-regex="$ignore"
# lcov text for any external dashboard that consumes it.
llvm-cov-18 export "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--format=lcov \
--ignore-filename-regex="$ignore" > coverage.lcov
{
echo '### Coverage'
echo '```'
cat coverage-report.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage report
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: |
coverage-report.txt
coverage.lcov
coverage-html/
retention-days: 30
- name: Show test logs on failure
if: failure()
run: |
echo "=== test-suite.log ==="
cat test-suite.log || true
for f in tests/*.log scripts/*.log; do
[ -f "$f" ] || continue
echo ""
echo "=== $f ==="
cat "$f"
done
- name: Upload failure logs
if: failure()
uses: actions/upload-artifact@v7
with:
name: wolfssh-coverage-logs
path: |
test-suite.log
tests/*.log
scripts/*.log
config.log
retention-days: 5

View File

@ -131,6 +131,12 @@ int main(void)
printf("FPKI\n");
#endif
/* certman.c's FPKI certificate profile enforcement. Separate from FPKI
* above. */
#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_FPKI)
printf("FPKI_PROFILE\n");
#endif
/* PQC Options */
#ifndef WOLFSSH_NO_MLDSA
printf("MLDSA\n");

View File

@ -5,6 +5,15 @@
# Not named PWD: the shell rewrites that variable on every cd, so a saved
# copy would not survive the cd to the repository root below.
TESTDIR=`pwd`
. ./wolfssh_options.sh
# No FPKI profiles exist in keys/, so skip this test which would fail.
# Drop this skip once conforming certificates are added.
if wolfssh_has FPKI_PROFILE; then
echo "wolfSSHd enforces FPKI profiles; test certs meet none, skipping"
exit 77
fi
cd ../../..
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then

View File

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

View File

@ -93,10 +93,24 @@ EOF
sudo env $SSHD_ENV "$SSHD_BIN" -d -E ./log.txt -f "$CONFIG"
# The PID of the started sshd is the one present now that was not there
# before. The daemon can still die after sudo returns, so guard the same
# way and let the caller's empty-PID check report it.
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
PID=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") | sed -n 's/^> *//p' | head -n1`
# before. wolfSSHd forks twice while daemonizing, so for a moment its two
# short lived parents are listed as well; wait for the new pids to settle
# on the single survivor. Recording a parent instead would leave
# stop_wolfsshd killing a pid that is already gone while the real daemon
# keeps the port. The daemon can also die after sudo returns, so leave PID
# empty in that case and let the caller's empty-PID check report it.
PID=""
for i in $(seq 1 50); do
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
NEW=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") \
| sed -n 's/^> *//p'`
NEW_COUNT=`printf '%s\n' $NEW | grep -c .` || NEW_COUNT=0
if [ "$NEW_COUNT" -eq 1 ]; then
PID="$NEW"
break
fi
sleep 0.1
done
printf "SSHD running on PID $PID\n"
}