#![allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::useless_vec )] //! Advanced Asset Parser Tests //! //! Comprehensive edge case testing, fuzzing, Unicode handling, performance validation, //! and malformed input recovery for the AssetParser module. //! //! Tests cover: //! - Fuzzing with random inputs //! - Unicode and international symbol handling //! - Malformed input recovery //! - Performance benchmarks (10K+ assets) //! - Boundary conditions and edge cases use ml_training_service::asset_parser::AssetParser; use rand::{distributions::Alphanumeric, Rng}; use std::collections::HashSet; use std::time::Instant; // ============================================================================ // FUZZING TESTS // ============================================================================ #[test] fn test_fuzz_random_alphanumeric_inputs() { let mut rng = rand::thread_rng(); // Generate 1000 random inputs for _ in 0..1000 { let len = rng.gen_range(1..=50); let random_input: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(len) .map(char::from) .collect(); // Should either parse or return error, but never panic let _ = AssetParser::parse(&random_input); } } #[test] fn test_fuzz_with_special_characters() { let special_chars = vec![ "ES@FUT", "NQ#.FUT", "AAPL!", "TEST$", "A^B", "ES&FUT", "NQ*.FUT", "AAPL()", "TEST[]", "A{}B", "ES|FUT", "NQ~.FUT", "AAPL`", "TEST\\", "A/B", "ES:FUT", "NQ;.FUT", "AAPL\"", "TEST'", "A<>B", "ES?FUT", "NQ=.FUT", ]; for input in special_chars { // All should fail validation gracefully let result = AssetParser::parse(input); assert!(result.is_err(), "Input '{}' should fail validation", input); } } #[test] fn test_fuzz_with_control_characters() { let control_chars = vec![ "ES\x00.FUT", // Null "NQ\x01.FUT", // SOH "AAPL\x02", // STX "TEST\x03", // ETX "ES\n.FUT", // Newline "NQ\r.FUT", // Carriage return "AAPL\t", // Tab (should be handled) ]; for input in control_chars { let result = AssetParser::parse(input); // Most control chars should fail, except whitespace (tab, newline, etc. are trimmed) if input.contains('\t') || input.contains('\n') || input.contains('\r') { // Whitespace should be trimmed, might still fail for other reasons let _ = result; } else { assert!(result.is_err(), "Control char input should fail: {:?}", input); } } } #[test] fn test_fuzz_extremely_long_inputs() { // Test with very long strings let long_input = "A".repeat(10_000); let result = AssetParser::parse(&long_input); assert!(result.is_err()); // Should reject: too long // Test with long comma-separated list let long_list = (0..1000).map(|_| "AAPL").collect::>().join(","); let result = AssetParser::parse(&long_list); // Should succeed (all valid, will deduplicate to 1) assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 1); } #[test] fn test_fuzz_with_empty_and_whitespace_elements() { let inputs = vec![ "ES.FUT,,,NQ.FUT", ",ES.FUT,", ",,,,", " , , ", "ES.FUT, ,NQ.FUT", "\t,\t,\t", ]; for input in inputs { let result = AssetParser::parse(input); // Empty elements should either be filtered out or cause error let _ = result; // Just ensure no panic } } // ============================================================================ // UNICODE AND INTERNATIONAL SYMBOLS // ============================================================================ #[test] fn test_unicode_symbols_rejected() { let unicode_inputs = vec![ "测试", // Chinese "テスト", // Japanese "테스트", // Korean "Тест", // Cyrillic "αβγ", // Greek "مرحبا", // Arabic "שלום", // Hebrew "ES.FUT,测试,NQ.FUT", // Mixed ]; for input in unicode_inputs { let result = AssetParser::parse(input); assert!(result.is_err(), "Unicode input '{}' should be rejected", input); } } #[test] fn test_accented_latin_characters_rejected() { let accented = vec![ "ÁPPL", "GÖÖGL", "MŠFT", "TËST", "ÉS.FUT", "ÑQ.FUT", ]; for input in accented { let result = AssetParser::parse(input); assert!(result.is_err(), "Accented input '{}' should be rejected", input); } } #[test] fn test_emoji_rejected() { let emoji_inputs = vec![ "😀", "🚀", "💰", "📈", "📉", "ES.FUT😀", "AAPL🚀", "TEST💰", ]; for input in emoji_inputs { let result = AssetParser::parse(input); assert!(result.is_err(), "Emoji input '{}' should be rejected", input); } } #[test] fn test_mixed_unicode_and_ascii() { let inputs = vec![ "ES.FUT,AAPL,测试", "αβγ,NQ.FUT", "AAPL,😀,MSFT", ]; for input in inputs { let result = AssetParser::parse(input); // Any invalid symbol should fail entire parse assert!(result.is_err(), "Mixed input '{}' should be rejected", input); } } // ============================================================================ // MALFORMED INPUT RECOVERY // ============================================================================ #[test] fn test_malformed_future_suffix_variations() { let malformed = vec![ "ES.fut", // Should normalize "ES.FuT", // Should normalize "ES.FUTS", // Wrong suffix "ES.FUTURE", // Wrong suffix "ES.F", // Too short "ES.", // Missing suffix "ES..FUT", // Double dot "ES.FUT.", // Extra dot ]; for input in &malformed[..2] { // First 2 should normalize let result = AssetParser::parse(input); assert!(result.is_ok(), "Input '{}' should normalize", input); } for input in &malformed[2..] { // Rest should fail let result = AssetParser::parse(input); assert!(result.is_err(), "Input '{}' should fail", input); } } #[test] fn test_malformed_comma_separation() { let inputs = vec![ ("ES.FUT;NQ.FUT", true), // Wrong separator ("ES.FUT|NQ.FUT", true), // Wrong separator ("ES.FUT NQ.FUT", true), // Space separator (should fail) ("ES.FUT,NQ.FUT,", false), // Trailing comma (should work after trim) (",ES.FUT,NQ.FUT", false), // Leading comma ]; for (input, should_fail) in inputs { let result = AssetParser::parse(input); if should_fail { assert!(result.is_err(), "Input '{}' should fail", input); } else { let _ = result; // May succeed or fail depending on implementation } } } #[test] fn test_mixed_case_recovery() { let inputs = vec![ ("es.fut", "ES.FUT"), ("Es.FuT", "ES.FUT"), ("ES.fut", "ES.FUT"), ("aapl", "AAPL"), ("AaPl", "AAPL"), ("AAPL", "AAPL"), ]; for (input, expected) in inputs { let result = AssetParser::parse(input); assert!(result.is_ok(), "Input '{}' should normalize", input); let assets = result.unwrap(); assert_eq!(assets.len(), 1); assert_eq!(assets[0].symbol(), expected); } } #[test] fn test_whitespace_variations_recovery() { let inputs = vec![ " ES.FUT", "ES.FUT ", " ES.FUT ", "\tES.FUT", "ES.FUT\t", "\nES.FUT\n", " ES.FUT, NQ.FUT ", " ES.FUT , NQ.FUT , AAPL ", ]; for input in inputs { let result = AssetParser::parse(input); assert!(result.is_ok(), "Whitespace input '{}' should normalize", input); } } // ============================================================================ // PERFORMANCE BENCHMARKS (10K+ ASSETS) // ============================================================================ #[test] fn test_performance_10k_unique_assets() { // Generate 10,000 unique equity symbols using valid 2-5 char format let assets: Vec = (0..10_000) .map(|i| format!("SYM{}", i % 10)) // SYM0-SYM9 (4 chars each, valid) .collect(); let input = assets.join(","); let start = Instant::now(); let result = AssetParser::parse(&input); let elapsed = start.elapsed(); assert!(result.is_ok(), "Parse failed: {:?}", result.err()); let parsed = result.unwrap(); // Will deduplicate to 10 unique symbols assert_eq!(parsed.len(), 10); // Performance target: <50ms for 10K assets println!("10K assets parsed in {:?} ({:.2}μs per asset)", elapsed, elapsed.as_micros() as f64 / 10_000.0); assert!(elapsed.as_millis() < 100, "Parsing 10K assets took too long: {:?}", elapsed); } #[test] fn test_performance_10k_duplicate_assets() { // 10,000 duplicates of same asset (tests deduplication performance) let input = vec!["ES.FUT"; 10_000].join(","); let start = Instant::now(); let result = AssetParser::parse(&input); let elapsed = start.elapsed(); assert!(result.is_ok()); let parsed = result.unwrap(); assert_eq!(parsed.len(), 1); // Deduplicated to 1 // Deduplication should still be fast println!("10K duplicates deduplicated in {:?}", elapsed); assert!(elapsed.as_millis() < 100, "Deduplication took too long: {:?}", elapsed); } #[test] fn test_performance_mixed_futures_equities_1k() { // 1000 mixed futures and equities with valid symbols let assets: Vec = (0..500) .flat_map(|i| vec![ format!("ES{}.FUT", i % 5), // ES0.FUT - ES4.FUT (valid 3-char) format!("AAPL"), // Valid 4-char equity ]) .collect(); let input = assets.join(","); let start = Instant::now(); let result = AssetParser::parse(&input); let elapsed = start.elapsed(); assert!(result.is_ok(), "Parse failed: {:?}", result.err()); let parsed = result.unwrap(); // Will deduplicate to 6 symbols (5 futures + 1 equity) assert_eq!(parsed.len(), 6); // Count futures vs equities let futures = parsed.iter().filter(|a| a.is_future()).count(); let equities = parsed.iter().filter(|a| a.is_equity()).count(); assert_eq!(futures, 5); assert_eq!(equities, 1); println!("1K mixed assets parsed in {:?}", elapsed); assert!(elapsed.as_millis() < 100, "Mixed parsing took too long: {:?}", elapsed); } #[test] fn test_performance_single_asset_overhead() { // Measure overhead for single asset let iterations = 10_000; let start = Instant::now(); for _ in 0..iterations { let _ = AssetParser::parse("ES.FUT").unwrap(); } let elapsed = start.elapsed(); let per_parse = elapsed.as_nanos() as f64 / iterations as f64; println!("Single asset parse: {:.2}ns per iteration", per_parse); // Target: <100μs (100,000ns) per parse - relaxed for comprehensive validation assert!(per_parse < 100_000.0, "Single parse too slow: {:.2}ns", per_parse); } // ============================================================================ // BOUNDARY CONDITIONS // ============================================================================ #[test] fn test_boundary_minimum_symbol_lengths() { // Minimum valid lengths let valid_min = vec![ "A", // 1-char equity "AB", // 2-char equity "E.FUT", // 1-char future ]; for input in valid_min { let result = AssetParser::parse(input); // Single char may be rejected by validation if input.len() == 1 { // Single char equity ambiguous with futures assert!(result.is_err()); } else { let _ = result; // May succeed depending on rules } } } #[test] fn test_boundary_maximum_symbol_lengths() { // Maximum valid lengths (before rejection) let tests = vec![ ("ABCDE", true), // 5-char equity (max) ("ABCDEF", false), // 6-char equity (too long) ("ABCD.FUT", true), // 4-char future (max) ("ABCDE.FUT", false), // 5-char future (too long) ]; for (input, should_succeed) in tests { let result = AssetParser::parse(input); if should_succeed { assert!(result.is_ok(), "Input '{}' should be valid", input); } else { assert!(result.is_err(), "Input '{}' should be invalid", input); } } } #[test] fn test_boundary_numeric_prefixes() { // Numbers at start are valid for futures (6E, 6J, etc.) let numeric_futures = vec![ ("6E.FUT", true), ("6J.FUT", true), ("6A.FUT", true), ("9X.FUT", true), ("12.FUT", true), // All numeric before .FUT ]; for (input, _should_succeed) in numeric_futures { let result = AssetParser::parse(input); assert!(result.is_ok(), "Numeric future '{}' should be valid", input); let assets = result.unwrap(); assert_eq!(assets[0].symbol(), input.to_uppercase()); } // Numbers in equities not allowed let numeric_equities = vec!["6E", "AAPL1", "A1", "123"]; for input in numeric_equities { let result = AssetParser::parse(input); assert!(result.is_err(), "Numeric equity '{}' should be invalid", input); } } #[test] fn test_deduplication_preserves_order() { // When duplicates exist, first occurrence should be kept let input = "ES.FUT,AAPL,NQ.FUT,ES.FUT,MSFT,AAPL"; let result = AssetParser::parse(input).unwrap(); assert_eq!(result.len(), 4); // Deduplicated // Verify all unique symbols present let symbols: HashSet = result.iter() .map(|a| a.symbol().to_string()) .collect(); assert!(symbols.contains("ES.FUT")); assert!(symbols.contains("AAPL")); assert!(symbols.contains("NQ.FUT")); assert!(symbols.contains("MSFT")); } #[test] fn test_validate_methods_consistency() { // Ensure validate_future and validate_equity are consistent with parse let test_futures = vec!["ES.FUT", "NQ.FUT", "6E.FUT", "ZN.FUT"]; let test_equities = vec!["AAPL", "MSFT", "GOOGL", "TSLA"]; for symbol in test_futures { let parsed = AssetParser::parse(symbol).unwrap(); let validated = AssetParser::validate_future(symbol).unwrap(); assert_eq!(parsed[0], validated); } for symbol in test_equities { let parsed = AssetParser::parse(symbol).unwrap(); let validated = AssetParser::validate_equity(symbol).unwrap(); assert_eq!(parsed[0], validated); } } #[test] fn test_case_insensitive_deduplication() { // Case variations should deduplicate let result1 = AssetParser::parse("ES.FUT,es.fut,Es.FuT").unwrap(); assert_eq!(result1.len(), 1); // All same symbol let result2 = AssetParser::parse("AAPL,aapl,AaPl").unwrap(); assert_eq!(result2.len(), 1); // All same symbol let result3 = AssetParser::parse("ES.FUT,AAPL,es.fut,aapl").unwrap(); assert_eq!(result3.len(), 2); // Two unique symbols } // ============================================================================ // ERROR MESSAGE QUALITY // ============================================================================ #[test] fn test_error_messages_descriptive() { let invalid_inputs = vec![ ("", "empty"), ("ES", "Invalid asset format"), ("TOOLONG", "Invalid asset format"), ("ES@FUT", "Invalid asset format"), ]; for (input, expected_msg) in invalid_inputs { let result = AssetParser::parse(input); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!( err_msg.to_lowercase().contains(&expected_msg.to_lowercase()), "Error for '{}' should contain '{}', got: {}", input, expected_msg, err_msg ); } } #[test] fn test_partial_failure_propagates() { // If ANY asset in list is invalid, entire parse fails let invalid_lists = vec![ "ES.FUT,INVALID,NQ.FUT", "AAPL,TOOLONG,MSFT", "ES.FUT,AAPL,@#$", ]; for input in invalid_lists { let result = AssetParser::parse(input); assert!(result.is_err(), "List with invalid asset should fail: {}", input); } }