This commit is contained in:
parent
33c68c2f44
commit
749f525f83
44 changed files with 5380 additions and 3532 deletions
160
project.cpp
160
project.cpp
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue