#![allow(clippy::type_complexity)]
use super::*;
fn assert_send<T: Send>() {}
#[test]
fn align_work_item_is_send() {
assert_send::<AlignWorkItem>();
}
#[test]
fn data_dependent_failures_are_recoverable() {
let make = |variant: fn(AlignmentFailure) -> AlignmentError| {
WorkFailure::Alignment(variant(AlignmentFailure::new(
SmolStr::new(""),
crate::types::Lang::En,
)))
};
let recoverable: [(&str, fn(AlignmentFailure) -> AlignmentError); 2] = [
("NoAlignmentPath", AlignmentError::NoAlignmentPath),
("EmptyText", AlignmentError::EmptyText),
];
for (name, ctor) in recoverable {
let f = make(ctor);
assert!(
alignment_failure_is_recoverable(&f),
"{name} must preserve ASR text",
);
}
}
#[test]
#[cfg_attr(
not(asry_w2v_en),
ignore = "needs the English wav2vec2 fixture: ASRY_FETCH_W2V=en cargo test --features alignment"
)]
fn too_short_chunk_recovers_to_empty_result() {
use core::num::NonZeroU32;
use mediatime::Timebase;
use crate::runner::aligner::{
AlignerKey, AlignmentSetBuilder, EnglishNormalizer, test_fixtures::english_aligner,
};
const ASR_TEXT: &str = "hello world";
let set = AlignmentSetBuilder::new()
.with_fallback(AlignmentFallback::Error)
.register(
AlignerKey::Lang(Lang::En),
english_aligner(Box::new(EnglishNormalizer::new())),
)
.build();
assert!(
matches!(set.lookup(&Lang::En), AlignmentLookup::Hit { .. }),
"the recovery under test lives on the Hit path; a Miss would prove nothing"
);
let job = AlignWorkItem {
chunk_id: ChunkId::from_raw(0),
samples: Arc::from(vec![0.0_f32; 200]),
sub_segments: Vec::new(),
text: SmolStr::new(ASR_TEXT),
language: Lang::En,
runs: Vec::new(),
abort_flag: Arc::new(AtomicBool::new(false)),
chunk_first_sample_in_stream: 0,
samples_to_output_range: Arc::new(|start, end| {
TimeRange::new(
start as i64,
end as i64,
Timebase::new(1, NonZeroU32::new(16_000).unwrap()),
)
}),
oov_decisions: Vec::new(),
};
let run_options = RunOptions::new().expect("RunOptions::new");
let result = run_one_alignment(&set, &job, &run_options).expect(
"`NoAlignmentPath` is classified recoverable, so the pool must absorb it into Ok(empty). \
An Err here would reach `handle_failure` upstream and turn a chunk carrying a perfectly \
good ASR transcript into Event::Error — alignment is best-effort, never destructive.",
);
assert!(
result.words().is_empty(),
"a dropped alignment contributes no words; got {:?}",
result.words()
);
assert_eq!(
job.text().as_str(),
ASR_TEXT,
"the input work item still carries the ASR text (input sanity, not the preservation proof)"
);
}
#[test]
fn backend_alignment_failures_stay_fatal() {
let make = |variant: fn(AlignmentFailure) -> AlignmentError| {
WorkFailure::Alignment(variant(AlignmentFailure::new(SmolStr::new(""), Lang::En)))
};
let fatal: [(&str, fn(AlignmentFailure) -> AlignmentError); 3] = [
("ModelInference", AlignmentError::ModelInference),
("Tokenization", AlignmentError::Tokenization),
("Normalization", AlignmentError::Normalization),
];
for (name, ctor) in fatal {
let f = make(ctor);
assert!(
!alignment_failure_is_recoverable(&f),
"{name} signals a backend/config bug; must propagate",
);
}
}
#[test]
fn liveness_and_registry_failures_stay_fatal() {
use core::time::Duration;
use crate::types::{AsrError, AsrFailure, Lang, WorkerKind};
assert!(!alignment_failure_is_recoverable(&WorkFailure::WorkerHang(
WorkerHangTimeout::new(WorkerKind::Alignment, Duration::from_secs(30))
)));
assert!(!alignment_failure_is_recoverable(
&WorkFailure::LanguageUnsupported(LanguageUnsupportedForAlignment::new(Lang::En))
));
assert!(!alignment_failure_is_recoverable(&WorkFailure::Asr(
AsrError::AllTemperaturesExhausted(AsrFailure::new(SmolStr::new("")))
)));
}
#[test]
fn bounds_source_counters_accumulate_distribution() {
use crate::align::BoundsSource;
let mut c = BoundsSourceCounters::default();
c.observe_bounds(BoundsSource::Dtw);
c.observe_bounds(BoundsSource::Dtw);
c.observe_bounds(BoundsSource::Segment);
c.observe_bounds(BoundsSource::Wholeclip);
c.observe_unaligned();
c.observe_unaligned();
assert_eq!(c.runs_total(), 4);
assert_eq!(c.runs_dtw(), 2);
assert_eq!(c.runs_segment(), 1);
assert_eq!(c.runs_wholeclip(), 1);
assert_eq!(c.runs_unaligned(), 2);
}
#[test]
fn bounds_source_counters_default_is_zero() {
let c = BoundsSourceCounters::default();
assert_eq!(c.runs_total(), 0);
assert_eq!(c.runs_dtw(), 0);
assert_eq!(c.runs_segment(), 0);
assert_eq!(c.runs_wholeclip(), 0);
assert_eq!(c.runs_unaligned(), 0);
}
#[test]
fn run_audio_slice_segment_bounds_clamp_to_chunk_length() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
100,
300,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 1_600);
assert_eq!(hi, 4_800);
}
#[test]
fn run_audio_slice_wholeclip_uses_full_chunk() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
i64::MIN,
i64::MAX,
0,
BoundsSource::Wholeclip,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 16_000);
}
#[test]
fn run_audio_slice_inverted_bounds_collapse_to_empty_slice() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
500,
100,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 0);
}
#[test]
fn run_audio_slice_negative_t0_collapses_to_empty_slice() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
-10,
100,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 0);
}
#[test]
fn run_audio_slice_out_of_chunk_t0_collapses_to_empty_slice_at_end() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
5_000,
6_000,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 16_000);
assert_eq!(hi, 16_000);
}
#[test]
fn run_audio_slice_ignores_chunk_first_sample_in_stream() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
100,
500,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 1_000_000_000);
assert_eq!(lo, 1600);
assert_eq!(hi, 8000);
}
#[test]
fn clip_sub_segments_offsets_into_run_local_space() {
use core::num::NonZeroU32;
let tb = mediatime::Timebase::new(1, NonZeroU32::new(16_000).unwrap());
let subs = vec![
TimeRange::new(2_000, 3_000, tb),
TimeRange::new(800, 2_400, tb),
TimeRange::new(8_000, 9_000, tb),
];
let out = clip_sub_segments(&subs, 1_600, 4_800, &Lang::En).expect("ok");
assert_eq!(out.len(), 2);
assert_eq!(out[0].start_pts(), 400);
assert_eq!(out[0].end_pts(), 1_400);
assert_eq!(out[1].start_pts(), 0);
assert_eq!(out[1].end_pts(), 800);
}
#[test]
fn sort_words_by_pts_orders_overlapping_runs() {
use core::num::NonZeroU32;
use mediatime::Timebase;
let tb = Timebase::new(1, NonZeroU32::new(16_000).unwrap());
let mk = |start: i64, end: i64, text: &str| {
crate::types::Word::new(SmolStr::new(text), TimeRange::new(start, end, tb), 1.0)
};
let mut words = vec![
mk(8000, 9000, "world"),
mk(0, 1000, "hello"),
mk(4000, 5000, "there"),
];
sort_words_by_pts(&mut words);
let texts: Vec<&str> = words.iter().map(|w| w.text()).collect();
assert_eq!(texts, vec!["hello", "there", "world"]);
let mut prev = i64::MIN;
for w in &words {
let s = w.range().start_pts();
assert!(
s >= prev,
"word starts must be monotone; got {s} after {prev}"
);
prev = s;
}
}
#[test]
fn sort_words_by_pts_breaks_ties_by_end_pts() {
use core::num::NonZeroU32;
use mediatime::Timebase;
let tb = Timebase::new(1, NonZeroU32::new(16_000).unwrap());
let mk = |start: i64, end: i64, text: &str| {
crate::types::Word::new(SmolStr::new(text), TimeRange::new(start, end, tb), 1.0)
};
let mut words = vec![mk(0, 2000, "longer"), mk(0, 1000, "shorter")];
sort_words_by_pts(&mut words);
assert_eq!(words[0].text(), "shorter");
assert_eq!(words[1].text(), "longer");
}
#[test]
fn check_abort_between_runs_returns_timeout_when_flag_set() {
let started = Instant::now();
let flag = AtomicBool::new(true);
let result = check_abort_between_runs(&flag, started);
assert!(
matches!(result, Err(WorkFailure::WorkerHang(_))),
"abort flag set → expected WorkerHangTimeout(Alignment); got {result:?}",
);
}
#[test]
fn semantic_oov_is_recoverable() {
use crate::types::Lang;
let f = WorkFailure::Alignment(AlignmentError::SemanticOutOfVocab(AlignmentFailure::new(
SmolStr::new("pronounced symbol"),
Lang::En,
)));
assert!(
alignment_failure_is_recoverable(&f),
"SemanticOutOfVocab must recover so ASR text isn't lost",
);
}
#[test]
fn tokenization_failed_stays_fatal() {
use crate::types::Lang;
let f = WorkFailure::Alignment(AlignmentError::Tokenization(AlignmentFailure::new(
SmolStr::new(""),
Lang::En,
)));
assert!(
!alignment_failure_is_recoverable(&f),
"TokenizationFailed signals a tokenizer/model mismatch; must stay fatal",
);
}
#[test]
fn check_abort_between_runs_passes_through_when_flag_clear() {
let started = Instant::now();
let flag = AtomicBool::new(false);
assert!(check_abort_between_runs(&flag, started).is_ok());
}
#[test]
fn outer_oov_decisions_shape_predicate() {
fn shape_ok(outer: usize, runs_len: usize) -> bool {
let expected = if runs_len == 0 { 1 } else { runs_len };
outer == 0 || outer == expected
}
assert!(shape_ok(0, 0));
assert!(shape_ok(1, 0));
assert!(!shape_ok(2, 0)); assert!(!shape_ok(3, 0));
assert!(shape_ok(0, 2));
assert!(shape_ok(2, 2));
assert!(!shape_ok(1, 2)); assert!(!shape_ok(3, 2));
}
#[test]
fn per_run_oov_decisions_are_indexed_by_run_idx() {
use crate::core::{OovDecision, OovEvent, OovKind, ResolvedOov};
fn synth(decision: OovDecision, char_idx: usize) -> ResolvedOov {
ResolvedOov::new(
OovEvent::new(OovKind::Symbol('?'), char_idx, 0, Lang::En),
decision,
)
}
let oov_decisions: Vec<Vec<ResolvedOov>> = vec![
vec![
synth(OovDecision::Wildcard, 0),
synth(OovDecision::Wildcard, 1),
synth(OovDecision::Wildcard, 2),
],
vec![
synth(OovDecision::Wildcard, 0),
synth(OovDecision::FailClosed, 1),
],
vec![],
];
for run_idx in 0..3 {
let slice = oov_decisions
.get(run_idx)
.map(|v| v.as_slice())
.unwrap_or(&[]);
match run_idx {
0 => {
assert_eq!(slice.len(), 3);
assert!(slice.iter().all(|r| r.decision() == OovDecision::Wildcard));
}
1 => {
assert_eq!(slice.len(), 2);
assert_eq!(slice[0].decision(), OovDecision::Wildcard);
assert_eq!(slice[1].decision(), OovDecision::FailClosed);
}
2 => assert!(slice.is_empty()),
_ => unreachable!(),
}
}
let oob = oov_decisions.get(99).map(|v| v.as_slice()).unwrap_or(&[]);
assert!(oob.is_empty());
}
#[test]
fn validate_oov_decision_languages_whole_chunk_match_passes() {
use crate::core::{OovDecision, OovEvent, OovKind, ResolvedOov};
let resolved = vec![vec![ResolvedOov::new(
OovEvent::new(OovKind::Symbol('&'), 2, 0, Lang::En),
OovDecision::Wildcard,
)]];
assert!(validate_oov_decision_languages(&[], &Lang::En, &resolved).is_ok());
}
#[test]
fn validate_oov_decision_languages_whole_chunk_mismatch_rejects() {
use crate::core::{OovDecision, OovEvent, OovKind, ResolvedOov};
let resolved = vec![vec![ResolvedOov::new(
OovEvent::new(OovKind::Symbol('&'), 2, 0, Lang::En),
OovDecision::Wildcard,
)]];
let result = validate_oov_decision_languages(&[], &Lang::Ko, &resolved);
match result {
Err(WorkFailure::Alignment(AlignmentError::Tokenization(payload))) => assert!(
payload
.message()
.contains("oov_decisions[0][0].event.language")
&& payload.message().contains("job.language"),
"diagnostic should cite the whole-chunk mismatch; got {message}",
message = payload.message(),
),
other => panic!("expected TokenizationFailed; got {other:?}"),
}
}
#[test]
fn validate_oov_decision_languages_per_run_mismatch_rejects() {
use crate::{
align::{BoundsSource, Run},
core::{OovDecision, OovEvent, OovKind, ResolvedOov},
};
use smol_str::SmolStr;
let runs = vec![
Run::new(
Lang::En,
SmolStr::from("AT&T"),
0,
1_000,
0,
BoundsSource::Segment,
),
Run::new(
Lang::Ko,
SmolStr::from("4번"),
1_000,
2_000,
1,
BoundsSource::Segment,
),
];
let resolved = vec![
vec![ResolvedOov::new(
OovEvent::new(OovKind::Symbol('&'), 2, 0, Lang::En),
OovDecision::Wildcard,
)],
vec![ResolvedOov::new(
OovEvent::new(OovKind::Symbol('4'), 0, 0, Lang::En),
OovDecision::Wildcard,
)],
];
let result = validate_oov_decision_languages(&runs, &Lang::En, &resolved);
match result {
Err(WorkFailure::Alignment(AlignmentError::Tokenization(payload))) => assert!(
payload.message().contains("oov_decisions[1][0]")
&& payload.message().contains("runs[1].language()"),
"diagnostic should cite the run index of the mismatch; got {message}",
message = payload.message(),
),
other => panic!("expected TokenizationFailed; got {other:?}"),
}
}
#[test]
fn validate_oov_decision_languages_empty_passes() {
let empty: Vec<Vec<ResolvedOov>> = Vec::new();
assert!(validate_oov_decision_languages(&[], &Lang::En, &empty).is_ok());
}
#[test]
fn clip_sub_segments_rejects_non_16000_timebase() {
use core::num::NonZeroU32;
let tb_48k = mediatime::Timebase::new(1, NonZeroU32::new(48_000).unwrap());
let subs = vec![TimeRange::new(2_000, 3_000, tb_48k)];
let result = clip_sub_segments(&subs, 1_600, 4_800, &Lang::En);
match result {
Err(WorkFailure::Alignment(AlignmentError::ModelInference(payload))) => {
let message = payload.message();
assert!(
message.contains("1/16000") && message.contains("48000"),
"expected diagnostic citing both timebases; got {message}",
message = message,
);
}
other => panic!("expected ModelInferenceFailed, got {other:?}"),
}
}