[test](trx-server): add SDR config validation unit tests

Adds 12 unit tests for ServerConfig::validate_sdr() covering: minimal
valid config, non-SDR skip, empty/missing args, zero sample_rate,
channel IF out-of-range (positive, negative, exactly at Nyquist), dual
stream_opus, tx_enabled with SDR backend, duplicate decoder, and
multiple simultaneous errors. Marks SDR-11 complete in SDR.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-24 22:12:27 +01:00
parent 1be5b3d4c2
commit dc3fd63a83
2 changed files with 197 additions and 1 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ This document specifies the requirements for a SoapySDR-based RX-only backend (`
| ID | Status | Task | Touches | Needs |
|----|--------|------|---------|-------|
| SDR-10 | `[x]` | Unit tests for `demod.rs`: known-input tone through each demodulator, check output frequency correct | `…/src/demod.rs` | SDR-05 |
| SDR-11 | `[ ]` | Unit tests for config validation: channel IF out-of-range, dual `stream_opus`, TX enabled with SDR backend, AGC fallback warning | `src/trx-server/src/config.rs` | SDR-03 |
| SDR-11 | `[x]` | Unit tests for config validation: channel IF out-of-range, dual `stream_opus`, TX enabled with SDR backend, AGC fallback warning | `src/trx-server/src/config.rs` | SDR-03 |
---
+196
View File
@@ -822,4 +822,200 @@ tokens = ["secret123"]
assert!(paths.contains(&home_dir.join(".trx-server.toml")));
}
}
// --- SDR-11: validate_sdr() unit tests ---
fn sdr_config_with_access(args: &str) -> ServerConfig {
let mut cfg = ServerConfig::default();
cfg.rig.access.access_type = Some("sdr".to_string());
cfg.rig.access.args = Some(args.to_string());
cfg.audio.tx_enabled = false;
cfg.sdr.sample_rate = 1_920_000;
cfg.sdr.center_offset_hz = 200_000;
cfg
}
fn add_channel(
cfg: &mut ServerConfig,
id: &str,
offset_hz: i64,
stream_opus: bool,
decoders: Vec<String>,
) {
let mut ch = SdrChannelConfig::default();
ch.id = id.to_string();
ch.offset_hz = offset_hz;
ch.stream_opus = stream_opus;
ch.decoders = decoders;
cfg.sdr.channels.push(ch);
}
#[test]
fn test_sdr_validate_ok_minimal() {
let mut cfg = sdr_config_with_access("driver=rtlsdr");
add_channel(&mut cfg, "primary", 0, false, vec![]);
let errors = cfg.validate_sdr();
assert!(
errors.is_empty(),
"expected no errors, got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_non_sdr_skips() {
let cfg = ServerConfig::default();
let errors = cfg.validate_sdr();
assert!(
errors.is_empty(),
"expected no errors for non-sdr config, got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_empty_args() {
let cfg = sdr_config_with_access("");
let errors = cfg.validate_sdr();
assert_eq!(errors.len(), 1, "expected exactly 1 error, got: {:?}", errors);
assert!(
errors[0].contains("args"),
"expected error to mention 'args', got: {}",
errors[0]
);
}
#[test]
fn test_sdr_validate_missing_args() {
let mut cfg = sdr_config_with_access("placeholder");
cfg.rig.access.args = None;
let errors = cfg.validate_sdr();
assert_eq!(errors.len(), 1, "expected exactly 1 error, got: {:?}", errors);
assert!(
errors[0].contains("args"),
"expected error to mention 'args', got: {}",
errors[0]
);
}
#[test]
fn test_sdr_validate_zero_sample_rate() {
let mut cfg = sdr_config_with_access("driver=rtlsdr");
cfg.sdr.sample_rate = 0;
let errors = cfg.validate_sdr();
assert!(
errors.iter().any(|e| e.contains("sample_rate")),
"expected error mentioning 'sample_rate', got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_channel_if_out_of_range() {
// sample_rate=1_000_000 => Nyquist=500_000
// center_offset_hz=0, offset_hz=600_000 => IF=600_000 > 500_000
let mut cfg = sdr_config_with_access("driver=rtlsdr");
cfg.sdr.sample_rate = 1_000_000;
cfg.sdr.center_offset_hz = 0;
add_channel(&mut cfg, "ch_high", 600_000, false, vec![]);
let errors = cfg.validate_sdr();
assert!(
errors
.iter()
.any(|e| e.contains("ch_high") && (e.contains("Nyquist") || e.contains("exceeds"))),
"expected error mentioning channel id and Nyquist/exceeds, got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_channel_if_negative_out_of_range() {
// sample_rate=1_000_000 => Nyquist=500_000
// center_offset_hz=0, offset_hz=-600_000 => IF=-600_000, abs=600_000 > 500_000
let mut cfg = sdr_config_with_access("driver=rtlsdr");
cfg.sdr.sample_rate = 1_000_000;
cfg.sdr.center_offset_hz = 0;
add_channel(&mut cfg, "ch_low", -600_000, false, vec![]);
let errors = cfg.validate_sdr();
assert!(
errors
.iter()
.any(|e| e.contains("ch_low") && (e.contains("Nyquist") || e.contains("exceeds"))),
"expected error mentioning channel id and Nyquist/exceeds, got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_channel_if_exactly_nyquist_is_invalid() {
// sample_rate=1_000_000 => Nyquist=500_000
// IF=500_000 is NOT strictly less than 500_000 => invalid
let mut cfg = sdr_config_with_access("driver=rtlsdr");
cfg.sdr.sample_rate = 1_000_000;
cfg.sdr.center_offset_hz = 0;
add_channel(&mut cfg, "ch_nyquist", 500_000, false, vec![]);
let errors = cfg.validate_sdr();
assert!(
errors
.iter()
.any(|e| e.contains("ch_nyquist") && (e.contains("Nyquist") || e.contains("exceeds"))),
"expected error for IF exactly at Nyquist, got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_dual_stream_opus() {
let mut cfg = sdr_config_with_access("driver=rtlsdr");
add_channel(&mut cfg, "ch1", 0, true, vec![]);
add_channel(&mut cfg, "ch2", 10_000, true, vec![]);
let errors = cfg.validate_sdr();
assert!(
errors.iter().any(|e| e.contains("stream_opus")),
"expected error mentioning 'stream_opus', got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_tx_enabled_with_sdr() {
let mut cfg = sdr_config_with_access("driver=rtlsdr");
cfg.audio.tx_enabled = true;
let errors = cfg.validate_sdr();
assert!(
errors.iter().any(|e| e.contains("tx_enabled")),
"expected error mentioning 'tx_enabled', got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_duplicate_decoder() {
let mut cfg = sdr_config_with_access("driver=rtlsdr");
add_channel(&mut cfg, "ch1", 0, false, vec!["ft8".to_string()]);
add_channel(&mut cfg, "ch2", 10_000, false, vec!["ft8".to_string()]);
let errors = cfg.validate_sdr();
assert!(
errors
.iter()
.any(|e| e.contains("ft8") || e.contains("decoder")),
"expected error mentioning 'ft8' or 'decoder', got: {:?}",
errors
);
}
#[test]
fn test_sdr_validate_multiple_errors() {
let mut cfg = sdr_config_with_access("placeholder");
cfg.rig.access.args = None;
cfg.sdr.sample_rate = 0;
cfg.audio.tx_enabled = true;
let errors = cfg.validate_sdr();
assert_eq!(
errors.len(),
3,
"expected exactly 3 errors, got: {:?}",
errors
);
}
}