test_cripts runs the cripts objects against the real plugin API and real header heaps, without starting a server. Nothing is stubbed: every TS API call a test makes goes to the same implementation traffic_server uses.
The two targets play the parts of traffic_server and a cript:
| Process | Loads with dlopen() | Which links | |
|---|---|---|---|
| In production | traffic_server | a compiled cript | libcripts |
| In the tests | test_cripts | test_cripts_cases.so | libcripts |
Inside the test_cripts process, in order:
test_cripts executable: main.cc, plus the core and tsapi_objects (the real TS API), symbols exported
main()
1. set up the core: pfreelist off, Diags, http_init(), api_init()
2. dlopen("test_cripts_cases.so") ------------------+
3. Catch::Session().run() runs the cases |
^ v
| test_cripts_cases.so module: the test_*.cc files
| - links libcripts; its static initializers run now, after step 1
+---------------- - each TEST_CASE registers with libCatch2 as the module loads
- its TS API calls, and libcripts', bind to symbols in test_cripts
Both targets link the same shared libCatch2, so the cases the module registers in step 2 are the ones step 3 runs.
libcripts' static initializers read state that the core sets up before it loads any plugin:
proxy object looks up the overridable configurations with TSHttpTxnConfigFind(). That reads a table the core builds in its own static initializers. A shared library‘s initializers run before the executable’s, so linking libcripts into the test executable would run the lookup against an empty table and TSFatal before main().cripts::Method constants copy the API's well-known method strings, which api_init() sets. Loaded any earlier, every constant would be empty.A real cript is dlopen()ed after the core is up, and the tests do the same.
The module must not link the core libraries. If it did, it would get private copies of the core's globals, for example the header token tables that http_init() sets up in the executable. It would then run against copies that were never initialized.
The cases register in the module, and main() runs them. That only works with one Catch2 registry for both, so libCatch2 has to be a shared library. lib/CMakeLists.txt builds it that way, and src/cripts/CMakeLists.txt stops the configure if that ever changes, because a static Catch2 would silently run no tests.
main()main() sets up the core, as traffic_server does before it loads plugins, and only then loads the cases:
cmd_disable_pfreelist keeps the allocators off the thread local free lists.EThread, so this_thread() gives the cripts Context and continuation allocations a thread, as on a net thread.Diags writes to stderr, so TSWarning(), TSError() and TSFatal() from the code under test print their messages instead of crashing.http_init() sets up the well-known header strings and the parser tables.api_init() sets the API's well-known strings, such as TS_HTTP_METHOD_GET.CriptsTestTxn.h)The cripts header and URL objects reach the request through calls such as TSHttpTxnClientReqGet(txnp, ...). These only check that txnp is a live HttpSM and return one of its t_state.hdr_info headers. cripts_test::Txn provides exactly that, without running the state machine:
HttpSM marked alive, with the HTTP configuration attached;set_server_response());cripts::Context for that transaction.A test case declares context the same way a cript has it, so the cripts macros work unchanged:
TEST_CASE("Header: add a field", "[cripts][Headers]") { cripts_test::Txn txn{"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n"}; auto context = txn.context(); borrow req = cripts::Client::Request::Get(); req["X-New"] = "value"; CHECK(txn.client_request_field("X-New") == "value"); }
When checking writes, check what the core holds rather than what cripts reads back:
client_request_field(), client_request_field_count() and client_url() read the core's HTTPHdr and URL directly, so a change that only lives in a cripts cache still fails the test.sm() gives access to the rest of the transaction state.cripts_test::str() turns a cripts component into a std::string. Catch2 needs this to print values, because the components only convert through a non-const GetSV().To test what happens across hooks, call context->reset(). It is what the Epilogue does on entering each hook. Code that depends on the current hook needs context->state.hook set first. For example, response status writes go to the header only in the response hooks.
test_<Area>.cc file. Add the file to the test_cripts_cases sources in src/cripts/CMakeLists.txt.add_catch2_test) and runs in its own process, so a case that aborts does not take the others with it. Cases must not depend on each other.LINK_DEPENDS), which is what makes ctest discover them.Run the tests with:
ctest --test-dir <build dir> -R test_cripts
To run a single case:
<build dir>/src/cripts/test_cripts "Header: add a field"
There is no event loop, no network connection and no session: ssnp is null, and there is no server VC. Hooks never fire, so the Epilogue's hook dispatch is not exercised. Behaviour that needs any of these belongs in an AuTest under tests/gold_tests/cripts/ instead.