The project has four test suites:
| Suite | Scope | Command | Requires |
|---|---|---|---|
| ROS node unit tests | All ROS 2 nodes (mocked ROS stack) | make test |
— |
| S3 GC unit tests | GC logic with mocked S3 client | make test-gc |
— |
| S3 storage integration tests | Upload, discovery, GC against real S3 (MinIO) | make test-storage |
Docker (MinIO) |
| Gate integration tests | Recording gate with live ROS 2 stack | make test-gate |
Docker (ROS 2 image) |
Run all integration tests at once with make test-integration.
Business logic is extracted from ROS 2 nodes into pure-Python modules so it can be tested without the full ROS mock stack:
| Pure module | Extracted from | Functions |
|---|---|---|
data_backend/upload.py |
recording_uploader.py |
upload_recording(), recover_pending_uploads(), retention_cleanup(), _resolve_s3_prefix() |
data_backend/canonical_path.py |
— (new) | CanonicalPathFields — metadata-derived S3 path computation |
data_backend/gc.py |
remote_gc_node.py |
GCConfig, discover_recordings(), run_gc_cycle_with_result(), compute_diagnostic_level() |
data_backend/upload_state.py |
— (new) | UploadStateDB — SQLite per-recording × per-target state tracking |
recording_gate.py |
recording_gate_node.py |
EscalationLevel, ConditionResult, ConditionChecker, load_gate_config(), evaluate_gate() |
The ROS nodes become thin wrappers: parameters, subscriptions, timers, and publishers stay in the node; all logic delegates to the extracted functions.
Unit tests (mocked ROS + mocked S3)
├── yubi-core/test/test_recording_gate.py — checker logic + config loading
├── yubi-core/test/test_scenario_recording_gate.py — 18 multi-step gate workflows
├── data-backend/tests/test_gc.py — GC logic with mock S3
├── data-backend/tests/test_upload.py — upload, recovery, retention, path rules
├── data-backend/tests/test_canonical_path.py — canonical path normalization + prefix
├── data-backend/tests/test_upload_state.py — SQLite state DB
├── data-backend/tests/test_scenario_gc.py — GC scenario workflows
├── data-backend/tests/test_scenario_upload.py — canonical/flat upload + GC interplay
└── data-backend/tests/test_scenario_upload_state.py — upload state scenario workflows
Integration tests (real services, no mocks)
├── data-backend/tests/test_integration_gc.py — GC discovery + deletion against real S3
├── data-backend/tests/test_integration_storage.py — full upload→GC lifecycle against real S3
└── yubi-core/test/test_integration_gate.py — gate node with live ROS 2 stack
make testAll ROS 2 and message dependencies are mocked via
conftest.py (mock_rclpy fixture). No ROS 2 installation required.
make test-gcUses mocked minio.Minio client. Integration tests are
excluded automatically (-m "not integration").
Scenario tests exercise realistic multi-step workflows through the gate and uploader pipelines. They use the same mock infrastructure as unit tests but test sequences of operations rather than individual methods.
| Test | Feature |
|---|---|
| Startup all-fail then gradual recovery | Fail-closed startup, per-checker recovery |
| Startup settle delay | settle_sec timer |
| Condition failure mid-recording | Re-settle trigger (recovery_sec) |
| Multiple escalation levels | Max escalation wins across checkers |
| Real checkers startup | TopicConditionChecker instances |
| Duration limit triggers stop | TopicConditionChecker with timeout_sec: -1.0 |
| Settle timeout expires with failures | Fail-safe settle completion |
| Config-driven gate construction (flat) | Legacy flat YAML format |
| Multi-group independent settling | Per-group settle_sec |
| Multi-group max escalation | Gate level = max across groups |
| TopicConditionChecker rate checking | min_rate_hz, rate_escalation |
| TopicConditionChecker absence grace period | absence_timeout_sec, absence_escalation |
| TopicConditionChecker content expression | condition field with msg.data < 100 |
| Debounce with gate integration | DebouncedChecker flap/reset/clear cycle |
| DiagnosticsErrorRate sliding window | Error accumulation, expiry, re-blocking |
| Config-driven grouped construction (v2) | Grouped YAML, default_type inheritance |
| Re-settle with different group timings | Per-group recovery_sec |
| TopicConditionChecker full lifecycle | All facets: absent → no msg → timeout → content → rate → pass |
Upload, retention, GC, and state-tracking workflows are exercised by the
data-backend scenario suites:
| Suite | Feature |
|---|---|
test_scenario_upload.py |
Flat/canonical upload, multi-target, GC interplay |
test_scenario_upload_state.py |
SQLite state transitions (pending → completed/failed), idempotent re-runs |
test_scenario_gc.py |
Strategy-based GC, orphan cleanup, retention |
Exercise the full upload and GC pipeline against a real MinIO instance.
Marked @pytest.mark.integration and auto-skip if MinIO is unreachable.
- Docker and Docker Compose
# Starts MinIO, runs tests, cleans up automatically
make test-storageDefined in docker-compose.test.yml:
| Setting | Value |
|---|---|
| Port | 19000 (avoids conflict with production 9000) |
| Console port | 19001 |
| Access key | testadmin |
| Secret key | testadmin123 |
| Bucket | test-gc-bucket (auto-created on startup) |
| Storage | Ephemeral (no persistent volume) |
Each test gets a unique S3 prefix (test_<name>_<uuid>/), cleaned up
after the test via the test_prefix fixture. Tests never interfere with
each other even when running in parallel.
| Test | What it exercises |
|---|---|
test_upload_and_discover |
upload_recording() → discover_recordings() round-trip |
test_enrich_reads_marker |
.recording_complete JSON → enrich_completed_at() parses timestamp |
test_enrich_fallback_no_marker |
Missing marker → falls back to latest_object_modified |
test_gc_cycle_deletes_eligible |
Age strategy deletes old eligible recordings, preserves ineligible |
test_gc_cycle_dry_run |
dry_run=True → no objects removed |
test_marker_json_roundtrip |
Upload marker → read back → verify JSON structure |
test_space_strategy_real |
Space strategy deletes oldest recordings first |
Exercise the recording gate node with a live ROS 2 stack inside the
project Docker image. Each test creates a real RecordingGateNode, publishes
real messages, and verifies gate level and diagnostics output.
# Build image and run tests (--build ensures latest code; cleans up afterward)
make test-gate
# Run with FastDDS instead of CycloneDDS
RMW_IMPLEMENTATION=rmw_fastrtps_cpp make test-gateGateTestHarness manages the test lifecycle:
- Writes a temporary gate config YAML
- Creates
RecordingGateNodewith the config - Creates a helper node that publishes test messages and subscribes to
gate output (
~/gate_level,/diagnostics) - Spins both nodes in a
MultiThreadedExecutoron a background thread - Provides
publish_*helpers andwait_for_gate_level()assertion
Tests pass on both CycloneDDS (default) and FastDDS. Override via the
RMW_IMPLEMENTATION environment variable.
| Test | Feature | Message types |
|---|---|---|
| Gate starts at HARD_STOP | Fail-closed behavior | — |
| Gate opens after settle | settle_sec with topic_condition |
Bool |
| Gate blocks on failure | Freshness timeout + recovery | Bool |
| Diagnostics per condition | DiagnosticArray format, hardware_id |
DiagnosticArray |
| Latched QoS late subscriber | TRANSIENT_LOCAL durability | UInt8 |
| TopicHealth rate + expression | condition + min_rate_hz |
Float64 |
| TF availability | Multiple dynamic TF frames | TransformStamped |
| TF missing frame | Partial TF → gate blocked | TransformStamped |
| TF staleness | max_age_sec with dynamic TF |
TransformStamped |
| PoseStamped expression | msg.pose.position.x bounds |
PoseStamped |
| JointState expression + rate | msg.effort[0] + min_rate_hz |
JointState |
| Diagnostics error rate | Sliding window, expiry, recovery | DiagnosticArray |
| Multi-group settling | Independent settle_sec per group |
Bool |
| Debounce filtering | Flap resets timer | Bool |
| Escalation levels | Level 0, 1, 2 transitions | Bool |
| Rate + freshness escalation | rate_escalation=1 vs escalation=2 |
Float64 |
make test-integrationThis starts MinIO, runs MinIO integration tests, runs gate integration tests (builds Docker image), and cleans up. Exits with failure if any suite fails.
| Target | Description |
|---|---|
make test |
Run ROS node unit tests |
make test-gc |
Run S3 GC unit tests (excludes integration) |
make lint |
Run ruff linter and formatter check |
make test-storage |
Start MinIO, run S3 storage integration tests, clean up |
make test-gate |
Build Docker image, run gate integration tests, clean up |
make test-integration |
Run all integration tests (storage + gate) |