#![cfg(feature = "cloud")]
#![allow(clippy::all, clippy::pedantic, clippy::float_cmp)]
use rust_tts_wrapper::engine::TtsEngine;
use rust_tts_wrapper::factory::create_engine;
use rust_tts_wrapper::types::{AudioFormat, Gender, SpeakOptions, SpeechPitch, SpeechRate};
use std::sync::Arc;
fn dummy_openai() -> Arc<dyn TtsEngine> {
create_engine("openai", r#"{"apiKey":"dummy"}"#).expect("openai engine")
}
#[test]
fn cloud_pause_returns_ok() {
let e = dummy_openai();
assert!(e.pause().is_ok());
}
#[test]
fn cloud_resume_returns_ok() {
let e = dummy_openai();
assert!(e.resume().is_ok());
}
#[test]
fn cloud_stop_returns_ok() {
let e = dummy_openai();
assert!(e.stop().is_ok());
}
#[test]
fn cloud_pause_resume_cycle_is_safe() {
let e = dummy_openai();
e.pause().unwrap();
e.resume().unwrap();
e.pause().unwrap();
e.resume().unwrap();
}
#[test]
fn cloud_check_credentials_returns_false_when_no_voices_url() {
let e = dummy_openai();
let result = e
.check_credentials()
.expect("check_credentials should not error");
assert!(
!result,
"engines without a voices_url must report Ok(false), not Ok(true)"
);
}
#[test]
fn cloud_get_voices_for_openai_returns_static_list_offline() {
let e = dummy_openai();
let voices = e.get_voices().expect("get_voices");
assert!(
!voices.is_empty(),
"openai should return static voice list offline"
);
assert_eq!(voices.len(), 11, "openai should have 11 static voices");
}
#[test]
fn speak_options_effective_rate_picks_float_over_preset() {
let opts = SpeakOptions {
rate: Some(1.7),
speech_rate: Some(SpeechRate::Slow),
..Default::default()
};
assert_eq!(opts.effective_rate(), 1.7);
}
#[test]
fn speak_options_effective_pitch_picks_float_over_preset() {
let opts = SpeakOptions {
pitch: Some(0.9),
speech_pitch: Some(SpeechPitch::High),
..Default::default()
};
assert_eq!(opts.effective_pitch(), 0.9);
}
#[test]
fn speak_options_effective_volume_picks_float() {
let opts = SpeakOptions {
volume: Some(0.42),
..Default::default()
};
assert!((opts.effective_volume() - 0.42).abs() < f32::EPSILON);
}
#[test]
fn speak_options_defaults_to_normal_rate_pitch_volume() {
let opts = SpeakOptions::default();
assert_eq!(opts.effective_rate(), 1.0);
assert_eq!(opts.effective_pitch(), 1.0);
assert_eq!(opts.effective_volume(), 1.0);
assert!(opts.voice.is_none());
assert!(opts.format.is_none());
assert!(!opts.use_speech_markdown);
assert!(!opts.use_word_boundary);
assert!(!opts.raw_ssml);
assert!(opts.extra.is_empty());
}
#[test]
fn speech_rate_xslow_is_half() {
assert_eq!(SpeechRate::XSlow.rate_value(), 0.5);
}
#[test]
fn speech_rate_slow_is_three_quarters() {
assert!(SpeechRate::Slow.rate_value() < SpeechRate::Medium.rate_value());
assert!(SpeechRate::Slow.rate_value() > SpeechRate::XSlow.rate_value());
}
#[test]
fn speech_rate_medium_is_one() {
assert_eq!(SpeechRate::Medium.rate_value(), 1.0);
}
#[test]
fn speech_rate_fast_is_above_one_below_xfast() {
assert!(SpeechRate::Fast.rate_value() > SpeechRate::Medium.rate_value());
assert!(SpeechRate::Fast.rate_value() < SpeechRate::XFast.rate_value());
}
#[test]
fn speech_rate_xfast_is_one_and_a_half() {
assert_eq!(SpeechRate::XFast.rate_value(), 1.5);
}
#[test]
fn speech_rate_monotonic_across_variants() {
let rates = [
SpeechRate::XSlow.rate_value(),
SpeechRate::Slow.rate_value(),
SpeechRate::Medium.rate_value(),
SpeechRate::Fast.rate_value(),
SpeechRate::XFast.rate_value(),
];
for w in rates.windows(2) {
assert!(w[0] < w[1], "rate presets must be monotonic: {rates:?}");
}
}
#[test]
fn speech_pitch_xlow_is_half() {
assert_eq!(SpeechPitch::XLow.pitch_value(), 0.5);
}
#[test]
fn speech_pitch_low_is_between_xlow_and_medium() {
assert!(SpeechPitch::Low.pitch_value() > SpeechPitch::XLow.pitch_value());
assert!(SpeechPitch::Low.pitch_value() < SpeechPitch::Medium.pitch_value());
}
#[test]
fn speech_pitch_medium_is_one() {
assert_eq!(SpeechPitch::Medium.pitch_value(), 1.0);
}
#[test]
fn speech_pitch_high_is_between_medium_and_xhigh() {
assert!(SpeechPitch::High.pitch_value() > SpeechPitch::Medium.pitch_value());
assert!(SpeechPitch::High.pitch_value() < SpeechPitch::XHigh.pitch_value());
}
#[test]
fn speech_pitch_xhigh_is_one_and_a_half() {
assert_eq!(SpeechPitch::XHigh.pitch_value(), 1.5);
}
#[test]
fn speech_pitch_monotonic_across_variants() {
let pitches = [
SpeechPitch::XLow.pitch_value(),
SpeechPitch::Low.pitch_value(),
SpeechPitch::Medium.pitch_value(),
SpeechPitch::High.pitch_value(),
SpeechPitch::XHigh.pitch_value(),
];
for w in pitches.windows(2) {
assert!(w[0] < w[1], "pitch presets must be monotonic: {pitches:?}");
}
}
#[test]
fn audio_format_display_all_variants() {
assert_eq!(AudioFormat::Mp3.to_string(), "mp3");
assert_eq!(AudioFormat::Wav.to_string(), "wav");
assert_eq!(AudioFormat::Ogg.to_string(), "ogg");
assert_eq!(AudioFormat::Opus.to_string(), "opus");
assert_eq!(AudioFormat::Aac.to_string(), "aac");
assert_eq!(AudioFormat::Flac.to_string(), "flac");
assert_eq!(AudioFormat::Pcm.to_string(), "pcm");
}
#[test]
fn audio_format_display_is_lowercase_no_padding() {
for f in [
AudioFormat::Mp3,
AudioFormat::Wav,
AudioFormat::Ogg,
AudioFormat::Opus,
AudioFormat::Aac,
AudioFormat::Flac,
AudioFormat::Pcm,
] {
let s = f.to_string();
assert_eq!(s, s.to_lowercase(), "{f:?} should be lowercase");
assert!(!s.contains(char::is_whitespace));
}
}
#[test]
#[ignore = "makes a real network call; run locally with --ignored"]
fn speak_with_options_threads_options_to_engine() {
let e = dummy_openai();
let opts = SpeakOptions {
voice: Some("alloy".into()),
rate: Some(1.25),
pitch: Some(0.9),
volume: Some(0.8),
..Default::default()
};
let _ = e.speak_with_options("hi", Some(&opts), None, None, None);
}
#[test]
#[ignore = "makes a real network call; run locally with --ignored"]
fn synth_with_boundaries_returns_audio_plus_boundaries() {
let e = dummy_openai();
let result = e.synth_with_boundaries("hello world test", None, 1.0, 1.0, 1.0);
if let Ok((audio, boundaries)) = result {
assert!(!audio.is_empty(), "audio should be non-empty on success");
assert_eq!(boundaries.len(), 3, "boundary count must match word count");
}
}
#[test]
fn voice_primary_language_returns_first_bcp47() {
use rust_tts_wrapper::types::{LanguageCode, Voice};
let v = Voice {
id: "x".into(),
name: "X".into(),
gender: Gender::Unknown,
provider: "test".into(),
language_codes: vec![
LanguageCode {
bcp47: "en-US".into(),
iso639_3: "eng".into(),
display: "English (United States)".into(),
},
LanguageCode {
bcp47: "en-GB".into(),
iso639_3: "eng".into(),
display: "English (UK)".into(),
},
],
};
assert_eq!(v.primary_language(), "en-US");
}
#[test]
fn voice_with_no_languages_primary_language_returns_empty() {
use rust_tts_wrapper::types::Voice;
let v = Voice {
id: "x".into(),
name: "X".into(),
gender: Gender::Unknown,
provider: "test".into(),
language_codes: vec![],
};
assert_eq!(v.primary_language(), "");
}
#[test]
fn speak_options_extra_map_round_trips_arbitrary_pairs() {
let mut extra = std::collections::HashMap::new();
extra.insert("style".into(), "whisper".into());
extra.insert("speed_override".into(), "1.5".into());
let opts = SpeakOptions {
extra,
..Default::default()
};
assert_eq!(opts.extra.get("style").map(String::as_str), Some("whisper"));
assert_eq!(
opts.extra.get("speed_override").map(String::as_str),
Some("1.5")
);
assert_eq!(opts.extra.len(), 2);
}
#[test]
fn speak_options_flags_default_false_and_toggle_independently() {
let opts = SpeakOptions {
use_speech_markdown: true,
..Default::default()
};
assert!(opts.use_speech_markdown);
assert!(!opts.use_word_boundary);
assert!(!opts.raw_ssml);
let opts = SpeakOptions {
use_word_boundary: true,
..Default::default()
};
assert!(!opts.use_speech_markdown);
assert!(opts.use_word_boundary);
assert!(!opts.raw_ssml);
let opts = SpeakOptions {
raw_ssml: true,
..Default::default()
};
assert!(!opts.use_speech_markdown);
assert!(!opts.use_word_boundary);
assert!(opts.raw_ssml);
}
#[test]
fn speak_options_format_field_round_trips() {
for f in [
AudioFormat::Mp3,
AudioFormat::Wav,
AudioFormat::Ogg,
AudioFormat::Opus,
AudioFormat::Aac,
AudioFormat::Flac,
AudioFormat::Pcm,
] {
let opts = SpeakOptions {
format: Some(f),
..Default::default()
};
assert_eq!(opts.format.unwrap().to_string(), f.to_string());
}
}
#[test]
fn concurrent_pause_resume_stop_does_not_panic_under_contention() {
let e = dummy_openai();
let mut handles = Vec::new();
for _ in 0..4 {
let e = e.clone();
handles.push(std::thread::spawn(move || {
for _ in 0..100 {
let _ = e.pause();
let _ = e.resume();
let _ = e.stop();
}
}));
}
for h in handles {
h.join().expect("worker thread panicked");
}
}