#![allow( clippy::all, unused_imports, unused_variables, dead_code, unused_mut, unused_assignments, non_camel_case_types, clippy::approx_constant, unexpected_cfgs, unused_must_use, unused_parens )] //! Integration tests for Apple Neural Engine (ANE) / Core ML functionality //! //! These tests verify end-to-end functionality of the ANE/CoreML backend, //! including hybrid pipeline switching, fallback behavior, and memory management. //! //! ## Running Tests //! //! ```bash //! # Run all ANE tests (requires Apple Silicon) //! cargo test --features coreml ane_integration //! //! # Run with hybrid pipeline support //! cargo test --features hybrid-ane ane_integration //! //! # Run on non-Apple Silicon (tests fallback behavior) //! cargo test ane_integration //! ``` // Import from the crate being tested // Note: CoreMLBackend methods require the coreml feature #[cfg(feature = "coreml")] use ruvllm::backends::CoreMLBackend; use ruvllm::backends::{ AneCapabilities, ComputeUnits, GenerateParams, LlmBackend, ModelArchitecture, ModelConfig, Quantization, }; use ruvllm::error::{Result, RuvLLMError}; // ============================================================================ // Platform Detection Helpers // ============================================================================ /// Check if running on Apple Silicon fn is_apple_silicon() -> bool { cfg!(all(target_os = "macos", target_arch = "aarch64")) } /// Check if ANE is available fn is_ane_available() -> bool { let caps = AneCapabilities::detect(); caps.available } // ============================================================================ // Core ML Backend Integration Tests // ============================================================================ #[test] fn test_ane_capabilities_detection() { let caps = AneCapabilities::detect(); if is_apple_silicon() { assert!(caps.available, "ANE should be available on Apple Silicon"); assert!(caps.tops > 0.0, "TOPS should be positive on Apple Silicon"); assert!( caps.max_model_size_mb > 0, "Max model size should be positive" ); assert!( !caps.supported_ops.is_empty(), "Should have supported operations" ); // Verify common operations are supported let expected_ops = ["MatMul", "GELU", "SiLU", "LayerNorm", "Softmax"]; for op in &expected_ops { assert!( caps.supported_ops.iter().any(|s| s == *op), "Operation {} should be supported", op ); } } else { assert!( !caps.available, "ANE should not be available on non-Apple Silicon" ); assert_eq!(caps.tops, 0.0, "TOPS should be 0 when unavailable"); assert_eq!( caps.max_model_size_mb, 0, "Max model size should be 0 when unavailable" ); assert!( caps.supported_ops.is_empty(), "No operations when unavailable" ); } } #[test] fn test_compute_units_selection() { // Test default selection let default = ComputeUnits::default(); assert_eq!(default, ComputeUnits::All); // Test ANE-focused configuration let ane_focus = ComputeUnits::CpuAndNeuralEngine; assert!(ane_focus.uses_ane()); assert!(!ane_focus.uses_gpu()); // Test GPU-focused configuration let gpu_focus = ComputeUnits::CpuAndGpu; assert!(!gpu_focus.uses_ane()); assert!(gpu_focus.uses_gpu()); // Test all units let all = ComputeUnits::All; assert!(all.uses_ane()); assert!(all.uses_gpu()); } #[test] fn test_model_suitability_for_ane() { let caps = AneCapabilities::detect(); if is_apple_silicon() { // Small models should be suitable assert!(caps.is_model_suitable(500), "500MB model should fit"); assert!(caps.is_model_suitable(1000), "1GB model should fit"); assert!(caps.is_model_suitable(2048), "2GB model should fit"); // Large models may not fit // (depends on actual device, but 10GB is likely too large) // Skip this assertion as it's hardware-dependent } } // ============================================================================ // Core ML Backend Creation Tests // ============================================================================ #[test] #[cfg(feature = "coreml")] fn test_coreml_backend_creation() { if is_apple_silicon() { let result = CoreMLBackend::new(); assert!(result.is_ok(), "Should create backend on Apple Silicon"); let backend = result.unwrap(); assert!(!backend.is_model_loaded()); assert!(backend.model_info().is_none()); } else { let result = CoreMLBackend::new(); assert!(result.is_err(), "Should fail on non-Apple Silicon"); } } #[test] #[cfg(feature = "coreml")] fn test_coreml_backend_configuration() { if !is_apple_silicon() { return; // Skip on non-Apple Silicon } let backend = CoreMLBackend::new() .unwrap() .with_compute_units(ComputeUnits::CpuAndNeuralEngine); let caps = backend.ane_capabilities(); assert!(caps.available); assert!(caps.tops > 0.0); } // ============================================================================ // Fallback Behavior Tests // ============================================================================ #[test] fn test_fallback_when_coreml_unavailable() { // When coreml feature is not enabled, CoreMLBackend type doesn't exist // so we can only test the AneCapabilities fallback #[cfg(not(feature = "coreml"))] { // Without coreml feature, ANE capabilities should report unavailable let caps = AneCapabilities::detect(); // On non-Apple Silicon or without the feature, it should gracefully handle this if !is_apple_silicon() { assert!( !caps.available, "ANE should not be available without coreml feature on non-Apple Silicon" ); } } #[cfg(feature = "coreml")] { if !is_apple_silicon() { let result = CoreMLBackend::new(); assert!(result.is_err()); let err = result.unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("not available"), "Should indicate ANE not available" ); } } } #[test] fn test_graceful_degradation() { // Even when ANE is not available, the AneCapabilities struct should work let caps = AneCapabilities { available: false, tops: 0.0, max_model_size_mb: 0, supported_ops: vec![], }; // All operations should return false/empty gracefully assert!(!caps.is_model_suitable(100)); assert!(!caps.is_model_suitable(0)); assert!(!caps.available); } // ============================================================================ // Model Loading Error Handling Tests // ============================================================================ #[test] #[cfg(all(feature = "coreml", target_os = "macos", target_arch = "aarch64"))] fn test_unsupported_model_format_error() { let mut backend = CoreMLBackend::new().unwrap(); // Try various unsupported formats let unsupported_formats = [ "model.safetensors", "model.bin", "model.pt", "model.pth", "model.onnx", ]; for format in &unsupported_formats { let result = backend.load_model(format, ModelConfig::default()); assert!( result.is_err(), "Should reject unsupported format: {}", format ); } } #[test] #[cfg(all(feature = "coreml", target_os = "macos", target_arch = "aarch64"))] fn test_nonexistent_model_error() { let mut backend = CoreMLBackend::new().unwrap(); let result = backend.load_model("/nonexistent/path/model.mlmodel", ModelConfig::default()); assert!(result.is_err()); } #[test] #[cfg(all(feature = "coreml", target_os = "macos", target_arch = "aarch64"))] fn test_gguf_conversion_error() { let mut backend = CoreMLBackend::new().unwrap(); // GGUF conversion is not yet implemented let result = backend.load_model("/path/to/model.gguf", ModelConfig::default()); assert!(result.is_err()); let err = result.unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("not") || err_str.contains("conversion"), "Error should mention conversion issue: {}", err_str ); } // ============================================================================ // Memory Management Tests // ============================================================================ #[test] #[cfg(all(feature = "coreml", target_os = "macos", target_arch = "aarch64"))] fn test_model_unloading() { let mut backend = CoreMLBackend::new().unwrap(); // Initial state assert!(!backend.is_model_loaded()); // Unload should be safe even without loaded model backend.unload_model(); assert!(!backend.is_model_loaded()); assert!(backend.model_info().is_none()); } #[test] #[cfg(all(feature = "coreml", target_os = "macos", target_arch = "aarch64"))] fn test_multiple_unload_calls() { let mut backend = CoreMLBackend::new().unwrap(); // Multiple unload calls should be safe for _ in 0..5 { backend.unload_model(); assert!(!backend.is_model_loaded()); } } // ============================================================================ // Hybrid Pipeline Tests // ============================================================================ #[cfg(feature = "hybrid-ane")] mod hybrid_pipeline_tests { use super::*; #[test] fn test_hybrid_feature_enabled() { // Verify hybrid-ane feature combines metal-compute and coreml // This test just confirms the feature flag works assert!(true, "Hybrid ANE feature is enabled"); } #[test] #[cfg(all(target_os = "macos", target_arch = "aarch64"))] fn test_hybrid_configuration() { // Test that we can configure for hybrid operation let ane_caps = AneCapabilities::detect(); if ane_caps.available { // In hybrid mode, we'd route: // - MatMul/FFN to ANE // - Attention to GPU (Metal) assert!(ane_caps.supported_ops.contains(&"MatMul".to_string())); } } } // ============================================================================ // Performance Characteristics Tests // ============================================================================ #[test] fn test_ane_tops_values() { // Test known TOPS values for various chips struct ChipSpec { name: &'static str, min_tops: f32, max_tops: f32, } // Known Apple Silicon TOPS ranges let chip_specs = [ ChipSpec { name: "M1", min_tops: 11.0, max_tops: 11.5, }, ChipSpec { name: "M1 Pro/Max", min_tops: 11.0, max_tops: 11.5, }, ChipSpec { name: "M2", min_tops: 15.0, max_tops: 16.0, }, ChipSpec { name: "M3", min_tops: 18.0, max_tops: 18.5, }, ChipSpec { name: "M4", min_tops: 35.0, max_tops: 40.0, }, ]; if is_apple_silicon() { let caps = AneCapabilities::detect(); // Detected TOPS should fall within one of the known ranges let in_known_range = chip_specs .iter() .any(|spec| caps.tops >= spec.min_tops && caps.tops <= spec.max_tops + 5.0); // Just verify it's a reasonable positive value assert!(caps.tops > 0.0, "TOPS should be positive"); assert!(caps.tops < 100.0, "TOPS should be reasonable (< 100)"); } } // ============================================================================ // Error Type Tests // ============================================================================ #[test] fn test_error_messages() { // Test that error messages are informative let caps = AneCapabilities { available: false, tops: 0.0, max_model_size_mb: 0, supported_ops: vec![], }; // Debug output should be readable let debug = format!("{:?}", caps); assert!(debug.contains("available")); assert!(debug.contains("false")); } #[test] #[cfg(feature = "coreml")] fn test_error_chain() { if !is_apple_silicon() { let result: Result = CoreMLBackend::new(); let err = result.unwrap_err(); // Error should be a Config error match &err { RuvLLMError::Config(msg) => { assert!(msg.contains("not available") || msg.contains("feature")); } other => { panic!("Expected Config error, got {:?}", other); } } } } // ============================================================================ // Thread Safety Tests // ============================================================================ #[test] fn test_ane_capabilities_thread_safe() { use std::sync::Arc; use std::thread; let caps = Arc::new(AneCapabilities::detect()); let handles: Vec<_> = (0..4) .map(|i| { let caps = Arc::clone(&caps); thread::spawn(move || { // Read operations should be thread-safe let _ = caps.available; let _ = caps.tops; let _ = caps.max_model_size_mb; let _ = caps.is_model_suitable(1000); let _ = format!("{:?}", caps); i }) }) .collect(); for handle in handles { handle.join().expect("Thread should complete successfully"); } } // ============================================================================ // Benchmark-style Tests (Run with --release) // ============================================================================ #[test] #[ignore] // Run with: cargo test --release -- --ignored fn test_ane_capabilities_detection_performance() { use std::time::Instant; let iterations = 1000; let start = Instant::now(); for _ in 0..iterations { let _ = AneCapabilities::detect(); } let duration = start.elapsed(); let avg_ns = duration.as_nanos() as f64 / iterations as f64; println!( "AneCapabilities::detect() average time: {:.2} ns ({:.2} us)", avg_ns, avg_ns / 1000.0 ); // Detection should be fast (< 1ms) assert!( avg_ns < 1_000_000.0, "Detection should be < 1ms, was {} ns", avg_ns ); } // ============================================================================ // Documentation Examples Tests // ============================================================================ #[test] fn test_readme_example_capabilities() { // Example from module documentation let caps = AneCapabilities::detect(); if caps.available { println!("ANE available with {} TOPS", caps.tops); println!("Max model size: {} MB", caps.max_model_size_mb); println!("Supported ops: {:?}", caps.supported_ops); } else { println!("ANE not available on this device"); } } #[test] fn test_readme_example_compute_units() { // Example from module documentation let units = ComputeUnits::CpuAndNeuralEngine; println!("Compute units: {}", units.description()); println!("Uses ANE: {}", units.uses_ane()); println!("Uses GPU: {}", units.uses_gpu()); assert!(units.uses_ane()); assert!(!units.uses_gpu()); } // ============================================================================ // Property-based Test Helpers // ============================================================================ #[test] fn test_model_suitability_monotonic() { // Model suitability should be monotonic: if a larger model fits, smaller ones should too let caps = AneCapabilities { available: true, tops: 38.0, max_model_size_mb: 2048, supported_ops: vec!["MatMul".to_string()], }; // If 2048 fits, all smaller sizes should fit if caps.is_model_suitable(2048) { for size in [0, 1, 100, 500, 1000, 1500, 2000, 2047] { assert!( caps.is_model_suitable(size), "Size {} should fit if {} fits", size, 2048 ); } } // If 2049 doesn't fit, all larger sizes shouldn't fit either if !caps.is_model_suitable(2049) { for size in [2050, 3000, 4096, 10000] { assert!( !caps.is_model_suitable(size), "Size {} should not fit if {} doesn't fit", size, 2049 ); } } }