diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration_tests.rs | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..cc1cfb3 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,321 @@ +// tests/integration_tests.rs +// Comprehensive integration tests for the Morse decoder + +use anyhow::Result; +use ditdah::{MorseDecoder, MorseGenerator}; +use hound::{SampleFormat, WavReader}; +use std::fs; +use std::io::Write; + +const TARGET_SAMPLE_RATE: u32 = 12000; +const CHUNK_SIZE: usize = 4096; + +#[derive(Debug)] +struct TestCase { + name: &'static str, + text: &'static str, + frequency: f32, + wpm: f32, + sample_rate: u32, + expected_accuracy: f32, // Minimum accuracy threshold (0.0 to 1.0) +} + +const TEST_CASES: &[TestCase] = &[ + // Basic tests + TestCase { + name: "simple_sos", + text: "SOS", + frequency: 600.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.8, + }, + TestCase { + name: "hello_world", + text: "HELLO WORLD", + frequency: 600.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.7, + }, + TestCase { + name: "alphabet", + text: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + frequency: 600.0, + wpm: 15.0, + sample_rate: 12000, + expected_accuracy: 0.6, + }, + // Different frequencies + TestCase { + name: "low_freq", + text: "TEST", + frequency: 300.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.7, + }, + TestCase { + name: "high_freq", + text: "TEST", + frequency: 1000.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.7, + }, + // Different WPM speeds + TestCase { + name: "slow_wpm", + text: "SLOW", + frequency: 600.0, + wpm: 10.0, + sample_rate: 12000, + expected_accuracy: 0.8, + }, + TestCase { + name: "fast_wpm", + text: "FAST", + frequency: 600.0, + wpm: 30.0, + sample_rate: 12000, + expected_accuracy: 0.6, + }, + // Numbers + TestCase { + name: "numbers", + text: "12345", + frequency: 600.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.7, + }, + // Mixed content + TestCase { + name: "mixed", + text: "CQ DE W1AW", + frequency: 600.0, + wpm: 20.0, + sample_rate: 12000, + expected_accuracy: 0.6, + }, + // Different sample rates + TestCase { + name: "different_sample_rate", + text: "RATE", + frequency: 600.0, + wpm: 20.0, + sample_rate: 44100, + expected_accuracy: 0.7, + }, +]; + +#[test] +fn run_comprehensive_test_suite() -> Result<()> { + println!("Running comprehensive Morse decoder test suite..."); + + // Create test directory + fs::create_dir_all("test_outputs")?; + + let mut results = Vec::new(); + let mut total_tests = 0; + let mut passed_tests = 0; + + // Create a detailed report file + let mut report_file = fs::File::create("test_outputs/test_report.txt")?; + writeln!(report_file, "Morse Decoder Test Report")?; + writeln!(report_file, "=========================")?; + writeln!(report_file)?; + + for test_case in TEST_CASES { + total_tests += 1; + println!("Running test: {}", test_case.name); + + let result = run_single_test(test_case); + let passed = result.is_ok(); + if passed { + passed_tests += 1; + } + + // Log detailed results + match &result { + Ok(test_result) => { + println!(" ✓ PASSED - Accuracy: {:.1}%", test_result.accuracy * 100.0); + writeln!( + report_file, + "TEST: {} - PASSED\n Expected: '{}'\n Decoded: '{}'\n Accuracy: {:.1}%\n WPM: {}, Freq: {}Hz, SR: {}Hz\n", + test_case.name, + test_case.text, + test_result.decoded_text, + test_result.accuracy * 100.0, + test_case.wpm, + test_case.frequency, + test_case.sample_rate + )?; + } + Err(e) => { + println!(" ✗ FAILED - {}", e); + writeln!( + report_file, + "TEST: {} - FAILED\n Expected: '{}'\n Error: {}\n WPM: {}, Freq: {}Hz, SR: {}Hz\n", + test_case.name, + test_case.text, + e, + test_case.wpm, + test_case.frequency, + test_case.sample_rate + )?; + } + } + + results.push((test_case, result)); + } + + // Summary + let pass_rate = (passed_tests as f32 / total_tests as f32) * 100.0; + println!("\nTest Summary:"); + println!(" Total tests: {}", total_tests); + println!(" Passed: {}", passed_tests); + println!(" Failed: {}", total_tests - passed_tests); + println!(" Pass rate: {:.1}%", pass_rate); + + writeln!(report_file, "\nSUMMARY:")?; + writeln!(report_file, " Total tests: {}", total_tests)?; + writeln!(report_file, " Passed: {}", passed_tests)?; + writeln!(report_file, " Failed: {}", total_tests - passed_tests)?; + writeln!(report_file, " Pass rate: {:.1}%", pass_rate)?; + + // Analyze failure patterns + let failed_tests: Vec<_> = results.iter().filter(|(_, r)| r.is_err()).collect(); + if !failed_tests.is_empty() { + writeln!(report_file, "\nFAILURE ANALYSIS:")?; + for (test_case, error) in failed_tests { + writeln!(report_file, " {} - {}", test_case.name, error.as_ref().unwrap_err())?; + } + } + + // If overall pass rate is too low, fail the test + if pass_rate < 50.0 { + panic!("Test suite failed with pass rate of {:.1}%. Check test_outputs/test_report.txt for details.", pass_rate); + } + + Ok(()) +} + +#[derive(Debug)] +struct TestResult { + decoded_text: String, + accuracy: f32, +} + +fn run_single_test(test_case: &TestCase) -> Result<TestResult> { + // Generate the test WAV file + let generator = MorseGenerator::new(test_case.sample_rate, test_case.frequency, test_case.wpm); + let wav_path = format!("test_outputs/{}.wav", test_case.name); + generator.generate_wav_file(test_case.text, &wav_path)?; + + // Decode the WAV file + let decoded_text = decode_wav_file(&wav_path)?; + + // Calculate accuracy + let accuracy = calculate_accuracy(test_case.text, &decoded_text); + + let result = TestResult { + decoded_text, + accuracy, + }; + + // Check if accuracy meets threshold + if accuracy >= test_case.expected_accuracy { + Ok(result) + } else { + Err(anyhow::anyhow!( + "Accuracy {:.1}% below threshold {:.1}%", + accuracy * 100.0, + test_case.expected_accuracy * 100.0 + )) + } +} + +fn decode_wav_file(path: &str) -> Result<String> { + let mut reader = WavReader::open(path)?; + let spec = reader.spec(); + + if spec.sample_format != SampleFormat::Int && spec.sample_format != SampleFormat::Float { + return Err(anyhow::anyhow!( + "Unsupported sample format: {:?}", + spec.sample_format + )); + } + + // Create decoder + let mut decoder = MorseDecoder::new(spec.sample_rate, TARGET_SAMPLE_RATE)?; + + // Read and process audio + let samples_f32: Vec<f32> = if spec.sample_format == SampleFormat::Int { + reader + .samples::<i16>() + .map(|s| s.unwrap() as f32 / 32768.0) + .collect() + } else { + reader.samples::<f32>().map(|s| s.unwrap()).collect() + }; + + // Convert to mono if necessary + let mono_samples: Vec<f32> = if spec.channels > 1 { + samples_f32 + .chunks_exact(spec.channels as usize) + .map(|chunk| chunk.iter().sum::<f32>() / spec.channels as f32) + .collect() + } else { + samples_f32 + }; + + // Process in chunks + for chunk in mono_samples.chunks(CHUNK_SIZE) { + decoder.process(chunk)?; + } + + // Finalize and get result + decoder.finalize() +} + +fn calculate_accuracy(expected: &str, actual: &str) -> f32 { + if expected.is_empty() { + return if actual.is_empty() { 1.0 } else { 0.0 }; + } + + let expected_clean = expected.to_uppercase().replace(" ", ""); + let actual_clean = actual.to_uppercase().replace(" ", "").replace("?", ""); + + if expected_clean.is_empty() { + return if actual_clean.is_empty() { 1.0 } else { 0.0 }; + } + + // Simple character-by-character comparison + let expected_chars: Vec<char> = expected_clean.chars().collect(); + let actual_chars: Vec<char> = actual_clean.chars().collect(); + + let max_len = expected_chars.len().max(actual_chars.len()); + let mut matches = 0; + + for i in 0..max_len { + let expected_char = expected_chars.get(i); + let actual_char = actual_chars.get(i); + + if expected_char == actual_char { + matches += 1; + } + } + + matches as f32 / max_len as f32 +} + +#[test] +fn test_accuracy_calculation() { + assert_eq!(calculate_accuracy("SOS", "SOS"), 1.0); + assert_eq!(calculate_accuracy("SOS", "SO"), 2.0/3.0); + assert_eq!(calculate_accuracy("SOS", "XOS"), 2.0/3.0); + assert_eq!(calculate_accuracy("HELLO", "WORLD"), 1.0/5.0); // Only L matches + assert_eq!(calculate_accuracy("", ""), 1.0); + assert_eq!(calculate_accuracy("A", ""), 0.0); +}
\ No newline at end of file |
