Test the installed package, not just the build tree
Found by building a consumer against a fresh install: an installed libakstdlib was not usable. 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 and leaves that dependency unresolvable. CI had been hiding it by installing libakerror@main in a separate step -- a different libakerror from the one actually compiled, which is the inconsistency TODO.md 2.3 recorded and the previous commit removed. Removing it exposed the real gap. CI now installs deps/libakerror, the same commit the top-level build compiles, so there is one libakerror in play and the install is consumable. tests/consumer/ is the check that would have caught it: a standalone project, configured against CMAKE_PREFIX_PATH rather than as part of this build, because being part of this build is exactly what would let it pass without testing anything. It exercises what only an install has -- find_package with a version request against the generated version file, find_dependency(akerror) resolving, and the exported akstdlib::akstdlib target -- and touches one function from each of the four sources, so a library installed with a source file missing from its link line fails there rather than in the next consumer to find it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y cmake gcc moreutils
|
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
|
# different libakerrors depending on where you built: the install went to
|
||||||
# /usr/local, while the build below is top-level and so compiles
|
# /usr/local, while the build below is top-level and so compiles
|
||||||
# deps/libakerror at the pinned commit via add_subdirectory -- the
|
# 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
|
# 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
|
# ships against, and a CI that tests a different one is testing something
|
||||||
# nobody runs. `cmake --install` below still installs both, so the
|
# nobody runs. It is installed here as well as compiled, because an
|
||||||
# find_package and pkg-config paths are exercised.
|
# 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
|
- name: build and test
|
||||||
run: |
|
run: |
|
||||||
# -DAKSL_WERROR=ON here and not locally: a warning that stops the build
|
# -DAKSL_WERROR=ON here and not locally: a warning that stops the build
|
||||||
@@ -36,6 +44,16 @@ jobs:
|
|||||||
cmake --build build
|
cmake --build build
|
||||||
sudo cmake --install build
|
sudo cmake --install build
|
||||||
ctest --test-dir build --output-on-failure
|
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 }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
# The sanitizer build is its own job rather than a step on the one above,
|
# The sanitizer build is its own job rather than a step on the one above,
|
||||||
|
|||||||
10
AGENTS.md
10
AGENTS.md
@@ -19,7 +19,9 @@ reasoning. The implementation is split by domain:
|
|||||||
|
|
||||||
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
|
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
|
||||||
helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must
|
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`;
|
in `cmake/` and `akstdlib.pc.in`. The vendored dependency is `deps/libakerror`;
|
||||||
update it as a submodule rather than editing generated files under `build/`,
|
update it as a submodule rather than editing generated files under `build/`,
|
||||||
`build-asan/` or `build-coverage/`.
|
`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
|
format attributes. Drop either in a refactor and every ordinary test still
|
||||||
passes.
|
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
|
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
|
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
|
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
||||||
|
|||||||
25
README.md
25
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
|
full of loops whose termination hangs on a single condition, so a bug of that
|
||||||
shape hangs the suite rather than failing it.
|
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
|
### 2. Sanitizers
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
21
tests/consumer/CMakeLists.txt
Normal file
21
tests/consumer/CMakeLists.txt
Normal file
@@ -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)
|
||||||
73
tests/consumer/main.c
Normal file
73
tests/consumer/main.c
Normal file
@@ -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 <akstdlib.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user