mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-10-26 01:20:39 -05:00
[CI] Publish a gcovr line-coverage report from the test run (#7404)
* [Tests] Register each test case with CTest instead of each test binary
Registering a test cost four edits across a dozen CMakeLists files: add_test,
add_executable, a conditional add_dependencies on gtest, and target_link_libraries.
The conditional branch was the trap -- it is only needed when GTest was fetched and
add_subdirectory'd, and omitting it yields undefined gtest symbols at link time --
so it was easy to leave out and impossible to notice until the link failed.
Fold all four steps into cockatrice_add_test() and migrate every test directory
onto it, with a cockatrice_add_manual_test() wrapper for the three benchmarks that
are built but deliberately kept out of ctest. The calls now read as a declaration
of what a test is rather than a transcript of how to register one.
While rewriting the registrations, replace add_test(NAME x COMMAND x) with
gtest_discover_tests(). Naming the binary as a single CTest entry meant a CI failure
reported only which binary failed, and ctest -R Suite.Case could not select an
individual case. Discovery raises the suite from 35 opaque entries to 469 real ones:
ctest -R ThemeMigrationTest.IsIdempotent now runs exactly that case, and
ctest -L server / -LE bench work off labels applied at registration.
Give every test a 60s TIMEOUT. CTest's implicit default is 1500s, so one hung
socket wait would stall CI for 25 minutes, and single_instance_manager_test already
does real QLocalSocket waits.
Dropping per-case granularity costs wall-clock time, since the suite becomes
process-spawn bound rather than CPU bound: serial ctest went from 3.8s to 30.8s. Run
it with -j in CI to recover it -- 7.6s at -j4, and 3.5s at -j8, both at or below
the old serial time. The binaries are parallel-safe: the only test claiming a fixed
resource scopes its local socket name to its own pid, so concurrent copies cannot
collide.
Also delete dummy_test, which asserted 1 == 1 and only proved the harness was
wired up -- something the other 468 cases already establish.
No test is added, removed, or changed by this commit; the 469 cases are the same
ones the 35 binaries contained.
* [Tests] Make QtTest available to test targets
QtTest was not among the modules test targets could link, so the Qt model and
signal contracts had no way to be verified from a test: QAbstractItemModelTester
and QSignalSpy both live there.
Add Test to _TEST_NEEDED in cmake/FindQtRuntime.cmake. That is the single place the
union of test-only Qt modules is declared, so this is the only file that needs to
change to make the module reachable from every test target.
Nothing uses it yet. This lands the capability ahead of the model-layer tests that
need it, so those can be added as tests alone rather than as test plus build-system
plumbing.
* [Tests] Correct the note on the oracle test compression defines
add_definitions() populates the directory's COMPILE_DEFINITIONS property, which is
consumed at generate time, so it reaches every target in the directory no matter
where it is called relative to add_executable(). The previous note claimed the
flags stayed confined to the benchmark because the find_package block came after
it; oracle_importer_test, created above that block, carries both defines too.
Verified harmless rather than a live bug: HAS_ZLIB and HAS_LZMA gate code only in
oracle/src/pages.cpp and oracle/src/pagetemplates.cpp, and no test target compiles
either. The ordering and the leaked defines are both pre-existing, so this only
corrects the explanation.
* [Tests] Carry one CTest label per test instead of several
gtest_discover_tests() can hand CTest exactly one label. GoogleTestAddTests.cmake
accumulates its generated script with `foreach(arg ${ARGN})`, so a label list reaches
the CTestTestfile as bare `LABELS ci server`, and CTest reads only the first value of a
property and drops the rest without a word.
The `ci` label that is appended to every registered test was therefore absent from every
test that also asked for `server` or `bench`: `ctest -L ci` reported 400 of 469 cases,
and `ctest --print-labels` never listed a second label anywhere in the build.
Reduce each test to exactly one of `ci`, `server`, or `bench`, which is all the
selections need. The three now partition the suite exactly, 400 + 64 + 5 = 469, and
`ctest -LE bench` still means everything but benchmarks. A second label is now a
configure-time FATAL_ERROR rather than a line that quietly means something else.
Anything orthogonal to a test's kind belongs in a helper flag beside the label, so it
composes with the group instead of competing for the single slot.
Also raise DISCOVERY_TIMEOUT off GoogleTestAddTests' 5s default. A plain debug binary
answers --gtest_list_tests in 0.04s, but that budget is thin once several binaries
discover concurrently behind a -j build, and being over it fails at the *link* step with
a bare "Process terminated due to timeout" that points nowhere near the cause.
* [Tests] Link gtest_main on the fetched-gtest path
The downloaded-gtest branch set GTEST_BOTH_LIBRARIES to `gtest` alone, while
FindGTest sets it to both halves. Every test target links that list rather than
picking an entry point, so on this path nothing provides main() and any test
source that does not define its own fails at link with `undefined reference to
main` pointing at crt1.o, naming neither gtest nor the line responsible.
That stayed latent while every test carried its own four-line main(). A test that
omits it now fails, and the error points at the C runtime rather than at gtest,
which is a long way from the cause. Restoring both halves makes the two paths
agree and drops the boilerplate requirement on new test sources.
The 18 sources that still define main() keep linking: gtest_main.cc defines
nothing but main, so the linker leaves its object in the archive unused rather
than reporting a duplicate.
* [Build] Add an ENABLE_SANITIZERS option for ASan and UBSan
Adds -DENABLE_SANITIZERS, off by default, building with AddressSanitizer and
UndefinedBehaviorSanitizer. The bulk of the work is in the three ways the obvious
implementation silently does the wrong thing.
Probe the flags on the link step, not just the compile step. check_cxx_compiler_flag()
only compiles, so every sanitizer flag comes back unsupported, because linking an ASan
object without -fsanitize=address leaves __asan_init undefined. The result is not a
build failure but a build that appears instrumented and is not, which is worse. Use
check_cxx_source_compiles() with CMAKE_REQUIRED_LINK_OPTIONS instead, and treat an
unsupported sanitizer pair as fatal since the cosmetic flags are allowed to degrade.
Opt vendored sources out per source, through a cockatrice_disable_sanitizers_for()
helper rather than a list of set_source_files_properties() calls at the root. The
helper has to exist because CMake resolves a source-file property against the directory
scope its target was declared in: setting the property from the root with absolute paths
is a no-op, silently, which is why the call sites sit next to the add_library and
add_executable that pull the vendored code in. SFMT, minizip, the LZMA SDK and Qxt SMTP
are excluded. oracle/src/qt-json is in the history but not in the tree, removed in
#7214.
Also mark those sources SKIP_PRECOMPILE_HEADERS. The PCH is built with the sanitizer
flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
-fno-sanitize=all does not and GCC then refuses the PCH, which is a fatal
-Werror=invalid-pch in the servatrice and oracle builds.
-fno-sanitize=vptr is not optional. Qt constructs QObject subclasses into raw storage
and writes the vptr afterwards, so the vptr check reports on essentially every QObject
instantiation, and it has no value here because nothing outside moc-generated code
inherits QObject.
Verified on the resulting build: our own translation units carry ASan and UBSan calls,
SFMT.o carries neither, and the full suite of 464 cases passes clean under
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 with UBSAN_OPTIONS=halt_on_error=0.
The test suite needs two adjustments to make this option usable at all, both of which
land here because they are consequences of COCKATRICE_SANITIZERS existing rather than
independent changes.
DISCOVERY_TIMEOUT rises from 60s to 300s under sanitizers. An instrumented binary takes
2.8s to answer --gtest_list_tests against 0.04s uninstrumented, and discovery runs
several binaries at once behind a -j build.
One target is excluded via a NO_SANITIZE flag, server_developer_role_test. Reaching
google::protobuf::TextFormat::Printer's destructor aborts on an absl assertion inside
system protobuf 33.4 whenever the consumer translation unit is built without NDEBUG,
which a Debug sanitizer build is. This reproduces with no Cockatrice code at all, just
"TextFormat::Printer p; p.PrintToString(msg, &out);" built with -fsanitize=address, so it
is upstream rather than ours. It is a flag and not a fourth label because "skip this in
the sanitizer leg" has to compose with a test's group rather than compete for the single
label slot, and because a flag excludes by not registering the cases at all, which
cannot rot the way a label silently can. The target still builds and still runs on the
legs without sanitizers.
* [CI] Run the suite once under ASan and UBSan
Adds --sanitize to .ci/compile.sh, mirroring --test, and turns it on for one entry of
the existing Linux Docker matrix: Ubuntu 26.04, which already had a test-enabled leg.
That reuses the image, the ccache layer and the existing step shape, so the CI diff is a
matrix key plus the argument plumbing. Debian 13 and Fedora 44 keep covering the
uninstrumented suite, so nothing is lost by instrumenting one leg.
Sanitizers stay off the release and package legs, which build with --release for
packaging: instrumenting those would change the shipped binaries' optimization profile
and build time for no benefit. macOS and Windows stay uninstrumented too, since ASan is
unreliable against the Qt build there and MSVC has no equivalent without /fsanitize=address.
--sanitize does not imply --test, so an instrumented build can be produced locally
without paying for ctest. When both are given, ctest runs with detect_leaks=0 because Qt
and the sanitizer runtimes both leak at exit by design and LeakSanitizer's default buries
real findings under hundreds of process-teardown reports, and with UBSan halt_on_error=0 so
one run collects many findings instead of stopping at the first. ASan keeps
abort_on_error=1, since a bad access leaves the process unusable and continuing only
produces a second, more confusing report from the wreckage.
* [CI] Publish a gcovr line-coverage report from the test run
Sanitizers tell us what a run did wrong; they say nothing about what it never
reached. This adds the other half: an instrumented build whose report says which
of our own lines no test executes.
Add an ENABLE_COVERAGE option that compiles with --coverage -O0 -g, mirroring the
ENABLE_SANITIZERS opt-in including the per-source carve-out that keeps vendored
trees out of the denominator. -O0 is deliberate: at higher optimisation levels GCC
merges and relocates lines, so the report would describe code that no longer exists
in that form. Only GCC is supported, since Clang's intermediate format needs
llvm-cov; asking for coverage under Clang is a hard error rather than a build that
silently produces unreadable .gcno files.
Keep the filters in a committed gcovr.cfg rather than in the invocation. The tests
themselves are excluded because they are compiled with the same instrumentation
and are only ever executed by the run doing the measuring, so leaving them in would
report a healthier project than we have. The process entry points and the client's
window chrome go too: each needs a display, an interactive terminal or a live
database before it does anything a headless test could observe, so they would
otherwise sit at 0% forever and bury real movement.
Give coverage its own matrix leg rather than a second flag on the sanitizer leg.
The two answer different questions, and combining them slows both down without
telling us more. The new variant key gives the leg its own Docker image and ccache
entry so the two legs cannot race on the same tag.
The leg publishes an HTML report with per-file pages, LCOV, Cobertura XML and a
JSON summary. gcovr is a report-time dependency rather than a build dependency, so
a missing one warns and skips instead of failing an otherwise green build.
* [CI] Resolve the coverage source root before the build dir cd
The previous commit computed SRC_ROOT after `cd "$BUILD_DIR"`, under a comment
claiming it ran before. `$0` is relative to the directory the script was invoked
from, so from inside the build dir `dirname "$0"` no longer resolves and the
subshell's `cd` fails. Under `set -e` that is fatal rather than an empty variable,
which took down every leg in the matrix, coverage or not, including the ones this
change does not otherwise touch.
Move it above the cd and guard it on MAKE_COVERAGE, since the coverage report is
the only consumer. Resolving it unconditionally meant a coverage-only detail was
paid for by every build in the fleet, and gave every one of them a way to fail.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
@@ -9,6 +9,7 @@ RUN apt-get update && \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
gcovr \
|
||||
git \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
# --server compiles servatrice
|
||||
# --test runs tests
|
||||
# --sanitize builds with AddressSanitizer and UndefinedBehaviorSanitizer, gcc/clang only
|
||||
# --coverage instruments the test run for line coverage and prints a gcovr summary, gcc only
|
||||
|
||||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||
# --evict-ccache <age> runs ccache eviction based on given age after build
|
||||
# --dir <dir> sets the name of the build dir, default is "build"
|
||||
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
||||
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir>)
|
||||
# exitcode: 1 for failure, 3 for invalid arguments
|
||||
|
||||
# Read arguments
|
||||
@@ -62,6 +64,10 @@ while [[ $# != 0 ]]; do
|
||||
MAKE_SANITIZE=1
|
||||
shift
|
||||
;;
|
||||
'--coverage')
|
||||
MAKE_COVERAGE=1
|
||||
shift
|
||||
;;
|
||||
'--debug')
|
||||
BUILDTYPE="Debug"
|
||||
shift
|
||||
@@ -136,6 +142,15 @@ if [[ ! $BUILD_DIR ]]; then
|
||||
BUILD_DIR="build"
|
||||
fi
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
# Resolved while the working directory is still the source root. $0 is relative to the
|
||||
# directory the script was invoked from, so after the cd below it no longer resolves, and
|
||||
# under `set -e` the failed subshell would abort the build rather than just yielding an empty
|
||||
# variable. Only the coverage report needs this, so only coverage pays for it.
|
||||
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
fi
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
# Set minimum CMake Version
|
||||
@@ -160,6 +175,9 @@ else
|
||||
# into the shipped binaries (libasan/libubsan Depends in the DEB).
|
||||
flags+=("-DENABLE_SANITIZERS=OFF")
|
||||
fi
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
flags+=("-DENABLE_COVERAGE=ON")
|
||||
fi
|
||||
if [[ $USE_CCACHE ]]; then
|
||||
flags+=("-DUSE_CCACHE=1")
|
||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||
@@ -341,6 +359,31 @@ if [[ $MAKE_TEST ]]; then
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
# gcovr is not a build dependency and is not available everywhere, so a missing one is reported
|
||||
# and skipped rather than failing an otherwise green build.
|
||||
echo "::group::Coverage report"
|
||||
GCOVR="$(command -v gcovr || true)"
|
||||
if [[ -z $GCOVR ]]; then
|
||||
echo "gcovr not found; install it to produce a coverage report"
|
||||
else
|
||||
"$GCOVR" --version
|
||||
# This script configures with `cmake ..`, so the build dir it was started from is the object
|
||||
# directory and the source root is one level up. Both are passed explicitly: gcovr otherwise
|
||||
# walks the current directory and trips over in-tree build directories left by other tooling,
|
||||
# whose stale .gcno files cannot be resolved. --config is named too, so the filters apply no
|
||||
# matter what directory gcovr happens to start from.
|
||||
GCOVR_ARGS=("." --root "$SRC_ROOT" --object-directory "." --config "$SRC_ROOT/gcovr.cfg")
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --txt --decisions
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --html-details coverage-details.html --html-title "Cockatrice line coverage"
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --lcov -o coverage.lcov
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --cobertura-pretty -o coverage.xml
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --json-summary-pretty -o coverage-summary.json
|
||||
echo "Coverage artifacts written to coverage-details.html, coverage.lcov, coverage.xml and coverage-summary.json"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if [[ $MAKE_INSTALL ]]; then
|
||||
echo "::group::Install"
|
||||
cmake --build . --target install --config "$BUILDTYPE"
|
||||
|
||||
@@ -13,11 +13,17 @@
|
||||
# --set-cache <location> sets the location to cache the image or for ccache
|
||||
#
|
||||
# requires: docker
|
||||
# uses env: NAME CACHE BUILD GET SAVE INTERACTIVE
|
||||
# uses env: NAME IMAGE_SUFFIX CACHE BUILD GET SAVE INTERACTIVE
|
||||
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
||||
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
||||
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
||||
#
|
||||
# IMAGE_SUFFIX is optional and only appended to the image name, never to NAME, so that a matrix
|
||||
# leg can have its own image and image cache without needing its own directory under .ci. Two legs
|
||||
# building and tagging the same image concurrently would race on `docker build --tag`.
|
||||
# It is deliberately not called SUFFIX: compile.sh already uses SUFFIX for output file names, and
|
||||
# a job-level SUFFIX would be silently overridden by those per-step env blocks.
|
||||
#
|
||||
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
||||
# note that the docker container will not inherit any environment variables!
|
||||
#
|
||||
@@ -81,7 +87,7 @@ if ! [[ $NAME ]]; then
|
||||
return 3
|
||||
fi
|
||||
|
||||
export IMAGE_NAME="${project_name,,}_${NAME,,}" # lower case
|
||||
export IMAGE_NAME="${project_name,,}_${NAME,,}${IMAGE_SUFFIX:+_${IMAGE_SUFFIX,,}}" # lower case
|
||||
|
||||
docker_dir=".ci/$NAME"
|
||||
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
||||
|
||||
44
.github/workflows/desktop-build.yml
vendored
44
.github/workflows/desktop-build.yml
vendored
@@ -149,17 +149,31 @@ jobs:
|
||||
# uninstrumented suite, so nothing is lost by instrumenting this one.
|
||||
sanitize: yes
|
||||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||
- distro: Ubuntu
|
||||
version: 26.04
|
||||
variant: cover # separate image and ccache, same distro
|
||||
|
||||
# The one leg that runs instrumented for line coverage and publishes the report as
|
||||
# a workflow artifact. It is a separate leg rather than a second flag on the
|
||||
# sanitizer leg because the two instruments answer different questions: the
|
||||
# sanitizer leg reports what the run did wrong, the coverage leg reports what the
|
||||
# run never touched at all. Combining them would also slow both down. Packaging is
|
||||
# skipped because the leg above already publishes the DEB for this distro.
|
||||
coverage: yes
|
||||
package: skip
|
||||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
||||
needs: configure
|
||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||
timeout-minutes: 70
|
||||
env:
|
||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
||||
CCACHE_EVICTION_AGE: 7d
|
||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CMAKE_GENERATOR: 'Ninja'
|
||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||
IMAGE_SUFFIX: ${{ matrix.variant }}
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
@@ -169,7 +183,7 @@ jobs:
|
||||
id: restore_ccache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
||||
path: ${{ env.CACHE }}
|
||||
|
||||
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
||||
@@ -181,6 +195,7 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
sanitize: '${{ matrix.sanitize }}'
|
||||
coverage: '${{ matrix.coverage }}'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
args=()
|
||||
@@ -190,9 +205,32 @@ jobs:
|
||||
|
||||
extra=()
|
||||
[[ $sanitize == yes ]] && extra+=(--sanitize)
|
||||
[[ $coverage == yes ]] && extra+=(--coverage)
|
||||
|
||||
RUN --server --debug --test "${extra[@]}" "${args[@]}"
|
||||
|
||||
# RUN bind-mounts the workspace into the container, so the report the build wrote into
|
||||
# build/ is already on the host and no second container is needed to collect it.
|
||||
- name: "Collect coverage report"
|
||||
if: matrix.coverage == 'yes'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
# --html-details writes one page per source file plus a stylesheet and script, and the
|
||||
# index links to all of them by relative name. Copying the index alone would publish a
|
||||
# report where every link 404s, so the whole family has to travel together.
|
||||
cp build/coverage-details* coverage/
|
||||
cp build/coverage.lcov build/coverage.xml build/coverage-summary.json coverage/
|
||||
ls -la coverage/
|
||||
|
||||
- name: "Upload coverage artifact"
|
||||
if: matrix.coverage == 'yes'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: "Build release package"
|
||||
id: build
|
||||
if: matrix.package != 'skip'
|
||||
|
||||
@@ -24,6 +24,8 @@ option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||
option(TEST "Build tests" OFF)
|
||||
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
||||
option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||
# Build instrumented for GCC line coverage, reported with gcovr
|
||||
option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
||||
# Use vcpkg regardless of OS
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
|
||||
@@ -213,9 +215,11 @@ else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
endif()
|
||||
|
||||
# Always included, because it defines cockatrice_disable_sanitizers_for() for the vendored
|
||||
# trees. It only touches the global flags when ENABLE_SANITIZERS is on.
|
||||
# Always included, because they define cockatrice_disable_sanitizers_for() and
|
||||
# cockatrice_disable_coverage_for() for the vendored trees. They only touch the global flags
|
||||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||
include(Sanitizers)
|
||||
include(Coverage)
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
|
||||
75
cmake/Coverage.cmake
Normal file
75
cmake/Coverage.cmake
Normal file
@@ -0,0 +1,75 @@
|
||||
# Line-coverage instrumentation opt-in, plus per-source opt-out
|
||||
# Input: ENABLE_COVERAGE
|
||||
# Defines: cockatrice_disable_coverage_for
|
||||
#
|
||||
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
||||
# block, so add_compile_options() reaches every library and executable defined below.
|
||||
#
|
||||
# Note the name deliberately shadows CMake's own built-in Modules/Coverage.cmake. It resolves
|
||||
# to this file only because the root CMakeLists.txt puts cmake/ at the front of
|
||||
# CMAKE_MODULE_PATH; if that ever moves, include(Coverage) would silently pick up the built-in
|
||||
# module, which only defines a `COVERAGE` custom target and touches no global flags, so
|
||||
# instrumentation would quietly stop happening rather than erroring.
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
|
||||
# Opt a set of vendored sources out of instrumentation.
|
||||
#
|
||||
# cockatrice_disable_coverage_for(<source> ...)
|
||||
#
|
||||
# Third-party code drags the total down with lines no test can ever reach, and a total that
|
||||
# cannot move is not a useful signal. This is the same carve-out the sanitizer build uses.
|
||||
#
|
||||
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
||||
# source-file property against the directory scope the target was declared in, so calling
|
||||
# this from the root with absolute paths silently does nothing. That is why the call sites
|
||||
# live next to the add_library/add_executable that pull the vendored code in.
|
||||
#
|
||||
# SKIP_PRECOMPILE_HEADERS mirrors the sanitizer helper. The precompiled header carries the
|
||||
# instrumentation flags, and mixing an instrumented PCH with an uninstrumented translation
|
||||
# unit makes GCC reject it.
|
||||
function(cockatrice_disable_coverage_for)
|
||||
if(NOT ENABLE_COVERAGE)
|
||||
return()
|
||||
endif()
|
||||
# There is no -fno-coverage: --coverage is shorthand for -fprofile-arcs -ftest-coverage, so both
|
||||
# halves have to be negated. CC rejects "-fno-coverage" outright, which is how the vendored C
|
||||
# files below hit it first.
|
||||
set_source_files_properties(
|
||||
${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-profile-arcs;-fno-test-coverage" SKIP_PRECOMPILE_HEADERS ON
|
||||
)
|
||||
endfunction()
|
||||
|
||||
if(ENABLE_COVERAGE)
|
||||
# Line coverage needs the compiler's own toolchain, so only GCC is supported. Clang's
|
||||
# equivalent is llvm-cov, which reads a different intermediate format and would need a
|
||||
# separate report path; rejecting it is better than emitting .gcno files that gcovr cannot
|
||||
# read.
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC, but the compiler is ${CMAKE_CXX_COMPILER_ID}. "
|
||||
"Clang coverage needs llvm-cov and is not wired up."
|
||||
)
|
||||
endif()
|
||||
|
||||
# Probe the way the flag will actually be used: compile AND link. A .gcno file comes from the
|
||||
# compile step, but the runtime that writes .gcda files comes from the link step, so probing
|
||||
# only the compile step would pass on a toolchain that cannot actually produce coverage.
|
||||
set(_cockatrice_saved_required_flags "${CMAKE_REQUIRED_FLAGS}")
|
||||
set(_cockatrice_saved_required_link_options "${CMAKE_REQUIRED_LINK_OPTIONS}")
|
||||
set(CMAKE_REQUIRED_FLAGS "-O0 -g")
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS -fprofile-arcs -ftest-coverage)
|
||||
check_cxx_source_compiles("int main() { return 0; }" COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||
set(CMAKE_REQUIRED_FLAGS "${_cockatrice_saved_required_flags}")
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${_cockatrice_saved_required_link_options}")
|
||||
|
||||
if(NOT COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||
message(FATAL_ERROR "ENABLE_COVERAGE is on, but this toolchain rejected the coverage flags.")
|
||||
endif()
|
||||
|
||||
# -O0 keeps the line mapping honest: at higher optimisation levels the compiler merges and
|
||||
# relocates lines, and the report describes code that no longer exists in that form.
|
||||
add_compile_options(--coverage -O0 -g)
|
||||
add_link_options(--coverage)
|
||||
|
||||
message(STATUS "Coverage instrumentation enabled (--coverage). Report with gcovr over the build dir.")
|
||||
endif()
|
||||
63
gcovr.cfg
Normal file
63
gcovr.cfg
Normal file
@@ -0,0 +1,63 @@
|
||||
# gcovr configuration for Cockatrice line coverage.
|
||||
#
|
||||
# Used by .ci/compile.sh --coverage and by hand:
|
||||
#
|
||||
# cmake -S . -B build/cover -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 -DENABLE_COVERAGE=ON
|
||||
# cmake --build build/cover -j 24
|
||||
# ctest --test-dir build/cover -LE bench -j 24
|
||||
# gcovr build/cover --root . --object-directory build/cover --txt --decisions
|
||||
#
|
||||
# The point of most of this file is subtraction. A raw report is dominated by code no test can
|
||||
# reach: vendored third-party trees, build directories left over from earlier experiments, and
|
||||
# the client's main() and window classes, which have no headless test. Those all dilute the one
|
||||
# number worth watching, so they are filtered out here rather than in every invocation.
|
||||
|
||||
# root is deliberately not set here. This file is normally invoked from the build directory
|
||||
# (compile.sh runs there, because it configures with `cmake ..`), so a `root` of `.` would
|
||||
# resolve to the build tree and every filter below would exclude everything. compile.sh passes
|
||||
# --root explicitly instead.
|
||||
|
||||
# Our own code only. This keeps generated Qt MOC output, protobuf output and anything CMake
|
||||
# built under the source tree out of the report.
|
||||
filter = ^cockatrice/
|
||||
filter = ^libcockatrice_[^/]+/
|
||||
filter = ^servatrice/
|
||||
filter = ^oracle/
|
||||
|
||||
# The tests are the vehicle, not the subject. They are compiled with the same instrumentation,
|
||||
# so leaving them in would add their near-perfect coverage to the numerator and quietly report a
|
||||
# healthier project than we have: test code is only ever executed by the run that is measuring
|
||||
# it. Helpers under tests/ are excluded on the same grounds.
|
||||
exclude = ^tests/
|
||||
|
||||
# Vendored third-party code is not ours to cover and is never going to move the number.
|
||||
exclude = ^vcpkg/
|
||||
exclude = ^doc/doxygen/theme/
|
||||
exclude = ^libcockatrice_rng/libcockatrice/rng/sfmt/
|
||||
exclude = ^libcockatrice_utility/libcockatrice/utility/peglib\.h$
|
||||
exclude = ^oracle/src/lzma/
|
||||
exclude = ^oracle/src/qt-json/
|
||||
exclude = ^oracle/src/zip/
|
||||
exclude = ^servatrice/src/smtp/
|
||||
|
||||
# Qt's own moc/pch/uic output is generated into the build tree, not the source tree, so the
|
||||
# filters above already keep it out. These cover CLion's directories, which sit in-tree and are
|
||||
# picked up by the object-directory walk.
|
||||
exclude = .*cmake-build-.*
|
||||
exclude = .*\.cache/
|
||||
|
||||
# Process entry points and the client's window chrome. Each of these needs a display, an
|
||||
# interactive terminal, or a live database before it does anything a test could observe, so no
|
||||
# headless run will ever move them. Left in, they contribute thousands of lines at 0% and drag
|
||||
# the total down by a fixed amount that hides movement in the code we can actually test. The
|
||||
# window header is excluded with its .cpp because gcovr attributes its inline template code
|
||||
# there, which is where most of its line count lives.
|
||||
exclude = ^cockatrice/src/main\.cpp$
|
||||
exclude = ^cockatrice/src/interface/window_main\.
|
||||
exclude = ^oracle/src/main\.cpp$
|
||||
exclude = ^servatrice/src/main\.cpp$
|
||||
|
||||
exclude-throw-branches = yes
|
||||
exclude-function-lines = yes
|
||||
|
||||
print-summary = yes
|
||||
@@ -13,6 +13,7 @@ add_library(
|
||||
|
||||
# SFMT is vendored and its own headers trip UBSan's alignment and vptr checks.
|
||||
cockatrice_disable_sanitizers_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||
cockatrice_disable_coverage_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||
|
||||
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ if(ZLIB_FOUND)
|
||||
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
# minizip and zlib are vendored.
|
||||
cockatrice_disable_sanitizers_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
cockatrice_disable_coverage_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
else()
|
||||
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
||||
endif()
|
||||
@@ -93,6 +94,7 @@ if(LIBLZMA_FOUND)
|
||||
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
||||
# The LZMA SDK is vendored.
|
||||
cockatrice_disable_sanitizers_for(src/lzma/decompress.cpp)
|
||||
cockatrice_disable_coverage_for(src/lzma/decompress.cpp)
|
||||
else()
|
||||
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
||||
endif()
|
||||
|
||||
@@ -31,6 +31,9 @@ set(servatrice_RESOURCES servatrice.qrc)
|
||||
cockatrice_disable_sanitizers_for(
|
||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||
)
|
||||
cockatrice_disable_coverage_for(
|
||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
||||
|
||||
@@ -26,6 +26,7 @@ if(ZLIB_FOUND)
|
||||
add_definitions("-DHAS_ZLIB")
|
||||
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
||||
cockatrice_disable_sanitizers_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||
cockatrice_disable_coverage_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
else()
|
||||
@@ -37,6 +38,7 @@ if(LIBLZMA_FOUND)
|
||||
add_definitions("-DHAS_LZMA")
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
||||
cockatrice_disable_sanitizers_for(../../oracle/src/lzma/decompress.cpp)
|
||||
cockatrice_disable_coverage_for(../../oracle/src/lzma/decompress.cpp)
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||
else()
|
||||
|
||||
Reference in New Issue
Block a user