|
| 1 | +# Subissue Draft for #1525-02: Add qBittorrent End-to-End Test |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Add a high-level end-to-end test that validates tracker behavior through a complete torrent-sharing |
| 6 | +scenario using real containerized BitTorrent clients, covering scenarios that lower-level unit and |
| 7 | +integration tests cannot reach. |
| 8 | + |
| 9 | +## Why Before the Refactor |
| 10 | + |
| 11 | +The persistence refactor changes storage behavior underneath the tracker. Having a real-client |
| 12 | +scenario that exercises a full download cycle (seeder uploads → leecher downloads → tracker |
| 13 | +records completion) gives a regression backstop that is not possible with protocol-level tests |
| 14 | +alone. |
| 15 | + |
| 16 | +## Scope |
| 17 | + |
| 18 | +- Follow the same pattern as the existing `e2e_tests_runner` binary |
| 19 | + (`src/console/ci/e2e/runner.rs`): a Rust binary that drives the whole scenario using |
| 20 | + `std::process::Command` to invoke `docker compose` and any container-side commands. |
| 21 | +- Use SQLite as the database backend; database compatibility across multiple versions is already |
| 22 | + covered by subissue #1525-01. |
| 23 | +- Cover one complete scenario: a seeder sharing a torrent that a leecher downloads in full. |
| 24 | +- The binary is responsible for scaffolding (generating a temporary config and torrent file), |
| 25 | + starting the services, sending commands into the qBittorrent containers (via their WebUI API |
| 26 | + or `docker exec`), polling for completion, asserting the result, and tearing down. |
| 27 | +- Do not re-test things already covered at a lower level: announce parsing, scrape format, |
| 28 | + whitelist/key logic, or multi-database compatibility. |
| 29 | + |
| 30 | +## Testing Principles |
| 31 | + |
| 32 | +The implementation must follow these quality rules. |
| 33 | + |
| 34 | +- **Isolation**: Each run of the E2E binary must be isolated from any other concurrently running |
| 35 | + instance. Achieve this by using a unique Docker Compose project name per run (e.g. |
| 36 | + `--project-name qbt-e2e-<random-suffix>`) so container names, networks, and volumes never |
| 37 | + collide with a parallel run. |
| 38 | +- **Independent system resources**: Do not bind services to fixed host ports. Let Docker assign |
| 39 | + ephemeral host ports and discover them from the compose output, so two simultaneous runs cannot |
| 40 | + conflict. Place all temporary files (tracker config, payload, `.torrent` file) in a |
| 41 | + `tempfile`-managed directory created at runner start and deleted on exit. |
| 42 | +- **Cleanup**: `docker compose down --volumes` must be called unconditionally — on success, on |
| 43 | + assertion failure, and on panic. Use a Rust `Drop` guard or equivalent to guarantee teardown |
| 44 | + even when the runner exits unexpectedly. |
| 45 | +- **Mock time when possible**: Use a configurable timeout (CLI argument or env var) for the |
| 46 | + leecher-completion poll rather than a hard-coded sleep. If any logic depends on wall-clock time |
| 47 | + (e.g. stale peer detection), inject a mockable clock consistent with the `clock` package used |
| 48 | + elsewhere in the codebase. |
| 49 | +- **Behavior, not implementation**: Assert the outcome the user cares about — the leecher holds a |
| 50 | + complete, byte-identical copy of the payload — not which internal tracker counters changed or |
| 51 | + which announce endpoints were called. |
| 52 | +- **Verified before done**: The binary must be executed end-to-end and produce a passing result in |
| 53 | + a clean environment before the subissue is closed. Include a run log in the PR description. |
| 54 | + |
| 55 | +## Reference QA Workflow |
| 56 | + |
| 57 | +`contrib/dev-tools/qa/run-qbittorrent-e2e.py` in the PR #1695 review branch demonstrates the |
| 58 | +scenario (seeder + leecher + tracker via Python subprocess). Treat it as a behavioral reference |
| 59 | +only; the implementation here will use `docker compose` instead of manual container management. |
| 60 | + |
| 61 | +## Proposed Branch |
| 62 | + |
| 63 | +- `1525-02-qbittorrent-e2e` |
| 64 | + |
| 65 | +## Tasks |
| 66 | + |
| 67 | +### 1) Add a docker compose file for the E2E scenario |
| 68 | + |
| 69 | +Add a compose file (e.g., `compose.qbittorrent-e2e.yaml`) that defines: |
| 70 | + |
| 71 | +- the tracker service configured with SQLite |
| 72 | +- a qbittorrent-seeder container |
| 73 | +- a qbittorrent-leecher container |
| 74 | + |
| 75 | +Steps: |
| 76 | + |
| 77 | +- Define a tracker service mounting a SQLite config file (generated by the runner). |
| 78 | +- Define seeder and leecher services using a suitable qBittorrent image. |
| 79 | +- Configure a shared network so all containers can reach each other and the tracker. |
| 80 | +- Define any volumes needed to mount the payload and torrent file into each client container. |
| 81 | +- Ensure `docker compose up --wait` exits cleanly when services are healthy. |
| 82 | +- Ensure `docker compose down --volumes` removes all containers and volumes. |
| 83 | + |
| 84 | +Acceptance criteria: |
| 85 | + |
| 86 | +- [ ] `docker compose -f compose.qbittorrent-e2e.yaml up --wait` starts all services without error. |
| 87 | +- [ ] `docker compose -f compose.qbittorrent-e2e.yaml down --volumes` leaves no orphaned resources. |
| 88 | + |
| 89 | +### 2) Implement the Rust runner binary |
| 90 | + |
| 91 | +Add a new binary (e.g., `src/bin/qbittorrent_e2e_runner.rs`) that follows the same structure as |
| 92 | +`src/console/ci/e2e/runner.rs`: |
| 93 | + |
| 94 | +- Parses CLI arguments or environment variables (compose file path, payload size, timeout). |
| 95 | +- Generates scaffolding: a temporary tracker config (SQLite) and a small deterministic payload |
| 96 | + with its `.torrent` file. |
| 97 | +- Calls `docker compose up` via `std::process::Command`. |
| 98 | +- Seeds the payload: injects the torrent and payload into the seeder container via the qBittorrent |
| 99 | + WebUI REST API (or `docker exec` as a fallback) and starts seeding. |
| 100 | +- Leaches the payload: injects the `.torrent` file into the leecher container and starts |
| 101 | + downloading. |
| 102 | +- Polls for completion: queries the leecher's WebUI API until the torrent state reaches |
| 103 | + `uploading` (100 % downloaded) or a timeout expires. |
| 104 | +- Asserts payload integrity: compares the downloaded file against the original (hash or byte |
| 105 | + comparison). |
| 106 | +- Calls `docker compose down --volumes` unconditionally (even on assertion failure), mirroring |
| 107 | + the cleanup pattern in `tracker_container.rs`. |
| 108 | + |
| 109 | +Steps: |
| 110 | + |
| 111 | +- Add a shared `docker compose` wrapper at `src/console/ci/compose.rs` (see below). This |
| 112 | + module is not specific to qBittorrent and is reused by the benchmark runner in subissue |
| 113 | + `#1525-03`. |
| 114 | +- Add a `qbittorrent` module under `src/console/ci/` (parallel to `e2e/`) containing: |
| 115 | + - `runner.rs` — main orchestration logic |
| 116 | + - `qbittorrent_client.rs` — HTTP calls to the qBittorrent WebUI API |
| 117 | +- **`src/console/ci/compose.rs` wrapper** — mirrors `docker.rs` but targets `docker compose` |
| 118 | + subcommands. Design it around a `DockerCompose` struct that holds the compose file path and |
| 119 | + project name: |
| 120 | + - `DockerCompose::new(file: &Path, project: &str) -> Self` |
| 121 | + - `up(&self) -> io::Result<()>` — runs `docker compose -f <file> -p <project> up --wait --detach` |
| 122 | + - `down(&self) -> io::Result<()>` — runs `docker compose -f <file> -p <project> down --volumes` |
| 123 | + - `port(&self, service: &str, container_port: u16) -> io::Result<u16>` — runs |
| 124 | + `docker compose -f <file> -p <project> port <service> <port>` and parses the host port so |
| 125 | + the runner never hard-codes ports |
| 126 | + - `exec(&self, service: &str, cmd: &[&str]) -> io::Result<Output>` — wraps |
| 127 | + `docker compose -f <file> -p <project> exec <service> <cmd…>` for injecting commands into |
| 128 | + running containers |
| 129 | + - Implement `Drop` on a `RunningCompose` guard returned by `up` that calls `down` |
| 130 | + unconditionally, matching the `RunningContainer::drop` pattern in `docker.rs` |
| 131 | + - Use `tracing` for progress output consistent with the rest of the runner |
| 132 | +- Generate a fixed small payload (e.g., 1 MiB of deterministic bytes) at runtime; store the |
| 133 | + `.torrent` file in a `tempfile` directory so it is cleaned up automatically. |
| 134 | +- Re-use `tracing` for progress output, consistent with the existing runner. |
| 135 | + |
| 136 | +Acceptance criteria: |
| 137 | + |
| 138 | +- [ ] The runner completes a full seeder → leecher download using the containerized tracker. |
| 139 | +- [ ] Payload integrity is verified after download (hash or byte comparison). |
| 140 | +- [ ] The runner can be executed repeatedly without manual setup or teardown. |
| 141 | +- [ ] No orphaned containers or volumes remain on success or failure. |
| 142 | +- [ ] The binary is documented in the top-level module doc comment with an example invocation. |
| 143 | +- [ ] Each invocation uses a unique compose project name so parallel runs do not conflict. |
| 144 | +- [ ] All temporary files are placed in a managed temp directory and deleted on exit. |
| 145 | +- [ ] No fixed host ports are used; ports are discovered dynamically from the compose output. |
| 146 | +- [ ] `docker compose down --volumes` is called unconditionally via a `Drop` guard. |
| 147 | + |
| 148 | +### 3) Document the E2E workflow |
| 149 | + |
| 150 | +Steps: |
| 151 | + |
| 152 | +- Document the local invocation command (e.g., `cargo run --bin qbittorrent_e2e_runner`). |
| 153 | +- Document any prerequisites (Docker, image availability, open ports). |
| 154 | +- Clarify that this test is not run in the standard `cargo test` suite due to resource |
| 155 | + requirements and describe how it is triggered in CI (opt-in env var or separate job). |
| 156 | + |
| 157 | +Acceptance criteria: |
| 158 | + |
| 159 | +- [ ] The test is documented and runnable without ad hoc manual steps. |
| 160 | + |
| 161 | +## Out of Scope |
| 162 | + |
| 163 | +- Testing multiple database backends (covered by subissue #1525-01). |
| 164 | +- Testing announce or scrape protocol correctness at the protocol level. |
| 165 | +- UDP tracker E2E (can be added later without redesigning the compose setup). |
| 166 | + |
| 167 | +## Definition of Done |
| 168 | + |
| 169 | +- [ ] `cargo test --workspace --all-targets` passes (or the E2E test is explicitly excluded with a |
| 170 | + documented opt-in flag). |
| 171 | +- [ ] `linter all` exits with code `0`. |
| 172 | +- [ ] The E2E runner has been executed successfully in a clean environment; a passing run log is |
| 173 | + included in the PR description. |
| 174 | + |
| 175 | +## References |
| 176 | + |
| 177 | +- EPIC: #1525 |
| 178 | +- Reference PR: #1695 |
| 179 | +- Reference implementation branch: `josecelano:pr-1684-review` — see EPIC for checkout |
| 180 | + instructions (`docs/issues/1525-overhaul-persistence.md`) |
| 181 | +- Reference script: `contrib/dev-tools/qa/run-qbittorrent-e2e.py` |
| 182 | +- Existing runner pattern: `src/console/ci/e2e/runner.rs` |
| 183 | +- Docker command wrapper: `src/console/ci/e2e/docker.rs` |
| 184 | +- Existing container wrapper patterns: `src/console/ci/e2e/tracker_container.rs` |
0 commit comments