276 lines
8.4 KiB
Rust
276 lines
8.4 KiB
Rust
|
|
use super::{
|
||
|
|
allow_gadget_cycle, detect_uac_card_candidates, init_tracing, next_stream_id,
|
||
|
|
open_ear_with_retry, open_hid_if_ready, open_with_retry, parse_uac_named_card_candidates,
|
||
|
|
parse_uac_numeric_card_ids, parse_uac_pcm_candidates, preferred_uac_device_candidates,
|
||
|
|
push_audio_candidate, push_audio_candidate_family, should_recover_hid_error, write_hid_report,
|
||
|
|
};
|
||
|
|
use serial_test::serial;
|
||
|
|
use std::collections::BTreeSet;
|
||
|
|
use std::sync::Arc;
|
||
|
|
use temp_env::with_var;
|
||
|
|
use tempfile::NamedTempFile;
|
||
|
|
use tempfile::tempdir;
|
||
|
|
use tokio::io::AsyncWriteExt;
|
||
|
|
use tokio::sync::Mutex;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn allow_gadget_cycle_tracks_env_presence() {
|
||
|
|
with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
|
||
|
|
assert!(!allow_gadget_cycle());
|
||
|
|
});
|
||
|
|
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
|
||
|
|
assert!(allow_gadget_cycle());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn should_recover_hid_error_matches_transport_failures() {
|
||
|
|
assert!(should_recover_hid_error(Some(libc::ENOTCONN)));
|
||
|
|
assert!(should_recover_hid_error(Some(libc::ESHUTDOWN)));
|
||
|
|
assert!(should_recover_hid_error(Some(libc::EPIPE)));
|
||
|
|
assert!(!should_recover_hid_error(Some(libc::EAGAIN)));
|
||
|
|
assert!(!should_recover_hid_error(None));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn next_stream_id_monotonically_increments() {
|
||
|
|
let first = next_stream_id();
|
||
|
|
let second = next_stream_id();
|
||
|
|
assert!(second > first);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn preferred_uac_device_candidates_keeps_custom_override_only() {
|
||
|
|
let candidates = preferred_uac_device_candidates("hw:7,0");
|
||
|
|
assert_eq!(candidates, vec!["hw:7,0", "plughw:7,0"]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn preferred_uac_device_candidates_handles_blank_and_plughw_overrides() {
|
||
|
|
assert!(preferred_uac_device_candidates(" ").is_empty());
|
||
|
|
assert_eq!(
|
||
|
|
preferred_uac_device_candidates(" plughw:8,2 "),
|
||
|
|
vec!["plughw:8,2", "hw:8,2"]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn preferred_uac_device_candidates_expands_known_aliases() {
|
||
|
|
let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
|
||
|
|
assert!(candidates.iter().any(|value| value == "hw:UAC2Gadget,0"));
|
||
|
|
assert!(
|
||
|
|
candidates
|
||
|
|
.iter()
|
||
|
|
.any(|value| value == "plughw:UAC2Gadget,0")
|
||
|
|
);
|
||
|
|
assert!(candidates.iter().any(|value| value == "hw:UAC2_Gadget,0"));
|
||
|
|
assert!(candidates.iter().any(|value| value == "hw:Composite,0"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn audio_candidate_helpers_dedupe_and_pair_hw_plughw_forms() {
|
||
|
|
let mut out = Vec::new();
|
||
|
|
let mut seen = BTreeSet::new();
|
||
|
|
|
||
|
|
push_audio_candidate(&mut out, &mut seen, " ");
|
||
|
|
push_audio_candidate(&mut out, &mut seen, " hw:9,0 ");
|
||
|
|
push_audio_candidate(&mut out, &mut seen, "hw:9,0");
|
||
|
|
push_audio_candidate_family(&mut out, &mut seen, "plughw:9,0");
|
||
|
|
push_audio_candidate_family(&mut out, &mut seen, " ");
|
||
|
|
|
||
|
|
assert_eq!(out, vec!["hw:9,0".to_string(), "plughw:9,0".to_string()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[cfg(coverage)]
|
||
|
|
#[serial]
|
||
|
|
fn preferred_uac_candidates_include_detected_cards_before_static_aliases() {
|
||
|
|
temp_env::with_vars(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
"LESAVKA_TEST_ASOUND_CARDS",
|
||
|
|
Some(
|
||
|
|
" 7 [DetectedGadget ]: USB-Audio - UAC2 Gadget\n\
|
||
|
|
8 [LesavkaAudio ]: USB-Audio - Lesavka\n",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"LESAVKA_TEST_ASOUND_PCM",
|
||
|
|
Some("07-03: USB Audio : USB Audio : playback 1 : capture 1\n"),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
|| {
|
||
|
|
let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
|
||
|
|
assert!(
|
||
|
|
candidates
|
||
|
|
.iter()
|
||
|
|
.any(|value| value == "hw:DetectedGadget,0")
|
||
|
|
);
|
||
|
|
assert!(candidates.iter().any(|value| value == "hw:LesavkaAudio,0"));
|
||
|
|
assert!(candidates.iter().any(|value| value == "hw:7,3"));
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn detect_uac_card_candidates_returns_hw_names_only() {
|
||
|
|
let live = detect_uac_card_candidates();
|
||
|
|
assert!(live.iter().all(|value| value.starts_with("hw:")));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_uac_card_helpers_collect_named_and_numeric_candidates() {
|
||
|
|
let cards = "\
|
||
|
|
0 [PCH ]: HDA-Intel - HDA Intel PCH\n\
|
||
|
|
2 [UAC2Gadget ]: USB-Audio - UAC2Gadget\n\
|
||
|
|
Lesavka USB Audio\n";
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
parse_uac_named_card_candidates(cards),
|
||
|
|
vec!["hw:UAC2Gadget,0"]
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
parse_uac_numeric_card_ids(cards).contains("2"),
|
||
|
|
"expected numeric card index for the gadget card"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_uac_card_helpers_ignore_malformed_candidates() {
|
||
|
|
let cards = "\
|
||
|
|
XX [BrokenGadget ]: USB-Audio - UAC2 Gadget\n\
|
||
|
|
03 NoBracketGadget : USB-Audio - Lesavka\n\
|
||
|
|
04 [ ]: USB-Audio - Composite\n\
|
||
|
|
05 [Composite ]: USB-Audio - Composite\n";
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
parse_uac_named_card_candidates(cards),
|
||
|
|
vec!["hw:BrokenGadget,0", "hw:Composite,0"]
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
parse_uac_numeric_card_ids(cards),
|
||
|
|
BTreeSet::from(["03".to_string(), "04".to_string(), "05".to_string()])
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_uac_pcm_candidates_expands_all_matching_device_indexes() {
|
||
|
|
let pcm = "\
|
||
|
|
00-00: PCH device : playback 1 : capture 1\n\
|
||
|
|
02-00: USB Audio : USB Audio : playback 1 : capture 1\n\
|
||
|
|
02-01: USB Audio #1 : USB Audio #1 : playback 1 : capture 1\n";
|
||
|
|
let ids = BTreeSet::from(["2".to_string()]);
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
parse_uac_pcm_candidates(pcm, &ids),
|
||
|
|
vec!["hw:2,0", "hw:2,1"]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_uac_pcm_candidates_normalizes_zeroes_and_skips_non_matching_cards() {
|
||
|
|
let pcm = "\
|
||
|
|
00-00: Zero card : playback 1 : capture 1\n\
|
||
|
|
09-03: Other card : playback 1 : capture 1\n\
|
||
|
|
bad line without separator\n";
|
||
|
|
let ids = BTreeSet::from(["0".to_string()]);
|
||
|
|
|
||
|
|
assert_eq!(parse_uac_pcm_candidates(pcm, &ids), vec!["hw:0,0"]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[serial]
|
||
|
|
async fn open_with_retry_opens_existing_file() {
|
||
|
|
let tmp = NamedTempFile::new().expect("temp file");
|
||
|
|
let mut file = open_with_retry(tmp.path().to_str().unwrap())
|
||
|
|
.await
|
||
|
|
.expect("open should succeed");
|
||
|
|
file.write_all(b"ok").await.expect("write temp file");
|
||
|
|
file.sync_all().await.expect("sync temp file");
|
||
|
|
assert_eq!(
|
||
|
|
tokio::fs::read(tmp.path()).await.expect("read temp file"),
|
||
|
|
b"ok"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn init_tracing_returns_a_guard_under_coverage() {
|
||
|
|
let _guard = init_tracing().expect("coverage tracing guard");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[serial]
|
||
|
|
async fn hid_open_helpers_return_contextual_errors_for_bad_paths() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let err = open_with_retry(dir.path().to_str().unwrap())
|
||
|
|
.await
|
||
|
|
.expect_err("directory should not open as HID file");
|
||
|
|
assert!(format!("{err:#}").contains("opening"));
|
||
|
|
|
||
|
|
let err = open_hid_if_ready(dir.path().to_str().unwrap())
|
||
|
|
.await
|
||
|
|
.expect_err("directory should be a hard open error");
|
||
|
|
assert!(format!("{err:#}").contains("opening"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[serial]
|
||
|
|
async fn write_hid_report_writes_bytes() {
|
||
|
|
let tmp = NamedTempFile::new().expect("temp file");
|
||
|
|
let file = tokio::fs::OpenOptions::new()
|
||
|
|
.write(true)
|
||
|
|
.truncate(true)
|
||
|
|
.open(tmp.path())
|
||
|
|
.await
|
||
|
|
.expect("open temp file");
|
||
|
|
let shared = Arc::new(Mutex::new(Some(file)));
|
||
|
|
|
||
|
|
write_hid_report(&shared, tmp.path().to_str().unwrap(), &[1, 2, 3, 4])
|
||
|
|
.await
|
||
|
|
.expect("write succeeds");
|
||
|
|
|
||
|
|
let contents = tokio::fs::read(tmp.path())
|
||
|
|
.await
|
||
|
|
.expect("read back temp file");
|
||
|
|
assert_eq!(&contents, &[1, 2, 3, 4]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[serial]
|
||
|
|
async fn write_hid_report_opens_lazily_when_handle_is_empty() {
|
||
|
|
let tmp = NamedTempFile::new().expect("temp file");
|
||
|
|
let shared = Arc::new(Mutex::new(None));
|
||
|
|
|
||
|
|
write_hid_report(&shared, tmp.path().to_str().unwrap(), &[9, 8])
|
||
|
|
.await
|
||
|
|
.expect("lazy write succeeds");
|
||
|
|
|
||
|
|
let contents = tokio::fs::read(tmp.path())
|
||
|
|
.await
|
||
|
|
.expect("read back temp file");
|
||
|
|
assert_eq!(&contents, &[9, 8]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn open_ear_with_retry_reports_bad_capture_device() {
|
||
|
|
let err = temp_env::with_vars(
|
||
|
|
[
|
||
|
|
("LESAVKA_AUDIO_INIT_ATTEMPTS", Some("1")),
|
||
|
|
("LESAVKA_AUDIO_INIT_DELAY_MS", Some("0")),
|
||
|
|
],
|
||
|
|
|| {
|
||
|
|
let runtime = tokio::runtime::Runtime::new().expect("test runtime");
|
||
|
|
match runtime.block_on(open_ear_with_retry(
|
||
|
|
"hw:DefinitelyMissingLesavkaDevice,99",
|
||
|
|
99,
|
||
|
|
)) {
|
||
|
|
Ok(_) => panic!("missing ALSA source should fail"),
|
||
|
|
Err(err) => err,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
assert!(!format!("{err:#}").is_empty());
|
||
|
|
}
|