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
313 changes: 289 additions & 24 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ jobs:
# instead of relying on the distro package.
- name: Install Doxygen 1.17.0
run: |
curl -sL https://github.com/doxygen/doxygen/releases/download/Release_1_17_0/doxygen-1.17.0.linux.bin.tar.gz -o /tmp/doxygen.tar.gz
# `--fail` and `-S`, for the reason ci.yml's Install sccache step
# gives at length (morph#672): without them, an HTTP 503 from the
# download host is a *successful* transfer of an error page, curl
# exits 0 having written it to the file, and the only diagnostic
# anyone sees is `gzip: stdin: not in gzip format` from tar -- which
# names the decompressor rather than the outage.
curl -sSL --fail https://github.com/doxygen/doxygen/releases/download/Release_1_17_0/doxygen-1.17.0.linux.bin.tar.gz -o /tmp/doxygen.tar.gz
tar -xzf /tmp/doxygen.tar.gz -C /tmp
echo "/tmp/doxygen-1.17.0/bin" >> "$GITHUB_PATH"

Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/drift-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ jobs:
- name: Assert UBSan halts instead of recovering
run: bash scripts/check_sanitizer_can_fail.sh

# scripts/check_sanitizer_instrumentation.sh's own self-test, and the
# neighbouring half of the same question: that gate asserts every binary
# ctest runs on a sanitizer leg carries that sanitizer's symbols
# (morph#542), where the step above asserts the instrumentation can
# actually fail the process (morph#541). The gate itself needs a built,
# test-registered sanitizer tree and so runs in ci.yml's three sanitizer
# jobs; its self-test needs neither -- hand-written CTestTestfile.cmake
# documents and fixtures built by `cc` from two lines of C -- so it runs
# here, in seconds, and still reports when one of those 20-minute legs
# has gone blind.
#
# It is also the only thing holding morph#675's narrow `--binary` mode to
# its terms. That mode answers "is this one file instrumented?" and
# applies no floor, which is correct for a developer with a single-target
# build and would be a vacuous pass in a workflow step. The self-test
# pins both halves: the mode is refused under GITHUB_ACTIONS, and the
# sweep still rejects a one-binary tree on the floor. Both assertions
# were confirmed to fail against a mutated checker before being trusted.
- name: Self-test the sanitizer-instrumentation checker
run: bash scripts/test_check_sanitizer_instrumentation.sh

dep-cache-selftest:
name: Dependency-cache self-test
runs-on: ubuntu-24.04
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ jobs:
- name: Install Mull ${{ env.MULL_VERSION }} for LLVM ${{ env.MULL_LLVM_MAJOR }}
run: |
asset="Mull-${MULL_LLVM_MAJOR}-${MULL_VERSION}-LLVM-22.1.2-ubuntu-amd64-26.04.deb"
curl -sSLO "https://github.com/mull-project/mull/releases/download/${MULL_VERSION}/${asset}"
# `--fail`, for the reason ci.yml's Install sccache step gives at
# length (morph#672): without it an HTTP 503 is a successful
# transfer of an error page, curl exits 0, and the 64 bytes of HTML
# land in ${asset} -- so the first thing to complain is dpkg-deb,
# about the archive, rather than curl about the download.
curl -sSLO --fail "https://github.com/mull-project/mull/releases/download/${MULL_VERSION}/${asset}"
mkdir -p "${PWD}/mull-${MULL_LLVM_MAJOR}"
dpkg-deb -x "${asset}" "${PWD}/mull-${MULL_LLVM_MAJOR}"
echo "MULL_PREFIX=${PWD}/mull-${MULL_LLVM_MAJOR}/usr" >> "$GITHUB_ENV"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/wasm-demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
# aqtinstall gives a matched host + wasm Qt pair (same cmake glue), so no
# host/target version skew.
- name: Install Qt (host desktop)
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: linux
Expand All @@ -59,7 +59,7 @@ jobs:
# Qt 6.7+ ships WebAssembly under host=all_os / target=wasm (not
# linux/desktop). The matching host desktop Qt above provides the tools.
- name: Install Qt (wasm, single-threaded)
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: all_os
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/wasm-ladder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
# (examples/IMPLEMENTATION.md rule 4's WASM clause), so the transport is
# not optional here the way it is for bank's local-only demo.
- name: Install Qt (host desktop)
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: linux
Expand All @@ -101,7 +101,7 @@ jobs:
dir: ${{ runner.temp }}/qt

- name: Install Qt (wasm, single-threaded)
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: all_os
Expand Down
4 changes: 2 additions & 2 deletions docs/spec/testing_charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ floor means anything.
| I/O error injection (ladder only) | `examples/common/testkit/fault_proxy.hpp` | Ladder Qt test suites | Those tests' own assertions |
| Fuzzing | `tests/fuzz/` (`fuzz_wire_decode`, `fuzz_dispatch_execute`), libFuzzer | Local / on demand (`-DMORPH_BUILD_FUZZERS=ON`; not a CI leg) | A crash, hang, or sanitizer trip during a campaign; regression cases preserved under `tests/fuzz/findings/` |
| AddressSanitizer | Compiler instrumentation | `linux-sanitizers` (`clang-asan`), `ladder-sanitizers` | The CI job (nonzero exit on any diagnostic) |
| UndefinedBehaviorSanitizer | Compiler instrumentation | `linux-sanitizers` (`clang-ubsan`), `ladder-sanitizers` | The CI job |
| UndefinedBehaviorSanitizer | Compiler instrumentation | `linux-sanitizers` (`clang-ubsan`), `ladder-sanitizers`, `bank-sanitizers` | The CI job |
| ThreadSanitizer | Compiler instrumentation | `linux-sanitizers` (`clang-tsan`), `kanban-tsan` | The CI job |
| A sanitizer leg can fail at all | `scripts/check_sanitizer_can_fail.sh` | `drift-guard` | The check: real undefined behaviour compiled with `apply_sanitizers()`'s own flags must make the process exit non-zero, and an unknown `AF_SANITIZER` must fail the configure (morph#541) |
| Every sanitized binary is really sanitized | `scripts/check_sanitizer_instrumentation.sh` | `linux-sanitizers`, `ladder-sanitizers`, `kanban-tsan` | The check: every binary `ctest` will run on a sanitizer leg must carry that sanitizer's runtime symbols (morph#542) |
| Every sanitized binary is really sanitized | `scripts/check_sanitizer_instrumentation.sh` | `linux-sanitizers`, `ladder-sanitizers`, `kanban-tsan` (self-tested in `drift-guard.yml`) | The check: every binary `ctest` will run on a sanitizer leg must carry that sanitizer's runtime symbols (morph#542). Its `--binary <file> <mode>` mode answers the same question about one named file, with no floor, and is refused under `GITHUB_ACTIONS` so it cannot stand in for the sweep on a CI leg (morph#675) |
| Valgrind (memcheck) | Runtime instrumentation | `valgrind` CI job | The CI job |
| Long-running / soak | `tests/soak/` | Local / on demand (`-DMORPH_BUILD_LOAD_TESTS=ON`; not a CI leg — see `docs/spec/testing_strategy.md`) | Those tests' own assertions over many cycles |
| Compile-time contract checks | `tests/compile_checks/` | Every configure that reaches `tests/CMakeLists.txt` | `FATAL_ERROR` at configure time |
Expand Down
71 changes: 68 additions & 3 deletions examples/bank/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,40 @@ target_compile_features(bank_lib PUBLIC cxx_std_23)
apply_bigobj(bank_lib)
# Note: deliberately NOT calling apply_warnings() here — the third-party ORM
# headers are not -Werror clean and would fail the build.
#
# apply_sanitizers() is a separate question and the answer is yes (morph#679).
# Until this line, `ladder_bank_server` was the *only* bank target that carried
# an AF_SANITIZER block, so a sanitizer configure with
# -DMORPH_BUILD_BANK_EXAMPLE=ON built bank_lib, bank_cli and every bank test
# binary with no instrumentation at all — and, worse than being uncovered,
# would have failed scripts/check_sanitizer_instrumentation.sh, because those
# test binaries *are* ctest commands. examples/TESTING.md states the
# convention this now follows: every target gets the block, not just the ones
# someone remembered.
#
# Instrumenting bank_lib does mean instrumenting the vendored Lightweight ORM
# headers it compiles in. Measured rather than assumed: ci.yml's
# `bank-sanitizers` job is the leg that runs this, and its banner records the
# finding count (zero) and the build cost.
#
# All of the blocks below are required together, not one of them optional. An
# instrumented bank_lib with an uninstrumented executable does not merely go
# unchecked, it fails to link: measured by removing bank_tests' block alone,
#
# undefined reference to `__ubsan_handle_type_mismatch_v1_abort'
# clang++: error: linker command failed with exit code 1
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_lib ${AF_SANITIZER})
endif()

# ── CLI driver ───────────────────────────────────────────────────────────────
add_executable(bank_cli src/cli/main.cpp)
target_link_libraries(bank_cli PRIVATE bank_lib)
target_compile_features(bank_cli PRIVATE cxx_std_23)
apply_bigobj(bank_cli)
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_cli ${AF_SANITIZER})
endif()

# ── ladder_bank_server: standalone server binary ─────────────────────────────
# Named `ladder_bank_server` because that is the name the scenario tooling
Expand Down Expand Up @@ -186,10 +214,29 @@ if(MORPH_BUILD_TESTS)
target_link_libraries(bank_tests PRIVATE bank_lib morph_test_main)
target_compile_features(bank_tests PRIVATE cxx_std_23)
apply_bigobj(bank_tests)
# A ctest command, so this block is not optional: without it the
# binary runs on a sanitizer leg carrying nothing, and
# scripts/check_sanitizer_instrumentation.sh fails the job rather than
# letting it report a clean run over an uninstrumented suite
# (morph#679).
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_tests ${AF_SANITIZER})
endif()

list(APPEND CMAKE_MODULE_PATH ${Catch2_DIR})
include(Catch)
catch_discover_tests(bank_tests DISCOVERY_MODE PRE_TEST)
# LABELS "bank" on all three bank suites (morph#679). Bank is not a
# rung, so morph_add_rung()'s "ladder"/"ladder-<rung>" labels never
# reach it and nothing else distinguished a bank test from a core one
# -- which is what a leg that wants to run bank's suites and only
# bank's suites needs. Catch2 tags are not ctest labels (no
# catch_discover_tests call in this repository passes
# ADD_TAGS_AS_LABELS), so `-L bank` is the only selector available,
# and CMakePresets.json's base-test sets `noTestsAction: error`, so a
# label that stopped matching fails the leg instead of running nothing.
catch_discover_tests(bank_tests
DISCOVERY_MODE PRE_TEST
PROPERTIES LABELS "bank")

# ── bank_gui_tests: the GUI's own suite ──────────────────────────────
# A second binary rather than more sources in bank_tests: bank_tests
Expand Down Expand Up @@ -240,8 +287,19 @@ if(MORPH_BUILD_TESTS)
target_compile_definitions(bank_gui_tests PRIVATE
MORPH_LADDER_SOURCE_ROOT="${PROJECT_SOURCE_DIR}")
apply_bigobj(bank_gui_tests)
# This is the binary that would have caught morph#663: the
# out-of-range double -> int64_t conversion lived in
# gui/controllers/Format.hpp, which this suite compiles and drives
# directly (tests/gui/test_bank_gui_format.cpp) with Qt6::Core and
# no display. Uninstrumented it exercised the conversion on every
# run and said nothing (morph#679).
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_gui_tests ${AF_SANITIZER})
endif()

catch_discover_tests(bank_gui_tests DISCOVERY_MODE PRE_TEST)
catch_discover_tests(bank_gui_tests
DISCOVERY_MODE PRE_TEST
PROPERTIES LABELS "bank")

# ── bank_gui_qml_tests: the QML layer, with a live engine ────────
# A third binary, and the reason is the second one's defining
Expand Down Expand Up @@ -296,14 +354,21 @@ if(MORPH_BUILD_TESTS)
MORPH_LADDER_SOURCE_ROOT="${PROJECT_SOURCE_DIR}"
MORPH_LADDER_TESTKIT_GUI_APP)
apply_bigobj(bank_gui_qml_tests)
# Also a ctest command, so also not optional -- see bank_tests
# above (morph#679).
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_gui_qml_tests ${AF_SANITIZER})
endif()

# offscreen, because the runner may have no display and this suite
# never looks at a pixel -- it reads property values off the items
# the engine created. The same variable the ladder's own GUI legs
# set for ctest (examples/TESTING.md).
catch_discover_tests(bank_gui_qml_tests
DISCOVERY_MODE PRE_TEST
PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
PROPERTIES
LABELS "bank"
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
endif()
endif()
endif()
10 changes: 10 additions & 0 deletions examples/bank/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ apply_bigobj(bank_gui_lib)
# Note: deliberately NOT calling apply_warnings() here, for the same reason
# bank_lib does not — this target includes the third-party ORM headers
# transitively and they are not -Werror clean.
#
# apply_sanitizers() is not the same question and does apply — see bank_lib's
# own block in ../CMakeLists.txt (morph#679). This is the library holding
# controllers/Format.hpp, where morph#663's undefined conversion was.
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_gui_lib ${AF_SANITIZER})
endif()

# ── bank_gui: the desktop client ─────────────────────────────────────────────
qt_add_executable(bank_gui
Expand Down Expand Up @@ -68,3 +75,6 @@ target_include_directories(bank_gui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(bank_gui PRIVATE bank_gui_lib Qt6::Quick Qt6::Qml Qt6::QuickControls2)
target_compile_features(bank_gui PRIVATE cxx_std_23)
apply_bigobj(bank_gui)
if(DEFINED AF_SANITIZER)
apply_sanitizers(bank_gui ${AF_SANITIZER})
endif()
Loading
Loading