diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 173613ec3..b4bc0b5d1 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -133,14 +133,41 @@ jobs: name: Run Unit Tests run: cargo test --tests --benches --examples --workspace --all-targets --all-features + database-compatibility: + name: Database Compatibility (${{ matrix.mysql-version }}) + runs-on: ubuntu-latest + needs: unit + + strategy: + matrix: + mysql-version: ["8.0", "8.4"] + + steps: + - id: checkout + name: Checkout Repository + uses: actions/checkout@v6 + + - id: setup + name: Setup Toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - id: cache + name: Enable Job Cache + uses: Swatinem/rust-cache@v2 + - id: database - name: Run MySQL Database Tests - run: TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true cargo test --package bittorrent-tracker-core + name: Run Database Compatibility Test + env: + TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST: "true" + TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG: ${{ matrix.mysql-version }} + run: cargo test -p bittorrent-tracker-core --features db-compatibility-tests run_mysql_driver_tests -- --nocapture e2e: name: E2E runs-on: ubuntu-latest - needs: unit + needs: database-compatibility strategy: matrix: diff --git a/docs/issues/1525-overhaul-persistence.md b/docs/issues/1525-overhaul-persistence.md index f1b3e623b..e25f09225 100644 --- a/docs/issues/1525-overhaul-persistence.md +++ b/docs/issues/1525-overhaul-persistence.md @@ -86,7 +86,7 @@ You can then browse or search it while working in the main repository. ### 1) Add DB compatibility matrix -- Spec file: `docs/issues/1525-01-persistence-test-coverage.md` +- Spec file: `docs/issues/1703-1525-01-persistence-test-coverage.md` - Outcome: compatibility matrix exercises SQLite and multiple MySQL versions; PostgreSQL slot reserved for subissue 8 diff --git a/docs/issues/1525-01-persistence-test-coverage.md b/docs/issues/1703-1525-01-persistence-test-coverage.md similarity index 72% rename from docs/issues/1525-01-persistence-test-coverage.md rename to docs/issues/1703-1525-01-persistence-test-coverage.md index 9baf1102e..be5ada114 100644 --- a/docs/issues/1525-01-persistence-test-coverage.md +++ b/docs/issues/1703-1525-01-persistence-test-coverage.md @@ -1,4 +1,6 @@ -# Subissue Draft for #1525-01: Add DB Compatibility Matrix +# Subissue #1703 (Draft for #1525-01): Add DB Compatibility Matrix + +- Issue: https://github.com/torrust/torrust-tracker/issues/1703 ## Goal @@ -42,7 +44,7 @@ The implementation must follow these quality rules for all new and modified test The PR #1695 review branch includes a QA script that defines the expected behavior: -- `run-db-compatibility-matrix.sh`: +- `database-compatibility` job in `.github/workflows/testing.yaml`: executes a compatibility matrix across SQLite, multiple MySQL versions, and multiple PostgreSQL versions. @@ -86,38 +88,30 @@ Steps: - PostgreSQL (reserved for subissue #1525-08): `TORRUST_TRACKER_CORE_POSTGRES_DRIVER_IMAGE_TAG` When `TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG` is not set, the test falls back to the - current hardcoded default (e.g. `8.0`), preserving existing behavior. The matrix script sets + current hardcoded default (e.g. `8.0`), preserving existing behavior. The CI matrix job sets this variable explicitly for each version in the loop, so unset means "run as today" and the matrix just expands that into multiple combinations. -- Add `contrib/dev-tools/qa/run-db-compatibility-matrix.sh` modeled after the PR prototype: - - `set -euo pipefail` - - define default version sets from env vars: - - `MYSQL_VERSIONS` defaulting to at least `8.0 8.4` - - `POSTGRES_VERSIONS` reserved for subissue #1525-08 - - run pre-checks once (`cargo check --workspace --all-targets`) - - run protocol/configuration tests once - - run SQLite driver tests once - - loop MySQL versions: `docker pull mysql:`, then run MySQL driver tests with - `TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=1` and - `TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG=` - - print a clear heading for each backend/version before executing tests - - fail fast on first failure with the failing backend/version visible in logs - - keep script complexity intentionally low; avoid re-implementing test logic already in test - functions -- Replace the current single MySQL `database` step in `.github/workflows/testing.yaml` with - execution of the new script. +- Add a dedicated `database-compatibility` workflow job (between unit and e2e) with matrix values for MySQL versions: + - include matrix values for at least `8.0` and `8.4` + - run `cargo test -p bittorrent-tracker-core --features db-compatibility-tests run_mysql_driver_tests -- --nocapture` + - set `TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true` + - set `TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG=` + - keep the test logic in Rust; use workflow matrix for version fan-out +- Replace the current single MySQL `database` step in `.github/workflows/testing.yaml` with a + dedicated `database-compatibility` job. Acceptance criteria: - [ ] DB image version injection is supported via `TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG` (and a reserved `POSTGRES` equivalent for subissue #1525-08). -- [ ] `contrib/dev-tools/qa/run-db-compatibility-matrix.sh` exists and runs successfully. -- [ ] The script exercises SQLite and at least two MySQL versions by default. +- [ ] `database-compatibility` workflow job runs successfully for each configured MySQL version. +- [ ] The workflow matrix exercises at least two MySQL versions by default. - [ ] Failures identify the backend/version combination that broke. -- [ ] The `database` job step in `.github/workflows/testing.yaml` runs the matrix script instead - of a single-version MySQL command. -- [ ] The script structure allows PostgreSQL to be added in subissue #1525-08 without a redesign. +- [ ] The dedicated `database-compatibility` job in `.github/workflows/testing.yaml` replaces the + old single-version MySQL command. +- [ ] The workflow matrix structure allows PostgreSQL to be added in subissue #1525-08 without a + redesign. - [ ] Tests do not hard-code host ports; `testcontainers` assigns random ports automatically. - [ ] All containers started by tests are removed unconditionally on test completion or failure. @@ -125,12 +119,13 @@ Acceptance criteria: Steps: -- Document the local invocation command for the matrix script. -- Document that the CI `database` step runs the same script. +- Document the local invocation command for the compatibility test using explicit feature + env + vars. +- Document that CI runs the same test through the `database-compatibility` workflow job matrix. Acceptance criteria: -- [ ] The matrix script is documented and runnable without ad hoc manual steps. +- [ ] The compatibility test command is documented and runnable without ad hoc manual steps. ## Out of Scope @@ -143,8 +138,8 @@ Acceptance criteria: - [ ] `cargo test --workspace --all-targets` passes. - [ ] `linter all` exits with code `0`. -- [ ] The matrix script has been executed successfully in a clean environment; a passing run log - is included in the PR description. +- [ ] The `database-compatibility` workflow job has been executed successfully in a clean + environment; a passing run log is included in the PR description. ## References @@ -152,4 +147,4 @@ Acceptance criteria: - Reference PR: #1695 - Reference implementation branch: `josecelano:pr-1684-review` — see EPIC for checkout instructions (`docs/issues/1525-overhaul-persistence.md`) -- Reference script: `contrib/dev-tools/qa/run-db-compatibility-matrix.sh` +- Reference job: `.github/workflows/testing.yaml` `database-compatibility` diff --git a/packages/http-protocol/src/v1/responses/scrape.rs b/packages/http-protocol/src/v1/responses/scrape.rs index 022735abc..30319bd6b 100644 --- a/packages/http-protocol/src/v1/responses/scrape.rs +++ b/packages/http-protocol/src/v1/responses/scrape.rs @@ -131,5 +131,25 @@ mod tests { String::from_utf8(expected_bytes.to_vec()).unwrap() ); } + + #[test] + fn should_encode_large_download_counts_as_i64() { + let info_hash = InfoHash::from_bytes(&[0x69; 20]); + let mut scrape_data = ScrapeData::empty(); + scrape_data.add_file( + &info_hash, + SwarmMetadata { + complete: 1, + downloaded: u32::MAX, + incomplete: 3, + }, + ); + + let response = Bencoded::from(scrape_data); + let bytes = response.body(); + let body = String::from_utf8(bytes).unwrap(); + + assert!(body.contains(&format!("downloadedi{}e", i64::from(u32::MAX)))); + } } } diff --git a/packages/tracker-core/Cargo.toml b/packages/tracker-core/Cargo.toml index fb864cde7..59c47dda2 100644 --- a/packages/tracker-core/Cargo.toml +++ b/packages/tracker-core/Cargo.toml @@ -13,6 +13,10 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = [ ] +db-compatibility-tests = [ ] + [dependencies] aquatic_udp_protocol = "0" bittorrent-primitives = "0.1.0" diff --git a/packages/tracker-core/src/databases/driver/mysql.rs b/packages/tracker-core/src/databases/driver/mysql.rs index da2f86ce8..ef91eb1f7 100644 --- a/packages/tracker-core/src/databases/driver/mysql.rs +++ b/packages/tracker-core/src/databases/driver/mysql.rs @@ -345,7 +345,7 @@ impl Database for Mysql { } } -#[cfg(test)] +#[cfg(all(test, feature = "db-compatibility-tests"))] mod tests { use std::sync::Arc; @@ -355,7 +355,8 @@ mod tests { Test for this driver are executed with: - `TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true cargo test` + `TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true \ + cargo test -p bittorrent-tracker-core --features db-compatibility-tests run_mysql_driver_tests` The `Database` trait is very simple and we only have one driver that needs a container. In the future we might want to use different approaches like: @@ -379,7 +380,9 @@ mod tests { impl StoppedMysqlContainer { async fn run(self, config: &MysqlConfiguration) -> Result> { - let container = GenericImage::new("mysql", "8.0") + let image_tag = std::env::var("TORRUST_TRACKER_CORE_MYSQL_DRIVER_IMAGE_TAG").unwrap_or_else(|_| "8.0".to_string()); + + let container = GenericImage::new("mysql", image_tag.as_str()) .with_exposed_port(config.internal_port.tcp()) // todo: this does not work //.with_wait_for(WaitFor::message_on_stdout("ready for connections")) @@ -454,6 +457,8 @@ mod tests { driver } + // This test is invoked by `.github/workflows/testing.yaml` in the + // `database-compatibility` job to validate supported MySQL versions. #[tokio::test] async fn run_mysql_driver_tests() -> Result<(), Box> { if std::env::var("TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST").is_err() { diff --git a/packages/udp-tracker-server/src/handlers/scrape.rs b/packages/udp-tracker-server/src/handlers/scrape.rs index 8bac05c1e..92160c2bd 100644 --- a/packages/udp-tracker-server/src/handlers/scrape.rs +++ b/packages/udp-tracker-server/src/handlers/scrape.rs @@ -53,19 +53,22 @@ pub async fn handle_scrape( Ok(build_response(request, &scrape_data)) } +fn udp_counter_from_u32(value: u32) -> i32 { + // Temporary saturation guard for UDP i32 counters. Proper type alignment across Rust and DB layers + // will be addressed in docs/issues/1525-07-align-rust-and-db-types.md. + i32::try_from(value).unwrap_or(i32::MAX) +} + fn build_response(request: &ScrapeRequest, scrape_data: &ScrapeData) -> Response { let mut torrent_stats: Vec = Vec::new(); for file in &scrape_data.files { let swarm_metadata = file.1; - #[allow(clippy::cast_possible_truncation)] - let scrape_entry = { - TorrentScrapeStatistics { - seeders: NumberOfPeers(I32::new(i64::from(swarm_metadata.complete) as i32)), - completed: NumberOfDownloads(I32::new(i64::from(swarm_metadata.downloaded) as i32)), - leechers: NumberOfPeers(I32::new(i64::from(swarm_metadata.incomplete) as i32)), - } + let scrape_entry = TorrentScrapeStatistics { + seeders: NumberOfPeers(I32::new(udp_counter_from_u32(swarm_metadata.complete))), + completed: NumberOfDownloads(I32::new(udp_counter_from_u32(swarm_metadata.downloaded))), + leechers: NumberOfPeers(I32::new(udp_counter_from_u32(swarm_metadata.incomplete))), }; torrent_stats.push(scrape_entry); @@ -458,4 +461,11 @@ mod tests { } } } + + #[test] + fn should_saturate_large_download_counts_for_udp_protocol() { + assert_eq!(super::udp_counter_from_u32(u32::MAX), i32::MAX); + assert_eq!(super::udp_counter_from_u32((i32::MAX as u32) + 1), i32::MAX); + assert_eq!(super::udp_counter_from_u32(42), 42); + } }