| semantic-links |
|
|---|
This directory contains main application-level integration tests. These tests verify behavior that can only be tested by running the complete Torrust Tracker application with multiple services coordinated through the application container.
Integration tests at this level should focus on application-level concerns:
- Multiple tracker instances: Running HTTP and UDP trackers simultaneously on different ports
- Global metrics aggregation: Metrics that aggregate data across all running tracker instances
- Application container lifecycle: Container initialization, service registration, shutdown coordination
- Job manager orchestration: Background jobs interacting with multiple services
- Cross-service coordination: Interactions between HTTP API, trackers, and core services
- Bootstrap and configuration: Application startup with complex multi-service configurations
- Health check aggregation: Health status across all registered services
Most tests should be in the corresponding packages/*/tests/ directories:
- Single-service behavior: Test HTTP tracker logic in
packages/axum-http-server/tests/ - Protocol parsing: Test in
packages/http-protocol/tests/orpackages/udp-protocol/tests/ - Core tracker logic: Test in
packages/tracker-core/tests/ - Database operations: Test in
packages/swarm-coordination-registry/tests/ - API endpoints: Test in
packages/axum-rest-api-server/tests/ - Individual component behavior: Always prefer package-level tests for isolated components
Guideline: If the test can be written at the package level, it should be. Only use main-level integration tests when you genuinely need the full application context.
Each top-level Rust source file in tests/ is a separate Cargo integration-test
executable (and therefore a separate operating-system process). A single test
executable manages one tracker application instance with a fixed initial
configuration. Scenario functions run sequentially against that instance.
A different initial configuration requires a separate top-level file. For example:
| File | Purpose |
|---|---|
tests/aggregate_stats_port_zero.rs |
Aggregate statistics with port-zero listeners (two HTTP nodes) |
tests/aggregate_stats_fixed_ports.rs |
Aggregate statistics with distinct fixed-port HTTP listeners |
tests/scaffold.rs |
Scaffolding demo — same pattern, isolated process |
Each binary defines a single #[tokio::test] runner that starts the tracker
once, then calls scenario functions sequentially. Scenario functions are plain
async functions that receive the AppContainer and assert behavior.
Cargo may run these binaries in parallel. Each binary binds to port 0
(OS-assigned ephemeral ports) by default, uses its own TempDir workspace,
and sets TORRUST_TRACKER_CONFIG_TOML_PATH only in its own process, so no
conflict occurs. Fixed-port binaries (e.g., aggregate_stats_fixed_ports.rs)
use distinct non-overlapping ports and must not run concurrently with other
binaries that use the same ports.
The 1:1 mapping between integration-test binaries and tracker configurations exists because the current application startup has several global side effects that prevent running multiple isolated tracker instances in the same process:
tracingglobal initialization (main blocker): Thetracingcrate initializes a global subscriber. Once set, it cannot be reset for a second tracker instance in the same process. This means two tracker applications sharing a process would share logging state and configuration.- Environment-variable config injection: The tracker reads its
configuration from the
TORRUST_TRACKER_CONFIG_TOML_PATHenvironment variable. Multiple tracker instances in the same process would race on this variable. - Static secrets and clock state: Values like seed secrets and the deterministic test clock are process-global. While these could be refactored into injected dependencies, the tracing global subscriber remains the fundamental blocker.
Until these global side effects are eliminated (tracked in #1430), each integration-test binary must start exactly one tracker instance with one fixed configuration. Scenario functions run sequentially against that shared instance.
All integration tests at this level must:
- Use port
0for bind addresses by default: The OS assigns free ephemeral ports, preventing conflicts when tests run in parallel. Fixed ports are permitted when the test scenario specifically requires distinct addresses (e.g., verifying per-instance behavior). Use non-overlapping port ranges and document the constraint. - Use isolated temporary workspaces: Use
tempfile::TempDirto create isolated directories with separate config files and storage subdirectories - Extract actual bound ports: Query
AppContainer'sRegistarto get the OS-assigned ports for making requests - Be independent: Each top-level test binary must be able to run in isolation or concurrently with others (it is the binary, not the function, that is the unit of isolation)
- Clean up resources: Use RAII patterns (temp dirs, handles) for automatic cleanup
tests/
├── AGENTS.md # This file
├── common/
│ ├── mod.rs # Re-exports from submodules
│ ├── workspace.rs # Tracker workspace setup and URL discovery
│ ├── announce.rs # HTTP and UDP announce helpers
│ └── statistics.rs # Aggregate statistics query helpers
├── aggregate_stats_port_zero.rs # Port-zero statistics (two HTTP + two UDP nodes)
├── aggregate_stats_fixed_ports.rs # Fixed-port statistics (two HTTP + two UDP nodes)
└── scaffold.rs # Scaffolding demo — pattern reference for new binaries
- Confirm it belongs here: Can this test be written at the package level? If yes, write it there.
- Determine the initial configuration: If your scenarios need a different tracker
configuration than the existing suite, create a new top-level file (e.g.,
tests/aggregate_stats_fixed_ports.rs). If they share the same configuration, add scenarios to the existing suite's runner function. - Reuse shared utilities: Import
mod common;and use the helpers intests/common/mod.rsfor workspace setup, tracker startup, and port discovery. - Use port
0by default: Bind services to port0unless the scenario specifically requires distinct fixed addresses. - Extract bound ports: Query the registar or
AppContainerto discover actual socket addresses. - Document the purpose: Add clear doc comments explaining what application-level behavior is being tested.
- Reference existing code: See
tests/aggregate_stats_fixed_ports.rsfor the canonical pattern: one#[tokio::test]runner, one config constant, scenario functions that receive theAppContainer.
- Issue #1419 - Infrastructure for parallel integration tests (execution model decision)
- Integration test scaffolding
- Shared test utilities
- Scaffolding demo