diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 00000000..f797828d --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later + +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential pkg-config cmake clang libclang-dev \ + libopus-dev libasound2-dev libsoapysdr-dev + + - name: Set up Rust + run: | + export PATH="$HOME/.cargo/bin:$PATH" + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal + fi + rustup toolchain install stable --profile minimal \ + --component rustfmt --component clippy + rustup default stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-${{ runner.os }}- + + - name: rustfmt + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo fmt --all -- --check + + - name: clippy + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo clippy --workspace --all-targets --all-features -- -D warnings + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential pkg-config cmake clang libclang-dev \ + libopus-dev libasound2-dev libsoapysdr-dev + + - name: Set up Rust + run: | + export PATH="$HOME/.cargo/bin:$PATH" + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal + fi + rustup toolchain install stable --profile minimal + rustup default stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-${{ runner.os }}- + + - name: Build + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo build --workspace --all-targets --locked + + - name: Test + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo test --workspace --locked + + reuse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: REUSE compliance + uses: fsfe/reuse-action@v5 diff --git a/REUSE.toml b/REUSE.toml index 725ec1a7..f46d1a72 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -11,6 +11,7 @@ path = [ "README.md", "trx-rs.toml.example", "docs/**", + "aidocs/**", "src/decoders/trx-ftx/README.md", "src/decoders/trx-wxsat/README.md", "assets/trx-logo.png", diff --git a/aidocs/wip.md b/aidocs/wip.md new file mode 100644 index 00000000..9dd10733 --- /dev/null +++ b/aidocs/wip.md @@ -0,0 +1,89 @@ +# Work In Progress — Project Improvement Areas + +Living tracker for engineering-infrastructure and hardening work identified +during a July 2026 repo scan. The architecture-level backlog +(`docs/Improvement-Areas.md`, P0–P3) is closed; these items focus on the +tooling, robustness, and product concerns *around* the code. + +Status legend: **Done** · **In progress** · **Not started** + +| # | Tier | Area | Status | +|----|------|------|--------| +| 1 | 1 — Infrastructure | Gitea Actions CI (fmt, clippy, test, REUSE) | In progress | +| 2 | 1 — Infrastructure | Supply-chain & lint governance (cargo-deny/audit, MSRV, `[workspace.lints]`) | Not started | +| 3 | 1 — Infrastructure | Release & deployment (container image, systemd units, binary releases) | Not started | +| 4 | 2 — Robustness | Panic-resilience audit (harden ~495 unwrap/expect/panic sites) | Not started | +| 5 | 2 — Robustness | `unsafe` SIMD safety scaffolding (`# Safety` docs, scalar↔SIMD equivalence tests) | Not started | +| 6 | 2 — Robustness | DSP performance benchmarks (criterion, guard optimization gains) | Not started | +| 7 | 2 — Robustness | Test-coverage measurement & gap-filling (llvm-cov; CAT backends, server tasks) | Not started | +| 8 | 3 — Product | Frontend modularization & tooling (split `app.js` into ES modules, ESLint, JS tests) | Not started | +| 9 | 3 — Product | Runtime observability (`/health`, Prometheus metrics) | Not started | +| 10 | 3 — Product | Documentation freshness & consolidation (reconcile `docs/` with code) | Not started | + +## Tier 1 — Infrastructure gaps + +### 1. Gitea Actions CI +No `.gitea/` / `.github/` / Woodpecker workflows exist, despite 768 tests. +Add a workflow running `cargo fmt --check`, `cargo clippy -D warnings`, +`cargo build`/`cargo test`, and REUSE lint on push + pull_request. +System deps: `pkg-config cmake libopus-dev libasound2-dev libsoapysdr-dev` +(the `soapysdr` backend is a default feature). + +Notes for the runner: assumes an `act_runner` registered with an +`ubuntu-latest` label and GitHub-action proxying enabled (used by +`actions/checkout`, `actions/cache`, `fsfe/reuse-action`). `clippy` runs +with `-D warnings`; core crates are already clean, but if the first full +`--all-features` run surfaces warnings in a less-travelled crate, fix them +(preferred) or temporarily soften that step. + +### 2. Supply-chain & lint governance +No `deny.toml`, `cargo audit`, `rustfmt.toml`/`clippy.toml`, declared MSRV, +or `[workspace.lints]`. Add dependency auditing to CI, pin an MSRV +(`rust-version`), and centralize lint policy in the workspace manifest. + +### 3. Release & deployment +No `Dockerfile`, systemd units, packaging, or release automation (only +`script/dummy-server.sh`). Add a container image, example systemd units for +`trx-server`/`trx-client`, and a tag-triggered static-binary release job. + +## Tier 2 — Robustness & correctness + +### 4. Panic-resilience audit +495 `unwrap()`/`expect()`/`panic!` sites, 11 in the hottest server files +(`audio.rs`, `rig_task.rs`). A panic there can drop a rig task or the +process. Convert hot-path panics to error propagation / graceful +degradation; reserve `expect` for documented invariants. + +### 5. `unsafe` SIMD safety scaffolding +13 `unsafe` blocks (AVX2 DSP). Add `# Safety` docs stating invariants, +confirm runtime feature detection is tested, and add property tests +asserting SIMD output matches the scalar fallback across random inputs. + +### 6. DSP performance benchmarks +`docs/Optimization-Guidelines.md` documents NCO/polyphase/AVX2 gains, but no +`criterion` benches guard them. Add benches for the demod/resample/FFT hot +paths so regressions surface as numbers. + +### 7. Test-coverage measurement & gap-filling +67 of 146 Rust files have no test module. Protocol is well covered; backends +(CAT BCD/ASCII encoding), `listener.rs`, and `config.rs` look thin. Wire up +`cargo-llvm-cov` and target the CAT backends and server tasks first. + +## Tier 3 — Product & maintainability + +### 8. Frontend modularization & tooling +`app.js` is 8,760 lines and `map-core.js` 3,515, with no modules, linter, or +tests. Keeping vanilla HTML+JS (no framework), split into native ES modules +by concern, add ESLint + Prettier, and add `node:test` unit tests for pure +logic (frequency formatting, unit math, decode parsing). + +### 9. Runtime observability +`tracing` is set up, but there is no `/health` endpoint or metrics +instrumentation. Add health/readiness endpoints and Prometheus-format +metrics (decode rates, reconnects, audio underruns, per-rig state). + +### 10. Documentation freshness & consolidation +`docs/` (13 files) is mostly dated 2026-03-29 while code moved into July, and +mixes planning artifacts with reference docs. Reconcile against current code, +separate "plans" from "reference," and fold still-true content into the +canonical docs. diff --git a/src/decoders/trx-ais/src/lib.rs b/src/decoders/trx-ais/src/lib.rs index 1eb5bdbe..d6e66e1d 100644 --- a/src/decoders/trx-ais/src/lib.rs +++ b/src/decoders/trx-ais/src/lib.rs @@ -243,7 +243,7 @@ fn parse_frame(frame: RawFrame, channel: &str) -> Option { let message_type = get_uint(&bits, 0, 6)? as u8; let repeat = get_uint(&bits, 6, 2)? as u8; - let mmsi = get_uint(&bits, 8, 30)? as u32; + let mmsi = get_uint(&bits, 8, 30)?; let mut msg = AisMessage { rig_id: None, diff --git a/src/decoders/trx-aprs/src/lib.rs b/src/decoders/trx-aprs/src/lib.rs index 2fb8fded..21f89242 100644 --- a/src/decoders/trx-aprs/src/lib.rs +++ b/src/decoders/trx-aprs/src/lib.rs @@ -638,7 +638,7 @@ mod tests { for (i, &ch) in b"N0CALL".iter().enumerate() { addr[i] = ch << 1; } - addr[6] = (0 << 1) | 1; // SSID=0, last=true + addr[6] = 1; // SSID=0, last=true let decoded = decode_ax25_address(&addr, 0); assert_eq!(decoded.call, "N0CALL"); @@ -652,7 +652,7 @@ mod tests { for (i, &ch) in b"SP2SJG".iter().enumerate() { addr[i] = ch << 1; } - addr[6] = (5 << 1) | 0; // SSID=5, last=false + addr[6] = 5 << 1; // SSID=5, last=false let decoded = decode_ax25_address(&addr, 0); assert_eq!(decoded.call, "SP2SJG"); @@ -667,7 +667,7 @@ mod tests { for (i, &ch) in b"W1AW ".iter().enumerate() { addr[i] = ch << 1; } - addr[6] = (0 << 1) | 1; + addr[6] = 1; let decoded = decode_ax25_address(&addr, 0); assert_eq!(decoded.call, "W1AW"); @@ -691,8 +691,8 @@ mod tests { for &ch in src_bytes.as_bytes().iter().take(6) { frame.push(ch << 1); } - frame.push((0 << 1) | 1); // SSID=0, last=true - // Control + PID + frame.push(1); // SSID=0, last=true + // Control + PID frame.push(0x03); // UI frame frame.push(0xF0); // No layer-3 protocol // Info field diff --git a/src/decoders/trx-ftx/src/common/callsign_hash.rs b/src/decoders/trx-ftx/src/common/callsign_hash.rs index c743f4aa..576df83d 100644 --- a/src/decoders/trx-ftx/src/common/callsign_hash.rs +++ b/src/decoders/trx-ftx/src/common/callsign_hash.rs @@ -160,18 +160,14 @@ impl CallsignHashTable { let mut idx = start_idx; loop { - match &self.entries[idx] { - Some(entry) => { - let stored = (entry.hash & HASH22_MASK) >> shift; - if stored == target { - return Some(entry.callsign.clone()); - } - idx = (idx + 1) % CALLSIGN_HASHTABLE_SIZE; - if idx == start_idx { - return None; - } - } - None => return None, + let entry = self.entries[idx].as_ref()?; + let stored = (entry.hash & HASH22_MASK) >> shift; + if stored == target { + return Some(entry.callsign.clone()); + } + idx = (idx + 1) % CALLSIGN_HASHTABLE_SIZE; + if idx == start_idx { + return None; } } } diff --git a/src/decoders/trx-ftx/src/common/ldpc.rs b/src/decoders/trx-ftx/src/common/ldpc.rs index 608590fc..853cc5ab 100644 --- a/src/decoders/trx-ftx/src/common/ldpc.rs +++ b/src/decoders/trx-ftx/src/common/ldpc.rs @@ -42,8 +42,8 @@ pub(crate) fn ldpc_check(codeword: &[u8; FTX_LDPC_N]) -> i32 { for m in 0..FTX_LDPC_M { let mut x: u8 = 0; let num_rows = FTX_LDPC_NUM_ROWS[m] as usize; - for i in 0..num_rows { - x ^= codeword[FTX_LDPC_NM[m][i] as usize - 1]; + for &nm in FTX_LDPC_NM[m].iter().take(num_rows) { + x ^= codeword[nm as usize - 1]; } if x != 0 { errors += 1; @@ -81,11 +81,11 @@ pub fn ldpc_decode( for j in 0..FTX_LDPC_M { let num_rows = FTX_LDPC_NUM_ROWS[j] as usize; let m_row = j * FTX_LDPC_N; - for ii1 in 0..num_rows { - let i1 = FTX_LDPC_NM[j][ii1] as usize - 1; + for &nm1 in FTX_LDPC_NM[j].iter().take(num_rows) { + let i1 = nm1 as usize - 1; let mut a = 1.0f32; - for ii2 in 0..num_rows { - let i2 = FTX_LDPC_NM[j][ii2] as usize - 1; + for &nm2 in FTX_LDPC_NM[j].iter().take(num_rows) { + let i2 = nm2 as usize - 1; if i2 != i1 { a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32); } @@ -97,8 +97,8 @@ pub fn ldpc_decode( // Hard decisions for i in 0..FTX_LDPC_N { let mut l = codeword[i]; - for j in 0..3 { - l += e_matrix[(FTX_LDPC_MN[i][j] as usize - 1) * FTX_LDPC_N + i]; + for &mn in FTX_LDPC_MN[i].iter().take(3) { + l += e_matrix[(mn as usize - 1) * FTX_LDPC_N + i]; } plain[i] = if l > 0.0 { 1 } else { 0 }; } @@ -113,12 +113,12 @@ pub fn ldpc_decode( // Update m[][] from e[][] for i in 0..FTX_LDPC_N { - for ji1 in 0..3 { - let j1 = FTX_LDPC_MN[i][ji1] as usize - 1; + for (ji1, &mn1) in FTX_LDPC_MN[i].iter().enumerate().take(3) { + let j1 = mn1 as usize - 1; let mut l = codeword[i]; - for ji2 in 0..3 { + for (ji2, &mn2) in FTX_LDPC_MN[i].iter().enumerate().take(3) { if ji1 != ji2 { - let j2 = FTX_LDPC_MN[i][ji2] as usize - 1; + let j2 = mn2 as usize - 1; l += e_matrix[j2 * FTX_LDPC_N + i]; } } diff --git a/src/decoders/trx-ftx/src/common/text.rs b/src/decoders/trx-ftx/src/common/text.rs index 6230a2dc..772a230f 100644 --- a/src/decoders/trx-ftx/src/common/text.rs +++ b/src/decoders/trx-ftx/src/common/text.rs @@ -64,10 +64,8 @@ pub fn charn(mut c: i32, table: CharTable) -> char { return EXTRAS[c as usize]; } } - CharTable::AlphanumSpaceSlash => { - if c == 0 { - return '/'; - } + CharTable::AlphanumSpaceSlash if c == 0 => { + return '/'; } _ => {} } @@ -116,10 +114,8 @@ pub fn nchar(c: char, table: CharTable) -> Option { '?' => return Some(n + 4), _ => {} }, - CharTable::AlphanumSpaceSlash => { - if c == '/' { - return Some(n); - } + CharTable::AlphanumSpaceSlash if c == '/' => { + return Some(n); } _ => {} } diff --git a/src/decoders/trx-rds/src/lib.rs b/src/decoders/trx-rds/src/lib.rs index d8460678..08574310 100644 --- a/src/decoders/trx-rds/src/lib.rs +++ b/src/decoders/trx-rds/src/lib.rs @@ -632,32 +632,17 @@ impl Candidate { } let segment = usize::from((block_b & 0x0003) as u8); let di = ((block_b >> 2) & 0x1) != 0; - match segment { - 0 => { - if self.state.dynamic_pty != Some(di) { - self.state.dynamic_pty = Some(di); - changed = true; - } - } - 1 => { - if self.state.compressed != Some(di) { - self.state.compressed = Some(di); - changed = true; - } - } - 2 => { - if self.state.artificial_head != Some(di) { - self.state.artificial_head = Some(di); - changed = true; - } - } - 3 => { - if self.state.stereo != Some(di) { - self.state.stereo = Some(di); - changed = true; - } - } - _ => {} + let di_flag = Some(di); + let slot = match segment { + 0 => &mut self.state.dynamic_pty, + 1 => &mut self.state.compressed, + 2 => &mut self.state.artificial_head, + 3 => &mut self.state.stereo, + _ => unreachable!("segment is masked to two bits"), + }; + if *slot != di_flag { + *slot = di_flag; + changed = true; } let [b0, b1] = block_d.to_be_bytes(); self.ps_bytes[segment * 2] = sanitize_text_byte(b0); @@ -1458,9 +1443,9 @@ mod tests { } // BPSK modulate onto the 57 kHz subcarrier. - for t in 0..n { + for (t, sample) in shaped.iter_mut().enumerate().take(n) { let phase = TAU * RDS_SUBCARRIER_HZ * t as f32 / sample_rate; - shaped[t] *= phase.cos(); + *sample *= phase.cos(); } shaped } diff --git a/src/decoders/trx-vdes/src/crc.rs b/src/decoders/trx-vdes/src/crc.rs index 6191bb1c..ad0043ee 100644 --- a/src/decoders/trx-vdes/src/crc.rs +++ b/src/decoders/trx-vdes/src/crc.rs @@ -134,9 +134,7 @@ mod tests { .flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1)) .collect(); // Append wrong CRC - for _ in 0..16 { - bits.push(0); - } + bits.resize(bits.len() + 16, 0); assert!(!check_crc16(&bits)); } diff --git a/src/decoders/trx-vdes/src/link_layer.rs b/src/decoders/trx-vdes/src/link_layer.rs index 361daaf6..a5b0ef08 100644 --- a/src/decoders/trx-vdes/src/link_layer.rs +++ b/src/decoders/trx-vdes/src/link_layer.rs @@ -346,8 +346,8 @@ mod tests { write_bits(&mut bits, 12, 32, 123456); // source_id write_bits(&mut bits, 44, 11, 20); // data_count = 20 // Fill some payload - for i in 55..75 { - bits[i] = (i % 2) as u8; + for (i, bit) in bits.iter_mut().enumerate().take(75).skip(55) { + *bit = (i % 2) as u8; } append_crc(&mut bits); diff --git a/src/decoders/trx-wefax/src/image.rs b/src/decoders/trx-wefax/src/image.rs index 4774d977..3666bc66 100644 --- a/src/decoders/trx-wefax/src/image.rs +++ b/src/decoders/trx-wefax/src/image.rs @@ -374,8 +374,8 @@ mod tests { let (y, m, d, h, mi, _) = unix_to_utc(1775055000); assert_eq!(y, 2026); // Just verify reasonable values without asserting exact date. - assert!(m >= 1 && m <= 12); - assert!(d >= 1 && d <= 31); + assert!((1..=12).contains(&m)); + assert!((1..=31).contains(&d)); assert!(h < 24); assert!(mi < 60); } diff --git a/src/decoders/trx-wefax/src/phase.rs b/src/decoders/trx-wefax/src/phase.rs index 9ef6fd4f..3a8989fa 100644 --- a/src/decoders/trx-wefax/src/phase.rs +++ b/src/decoders/trx-wefax/src/phase.rs @@ -161,10 +161,8 @@ mod tests { for line_idx in 0..20 { let mut line = vec![1.0f32; spl]; - for j in pulse_start..pulse_start + pw { - if j < spl { - line[j] = 0.0; - } + for slot in line.iter_mut().skip(pulse_start).take(pw) { + *slot = 0.0; } let result = det.process(&line); if let Some(offset) = result { diff --git a/src/decoders/trx-wspr/src/protocol.rs b/src/decoders/trx-wspr/src/protocol.rs index 7055cc29..bd245507 100644 --- a/src/decoders/trx-wspr/src/protocol.rs +++ b/src/decoders/trx-wspr/src/protocol.rs @@ -483,7 +483,7 @@ mod tests { let c4 = idx27(b'T'); let c5 = idx27(b' '); let n1 = ((c0 * 36 + c1) * 10 + c2) * 27u32.pow(3) + c3 * 27u32.pow(2) + c4 * 27 + c5; - let m1 = (179 - 10 * 5 - 2) * 180 + 10 * 13 + 0; // FN20 + let m1 = (179 - 10 * 5 - 2) * 180 + 10 * 13; // FN20 (final term is 0) let power_code = 37u32; let mut input_bits = [0u8; NBITS]; @@ -530,8 +530,8 @@ mod tests { fn interleave_deinterleave_roundtrip() { // Create a sequence of distinguishable values let mut original = [0u8; NSYMS]; - for i in 0..NSYMS { - original[i] = (i % 256) as u8; + for (i, slot) in original.iter_mut().enumerate() { + *slot = (i % 256) as u8; } let interleaved = interleave(&original); diff --git a/src/trx-client/src/audio_client.rs b/src/trx-client/src/audio_client.rs index a054dcbe..593c6b8b 100644 --- a/src/trx-client/src/audio_client.rs +++ b/src/trx-client/src/audio_client.rs @@ -356,59 +356,6 @@ async fn run_single_rig_audio_client( } } -#[cfg(test)] -mod tests { - use super::{resolve_audio_addr, AudioConnectConfig}; - use std::collections::HashMap; - - #[test] - fn resolve_audio_addr_prefers_fixed_url() { - let mut rig_connect = HashMap::new(); - rig_connect.insert( - "home-hf".to_string(), - AudioConnectConfig::fixed("audio.example.com:4700".to_string()), - ); - - let addr = resolve_audio_addr( - "home-hf", - Some(4531), - &rig_connect, - &AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531), - ); - assert_eq!(addr, "audio.example.com:4700"); - } - - #[test] - fn resolve_audio_addr_uses_advertised_port_with_remote_host() { - let mut rig_connect = HashMap::new(); - rig_connect.insert( - "home-hf".to_string(), - AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531), - ); - - let addr = resolve_audio_addr( - "home-hf", - Some(4600), - &rig_connect, - &AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531), - ); - assert_eq!(addr, "control.example.com:4600"); - } - - #[test] - fn resolve_audio_addr_falls_back_to_default_port() { - let rig_connect = HashMap::new(); - - let addr = resolve_audio_addr( - "home-hf", - None, - &rig_connect, - &AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531), - ); - assert_eq!(addr, "fallback.example.com:4531"); - } -} - /// Handle a single TCP connection for one rig. Similar to `handle_audio_connection` /// but publishes to per-rig channels directly and mirrors to global when selected. #[allow(clippy::too_many_arguments)] @@ -767,3 +714,56 @@ async fn handle_single_rig_connection( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{resolve_audio_addr, AudioConnectConfig}; + use std::collections::HashMap; + + #[test] + fn resolve_audio_addr_prefers_fixed_url() { + let mut rig_connect = HashMap::new(); + rig_connect.insert( + "home-hf".to_string(), + AudioConnectConfig::fixed("audio.example.com:4700".to_string()), + ); + + let addr = resolve_audio_addr( + "home-hf", + Some(4531), + &rig_connect, + &AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531), + ); + assert_eq!(addr, "audio.example.com:4700"); + } + + #[test] + fn resolve_audio_addr_uses_advertised_port_with_remote_host() { + let mut rig_connect = HashMap::new(); + rig_connect.insert( + "home-hf".to_string(), + AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531), + ); + + let addr = resolve_audio_addr( + "home-hf", + Some(4600), + &rig_connect, + &AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531), + ); + assert_eq!(addr, "control.example.com:4600"); + } + + #[test] + fn resolve_audio_addr_falls_back_to_default_port() { + let rig_connect = HashMap::new(); + + let addr = resolve_audio_addr( + "home-hf", + None, + &rig_connect, + &AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531), + ); + assert_eq!(addr, "fallback.example.com:4531"); + } +} diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index 3d57ede5..9e7daf10 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -1110,36 +1110,40 @@ url = "remote.example.com:4530" #[test] fn test_validate_rejects_duplicate_remote_names() { - let mut config = ClientConfig::default(); - config.remotes = vec![ - RemoteEntry { - name: "dup".to_string(), - url: "a:4530".to_string(), - rig_id: None, - auth: RemoteAuthConfig::default(), - poll_interval_ms: 750, - }, - RemoteEntry { - name: "dup".to_string(), - url: "b:4530".to_string(), - rig_id: None, - auth: RemoteAuthConfig::default(), - poll_interval_ms: 750, - }, - ]; + let config = ClientConfig { + remotes: vec![ + RemoteEntry { + name: "dup".to_string(), + url: "a:4530".to_string(), + rig_id: None, + auth: RemoteAuthConfig::default(), + poll_interval_ms: 750, + }, + RemoteEntry { + name: "dup".to_string(), + url: "b:4530".to_string(), + rig_id: None, + auth: RemoteAuthConfig::default(), + poll_interval_ms: 750, + }, + ], + ..Default::default() + }; assert!(config.validate().unwrap_err().contains("duplicate name")); } #[test] fn test_validate_rejects_empty_remote_name() { - let mut config = ClientConfig::default(); - config.remotes = vec![RemoteEntry { - name: "".to_string(), - url: "a:4530".to_string(), - rig_id: None, - auth: RemoteAuthConfig::default(), - poll_interval_ms: 750, - }]; + let config = ClientConfig { + remotes: vec![RemoteEntry { + name: "".to_string(), + url: "a:4530".to_string(), + rig_id: None, + auth: RemoteAuthConfig::default(), + poll_interval_ms: 750, + }], + ..Default::default() + }; assert!(config .validate() .unwrap_err() @@ -1148,14 +1152,16 @@ url = "remote.example.com:4530" #[test] fn test_validate_rejects_empty_remote_url() { - let mut config = ClientConfig::default(); - config.remotes = vec![RemoteEntry { - name: "hf".to_string(), - url: " ".to_string(), - rig_id: None, - auth: RemoteAuthConfig::default(), - poll_interval_ms: 750, - }]; + let config = ClientConfig { + remotes: vec![RemoteEntry { + name: "hf".to_string(), + url: " ".to_string(), + rig_id: None, + auth: RemoteAuthConfig::default(), + poll_interval_ms: 750, + }], + ..Default::default() + }; assert!(config .validate() .unwrap_err() @@ -1164,14 +1170,16 @@ url = "remote.example.com:4530" #[test] fn test_validate_rejects_zero_remote_poll_interval() { - let mut config = ClientConfig::default(); - config.remotes = vec![RemoteEntry { - name: "hf".to_string(), - url: "a:4530".to_string(), - rig_id: None, - auth: RemoteAuthConfig::default(), - poll_interval_ms: 0, - }]; + let config = ClientConfig { + remotes: vec![RemoteEntry { + name: "hf".to_string(), + url: "a:4530".to_string(), + rig_id: None, + auth: RemoteAuthConfig::default(), + poll_interval_ms: 0, + }], + ..Default::default() + }; assert!(config .validate() .unwrap_err() diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index b7d05d76..5e235f72 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -1678,7 +1678,7 @@ mod tests { #[test] fn global_target_for_snapshot_skips_other_server_selection() { let snapshot = sample_snapshot(); - let rigs = vec![RigEntry { + let rigs = [RigEntry { rig_id: "hf".to_string(), display_name: Some("Gdansk HF".to_string()), state: snapshot, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs index 1992c36f..1b91871c 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs @@ -1596,7 +1596,7 @@ mod tests { let t = i as f32 / fs; adj_phase += adj_mod_index * adj_composite[i]; let adj = Complex::from_polar(0.5, adj_phase + TAU * adj_freq_offset * t); - *s = *s + adj; + *s += adj; } let mut decoder = WfmStereoDecoder::new( @@ -1673,7 +1673,7 @@ mod tests { // Mix at 70 % of the main signal's amplitude — strong enough to // overcome the FM capture effect and visibly degrade pilot coherence. for (s, intf) in iq.iter_mut().zip(intf_iq.iter()) { - *s = *s + intf * 0.7; + *s += intf * 0.7; } let mut decoder = WfmStereoDecoder::new(