blob: 729f3b824725428d9f8a4049172ae374f7638e23 [file] [view]
# Cripts unit tests
`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.
## Layout
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.
### Why the cases are a dlopen()ed module
libcripts' static initializers read state that the core sets up before it loads any plugin:
- The global `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()`.
- The `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.
### Why the module takes only the core's headers
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.
### Why libCatch2 must be shared
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.
### Setup in `main()`
`main()` sets up the core, as `traffic_server` does before it loads plugins, and only then loads the cases:
- There are no event threads, so `cmd_disable_pfreelist` keeps the allocators off the thread local free lists.
- The main thread is registered as an `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`.
## The test transaction (`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:
- an `HttpSM` marked alive, with the HTTP configuration attached;
- a client request parsed by the real parser, and optionally a server response (`set_server_response()`);
- a `cripts::Context` for that transaction.
A test case declares `context` the same way a cript has it, so the cripts macros work unchanged:
```cpp
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.
## Adding tests
- Put new cases in a `test_<Area>.cc` file. Add the file to the `test_cripts_cases` sources in
`src/cripts/CMakeLists.txt`.
- Each Catch2 case is registered as its own ctest (`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.
- New cases only change the module. The executable is relinked when the module changes (`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"
```
## Limits
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.