mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-08-06 23:03:13 +00:00
93 lines
6.4 KiB
Markdown
93 lines
6.4 KiB
Markdown
|
|
# Framework vs Application Boundaries
|
||
|
|
|
||
|
|
This chapter covers the rules that keep the framework application-agnostic and how they are enforced in practice.
|
||
|
|
|
||
|
|
[Ownership and Design Boundaries](boundaries.md) explains the ownership split. This chapter covers the concrete rules, enforcement mechanisms, and signs that code is in the wrong layer.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## The Rule
|
||
|
|
|
||
|
|
Dependencies point in exactly one direction: application repositories depend on framework crates, never the reverse. The framework knows applications only through the traits in [Public Extension Points](extension-points.md); everything app-specific (node configs, HTTP clients, readiness semantics, observers, workloads) lives in the application's own integration crates.
|
||
|
|
|
||
|
|
The in-repo `examples/` workspace models the application side: each app has an integration crate (`<app>-runtime-ext`) implementing `Application` and the per-backend environment traits, and a workloads crate (`<app>-runtime-workloads`) implementing `Workload` and `Expectation`. No framework crate names an example app in its `Cargo.toml`; see the dependency diagram in [Crate and API Map](crate-map.md).
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
graph LR
|
||
|
|
subgraph Application side
|
||
|
|
ext["<app>-runtime-ext<br/>Application, env impls, Observer"]
|
||
|
|
wl["<app>-runtime-workloads<br/>Workload, Expectation"]
|
||
|
|
end
|
||
|
|
subgraph Framework side
|
||
|
|
core[testing-framework-core]
|
||
|
|
appl[testing-framework-app]
|
||
|
|
runners[deployers]
|
||
|
|
end
|
||
|
|
ext --> core
|
||
|
|
ext --> runners
|
||
|
|
wl --> core
|
||
|
|
appl --> core
|
||
|
|
runners --> core
|
||
|
|
```
|
||
|
|
|
||
|
|
Traits cross the boundary; concrete application types never do.
|
||
|
|
|
||
|
|
**What belongs where:**
|
||
|
|
|
||
|
|
| Concern | Framework | Application repo |
|
||
|
|
|---|---|---|
|
||
|
|
| Process supervision, port allocation, tempdirs, teardown ordering | yes | — |
|
||
|
|
| Scenario scheduling, expectations lifecycle, readiness/retry policy | yes | — |
|
||
|
|
| Observation runtime (cycles, history, failure tracking) | yes | — |
|
||
|
|
| `Application` impl, node config types, config rendering | — | yes |
|
||
|
|
| Domain node clients and typed app handles | — | yes |
|
||
|
|
| Readiness closures with domain semantics (leader elected, stream exists) | — | yes |
|
||
|
|
| `Observer` impls and their snapshot/event types | — | yes |
|
||
|
|
| Binary provider *configuration* (env var names, build commands) | — | yes |
|
||
|
|
| Config templates for a specific application | — | yes |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Enforcement Mechanisms
|
||
|
|
|
||
|
|
**The type system.** The scenario engine is generic over `Application`, so core code physically cannot reference your node client or config, because there is no concrete type to name. The app layer goes further: `AppHostEnv` sets `NodeClient = ()` and its `build_node_client` returns an error, forcing application clients to travel as typed handles owned by the app side rather than leaking into the environment.
|
||
|
|
|
||
|
|
**Runtime registration errors.** Two rules are enforced with hard errors instead of silent replacement:
|
||
|
|
|
||
|
|
- Registering two runtime extension factories that produce the same type fails at prepare time with `duplicate runtime extension type registered: ...`. This is also why a scenario takes exactly one `with_app`; compose multiple apps inside one root `AppDeployment` instead.
|
||
|
|
- Exposing a handle twice under the same type and name fails with `app handle is already exposed: ...` (`AppDeployError::DuplicateHandle`).
|
||
|
|
|
||
|
|
**The boundary check script.** `scripts/run/check-boundaries.sh` guards the adopter side of the line. What it actually does:
|
||
|
|
|
||
|
|
- resolves a sibling adopter checkout at `../nomos-node/tests/testing_framework/lb-topology` and fails if it is missing;
|
||
|
|
- greps that crate's `src/` and `Cargo.toml` for extension-specific identifiers (`cfgsync`, `ComposeDeployEnv`, `K8sDeployEnv`, `runner-compose`, `runner-k8s`, `DEFAULT_CFGSYNC_PORT`, `DEFAULT_ASSETS_STACK_DIR`) and fails on any hit.
|
||
|
|
|
||
|
|
The same rule can be applied to other integration crates: a topology-level crate remains backend-agnostic, so references to a specific deployer or cfgsync internals are treated as violations. Backend names belong in the per-backend environment modules; compare `local_env.rs`, `compose_env.rs`, and `k8s_env.rs` in `examples/openraft_kv/testing/integration/src/`.
|
||
|
|
|
||
|
|
**Crate docs as contract.** `testing-framework-app` states the ownership boundary in its crate docs: implement `AppDeployment` in the application repository, compose children through `DeployContext`, and let handles own deployed resources. The `multi_app` README says the same from the other direction: for composed systems, prefer the app-layer shape "instead of building a fake outer cluster or adding app-specific code to TF".
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Signs Your Code Is on the Wrong Side
|
||
|
|
|
||
|
|
Symptoms that application code has leaked into the framework:
|
||
|
|
|
||
|
|
- A config template, launch flag, or port convention for one specific application sitting in `testing-framework/` or `cfgsync/`.
|
||
|
|
- A framework crate importing an example (or adopter) crate, or matching on an application name.
|
||
|
|
- A "generic" helper in core whose only caller is one app and whose parameters mirror that app's config fields.
|
||
|
|
|
||
|
|
Symptoms that framework mechanics are being re-implemented in the application repo:
|
||
|
|
|
||
|
|
- Hand-rolled process spawn/kill/teardown code where `LocalProcessApp` or `LocalAppCluster` would do.
|
||
|
|
- A custom polling loop with history and error tracking that duplicates the observation runtime; implement `Observer` instead.
|
||
|
|
- Re-implementing binary resolution, caching, or fallback chains instead of configuring `BinaryProvider` types.
|
||
|
|
- A bespoke "wait until cluster healthy" loop instead of readiness closures plus `DeploymentPolicy` (see [Readiness, Retry, and Artifact Preservation](deployment-policies.md)).
|
||
|
|
|
||
|
|
A framework addition should compile and make sense with a different application plugged in. Application-specific code belongs in the application repository.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Backend Scope of the App Layer
|
||
|
|
|
||
|
|
The composition layer is local-only today, and this is visible in the dependency graph: `testing-framework-app` depends on core and `testing-framework-runner-local` only, `AppHostLocalDeployer` is an alias for `ProcessDeployer<AppHostEnv>`, and `DeployContext::deploy_local_cluster` / `LocalAppCluster` require `LocalDeployerEnv`. The compose and k8s deployers remain single-application. Do not work around this by teaching the framework about your app's containers: run composed stacks locally, and use the [Compose](deployer-compose.md) or [Kubernetes](deployer-k8s.md) deployer for uniform clusters. Details in [Backend Scope](app-backend-scope.md).
|