Testing
cxpm is tested by two projects of its own workspace, built and run by cxpm test:
| Project | What it is | Cases |
|---|---|---|
tests/unit |
The library tested in process, with every external process replaced by a recording | 172 |
tests/cli |
The installed cxpm and cxpm-descriptor-sandbox run as a user would run them, with a real compiler |
17 |
Running the suite
cxpm test # with an installed cxpm
./cxpm-build.sh --test # on a machine that has none: bootstrap first
172/172 tests passed
cxpm-unit-tests passed
17/17 tests passed
cxpm-cli-tests passed
2 of 2 tests projects passed
cxpm test builds the workspace, then each tests project, and runs its executable from the
tests project’s directory. Either executable runs one case alone with --run 'group::case', for example
tests/unit/.cxpm/build/cxpm-unit-tests/cxpm-unit-tests --run 'Fetcher::...'.
The CMake build runs the same two executables through CTest: ctest --test-dir build, or
ctest --test-dir build -L unit for the unit suite alone. The command line suite tests an
installed cxpm, so a CTest fixture first installs the build into build/tests-sysroot.
CI (.github/workflows/ci.yaml) runs ./cxpm-build.sh --test --install in a container that has
a compiler and no other build system.
Where the tests live
tests/unit/package.cpp the cxpm-unit-tests project
tests/unit/src/main.cpp cx::core::testing::run_all
tests/unit/src/all_tests.cpp includes every *Test.hpp
tests/unit/src/Support.hpp temporary directories, recorded processes, scoped environment
tests/unit/src/*Test.hpp one header per component
tests/cli/package.cpp the cxpm-cli-tests project
tests/cli/src/main.cpp every command line case
tests/cli/fixtures/ projects the cases build
The command line suite runs what was built
tests/cli depends on cxpm, cxpm-descriptor-sandbox and a toolchain module, so cxpm test
installs them into the workspace sysroot first. The suite then starts
$CXPM_TEST_SYSROOT/usr/bin/cxpm (cxpm test sets that variable, and puts the sysroot’s bin
on PATH) and asserts on exit codes, output and the files left behind. It copies each fixture
into a temporary directory, so nothing is ever built inside the repository.
It covers: usage and every error path; generate; building the example projects from
package.cpp and package.json; install and uninstall under --root; a target whose name
holds spaces and quotes; the sandbox helper on a real plugin; and confinement, with a manifest
that reads a file outside its project and one that opens a socket. The confinement cases are
skipped, and say so, where bubblewrap cannot create a namespace.
Self-registering groups
The unit suite runs on cx-core-testing. Each header declares one cx::core::testing::test_group
per class it covers, in namespace CXPM::Tests:
// tests/unit/src/ManifestApiTest.hpp
namespace CXPM::Tests {
inline static cx::core::testing::test_group status_tests{
"Status",
{
{"Success formats as Status::Success",
[](cx::core::testing::test_context &ctx) {
ctx.check_equal(std::to_string(CXPM::Status::Success),
std::string("Status::Success"));
}},
}};
} // namespace CXPM::Tests
Constructing the group at static-initialization time registers it; run_all runs every
registered case, prints one line per case and returns non-zero if any check failed or any case
threw. tests/unit/src/all_tests.cpp includes every *Test.hpp, so every group coexists in one
translation unit, which surfaces helper-name collisions and ODR problems when the suite compiles.
A new *Test.hpp is added to that file.
Nothing test-related is installed: the tests projects are not members of the workspace’s
packages, only of its tests.
How the hard parts are tested without a compiler
Every external process cxpm starts goes through CXPM::Controllers::ProcessRunner, and a test
can replace what it does (ProcessRunner::handler_set). Support.hpp’s RecordedProcesses
records each argument vector and answers with a canned result, so the orchestration that used to
be reachable only through a real build is unit-tested:
ToolchainDriver: the exact compile, link and archive argument vectors for each target type, including names and paths with spaces and quotes; parallel compilation; stopping at the first failure.ToolchainManager: discovery order,toolchain.cppwinning overtoolchain.json, plugin names that cannot collide, selection by name and by language, unusable toolchains skipped.ProjectManager: manifest loading in both formats, every stage of the lifecycle and what makes each one stale, workspaces and dependency order,cxpm test,compile_commands.json.Fetcher,Stagingand the package managers: the lock file, the staged layout of each target type, and the archive, pacman, rpm and deb packages byte by byte where the format allows.ProcessRunner: what a confined process is allowed to see.DescriptorPlugin/Installation: the plugin compile command, reuse of an up-to-date plugin, the failure message, and every environment override.PkgConfigManager: thepkg-configqueries and.pcrendering.ApplicationView: every command’s exit code and output, run in process with captured streams.
What needs real tools is covered by the command line suite: a real compiler building and
loading a real plugin, the cxpm-descriptor-sandbox helper loading it, and bubblewrap confining
both.
Sanitizers
tests/unit/package.cpp builds the unit suite with AddressSanitizer and
UndefinedBehaviorSanitizer. The full suite passes clean under both.
Known gaps
examples/archive-object,examples/shared-objectand the gstreamer example are not built by the command line suite; those target types are covered by unit tests only.- The sandbox helper’s resource limits (CPU, memory, wall clock) are not exercised by a test that actually exceeds them.
- There is no coverage report: the CMake build no longer has a
coveragetarget. Building withCompilerOptions::Coverageand runninggcovrby hand still works. - Installing native packages (
pacman -U,rpm -i,dpkg -i) is not part of the suite: it needs root and a matching system. The packages themselves are checked by unit tests.