tests and eurc
All checks were successful
Deploy / build-deploy (push) Successful in 4m19s

This commit is contained in:
Jorijn van der Graaf 2026-08-15 00:54:05 +02:00
commit 749f525f83
44 changed files with 5380 additions and 3532 deletions

View file

@ -20,12 +20,25 @@ using namespace Crafter;
// server — the native HTTP server that will render the same pages
// server-side for crawlers and no-JS clients, and later
// host the shop API. No Crafter.Graphics.
// crafter-build -- --product=server
// crafter-build --product=server
//
// One project file rather than three, following the imsd convention: a single
// CrafterBuildProject returns a single Configuration, so the products are
// branches and the shared library is a `static unique_ptr<Configuration>`
// both branches can depend on.
//
// The tests hang off the server product (tests/<Name>/main.cpp, the
// Crafter.Network layout):
//
// crafter-build test --product=server
//
// Two layers, both under the same runner. The unit suites replaced the
// --selftest flag the server binary used to carry: same assertions, but each
// suite is its own binary with the framework's parallel runner, timeouts and
// crash detection instead of one grab-bag function. The black-box suites
// (ShouldServeRoutes and friends) replaced tools/e2e.sh: each spawns the
// freshly built binary on its own scratch port — via tests/harness/ — and
// asserts over real HTTP the things only a real request can show.
// Catcrafts.Shared — target-neutral page renderers, built as a static library
// for whichever target the selected product is using. `static` because the
@ -39,6 +52,14 @@ using namespace Crafter;
// .cpp lands here that would be a wrong-architecture link.
static Configuration* SharedLibrary(std::span<const std::string_view> args) {
static auto shared = std::make_unique<Configuration>();
// Idempotent, and it MUST be: GetInterfacesAndImplementations may run only
// once — a second call re-parses the same .cppm files into duplicate
// Module entries, and the duplicated units then compile concurrently into
// the same PCM paths, failing the build with "module not found" at
// whichever partition lost the race. The server branch reaches here twice
// (once via ServerCore, once directly), which is what made this real.
// Only one product branch runs per process, so first-call args win.
if (!shared->name.empty()) return shared.get();
shared->path = "./";
shared->name = "Catcrafts.Shared";
shared->outputName = "Catcrafts.Shared";
@ -63,6 +84,53 @@ static Configuration* SharedLibrary(std::span<const std::string_view> args) {
return shared.get();
}
// Catcrafts.ServerCore — every implementation unit of the server except
// main.cpp, as a static library. The split exists for the tests: a test can
// only LINK a library-type dependency (crafter-build exports -L/-l for
// libraries alone), and the Mollie/EURC/Sendcloud parsers, the invoice
// builder and the bunq classifier all live here. The exe is main.cpp plus
// this library, so the split costs nothing at deploy time.
//
// Same `static unique_ptr` convention as SharedLibrary, and the same warning:
// cfg.dependencies holds a raw pointer, so the Configuration must outlive
// CrafterBuildProject.
static Configuration* ServerCore(std::span<const std::string_view> args, Configuration* network) {
static auto core = std::make_unique<Configuration>();
// Same idempotence rule as SharedLibrary — see the warning there.
if (!core->name.empty()) return core.get();
core->path = "./";
core->name = "Catcrafts.ServerCore";
core->outputName = "Catcrafts.ServerCore";
ApplyStandardArgs(*core, args);
core->type = ConfigurationType::LibraryStatic;
core->dependencies = { SharedLibrary(args), network };
std::array<fs::path, 1> ifaces = {
"server/interfaces/Catcrafts.Server",
};
std::array<fs::path, 8> impls = {
"server/implementations/Catcrafts.Server-Http",
"server/implementations/Catcrafts.Server-Orders",
"server/implementations/Catcrafts.Server-Mollie",
"server/implementations/Catcrafts.Server-Invoice",
"server/implementations/Catcrafts.Server-Eurc",
"server/implementations/Catcrafts.Server-Shipping",
"server/implementations/Catcrafts.Server-Mail",
"server/implementations/Catcrafts.Server-Financials",
};
core->GetInterfacesAndImplementations(ifaces, impls);
// Both rails reach their provider over TLS, which is what libssl is for
// here. libcrypto is named again on its own account: the bunq mutation
// callback verifies an RSA-SHA256 body signature through EVP, so this
// library calls libcrypto directly rather than only inheriting it as
// libssl's dependency. On the lib rather than the exe because link flags
// propagate to consumers — the exe and every test get them from here.
core->linkFlags.push_back("-lssl");
core->linkFlags.push_back("-lcrypto");
return core.get();
}
// Payload trimming, release only.
//
// The wasi-libc / wasi-libc++ static libs ship with DWARF and wasm-ld keeps
@ -108,38 +176,23 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
.args = netArgs,
});
Configuration* core = ServerCore(args, network);
Configuration* shared = SharedLibrary(args);
Configuration cfg;
cfg.path = "./";
cfg.name = "Catcrafts.Server";
cfg.outputName = "catcrafts-server";
cfg.type = ConfigurationType::Executable;
ApplyStandardArgs(cfg, args);
cfg.dependencies = { SharedLibrary(args), network };
cfg.dependencies = { core, shared, network };
std::array<fs::path, 1> ifaces = {
"server/interfaces/Catcrafts.Server",
};
std::array<fs::path, 9> impls = {
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = {
"server/implementations/main",
"server/implementations/Catcrafts.Server-Http",
"server/implementations/Catcrafts.Server-Orders",
"server/implementations/Catcrafts.Server-Mollie",
"server/implementations/Catcrafts.Server-Invoice",
"server/implementations/Catcrafts.Server-Coingate",
"server/implementations/Catcrafts.Server-Shipping",
"server/implementations/Catcrafts.Server-Mail",
"server/implementations/Catcrafts.Server-Financials",
};
cfg.GetInterfacesAndImplementations(ifaces, impls);
// Both rails reach their provider over TLS, which is what libssl is
// for here. libcrypto is named again on its own account: the bunq
// mutation callback verifies an RSA-SHA256 body signature through
// EVP, so this product calls libcrypto directly rather than only
// inheriting it as libssl's dependency.
cfg.linkFlags.push_back("-lssl");
cfg.linkFlags.push_back("-lcrypto");
// libmsquic.so.2 is built in crafter-build's external cache, and the
// RUNPATH pointing there only exists on the build machine — the first
// deploy to a real host died in the loader (exit 127) because of it.
@ -152,6 +205,69 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.linkFlags.push_back("-Wl,-rpath,/srv/catcrafts-app");
ApplyReleaseTrimming(cfg);
// ── tests ─────────────────────────────────────────────────────
// crafter-build test --product=server
//
// Each suite is tests/<Name>/main.cpp. The first group tests
// Catcrafts.Shared alone — the escaping/JSON/form/money layers that
// produce every byte of markup the site emits. The second group also
// links the server core: provider payload parsers, the invoice and
// email builders, and the financials ingest.
cfg.AddTest("ShouldEscapeHtml").Dependencies({ shared });
cfg.AddTest("ShouldParseJson").Dependencies({ shared });
cfg.AddTest("ShouldValidateForms").Dependencies({ shared });
cfg.AddTest("ShouldComputeMoney").Dependencies({ shared });
cfg.AddTest("ShouldShipContent").Dependencies({ shared });
cfg.AddTest("ShouldBuildMediaLadders").Dependencies({ shared });
cfg.AddTest("ShouldRenderMarkdown").Dependencies({ shared });
cfg.AddTest("ShouldServePosts").Dependencies({ shared });
cfg.AddTest("ShouldParseSendcloudRates").Dependencies({ core, shared });
cfg.AddTest("ShouldMintOrderTokens").Dependencies({ core, shared });
cfg.AddTest("ShouldParseMolliePayments").Dependencies({ core, shared });
cfg.AddTest("ShouldParseEurcChains").Dependencies({ core, shared });
cfg.AddTest("ShouldGuardRequestProvenance").Dependencies({ core, shared });
cfg.AddTest("ShouldBuildInvoices").Dependencies({ core, shared });
cfg.AddTest("ShouldPublishFinancials").Dependencies({ core, shared });
// ── black-box suites (the tools/e2e.sh port) ──────────────────
// Each spawns the REAL binary — depending on &cfg is what builds it
// first (an Executable dep is built and mtime-tracked, it just links
// nothing) — on its OWN port: the suites run in parallel, so a shared
// port would race. The harness module is compiled into each suite via
// the interfaces overload; Args hands each binary the exact path to
// the exe this configuration produces, so a stale sibling variant
// under bin/ can never be the thing under test.
//
// BinDir() must be read AFTER ApplyReleaseTrimming: compileFlags are
// part of the variant hash, so reading it earlier would name a
// directory the build never writes.
std::array<fs::path, 1> harness = { "tests/harness/Catcrafts.E2eHarness" };
const std::string serverBin = (cfg.BinDir() / cfg.outputName).string();
cfg.AddTest("ShouldServeRoutes", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldStayScriptFree", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldEmitStructuredData", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldBootWasmAtDepth", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldServePostPages", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldServeFinancialsLive", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
cfg.AddTest("ShouldSellTheShopFront", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin });
// The full order lifecycle signs real invoices (ephemeral key) and
// waits out the reconciler and mailer cadences, so it both needs gpg
// and deserves more than the 60 s default.
cfg.AddTest("ShouldProcessCheckout", harness)
.Dependencies({ &cfg, shared, network }).Args({ serverBin })
.Requires("tool:gpg")
.Timeout(std::chrono::seconds(180));
return cfg;
}