From 18bbf2485144abcd337a897711a473a737850e32 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 11 May 2026 16:18:27 +0100 Subject: [PATCH 1/3] feat(tracker-client): add udp response format option --- .../src/console/clients/udp/app.rs | 44 ++++- .../src/console/clients/udp/responses/json.rs | 51 ++++- ...nt-add-option-show-response-pretty-json.md | 175 ++++++++++++++++-- 3 files changed, 243 insertions(+), 27 deletions(-) diff --git a/console/tracker-client/src/console/clients/udp/app.rs b/console/tracker-client/src/console/clients/udp/app.rs index 5e1013427..2a979045f 100644 --- a/console/tracker-client/src/console/clients/udp/app.rs +++ b/console/tracker-client/src/console/clients/udp/app.rs @@ -5,7 +5,15 @@ //! Announce request (minimal): //! //! ```text -//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 +//! ``` +//! +//! Announce request (pretty JSON output): +//! +//! ```text +//! cargo run --bin udp_tracker_client announce \ +//! 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 \ +//! --format pretty //! ``` //! //! Announce request (all optional parameters): @@ -41,7 +49,15 @@ //! Scrape request: //! //! ```text -//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 +//! ``` +//! +//! Scrape request (pretty JSON output): +//! +//! ```text +//! cargo run --bin udp_tracker_client scrape \ +//! 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 \ +//! --format pretty //! ``` //! //! Scrape response: @@ -73,8 +89,8 @@ //! You can use an URL with instead of the socket address. For example: //! //! ```text -//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 +//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 //! ``` //! //! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`. @@ -117,6 +133,12 @@ impl From for AnnounceEvent { } } +#[derive(Clone, Copy, Debug, ValueEnum)] +enum OutputFormat { + Compact, + Pretty, +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { @@ -149,12 +171,16 @@ enum Command { key: Option, #[arg(long = "peers-wanted")] peers_wanted: Option, + #[arg(long, value_enum, default_value_t = OutputFormat::Compact)] + format: OutputFormat, }, Scrape { #[arg(value_parser = parse_socket_addr)] tracker_socket_addr: SocketAddr, #[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')] info_hashes: Vec, + #[arg(long, value_enum, default_value_t = OutputFormat::Compact)] + format: OutputFormat, }, } @@ -168,7 +194,7 @@ pub async fn run() -> anyhow::Result<()> { let args = Args::parse(); - let response = match args.command { + let (response, output_format) = match args.command { Command::Announce { tracker_socket_addr: remote_addr, info_hash, @@ -181,6 +207,7 @@ pub async fn run() -> anyhow::Result<()> { peer_id, key, peers_wanted, + format, } => { let params = AnnounceParams { event: event.map(Into::into), @@ -202,16 +229,17 @@ pub async fn run() -> anyhow::Result<()> { key, peers_wanted, }; - handle_announce(remote_addr, &info_hash, ¶ms).await? + (handle_announce(remote_addr, &info_hash, ¶ms).await?, format) } Command::Scrape { tracker_socket_addr: remote_addr, info_hashes, - } => handle_scrape(remote_addr, &info_hashes).await?, + format, + } => (handle_scrape(remote_addr, &info_hashes).await?, format), }; let response: SerializableResponse = response.into(); - let response_json = response.to_json_string()?; + let response_json = response.to_json_string(matches!(output_format, OutputFormat::Pretty))?; print!("{response_json}"); diff --git a/console/tracker-client/src/console/clients/udp/responses/json.rs b/console/tracker-client/src/console/clients/udp/responses/json.rs index 5d2bd6b89..ce3fae422 100644 --- a/console/tracker-client/src/console/clients/udp/responses/json.rs +++ b/console/tracker-client/src/console/clients/udp/responses/json.rs @@ -12,14 +12,59 @@ pub trait ToJson { /// /// Will return an error if serialization fails. /// - fn to_json_string(&self) -> anyhow::Result + fn to_json_string(&self, pretty: bool) -> anyhow::Result where Self: Serialize, { - let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; + let json = if pretty { + serde_json::to_string_pretty(self).context("response JSON pretty serialization")? + } else { + serde_json::to_string(self).context("response JSON compact serialization")? + }; - Ok(pretty_json) + Ok(json) } } impl ToJson for SerializableResponse {} + +#[cfg(test)] +mod tests { + use serde::Serialize; + + use super::ToJson; + + #[derive(Serialize)] + struct SampleResponse { + transaction_id: i32, + seeders: i32, + } + + impl ToJson for SampleResponse {} + + #[test] + fn it_should_serialize_compact_json_when_pretty_is_false() { + let response = SampleResponse { + transaction_id: 10, + seeders: 2, + }; + + let json = response.to_json_string(false).expect("it should serialize compact JSON"); + + assert_eq!(json, "{\"transaction_id\":10,\"seeders\":2}"); + } + + #[test] + fn it_should_serialize_pretty_json_when_pretty_is_true() { + let response = SampleResponse { + transaction_id: 10, + seeders: 2, + }; + + let json = response.to_json_string(true).expect("it should serialize pretty JSON"); + + assert!(json.contains('\n')); + assert!(json.contains(" \"transaction_id\": 10")); + assert!(json.contains(" \"seeders\": 2")); + } +} diff --git a/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md b/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md index 7867345f5..6e3730d0e 100644 --- a/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md +++ b/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md @@ -83,14 +83,14 @@ cargo run -p torrust-tracker-client --bin udp_tracker_client announce \ ## Goals -- [ ] Add a `--format` option to UDP `announce` and `scrape` -- [ ] Change default output to `compact` -- [ ] Support `pretty` output for human-readable inspection -- [ ] Keep response DTO conversion unchanged -- [ ] Update CLI docs/examples -- [ ] `linter all` exits with code `0` -- [ ] `cargo machete` reports no unused dependencies -- [ ] Existing tests keep passing +- [x] Add a `--format` option to UDP `announce` and `scrape` +- [x] Change default output to `compact` +- [x] Support `pretty` output for human-readable inspection +- [x] Keep response DTO conversion unchanged +- [x] Update CLI docs/examples +- [x] `linter all` exits with code `0` +- [x] `cargo machete` reports no unused dependencies +- [x] Existing tests keep passing ## Implementation Plan @@ -128,16 +128,159 @@ Update examples to show both default and explicit `--format pretty` usage. ## Acceptance Criteria -- [ ] Running UDP `announce --format pretty` prints multiline JSON -- [ ] Running UDP `announce --format compact` prints single-line JSON -- [ ] Running UDP `scrape --format pretty` prints multiline JSON -- [ ] Omitting `--format` produces compact single-line JSON +- [x] Running UDP `announce --format pretty` prints multiline JSON +- [x] Running UDP `announce --format compact` prints single-line JSON +- [x] Running UDP `scrape --format pretty` prints multiline JSON +- [x] Omitting `--format` produces compact single-line JSON - [ ] When fallback JSON is produced, `--format pretty` prints indented JSON and default output remains compact -- [ ] Invalid format values are rejected by clap with usage guidance -- [ ] `linter all` exits with code `0` -- [ ] `cargo machete` reports no unused dependencies -- [ ] Existing tests pass +- [x] Invalid format values are rejected by clap with usage guidance +- [x] `linter all` exits with code `0` +- [x] `cargo machete` reports no unused dependencies +- [x] Existing tests pass + +## Manual Verification + +Environment used: + +- Local tracker started with default development config (`tracker.development.sqlite3.toml`) +- Command target: `udp://127.0.0.1:6969/scrape` +- Info hash: `000620bbc6c52d5a96d98f6c0f1dfa523a40df82` + +### Compact output + +Command: + +```text +./target/debug/udp_tracker_client scrape \ + udp://127.0.0.1:6969/scrape \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 \ + --format compact +``` + +Captured output: + +```json +{ + "Scrape": { + "transaction_id": -888840697, + "torrent_stats": [{ "seeders": 0, "completed": 0, "leechers": 0 }] + } +} +``` + +### Pretty output + +Command: + +```text +./target/debug/udp_tracker_client scrape \ + udp://127.0.0.1:6969/scrape \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 \ + --format pretty +``` + +Captured output: + +```json +{ + "Scrape": { + "transaction_id": -888840697, + "torrent_stats": [ + { + "seeders": 0, + "completed": 0, + "leechers": 0 + } + ] + } +} +``` + +### Additional checks + +Command: + +```text +./target/debug/udp_tracker_client announce \ + udp://127.0.0.1:6969/announce \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 \ + --format compact +``` + +Captured output: + +```json +{ + "AnnounceIpv4": { + "transaction_id": -888840697, + "announce_interval": 120, + "leechers": 0, + "seeders": 1, + "peers": [] + } +} +``` + +Command: + +```text +./target/debug/udp_tracker_client announce \ + udp://127.0.0.1:6969/announce \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 \ + --format pretty +``` + +Captured output: + +```json +{ + "AnnounceIpv4": { + "transaction_id": -888840697, + "announce_interval": 120, + "leechers": 0, + "seeders": 2, + "peers": ["0.0.0.0:46251"] + } +} +``` + +Command: + +```text +./target/debug/udp_tracker_client scrape \ + udp://127.0.0.1:6969/scrape \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 +``` + +Captured output: + +```json +{ + "Scrape": { + "transaction_id": -888840697, + "torrent_stats": [{ "seeders": 2, "completed": 0, "leechers": 0 }] + } +} +``` + +Command: + +```text +./target/debug/udp_tracker_client scrape \ + udp://127.0.0.1:6969/scrape \ + 000620bbc6c52d5a96d98f6c0f1dfa523a40df82 \ + --format invalid +``` + +Captured output: + +```text +error: invalid value 'invalid' for '--format ' + [possible values: compact, pretty] + +For more information, try '--help'. +``` ## Key Files From 6c9ad336a35c33d4e4ebc3676ea7b733d4e26029 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 11 May 2026 16:37:10 +0100 Subject: [PATCH 2/3] docs(skills): add public tracker testing skill links --- .../public-trackers-for-testing/SKILL.md | 111 ++++++++++++++++++ .../src/console/clients/http/app.rs | 1 + .../src/console/clients/udp/app.rs | 1 + 3 files changed, 113 insertions(+) create mode 100644 .github/skills/dev/testing/public-trackers-for-testing/SKILL.md diff --git a/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md b/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md new file mode 100644 index 000000000..9112cb719 --- /dev/null +++ b/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md @@ -0,0 +1,111 @@ +--- +name: public-trackers-for-testing +description: Public tracker targets for manual testing and debugging of tracker clients. Use when validating announce/scrape behavior against live services, comparing local vs public behavior, or diagnosing network timeouts. Triggers on "public tracker", "test against demo tracker", "debug tracker timeout", or "which tracker should I use". +metadata: + author: torrust + version: "1.0" +--- + +# Public Trackers for Testing + +## Skill Links + +This skill depends on these artifacts. If any of them change, review this skill. + +- `console/tracker-client/src/console/clients/udp/app.rs` +- `console/tracker-client/src/console/clients/http/app.rs` +- `.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md` + +Use the marker `skill-link: public-trackers-for-testing` in affected artifacts. + +## Purpose + +Use this skill to choose reliable public tracker endpoints for manual verification and debugging. + +It provides: + +- preferred endpoint order +- copy-paste test commands +- timeout triage and fallback workflow + +## Preferred Target Order + +When testing against public services, use this order: + +1. Tracker demo (newer, usually lower load) +2. Index+Tracker demo (older, can be busy) +3. Local tracker fallback for deterministic checks + +## Public Endpoints + +### Tracker Demo (preferred) + +Repository: + +- HTTP: `https://http1.torrust-tracker-demo.com:443/announce` +- UDP: `udp://udp1.torrust-tracker-demo.com:6969/announce` + +### Index+Tracker Demo (secondary) + +Repository: + +- HTTP: `https://tracker.torrust-demo.com/announce` +- UDP: `udp://tracker.torrust-demo.com:6969/announce` + +## Quick Commands + +Use a test info hash: + +```bash +INFO_HASH=000620bbc6c52d5a96d98f6c0f1dfa523a40df82 +``` + +### UDP scrape (preferred public demo) + +```bash +cargo run -q -p torrust-tracker-client --bin udp_tracker_client scrape \ + udp://udp1.torrust-tracker-demo.com:6969/scrape \ + "$INFO_HASH" \ + --format pretty +``` + +### UDP announce (preferred public demo) + +```bash +cargo run -q -p torrust-tracker-client --bin udp_tracker_client announce \ + udp://udp1.torrust-tracker-demo.com:6969/announce \ + "$INFO_HASH" \ + --format compact +``` + +### HTTP announce (preferred public demo) + +```bash +cargo run -q -p torrust-tracker-client --bin http_tracker_client announce \ + https://http1.torrust-tracker-demo.com:443 \ + "$INFO_HASH" +``` + +## Timeout Triage + +If a public target times out: + +1. Retry once against the same target. +2. Retry against the other public demo. +3. If both fail, run locally and verify behavior deterministically. + +Do not assume client regression from a single public timeout. + +## Local Fallback + +Use this workflow when public trackers are unavailable or overloaded: + +- `.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md` + +Then re-run the same client command against `127.0.0.1`. + +## Notes + +- Public demo load varies over time. +- Trackers may contain existing swarm state, so results can differ from clean local runs. +- Prefer local checks for acceptance criteria that require deterministic values. diff --git a/console/tracker-client/src/console/clients/http/app.rs b/console/tracker-client/src/console/clients/http/app.rs index bf63451ad..e412c7245 100644 --- a/console/tracker-client/src/console/clients/http/app.rs +++ b/console/tracker-client/src/console/clients/http/app.rs @@ -1,4 +1,5 @@ //! HTTP Tracker client: +//! skill-link: public-trackers-for-testing //! //! Examples: //! diff --git a/console/tracker-client/src/console/clients/udp/app.rs b/console/tracker-client/src/console/clients/udp/app.rs index 2a979045f..4834b89ee 100644 --- a/console/tracker-client/src/console/clients/udp/app.rs +++ b/console/tracker-client/src/console/clients/udp/app.rs @@ -1,4 +1,5 @@ //! UDP Tracker client: +//! skill-link: public-trackers-for-testing //! //! Examples: //! From dc200b850ba67e050397aaa12eeb648c50732d26 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 11 May 2026 17:01:56 +0100 Subject: [PATCH 3/3] docs: address Copilot PR review suggestions --- .../public-trackers-for-testing/SKILL.md | 2 ++ ...ient-add-option-show-response-pretty-json.md | 17 ++--------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md b/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md index 9112cb719..91a229b0f 100644 --- a/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md +++ b/.github/skills/dev/testing/public-trackers-for-testing/SKILL.md @@ -43,6 +43,7 @@ When testing against public services, use this order: Repository: - HTTP: `https://http1.torrust-tracker-demo.com:443/announce` +- HTTP: `https://http1.torrust-tracker-demo.com:443` - UDP: `udp://udp1.torrust-tracker-demo.com:6969/announce` ### Index+Tracker Demo (secondary) @@ -50,6 +51,7 @@ Repository: Repository: - HTTP: `https://tracker.torrust-demo.com/announce` +- HTTP: `https://tracker.torrust-demo.com` - UDP: `udp://tracker.torrust-demo.com:6969/announce` ## Quick Commands diff --git a/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md b/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md index 6e3730d0e..ee2eaa10a 100644 --- a/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md +++ b/docs/issues/open/1563-udp-tracker-client-add-option-show-response-pretty-json.md @@ -161,12 +161,7 @@ Command: Captured output: ```json -{ - "Scrape": { - "transaction_id": -888840697, - "torrent_stats": [{ "seeders": 0, "completed": 0, "leechers": 0 }] - } -} +{"Scrape":{"transaction_id":-888840697,"torrent_stats":[{"seeders":0,"completed":0,"leechers":0}]}} ``` ### Pretty output @@ -211,15 +206,7 @@ Command: Captured output: ```json -{ - "AnnounceIpv4": { - "transaction_id": -888840697, - "announce_interval": 120, - "leechers": 0, - "seeders": 1, - "peers": [] - } -} +{"AnnounceIpv4":{"transaction_id":-888840697,"announce_interval":120,"leechers":0,"seeders":1,"peers":[]}} ``` Command: