# motorcortex-rust test suite
Three tiers:
- **`cargo test --lib`** — `#[cfg(test)] mod tests` blocks inside
`src/`, exercising crate-private items (proto helpers, dtype
dispatchers, `Subscription` protocol-1 decoding, `ConnectionState`,
URL parsing). Offline, hermetic, no network.
- **`cargo test --test unit`** — `tests/unit.rs`, offline tests that
only use the public crate surface (parameter-value trait coverage,
`Parameters` impls, `TimeSpec` layout, message hash/encode
round-trips). Also hermetic.
- **`cargo test --test integration -- --test-threads=1`** — drives
the vendored C++ `tests/server` over real sockets. Requires the
server binary to be built first.
## Quick start
```bash
# 1. Offline (lib + unit).
cargo test --lib
cargo test --test unit
# 2. Build the test_server (once).
cmake -S tests/server -B tests/server/build -DCMAKE_BUILD_TYPE=Release
cmake --build tests/server/build -j
# 3. Integration (needs the compiled test_server; cert lives in the repo).
# --test-threads=1 serialises tests — they share a single server
# instance and the same `root/Control/dummy*` parameters.
cargo test --test integration -- --test-threads=1
# 4. Merged coverage via cargo-llvm-cov.
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov --locked
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --lib
cargo llvm-cov --no-report --test unit
cargo llvm-cov --no-report --test integration -- --test-threads=1
cargo llvm-cov report --summary-only \
--ignore-filename-regex 'src/msg/motorcortex_msg\.rs|/build\.rs|examples/'
```
Current branch target: **≥ 90 % regions** merged. Last measurement
on `async-rewrite`: 94.95 %.
## Layout
```
tests/
├── README.md # this file
├── unit.rs # offline tests via the public API
├── integration.rs # integration binary entry point
├── integration/ # submodules (pulled in via #[path])
│ ├── async_connect.rs # Request connect/disconnect
│ ├── async_session.rs # login/logout cycle
│ ├── async_tree.rs # tree fetch, tree-hash, connect_to
│ ├── async_parameters.rs # get/set + batch + broad type coverage
│ ├── async_groups.rs # create/remove_group
│ ├── async_subscribe.rs # notify / latest / stream
│ └── blocking.rs # blocking façade smoke tests
├── common/ # shared helpers
│ ├── mod.rs # re-exports for tests/integration.rs
│ ├── parameters.rs # test_values / test_vectors (async)
│ └── setup.rs # ctor::ctor / ctor::dtor — spawns test_server
└── server/ # vendored C++ test_server
├── CMakeLists.txt
├── main.cpp
├── control/ # MainControlLoop
└── config/ # config.json, parameters.json, …
```
`tests/integration.rs` uses `#[path = "integration/..."]` on each
submodule because Rust would otherwise try to resolve `mod integration;`
to both the file and the `integration/` directory — see the comment at
the top of the file.
## TLS test certs
Two certs live in the repo, mirroring motorcortex-python:
- `tests/server/config/motorcortex.pem` — server cert + private key,
loaded by `test_server` via `config.json` (relative filename).
- `tests/mcx.cert.crt` — self-signed Motorcortex Root CA that signed
the server cert. `tests/integration.rs` points `CERT_PATH` at it
via `CARGO_MANIFEST_DIR`, so the client can verify the TLS
handshake.
Both are valid for `localhost` (the server cert's SAN covers it).
## Writing new integration tests
1. Create `tests/integration/<name>.rs`.
2. Register it in `tests/integration.rs`:
```rust
#[path = "integration/<name>.rs"]
mod <name>;
```
3. Use the public `core::*` or `blocking::*` types directly; the
`common::setup` module activates the `ctor::ctor` / `ctor::dtor`
hooks that spawn / reap the vendored `test_server` once per binary
— don't import `common::setup` explicitly.
```rust
use motorcortex_rust::core::Request;
use motorcortex_rust::ConnectionOptions;
use crate::{CERT_PATH, URL_REQ};
#[tokio::test]
async fn test_something() {
let opts = ConnectionOptions::new(CERT_PATH.to_string(), 5000, 5000);
let req = Request::connect_to(URL_REQ, opts).await.expect("connect");
req.disconnect().await.expect("disconnect");
}
```
For broad-type-coverage round-trips, call `test_values(&req).await`
or `test_vectors(&req).await` from `crate::common::parameters`.
## Known gaps
- **Parallelism hazard for integration tests.** Every integration
test writes into the same `root/Control/dummy*` parameter namespace
on the single `test_server` instance, so a plain `cargo test`
(Rust's default is N threads) races them and fails intermittently
on the get/set round-trips. Always run integration with
`-- --test-threads=1`. CI does this in `.gitlab-ci.yml` already.
Fixing would require per-test parameter namespaces on the server
side.
- **`connection/connection_manager.rs` error branches** aren't
covered. NNG's `rv != 0` returns are gated behind conditions we
can't trigger without mocking — `nng_tls_config_alloc` failure,
`nng_send` failure mid-session, etc. Coverage on that file sits
at ~73 %; the remaining gap is entirely these paths. Would need
fault-injection infrastructure to address.
## Coverage
Coverage is driven by
[`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov). The CI
runs all three suites with `--no-report` so the raw shards stay
under `target/llvm-cov-target/`, then the `coverage` stage merges
them into a Cobertura `coverage.xml`. See `.gitlab-ci.yml` for the
exact invocation.
The `LLVM_COV_IGNORE` regex excludes `src/msg/motorcortex_msg.rs`
(prost-generated, not hand-written) and `build.rs` from the merged
percentage.