diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index df717a9..0218803 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y cmake gcc moreutils - # This step used to clone and install libakerror@main as well. That was two + # This step used to clone and install libakerror@main. That was two # different libakerrors depending on where you built: the install went to # /usr/local, while the build below is top-level and so compiles # deps/libakerror at the pinned commit via add_subdirectory -- the @@ -25,8 +25,16 @@ jobs: # # The submodule wins. It is the version this repository pins, tests and # ships against, and a CI that tests a different one is testing something - # nobody runs. `cmake --install` below still installs both, so the - # find_package and pkg-config paths are exercised. + # nobody runs. It is installed here as well as compiled, because an + # installed libakstdlib is not usable without it: akstdlibConfig.cmake + # calls find_dependency(akerror), and the top-level build pulls the + # submodule in EXCLUDE_FROM_ALL so `cmake --install` on this project + # installs only this project. + - name: install the pinned libakerror + run: | + cmake -S deps/libakerror -B build-akerror + cmake --build build-akerror + sudo cmake --install build-akerror - name: build and test run: | # -DAKSL_WERROR=ON here and not locally: a warning that stops the build @@ -36,6 +44,16 @@ jobs: cmake --build build sudo cmake --install build ctest --test-dir build --output-on-failure + # Build something against what was just installed. The suite links the + # build tree, so it says nothing about whether the *installed* package is + # usable -- and the two have come apart before: find_dependency(akerror) + # resolving is a property of the install, not of the build. + - name: consume the installed package + run: | + sudo ldconfig + cmake -S tests/consumer -B build-consumer + cmake --build build-consumer + ./build-consumer/consumer - run: echo "🍏 This job's status is ${{ job.status }}." # The sanitizer build is its own job rather than a step on the one above, diff --git a/AGENTS.md b/AGENTS.md index f67ec57..49914a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,9 @@ reasoning. The implementation is split by domain: Tests are one-file CTest executables under `tests/test_.c`, with shared helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must -*fail* to compile -- see the testing section below. CMake package templates are +*fail* to compile, and `tests/consumer/` is a standalone project built against +the *installed* package rather than the build tree -- see the testing section +below. CMake package templates are in `cmake/` and `akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a submodule rather than editing generated files under `build/`, `build-asan/` or `build-coverage/`. @@ -108,6 +110,12 @@ guarantees the compiler enforces and nothing else does: `AKERR_NOIGNORE` and the format attributes. Drop either in a refactor and every ordinary test still passes. +`tests/consumer/` is configured standalone against `CMAKE_PREFIX_PATH`, not as +part of this build, because being part of this build is what would let it pass +without testing anything. It covers the paths only an install has: the version +file, `find_dependency(akerror)` resolving, and the exported +`akstdlib::akstdlib` target. CI runs it after `cmake --install`. + Coverage is 99.5% of lines and 100% of functions across all four sources; CI gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run `cmake --build build-coverage --target coverage` and check the uncovered-line diff --git a/README.md b/README.md index 95813da..9c1f79e 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,31 @@ Every test is capped with a 30-second CTest `TIMEOUT`. The list and tree code is full of loops whose termination hangs on a single condition, so a bug of that shape hangs the suite rather than failing it. +### 1a. The installed package + +```sh +cmake -S deps/libakerror -B build-akerror && cmake --build build-akerror +cmake --install build-akerror --prefix /some/prefix +cmake --install build --prefix /some/prefix +cmake -S tests/consumer -B build-consumer -DCMAKE_PREFIX_PATH=/some/prefix +cmake --build build-consumer && ./build-consumer/consumer +``` + +The suite links the build tree, so it says nothing about whether an *installed* +libakstdlib is usable. `tests/consumer/` is a standalone project that does the +things only an install exercises: `find_package(akstdlib 0.2)` against the +generated version file, `akstdlibConfig.cmake`'s `find_dependency(akerror)`, and +the exported `akstdlib::akstdlib` target. It touches one function from each of +the four sources, so a library installed with a source file missing from its link +line fails here rather than in whatever consumer finds it next. + +Note that libakerror has to be installed too. A top-level build compiles the +vendored copy with `EXCLUDE_FROM_ALL`, so `cmake --install` on this project +installs only this project -- and an installed libakstdlib whose +`find_dependency(akerror)` cannot resolve is not usable. Install the submodule's +copy, not `libakerror@main`: that is the version this repository pins and tests +against, and it is what CI does. + ### 2. Sanitizers ```sh diff --git a/tests/consumer/CMakeLists.txt b/tests/consumer/CMakeLists.txt new file mode 100644 index 0000000..b558d9c --- /dev/null +++ b/tests/consumer/CMakeLists.txt @@ -0,0 +1,21 @@ +# A consumer of the *installed* package, not of the build tree. +# +# The test suite links the build tree, so it says nothing about whether an +# installed libakstdlib is usable -- and the two have come apart before. This is +# the smallest thing that exercises the paths only an install has: the version +# file (find_package with a version request), akstdlibConfig.cmake's +# find_dependency(akerror), and the exported akstdlib::akstdlib target. +# +# Configured standalone against CMAKE_PREFIX_PATH rather than being part of the +# main build, because being part of the main build is exactly what would let it +# pass without testing anything. +cmake_minimum_required(VERSION 3.10) +project(akstdlib_consumer LANGUAGES C) + +# A version request, so akstdlibConfigVersion.cmake has to be present and has to +# agree. Deliberately MAJOR.MINOR rather than the full version: that is the +# compatibility promise the soname makes, so it is the one worth asserting. +find_package(akstdlib 0.2 REQUIRED) + +add_executable(consumer main.c) +target_link_libraries(consumer PRIVATE akstdlib::akstdlib) diff --git a/tests/consumer/main.c b/tests/consumer/main.c new file mode 100644 index 0000000..0421c07 --- /dev/null +++ b/tests/consumer/main.c @@ -0,0 +1,73 @@ +/* + * Smoke test for the installed package. See CMakeLists.txt beside this file. + * + * Deliberately touches one function from each translation unit -- the version + * entry points, a string buffer, a formatted append and a print -- so that a + * library installed with a source file missing from its link line fails here + * rather than in whatever consumer discovers it next. + * + * Returns a distinct status per step, so a CI failure names the step. + */ + +#include + +int main(void) +{ + akerr_ErrorContext *e = NULL; + aksl_StrBuf buf; + const char *text = NULL; + size_t len = 0; + uint32_t hash = 0; + int count = 0; + + /* The header and the .so have to agree, which is the whole point of the + * version machinery: the soname catches this first in normal use, and this + * catches a hand-install that bypassed it. */ + e = AKSL_VERSION_CHECK(); + if ( e != NULL ) { + return 1; + } + + /* src/collections.c */ + e = aksl_strbuf_init(&buf, 0); + if ( e != NULL ) { + return 2; + } + e = aksl_strbuf_appendf(&buf, "libakstdlib %s, soname %s", + aksl_version_string(), aksl_version_soname()); + if ( e != NULL ) { + return 3; + } + e = aksl_strbuf_cstr(&buf, &text); + if ( e != NULL ) { + return 4; + } + + /* src/string.c */ + e = aksl_strlen(text, &len); + if ( e != NULL ) { + return 5; + } + + /* src/stdlib.c */ + e = aksl_strhash_djb2(text, len, &hash); + if ( e != NULL ) { + return 6; + } + e = aksl_printf(&count, "%s (%zu bytes, djb2 %u)\n", text, len, hash); + if ( e != NULL ) { + return 7; + } + + /* src/stream.c */ + e = aksl_fflush(NULL); + if ( e != NULL ) { + return 8; + } + + e = aksl_strbuf_free(&buf); + if ( e != NULL ) { + return 9; + } + return 0; +}