Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
# gcc/make are preinstalled; lcov (and valgrind) are not.
- run: sudo apt-get update && sudo apt-get install -y valgrind lcov
# gcc/make are preinstalled; lcov (and valgrind) are not. libmbedtls-dev
# provides the real <mbedtls/md.h> + -lmbedcrypto the TOTP harness links.
- run: sudo apt-get update && sudo apt-get install -y valgrind lcov libmbedtls-dev
# Run harnesses + (re)generate test/coverage/html/ via the test Makefile.
- run: make -C test coverage
# Enable Pages via the job token (no admin CLI scope needed).
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: test
# PR's ci wrapper script, so this suite shares no files with the format PR.
on:
push:
branches: [chore/host-tests-suite, master]
branches: [chore/host-tests-suite, chore/whole-coverage, master]
pull_request:

jobs:
Expand All @@ -16,8 +16,9 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
# gcc/make are preinstalled; valgrind + lcov are not.
- run: sudo apt-get update && sudo apt-get install -y valgrind lcov
# gcc/make are preinstalled; valgrind + lcov are not. libmbedtls-dev
# provides the real <mbedtls/md.h> + -lmbedcrypto the TOTP harness links.
- run: sudo apt-get update && sudo apt-get install -y valgrind lcov libmbedtls-dev
# Hard gate: ASan+UBSan, then Valgrind. Coverage is report-only but must
# still build/run cleanly. Each step must exit 0 or the job fails.
- run: make -C test asan
Expand Down
16 changes: 10 additions & 6 deletions storage/storage.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,16 @@ static const struct lfs_config LFS_CFG = {
.erase = flash_erase,
.sync = flash_sync,

.read_size = LFS_READ_SIZE,
.prog_size = LFS_PROG_SIZE,
.block_size = LFS_BLOCK_SIZE,
.block_count = LFS_BLOCK_COUNT,
.cache_size = LFS_CACHE_SIZE,
.lookahead_size = LFS_LOOKAHEAD,
.read_size = LFS_READ_SIZE,
.prog_size = LFS_PROG_SIZE,
.block_size = LFS_BLOCK_SIZE,
.block_count = LFS_BLOCK_COUNT,
.cache_size = LFS_CACHE_SIZE,
// lookahead_size is in BYTES (littlefs tracks 8 blocks per byte and memsets
// this many bytes into lookahead_buffer). It must equal the buffer size, so
// derive it from the buffer to keep the two in lockstep; LFS_LOOKAHEAD/8 = 8
// bytes tracks all 64 blocks in one pass.
.lookahead_size = sizeof(lfs_lookahead_buf),
.block_cycles = 500, // wear leveling hint

.read_buffer = lfs_read_buf,
Expand Down
115 changes: 105 additions & 10 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,49 @@ SECRET_QR_SRCS := harness_secret_qr.c $(SHARED)/random.c $(BASE32)/base32.c \
$(QRCODEGEN)/qrcodegen.c
BASE64_SRCS := harness_base64.c $(BASE64)/base64.c

# TOTP harness needs the REAL HMAC-SHA1, so it builds against the system mbedtls
# (libmbedtls-dev) rather than the compile-only stub/mbedtls/md.h. `-idirafter
# stub` keeps the hardware/rtc.h stub reachable while letting the real
# <mbedtls/md.h> (a system dir) win the search for totp.c's HMAC include.
# CI: the test job must apt-install `libmbedtls-dev` for this to build.
TOTP_SRCS := harness_totp.c $(SHARED)/totp.c
TOTP_INCLUDES := -I$(ROOT) -I$(ROOT)/hardware -I$(SHARED) -idirafter stub
TOTP_LIBS := -lmbedcrypto

# Storage harness: storage.c + backup.c + the REAL littlefs (lfs.c/lfs_util.c),
# run against a RAM-backed XIP window the harness maps in (see harness_storage.c).
# LFS_NO_MALLOC/LFS_NO_DEBUG mirror the firmware's CMakeLists so lfs.h/lfs_util.h
# match. The shared INCLUDES already resolve stub/, storage/ and libs/littlefs/.
STORAGE_SRCS := harness_storage.c $(ROOT)/storage/storage.c $(ROOT)/storage/backup.c \
$(ROOT)/libs/littlefs/lfs.c $(ROOT)/libs/littlefs/lfs_util.c
STORAGE_DEFS := -DLFS_NO_MALLOC -DLFS_NO_DEBUG

# Commands harness: links serial/commands.c (the dispatcher) against SPY handler
# stubs + a stub buzzer, all inside harness_commands.c. No real key/system/
# network/backup handlers are pulled in — the harness drives commands_dispatch()
# and asserts routing / admin gating / argc validation / unknown handling. The
# shared INCLUDES resolve stub/pico + hardware/buzzer.h.
COMMANDS_SRCS := harness_commands.c $(ROOT)/serial/commands.c

# Whole-codebase coverage: EVERY first-party .c, so the report lists the whole
# firmware (the harness-exercised modules at their real %, the rest at an honest
# 0%). These are compiled instrumented but NOT linked or run — a .gcno alone is
# enough for a file to appear in the report. The terse host stubs under stub/
# stand in for the pico-sdk / cyw43 / lwip / mbedtls headers they pull.
WHOLE_SRCS := $(ROOT)/hardware/buzzer.c $(ROOT)/hardware/clock.c \
$(ROOT)/hardware/keypad.c $(ROOT)/hardware/latch.c \
$(ROOT)/hardware/led.c $(ROOT)/hardware/light.c $(ROOT)/main.c \
$(ROOT)/network/ntp.c $(ROOT)/network/wifi.c \
$(ROOT)/serial/commands.c $(ROOT)/serial/commands_backup.c \
$(ROOT)/serial/commands_keys.c $(ROOT)/serial/commands_network.c \
$(ROOT)/serial/commands_system.c $(ROOT)/serial/console.c \
$(ROOT)/shared/random.c $(ROOT)/shared/totp.c \
$(ROOT)/storage/backup.c $(ROOT)/storage/storage.c

# LittleFS build knobs, mirrored from the top-level CMakeLists so storage.c and
# backup.c see the same lfs.h / lfs_util.h configuration the firmware does.
WHOLE_DEFS := -DLFS_NO_MALLOC -DLFS_NO_DEBUG

CC := gcc
CSTD := -std=c11
WARN := -Wall -Wextra
Expand Down Expand Up @@ -80,10 +123,14 @@ check-submodules:
exit 1; }

# --- ASan + UBSan -----------------------------------------------------------
asan: check-submodules $(BUILD)/asan_secret_qr $(BUILD)/asan_base64
asan: check-submodules $(BUILD)/asan_secret_qr $(BUILD)/asan_base64 $(BUILD)/asan_totp \
$(BUILD)/asan_storage $(BUILD)/asan_commands
@echo "== running asan/ubsan harnesses =="
$(BUILD)/asan_secret_qr
$(BUILD)/asan_base64
$(BUILD)/asan_totp
$(BUILD)/asan_storage
$(BUILD)/asan_commands
@echo "== asan OK =="

$(BUILD)/asan_secret_qr: $(SECRET_QR_SRCS) | $(BUILD)
Expand All @@ -92,11 +139,24 @@ $(BUILD)/asan_secret_qr: $(SECRET_QR_SRCS) | $(BUILD)
$(BUILD)/asan_base64: $(BASE64_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(INCLUDES) $(BASE64_SRCS) -o $@

$(BUILD)/asan_totp: $(TOTP_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(TOTP_INCLUDES) $(TOTP_SRCS) $(TOTP_LIBS) -o $@

$(BUILD)/asan_storage: $(STORAGE_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(STORAGE_DEFS) $(INCLUDES) $(STORAGE_SRCS) -o $@

$(BUILD)/asan_commands: $(COMMANDS_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(INCLUDES) $(COMMANDS_SRCS) -o $@

# --- Valgrind ---------------------------------------------------------------
valgrind: check-submodules $(BUILD)/vg_secret_qr $(BUILD)/vg_base64
valgrind: check-submodules $(BUILD)/vg_secret_qr $(BUILD)/vg_base64 $(BUILD)/vg_totp \
$(BUILD)/vg_storage $(BUILD)/vg_commands
@echo "== running harnesses under valgrind =="
valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_secret_qr
valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_base64
valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_totp
valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_storage
valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_commands
@echo "== valgrind OK =="

$(BUILD)/vg_secret_qr: $(SECRET_QR_SRCS) | $(BUILD)
Expand All @@ -105,26 +165,59 @@ $(BUILD)/vg_secret_qr: $(SECRET_QR_SRCS) | $(BUILD)
$(BUILD)/vg_base64: $(BASE64_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(INCLUDES) $(BASE64_SRCS) -o $@

# --- Coverage (lcov/genhtml HTML report for the harness-exercised modules) --
# Build & run the real harnesses with --coverage (producing .gcda), capture
# the run data, filter to first-party sources (dropping the vendored libs and
# the test harnesses themselves), and render coverage/html/ with branches.
# The report covers exactly the modules the harnesses exercise (base32.c,
# base64.c, random.c); HW-only firmware is not built host-side and is absent.
$(BUILD)/vg_totp: $(TOTP_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(TOTP_INCLUDES) $(TOTP_SRCS) $(TOTP_LIBS) -o $@

$(BUILD)/vg_storage: $(STORAGE_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(STORAGE_DEFS) $(INCLUDES) $(STORAGE_SRCS) -o $@

$(BUILD)/vg_commands: $(COMMANDS_SRCS) | $(BUILD)
$(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(INCLUDES) $(COMMANDS_SRCS) -o $@

# --- Coverage (lcov/genhtml HTML report for the WHOLE first-party codebase) --
# Build & run the real harnesses with --coverage (producing .gcda), AND compile
# every first-party .c instrumented but unlinked (compile-only, producing .gcno
# only). A --initial capture turns the whole .gcno set into a 0% baseline; the
# run capture carries the executed lines; merging the two yields a report that
# spans the whole firmware — the harness-exercised modules (base32/base64/
# random/...) at their real %, every other first-party file at an honest 0%.
# Everything outside the first-party tree (vendored libs, the harnesses, /usr)
# is filtered out.
coverage: check-submodules | $(COV_DIR)/obj
@echo "== coverage: building + running harnesses =="
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(INCLUDES) $(SECRET_QR_SRCS) \
-o $(COV_DIR)/obj/cov_secret_qr
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(INCLUDES) $(BASE64_SRCS) \
-o $(COV_DIR)/obj/cov_base64
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(TOTP_INCLUDES) $(TOTP_SRCS) $(TOTP_LIBS) \
-o $(COV_DIR)/obj/cov_totp
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(STORAGE_DEFS) $(INCLUDES) $(STORAGE_SRCS) \
-o $(COV_DIR)/obj/cov_storage
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(INCLUDES) $(COMMANDS_SRCS) \
-o $(COV_DIR)/obj/cov_commands
@echo "== coverage: running instrumented harnesses =="
$(COV_DIR)/obj/cov_secret_qr
$(COV_DIR)/obj/cov_base64
@echo "== coverage: capturing run data =="
$(COV_DIR)/obj/cov_totp
$(COV_DIR)/obj/cov_storage
$(COV_DIR)/obj/cov_commands
@echo "== coverage: compiling whole first-party codebase (compile-only) =="
@for src in $(WHOLE_SRCS); do \
obj=$(COV_DIR)/obj/whole_$$(echo $$src | tr '/.' '__').o; \
echo " CC $$src"; \
$(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(WHOLE_DEFS) $(INCLUDES) \
-c $$src -o $$obj || exit 1; \
done
@echo "== coverage: capturing baseline (all .gcno at 0%) + run data =="
$(LCOV) --capture --initial --directory $(COV_DIR)/obj \
--output-file $(COV_DIR)/baseline.info
$(LCOV) --capture --directory $(COV_DIR)/obj \
--output-file $(COV_DIR)/run.info
$(LCOV) --add-tracefile $(COV_DIR)/baseline.info \
--add-tracefile $(COV_DIR)/run.info \
--output-file $(COV_DIR)/merged.info
@echo "== coverage: filtering to first-party sources =="
$(LCOV) --extract $(COV_DIR)/run.info '$(REPO_ROOT)/*' \
$(LCOV) --extract $(COV_DIR)/merged.info '$(REPO_ROOT)/*' \
--output-file $(COV_DIR)/first.info
$(LCOV) --remove $(COV_DIR)/first.info \
'*/libs/qrcodegen/*' '*/libs/littlefs/*' '*/test/*' '/usr/*' \
Expand All @@ -133,6 +226,8 @@ coverage: check-submodules | $(COV_DIR)/obj
genhtml $(LCOV_RC) $(GENHTML_IGNORE) --branch-coverage \
--legend --title "hslock host coverage" \
--output-directory $(COV_DIR)/html $(COV_DIR)/coverage.info
@echo "== coverage summary =="
$(LCOV) --summary $(COV_DIR)/coverage.info
@echo "== coverage OK: report at $(COV_DIR)/html/index.html =="

$(BUILD):
Expand Down
Loading
Loading