Compare commits

..
Author SHA1 Message Date
sjg 4b17b4ac1d [style](trx-ftx): use iterators in LDPC single-index loops
CI / lint (pull_request) Successful in 2m35s
CI / test (pull_request) Successful in 3m22s
CI / reuse (pull_request) Successful in 7s
CI / lint (push) Successful in 2m34s
CI / test (push) Successful in 3m26s
CI / reuse (push) Successful in 7s
clippy needless_range_loop (rust 1.97) flagged the loops in ldpc_check
and ldpc_decode that use a range only to index one array. Replace them
with iterator/enumerate forms. The belief-propagation loops that index
several arrays by the same variable are left as-is (not flagged).

Behaviour is unchanged; the transformations are index-for-index
equivalent. Verified the lib compiles and is clippy-clean; the crate's
LDPC tests run in the CI test job (they need a dev-dependency not
available in the local offline sandbox).

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-18 10:43:01 +02:00
sjg 977f7b709e [style](trx-ais): drop redundant u32 cast
CI / lint (pull_request) Failing after 2m28s
CI / test (pull_request) Successful in 3m23s
CI / reuse (pull_request) Successful in 7s
get_uint returns Option<u32>, so `? as u32` is an unnecessary same-type
cast flagged by clippy under -D warnings (rust 1.97).

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-18 10:28:55 +02:00
sjg a2838b06a2 [style](trx-ftx): fix clippy question_mark and collapsible_match
CI / lint (pull_request) Failing after 2m22s
CI / test (pull_request) Successful in 3m27s
CI / reuse (pull_request) Successful in 8s
CI runs a newer clippy (1.97) than was available locally, which flagged
three lints in trx-ftx not caught earlier:

- question_mark: replace the Some/None match in CallsignHashTable::lookup
  with `self.entries[idx].as_ref()?`
- collapsible_match: fold the nested `if` in text.rs char/nchar into match
  guards on the AlphanumSpaceSlash arm

Behaviour is unchanged; verified clean with nightly clippy (1.93).

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-18 09:25:12 +02:00
sjg c6cd661676 [style](trx-rs): fix clippy warnings for -D warnings CI
CI / lint (pull_request) Failing after 4m14s
CI / test (pull_request) Successful in 15m22s
CI / reuse (pull_request) Successful in 7s
The CI lint job runs clippy with -D warnings, which surfaced a set of
existing warnings across decoders, the client, and the soapysdr backend.
Resolve them so the workspace is clean under the enforced lint level:

- collapsible_match / identity_op / needless_range_loop / same_item_push
  in trx-rds, trx-wspr, trx-vdes, trx-wefax, trx-aprs (mostly tests)
- field_reassign_with_default -> struct-update syntax in trx-client config
  tests
- assign_op_pattern, useless vec!, and test-module ordering picked up by
  cargo clippy --fix in trx-client and the soapysdr WFM tests

No behaviour changes; all affected crates' tests pass.

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-17 23:49:12 +02:00
sjg bf08c7ebc0 [chore](trx-rs): install libclang for soapysdr-sys in CI
CI / lint (pull_request) Failing after 4m7s
CI / reuse (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
soapysdr-sys builds bindings with bindgen, which needs libclang at build
time. Add clang and libclang-dev to the system dependencies so the
soapysdr backend (a default feature) compiles under CI.

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-17 23:30:49 +02:00
sjg c7470acc1c [chore](trx-rs): fix Rust PATH handling in CI
CI / lint (pull_request) Failing after 3m18s
CI / reuse (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
The Set up Rust step wrote the cargo bin dir to GITHUB_PATH, which does
not help within the same step and is not relied upon across steps on the
Gitea act_runner. Prepend $HOME/.cargo/bin to PATH directly in the setup
and each cargo step instead, so rustup and cargo resolve regardless of
GITHUB_PATH support.

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-17 23:25:21 +02:00
16 changed files with 174 additions and 185 deletions
+16 -8
View File
@@ -22,15 +22,15 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
build-essential pkg-config cmake \ build-essential pkg-config cmake clang libclang-dev \
libopus-dev libasound2-dev libsoapysdr-dev libopus-dev libasound2-dev libsoapysdr-dev
- name: Set up Rust - name: Set up Rust
run: | run: |
export PATH="$HOME/.cargo/bin:$PATH"
if ! command -v rustup >/dev/null 2>&1; then if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi fi
rustup toolchain install stable --profile minimal \ rustup toolchain install stable --profile minimal \
--component rustfmt --component clippy --component rustfmt --component clippy
@@ -47,10 +47,14 @@ jobs:
restore-keys: cargo-${{ runner.os }}- restore-keys: cargo-${{ runner.os }}-
- name: rustfmt - name: rustfmt
run: cargo fmt --all -- --check run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo fmt --all -- --check
- name: clippy - name: clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo clippy --workspace --all-targets --all-features -- -D warnings
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -61,15 +65,15 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
build-essential pkg-config cmake \ build-essential pkg-config cmake clang libclang-dev \
libopus-dev libasound2-dev libsoapysdr-dev libopus-dev libasound2-dev libsoapysdr-dev
- name: Set up Rust - name: Set up Rust
run: | run: |
export PATH="$HOME/.cargo/bin:$PATH"
if ! command -v rustup >/dev/null 2>&1; then if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi fi
rustup toolchain install stable --profile minimal rustup toolchain install stable --profile minimal
rustup default stable rustup default stable
@@ -85,10 +89,14 @@ jobs:
restore-keys: cargo-${{ runner.os }}- restore-keys: cargo-${{ runner.os }}-
- name: Build - name: Build
run: cargo build --workspace --all-targets --locked run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo build --workspace --all-targets --locked
- name: Test - name: Test
run: cargo test --workspace --locked run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo test --workspace --locked
reuse: reuse:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1 -1
View File
@@ -243,7 +243,7 @@ fn parse_frame(frame: RawFrame, channel: &str) -> Option<AisMessage> {
let message_type = get_uint(&bits, 0, 6)? as u8; let message_type = get_uint(&bits, 0, 6)? as u8;
let repeat = get_uint(&bits, 6, 2)? 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 { let mut msg = AisMessage {
rig_id: None, rig_id: None,
+4 -4
View File
@@ -638,7 +638,7 @@ mod tests {
for (i, &ch) in b"N0CALL".iter().enumerate() { for (i, &ch) in b"N0CALL".iter().enumerate() {
addr[i] = ch << 1; 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); let decoded = decode_ax25_address(&addr, 0);
assert_eq!(decoded.call, "N0CALL"); assert_eq!(decoded.call, "N0CALL");
@@ -652,7 +652,7 @@ mod tests {
for (i, &ch) in b"SP2SJG".iter().enumerate() { for (i, &ch) in b"SP2SJG".iter().enumerate() {
addr[i] = ch << 1; 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); let decoded = decode_ax25_address(&addr, 0);
assert_eq!(decoded.call, "SP2SJG"); assert_eq!(decoded.call, "SP2SJG");
@@ -667,7 +667,7 @@ mod tests {
for (i, &ch) in b"W1AW ".iter().enumerate() { for (i, &ch) in b"W1AW ".iter().enumerate() {
addr[i] = ch << 1; addr[i] = ch << 1;
} }
addr[6] = (0 << 1) | 1; addr[6] = 1;
let decoded = decode_ax25_address(&addr, 0); let decoded = decode_ax25_address(&addr, 0);
assert_eq!(decoded.call, "W1AW"); assert_eq!(decoded.call, "W1AW");
@@ -691,7 +691,7 @@ mod tests {
for &ch in src_bytes.as_bytes().iter().take(6) { for &ch in src_bytes.as_bytes().iter().take(6) {
frame.push(ch << 1); frame.push(ch << 1);
} }
frame.push((0 << 1) | 1); // SSID=0, last=true frame.push(1); // SSID=0, last=true
// Control + PID // Control + PID
frame.push(0x03); // UI frame frame.push(0x03); // UI frame
frame.push(0xF0); // No layer-3 protocol frame.push(0xF0); // No layer-3 protocol
@@ -160,8 +160,7 @@ impl CallsignHashTable {
let mut idx = start_idx; let mut idx = start_idx;
loop { loop {
match &self.entries[idx] { let entry = self.entries[idx].as_ref()?;
Some(entry) => {
let stored = (entry.hash & HASH22_MASK) >> shift; let stored = (entry.hash & HASH22_MASK) >> shift;
if stored == target { if stored == target {
return Some(entry.callsign.clone()); return Some(entry.callsign.clone());
@@ -171,9 +170,6 @@ impl CallsignHashTable {
return None; return None;
} }
} }
None => return None,
}
}
} }
/// Age all entries and remove those older than `max_age`. /// Age all entries and remove those older than `max_age`.
+12 -12
View File
@@ -42,8 +42,8 @@ pub(crate) fn ldpc_check(codeword: &[u8; FTX_LDPC_N]) -> i32 {
for m in 0..FTX_LDPC_M { for m in 0..FTX_LDPC_M {
let mut x: u8 = 0; let mut x: u8 = 0;
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize; let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
for i in 0..num_rows { for &nm in FTX_LDPC_NM[m].iter().take(num_rows) {
x ^= codeword[FTX_LDPC_NM[m][i] as usize - 1]; x ^= codeword[nm as usize - 1];
} }
if x != 0 { if x != 0 {
errors += 1; errors += 1;
@@ -81,11 +81,11 @@ pub fn ldpc_decode(
for j in 0..FTX_LDPC_M { for j in 0..FTX_LDPC_M {
let num_rows = FTX_LDPC_NUM_ROWS[j] as usize; let num_rows = FTX_LDPC_NUM_ROWS[j] as usize;
let m_row = j * FTX_LDPC_N; let m_row = j * FTX_LDPC_N;
for ii1 in 0..num_rows { for &nm1 in FTX_LDPC_NM[j].iter().take(num_rows) {
let i1 = FTX_LDPC_NM[j][ii1] as usize - 1; let i1 = nm1 as usize - 1;
let mut a = 1.0f32; let mut a = 1.0f32;
for ii2 in 0..num_rows { for &nm2 in FTX_LDPC_NM[j].iter().take(num_rows) {
let i2 = FTX_LDPC_NM[j][ii2] as usize - 1; let i2 = nm2 as usize - 1;
if i2 != i1 { if i2 != i1 {
a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32); a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32);
} }
@@ -97,8 +97,8 @@ pub fn ldpc_decode(
// Hard decisions // Hard decisions
for i in 0..FTX_LDPC_N { for i in 0..FTX_LDPC_N {
let mut l = codeword[i]; let mut l = codeword[i];
for j in 0..3 { for &mn in FTX_LDPC_MN[i].iter().take(3) {
l += e_matrix[(FTX_LDPC_MN[i][j] as usize - 1) * FTX_LDPC_N + i]; l += e_matrix[(mn as usize - 1) * FTX_LDPC_N + i];
} }
plain[i] = if l > 0.0 { 1 } else { 0 }; plain[i] = if l > 0.0 { 1 } else { 0 };
} }
@@ -113,12 +113,12 @@ pub fn ldpc_decode(
// Update m[][] from e[][] // Update m[][] from e[][]
for i in 0..FTX_LDPC_N { for i in 0..FTX_LDPC_N {
for ji1 in 0..3 { for (ji1, &mn1) in FTX_LDPC_MN[i].iter().enumerate().take(3) {
let j1 = FTX_LDPC_MN[i][ji1] as usize - 1; let j1 = mn1 as usize - 1;
let mut l = codeword[i]; 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 { 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]; l += e_matrix[j2 * FTX_LDPC_N + i];
} }
} }
+2 -6
View File
@@ -64,11 +64,9 @@ pub fn charn(mut c: i32, table: CharTable) -> char {
return EXTRAS[c as usize]; return EXTRAS[c as usize];
} }
} }
CharTable::AlphanumSpaceSlash => { CharTable::AlphanumSpaceSlash if c == 0 => {
if c == 0 {
return '/'; return '/';
} }
}
_ => {} _ => {}
} }
@@ -116,11 +114,9 @@ pub fn nchar(c: char, table: CharTable) -> Option<i32> {
'?' => return Some(n + 4), '?' => return Some(n + 4),
_ => {} _ => {}
}, },
CharTable::AlphanumSpaceSlash => { CharTable::AlphanumSpaceSlash if c == '/' => {
if c == '/' {
return Some(n); return Some(n);
} }
}
_ => {} _ => {}
} }
+12 -27
View File
@@ -632,33 +632,18 @@ impl Candidate {
} }
let segment = usize::from((block_b & 0x0003) as u8); let segment = usize::from((block_b & 0x0003) as u8);
let di = ((block_b >> 2) & 0x1) != 0; let di = ((block_b >> 2) & 0x1) != 0;
match segment { let di_flag = Some(di);
0 => { let slot = match segment {
if self.state.dynamic_pty != Some(di) { 0 => &mut self.state.dynamic_pty,
self.state.dynamic_pty = Some(di); 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; 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 [b0, b1] = block_d.to_be_bytes(); let [b0, b1] = block_d.to_be_bytes();
self.ps_bytes[segment * 2] = sanitize_text_byte(b0); self.ps_bytes[segment * 2] = sanitize_text_byte(b0);
self.ps_bytes[segment * 2 + 1] = sanitize_text_byte(b1); self.ps_bytes[segment * 2 + 1] = sanitize_text_byte(b1);
@@ -1458,9 +1443,9 @@ mod tests {
} }
// BPSK modulate onto the 57 kHz subcarrier. // 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; let phase = TAU * RDS_SUBCARRIER_HZ * t as f32 / sample_rate;
shaped[t] *= phase.cos(); *sample *= phase.cos();
} }
shaped shaped
} }
+1 -3
View File
@@ -134,9 +134,7 @@ mod tests {
.flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1)) .flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1))
.collect(); .collect();
// Append wrong CRC // Append wrong CRC
for _ in 0..16 { bits.resize(bits.len() + 16, 0);
bits.push(0);
}
assert!(!check_crc16(&bits)); assert!(!check_crc16(&bits));
} }
+2 -2
View File
@@ -346,8 +346,8 @@ mod tests {
write_bits(&mut bits, 12, 32, 123456); // source_id write_bits(&mut bits, 12, 32, 123456); // source_id
write_bits(&mut bits, 44, 11, 20); // data_count = 20 write_bits(&mut bits, 44, 11, 20); // data_count = 20
// Fill some payload // Fill some payload
for i in 55..75 { for (i, bit) in bits.iter_mut().enumerate().take(75).skip(55) {
bits[i] = (i % 2) as u8; *bit = (i % 2) as u8;
} }
append_crc(&mut bits); append_crc(&mut bits);
+2 -2
View File
@@ -374,8 +374,8 @@ mod tests {
let (y, m, d, h, mi, _) = unix_to_utc(1775055000); let (y, m, d, h, mi, _) = unix_to_utc(1775055000);
assert_eq!(y, 2026); assert_eq!(y, 2026);
// Just verify reasonable values without asserting exact date. // Just verify reasonable values without asserting exact date.
assert!(m >= 1 && m <= 12); assert!((1..=12).contains(&m));
assert!(d >= 1 && d <= 31); assert!((1..=31).contains(&d));
assert!(h < 24); assert!(h < 24);
assert!(mi < 60); assert!(mi < 60);
} }
+2 -4
View File
@@ -161,10 +161,8 @@ mod tests {
for line_idx in 0..20 { for line_idx in 0..20 {
let mut line = vec![1.0f32; spl]; let mut line = vec![1.0f32; spl];
for j in pulse_start..pulse_start + pw { for slot in line.iter_mut().skip(pulse_start).take(pw) {
if j < spl { *slot = 0.0;
line[j] = 0.0;
}
} }
let result = det.process(&line); let result = det.process(&line);
if let Some(offset) = result { if let Some(offset) = result {
+3 -3
View File
@@ -483,7 +483,7 @@ mod tests {
let c4 = idx27(b'T'); let c4 = idx27(b'T');
let c5 = idx27(b' '); let c5 = idx27(b' ');
let n1 = ((c0 * 36 + c1) * 10 + c2) * 27u32.pow(3) + c3 * 27u32.pow(2) + c4 * 27 + c5; 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 power_code = 37u32;
let mut input_bits = [0u8; NBITS]; let mut input_bits = [0u8; NBITS];
@@ -530,8 +530,8 @@ mod tests {
fn interleave_deinterleave_roundtrip() { fn interleave_deinterleave_roundtrip() {
// Create a sequence of distinguishable values // Create a sequence of distinguishable values
let mut original = [0u8; NSYMS]; let mut original = [0u8; NSYMS];
for i in 0..NSYMS { for (i, slot) in original.iter_mut().enumerate() {
original[i] = (i % 256) as u8; *slot = (i % 256) as u8;
} }
let interleaved = interleave(&original); let interleaved = interleave(&original);
+53 -53
View File
@@ -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` /// 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. /// but publishes to per-rig channels directly and mirrors to global when selected.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -767,3 +714,56 @@ async fn handle_single_rig_connection(
Ok(()) 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");
}
}
+20 -12
View File
@@ -1110,8 +1110,8 @@ url = "remote.example.com:4530"
#[test] #[test]
fn test_validate_rejects_duplicate_remote_names() { fn test_validate_rejects_duplicate_remote_names() {
let mut config = ClientConfig::default(); let config = ClientConfig {
config.remotes = vec![ remotes: vec![
RemoteEntry { RemoteEntry {
name: "dup".to_string(), name: "dup".to_string(),
url: "a:4530".to_string(), url: "a:4530".to_string(),
@@ -1126,20 +1126,24 @@ url = "remote.example.com:4530"
auth: RemoteAuthConfig::default(), auth: RemoteAuthConfig::default(),
poll_interval_ms: 750, poll_interval_ms: 750,
}, },
]; ],
..Default::default()
};
assert!(config.validate().unwrap_err().contains("duplicate name")); assert!(config.validate().unwrap_err().contains("duplicate name"));
} }
#[test] #[test]
fn test_validate_rejects_empty_remote_name() { fn test_validate_rejects_empty_remote_name() {
let mut config = ClientConfig::default(); let config = ClientConfig {
config.remotes = vec![RemoteEntry { remotes: vec![RemoteEntry {
name: "".to_string(), name: "".to_string(),
url: "a:4530".to_string(), url: "a:4530".to_string(),
rig_id: None, rig_id: None,
auth: RemoteAuthConfig::default(), auth: RemoteAuthConfig::default(),
poll_interval_ms: 750, poll_interval_ms: 750,
}]; }],
..Default::default()
};
assert!(config assert!(config
.validate() .validate()
.unwrap_err() .unwrap_err()
@@ -1148,14 +1152,16 @@ url = "remote.example.com:4530"
#[test] #[test]
fn test_validate_rejects_empty_remote_url() { fn test_validate_rejects_empty_remote_url() {
let mut config = ClientConfig::default(); let config = ClientConfig {
config.remotes = vec![RemoteEntry { remotes: vec![RemoteEntry {
name: "hf".to_string(), name: "hf".to_string(),
url: " ".to_string(), url: " ".to_string(),
rig_id: None, rig_id: None,
auth: RemoteAuthConfig::default(), auth: RemoteAuthConfig::default(),
poll_interval_ms: 750, poll_interval_ms: 750,
}]; }],
..Default::default()
};
assert!(config assert!(config
.validate() .validate()
.unwrap_err() .unwrap_err()
@@ -1164,14 +1170,16 @@ url = "remote.example.com:4530"
#[test] #[test]
fn test_validate_rejects_zero_remote_poll_interval() { fn test_validate_rejects_zero_remote_poll_interval() {
let mut config = ClientConfig::default(); let config = ClientConfig {
config.remotes = vec![RemoteEntry { remotes: vec![RemoteEntry {
name: "hf".to_string(), name: "hf".to_string(),
url: "a:4530".to_string(), url: "a:4530".to_string(),
rig_id: None, rig_id: None,
auth: RemoteAuthConfig::default(), auth: RemoteAuthConfig::default(),
poll_interval_ms: 0, poll_interval_ms: 0,
}]; }],
..Default::default()
};
assert!(config assert!(config
.validate() .validate()
.unwrap_err() .unwrap_err()
+1 -1
View File
@@ -1678,7 +1678,7 @@ mod tests {
#[test] #[test]
fn global_target_for_snapshot_skips_other_server_selection() { fn global_target_for_snapshot_skips_other_server_selection() {
let snapshot = sample_snapshot(); let snapshot = sample_snapshot();
let rigs = vec![RigEntry { let rigs = [RigEntry {
rig_id: "hf".to_string(), rig_id: "hf".to_string(),
display_name: Some("Gdansk HF".to_string()), display_name: Some("Gdansk HF".to_string()),
state: snapshot, state: snapshot,
@@ -1596,7 +1596,7 @@ mod tests {
let t = i as f32 / fs; let t = i as f32 / fs;
adj_phase += adj_mod_index * adj_composite[i]; adj_phase += adj_mod_index * adj_composite[i];
let adj = Complex::from_polar(0.5, adj_phase + TAU * adj_freq_offset * t); let adj = Complex::from_polar(0.5, adj_phase + TAU * adj_freq_offset * t);
*s = *s + adj; *s += adj;
} }
let mut decoder = WfmStereoDecoder::new( let mut decoder = WfmStereoDecoder::new(
@@ -1673,7 +1673,7 @@ mod tests {
// Mix at 70 % of the main signal's amplitude — strong enough to // Mix at 70 % of the main signal's amplitude — strong enough to
// overcome the FM capture effect and visibly degrade pilot coherence. // overcome the FM capture effect and visibly degrade pilot coherence.
for (s, intf) in iq.iter_mut().zip(intf_iq.iter()) { 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( let mut decoder = WfmStereoDecoder::new(