rust-tts-wrapper 0.1.0

Cross-platform TTS wrapper with C API — mirrors js-tts-wrapper / SwiftTTSWrapper
Documentation
//! Integration tests for the TTS wrapper.

use rust_tts_wrapper::factory;

#[test]
fn test_engine_list_contains_system() {
    let engines = factory::engine_list();
    assert!(
        engines.iter().any(|e| e.id == "system"),
        "System engine must be registered"
    );
}

#[test]
fn test_engine_list_contains_cloud() {
    let engines = factory::engine_list();
    let cloud_ids = [
        "openai",
        "elevenlabs",
        "azure",
        "google",
        "cartesia",
        "deepgram",
        "playht",
        "fishaudio",
        "hume",
        "mistral",
        "murf",
        "resemble",
        "unrealspeech",
        "upliftai",
        "watson",
        "witai",
        "xai",
        "modelslab",
        "polly",
    ];
    for id in &cloud_ids {
        assert!(
            engines.iter().any(|e| e.id == *id),
            "Cloud engine '{id}' must be registered"
        );
    }
}

#[test]
fn test_engine_count() {
    let engines = factory::engine_list();
    assert!(
        engines.len() >= 21,
        "Must have at least 21 engines, got {}",
        engines.len()
    );
}

#[test]
fn test_cloud_engine_needs_credentials() {
    let engines = factory::engine_list();
    let openai = engines
        .iter()
        .find(|e| e.id == "openai")
        .expect("openai engine");
    assert!(openai.needs_credentials);
    assert!(openai.credential_keys_json.contains("apiKey"));
}

#[test]
fn test_system_engine_no_credentials() {
    let engines = factory::engine_list();
    let system = engines
        .iter()
        .find(|e| e.id == "system")
        .expect("system engine");
    assert!(!system.needs_credentials);
}

#[test]
fn test_sherpaonnx_no_credentials() {
    let engines = factory::engine_list();
    let sherpa = engines
        .iter()
        .find(|e| e.id == "sherpaonnx")
        .expect("sherpaonnx engine");
    assert!(!sherpa.needs_credentials);
}

#[test]
fn test_create_system_engine() {
    let engine = factory::create_engine("system", "");
    assert!(engine.is_some(), "System engine must be creatable");
    assert_eq!(engine.unwrap().engine_id(), "system");
}

#[test]
fn test_create_cloud_engine() {
    let engine = factory::create_engine("openai", r#"{"apiKey":"test-key"}"#);
    assert!(
        engine.is_some(),
        "OpenAI engine must be creatable with credentials"
    );
}

#[test]
fn test_create_unknown_engine() {
    let engine = factory::create_engine("nonexistent_engine", "");
    assert!(engine.is_none(), "Unknown engines should return None");
}

#[test]
fn test_system_engine_stop_graceful() {
    let engine = factory::create_engine("system", "").expect("system engine");
    let result = engine.stop();
    if result.is_err() {
        let err = result.unwrap_err();
        assert!(
            err.to_string().contains("not connected"),
            "Expected 'not connected' error, got: {err}"
        );
    }
}

#[test]
fn test_create_all_cloud_engines() {
    let cloud_ids = [
        "openai",
        "elevenlabs",
        "azure",
        "google",
        "cartesia",
        "deepgram",
        "playht",
        "fishaudio",
        "hume",
        "mistral",
        "murf",
        "resemble",
        "unrealspeech",
        "upliftai",
        "xai",
        "modelslab",
    ];
    for id in &cloud_ids {
        let engine = factory::create_engine(id, r#"{"apiKey":"test-key"}"#);
        assert!(engine.is_some(), "Engine '{id}' should be creatable");
    }
}

#[test]
fn test_create_azure_with_region() {
    let engine = factory::create_engine(
        "azure",
        r#"{"subscriptionKey":"test-key","region":"eastus"}"#,
    );
    assert!(engine.is_some());
}

#[test]
fn test_create_watson_with_all_creds() {
    let engine = factory::create_engine(
        "watson",
        r#"{"apiKey":"test-key","region":"us-east","instanceId":"test-id"}"#,
    );
    assert!(engine.is_some());
}

#[test]
fn test_create_polly_with_all_creds() {
    let engine = factory::create_engine(
        "polly",
        r#"{"accessKeyId":"test","secretAccessKey":"test","region":"us-east-1"}"#,
    );
    assert!(engine.is_some());
}

#[test]
fn test_sherpaonnx_engine_has_voices() {
    let engine = factory::create_engine("sherpaonnx", "").expect("sherpaonnx engine");
    let voices = engine.get_voices().expect("voices");
    assert!(!voices.is_empty(), "SherpaONNX should have voices");
    assert_eq!(voices[0].provider, "sherpaonnx");
}

#[test]
fn test_engine_id_matches() {
    let engine = factory::create_engine("openai", r#"{"apiKey":"test-key"}"#).unwrap();
    assert_eq!(engine.engine_id(), "openai");
}

#[test]
fn test_normalize_gender() {
    use rust_tts_wrapper::types::{normalize_gender, Gender};
    assert_eq!(normalize_gender("Female"), Gender::Female);
    assert_eq!(normalize_gender("female"), Gender::Female);
    assert_eq!(normalize_gender("Male"), Gender::Male);
    assert_eq!(normalize_gender("male"), Gender::Male);
    assert_eq!(normalize_gender(""), Gender::Unknown);
    assert_eq!(normalize_gender("other"), Gender::Unknown);
}

#[test]
fn test_voice_struct_fields() {
    use rust_tts_wrapper::types::{Gender, LanguageCode, Voice};
    let voice = Voice {
        id: "test-voice".to_string(),
        name: "Test Voice".to_string(),
        gender: Gender::Female,
        provider: "test".to_string(),
        language_codes: vec![LanguageCode {
            bcp47: "en-US".to_string(),
            iso639_3: "eng".to_string(),
            display: "English (United States)".to_string(),
        }],
    };
    assert_eq!(voice.primary_language(), "en-US");
    assert_eq!(voice.language_codes.len(), 1);
    assert_eq!(voice.gender, Gender::Female);
}

#[test]
fn test_word_boundary_estimation() {
    use rust_tts_wrapper::engine::estimate_word_boundaries;
    let boundaries = estimate_word_boundaries("Hello world this is a test");
    assert_eq!(boundaries.len(), 6);
    assert_eq!(boundaries[0].text, "Hello");
    assert_eq!(boundaries[0].offset, 0);
    assert!(boundaries[0].duration > 0);
    // Offsets should be monotonically increasing
    for i in 1..boundaries.len() {
        assert!(boundaries[i].offset > boundaries[i - 1].offset);
    }
}

#[test]
fn test_word_boundary_empty_text() {
    use rust_tts_wrapper::engine::estimate_word_boundaries;
    let boundaries = estimate_word_boundaries("");
    assert!(boundaries.is_empty());
}

#[test]
fn test_speech_markdown_detection() {
    use rust_tts_wrapper::engine::preprocess_speech_markdown;
    let (result, is_ssml) =
        preprocess_speech_markdown("Hello (world)[emphasis:\"strong\"]", "azure");
    assert!(is_ssml);
    assert!(result.contains("<speak>"));
}

#[test]
fn test_speech_markdown_plain_text_passthrough() {
    use rust_tts_wrapper::engine::preprocess_speech_markdown;
    let (result, is_ssml) = preprocess_speech_markdown("Hello world", "azure");
    assert!(!is_ssml);
    assert_eq!(result, "Hello world");
}

#[test]
fn test_speech_markdown_azure_platform() {
    use rust_tts_wrapper::engine::preprocess_speech_markdown;
    let (result, is_ssml) = preprocess_speech_markdown("This is +important+", "azure");
    assert!(is_ssml);
    assert!(result.contains("microsoft") || result.contains("<speak>"));
}

#[test]
fn test_speech_markdown_google_platform() {
    use rust_tts_wrapper::engine::preprocess_speech_markdown;
    let (_result, is_ssml) = preprocess_speech_markdown("This is +important+", "google");
    assert!(is_ssml);
}

#[test]
fn test_speak_options_defaults() {
    use rust_tts_wrapper::types::SpeakOptions;
    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.use_speech_markdown);
    assert!(!opts.use_word_boundary);
    assert!(!opts.raw_ssml);
}

#[test]
fn test_speak_options_with_presets() {
    use rust_tts_wrapper::types::{SpeakOptions, SpeechPitch, SpeechRate};
    let opts = SpeakOptions {
        speech_rate: Some(SpeechRate::Fast),
        speech_pitch: Some(SpeechPitch::Low),
        volume: Some(0.8),
        ..Default::default()
    };
    assert_eq!(opts.effective_rate(), 1.25);
    assert_eq!(opts.effective_pitch(), 0.75);
    assert_eq!(opts.effective_volume(), 0.8);
}

#[test]
fn test_speak_options_float_overrides_preset() {
    use rust_tts_wrapper::types::{SpeakOptions, SpeechRate};
    let opts = SpeakOptions {
        rate: Some(1.5),
        speech_rate: Some(SpeechRate::Slow),
        ..Default::default()
    };
    assert_eq!(opts.effective_rate(), 1.5);
}

#[test]
fn test_audio_format_display() {
    use rust_tts_wrapper::types::AudioFormat;
    assert_eq!(AudioFormat::Mp3.to_string(), "mp3");
    assert_eq!(AudioFormat::Wav.to_string(), "wav");
    assert_eq!(AudioFormat::Pcm.to_string(), "pcm");
}

#[test]
fn test_speech_rate_values() {
    use rust_tts_wrapper::types::SpeechRate;
    assert_eq!(SpeechRate::XSlow.rate_value(), 0.5);
    assert_eq!(SpeechRate::Medium.rate_value(), 1.0);
    assert_eq!(SpeechRate::XFast.rate_value(), 1.5);
}

#[test]
fn test_speech_pitch_values() {
    use rust_tts_wrapper::types::SpeechPitch;
    assert_eq!(SpeechPitch::XLow.pitch_value(), 0.5);
    assert_eq!(SpeechPitch::Medium.pitch_value(), 1.0);
    assert_eq!(SpeechPitch::XHigh.pitch_value(), 1.5);
}

#[test]
fn test_gender_enum() {
    use rust_tts_wrapper::types::Gender;
    assert_eq!(Gender::Male.to_string(), "Male");
    assert_eq!(Gender::Female.to_string(), "Female");
    assert_eq!(Gender::Unknown.to_string(), "Unknown");
}

#[test]
fn test_word_boundary_with_wpm() {
    use rust_tts_wrapper::engine::estimate_word_boundaries_with_wpm;
    let fast = estimate_word_boundaries_with_wpm("Hello world", 300.0);
    let slow = estimate_word_boundaries_with_wpm("Hello world", 75.0);
    assert!(fast[0].duration < slow[0].duration);
}