motorcortex-rust 0.5.0

Motorcortex Rust: a Rust client for the Motorcortex Core real-time control system (async + blocking).
Documentation
# 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.