Skip to main content

optirs_core/plugin/validation/
mod.rs

1// Plugin validation and testing framework
2//
3// This module provides comprehensive validation and testing capabilities for optimizer plugins,
4// including functionality tests, performance tests, convergence validation, and compliance checks.
5
6#[allow(dead_code)]
7use super::core::*;
8use super::sdk::*;
9use scirs2_core::ndarray::Array1;
10use scirs2_core::numeric::Float;
11use std::collections::HashMap;
12use std::fmt::Debug;
13use std::time::{Duration, Instant};
14
15/// Comprehensive plugin validation framework
16#[derive(Debug)]
17pub struct PluginValidationFramework<A: Float> {
18    /// Validation configuration
19    config: ValidationConfig,
20    /// Test suites
21    test_suites: Vec<Box<dyn ValidationTestSuite<A>>>,
22    /// Compliance checkers
23    compliance_checkers: Vec<Box<dyn ComplianceChecker>>,
24    /// Performance benchmarker
25    benchmarker: PerformanceBenchmarker<A>,
26    /// Results storage
27    results: ValidationResults<A>,
28}
29
30/// Validation configuration
31#[derive(Debug, Clone)]
32pub struct ValidationConfig {
33    /// Enable strict validation
34    pub strict_mode: bool,
35    /// Numerical tolerance
36    pub numerical_tolerance: f64,
37    /// Performance tolerance (percentage)
38    pub performance_tolerance: f64,
39    /// Maximum test duration
40    pub max_test_duration: Duration,
41    /// Enable memory leak detection
42    pub check_memory_leaks: bool,
43    /// Enable thread safety testing
44    pub check_thread_safety: bool,
45    /// Enable convergence testing
46    pub check_convergence: bool,
47    /// Random seed for reproducible tests
48    pub random_seed: u64,
49    /// Test data sizes
50    pub test_data_sizes: Vec<usize>,
51}
52
53/// Validation test suite trait
54pub trait ValidationTestSuite<A: Float>: Debug {
55    /// Run all tests in the suite
56    fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult;
57
58    /// Get suite name
59    fn name(&self) -> &str;
60
61    /// Get suite description
62    fn description(&self) -> &str;
63
64    /// Get test count
65    fn test_count(&self) -> usize;
66}
67
68/// Individual test suite result
69#[derive(Debug, Clone)]
70pub struct SuiteResult {
71    /// Suite name
72    pub suite_name: String,
73    /// Test results
74    pub test_results: Vec<TestResult>,
75    /// Overall suite passed
76    pub suite_passed: bool,
77    /// Execution time
78    pub execution_time: Duration,
79    /// Summary statistics
80    pub summary: TestSummary,
81    /// Whether this suite actually ran real checks against the plugin.
82    /// `false` means the suite could not be executed (e.g. a required
83    /// capability is unavailable) -- such results are excluded from the
84    /// overall score and the pass gate rather than counted as a pass.
85    pub verified: bool,
86}
87
88/// Test execution summary
89#[derive(Debug, Clone)]
90pub struct TestSummary {
91    /// Total tests run
92    pub total_tests: usize,
93    /// Passed tests
94    pub passed_tests: usize,
95    /// Failed tests
96    pub failed_tests: usize,
97    /// Skipped tests
98    pub skipped_tests: usize,
99    /// Success rate (0.0 to 1.0)
100    pub success_rate: f64,
101}
102
103/// Compliance checker trait
104pub trait ComplianceChecker: Debug {
105    /// Check plugin compliance
106    fn check_compliance(&self, plugininfo: &PluginInfo) -> ComplianceResult;
107
108    /// Get checker name
109    fn name(&self) -> &str;
110
111    /// Get compliance requirements
112    fn requirements(&self) -> Vec<ComplianceRequirement>;
113}
114
115/// Compliance check result
116#[derive(Debug, Clone)]
117pub struct ComplianceResult {
118    /// Compliance check passed
119    pub compliant: bool,
120    /// Violations found
121    pub violations: Vec<ComplianceViolation>,
122    /// Warnings
123    pub warnings: Vec<String>,
124    /// Compliance score (0.0 to 1.0)
125    pub compliance_score: f64,
126    /// Whether this checker actually inspected the plugin's declared
127    /// metadata. `false` means the category is not decidable from the data
128    /// this checker receives (e.g. performance conformance needs benchmark
129    /// measurements, not just `PluginInfo`) -- such results are excluded
130    /// from the overall score and the pass gate rather than counted as a
131    /// pass.
132    pub verified: bool,
133}
134
135/// Compliance violation
136#[derive(Debug, Clone)]
137pub struct ComplianceViolation {
138    /// Violation type
139    pub violation_type: ViolationType,
140    /// Violation description
141    pub description: String,
142    /// Severity level
143    pub severity: ViolationSeverity,
144    /// Suggested fix
145    pub suggested_fix: Option<String>,
146}
147
148/// Types of compliance violations
149#[derive(Debug, Clone)]
150pub enum ViolationType {
151    /// Missing required metadata
152    MissingMetadata,
153    /// Invalid configuration
154    InvalidConfiguration,
155    /// Security violation
156    SecurityViolation,
157    /// Performance violation
158    PerformanceViolation,
159    /// API violation
160    ApiViolation,
161    /// Documentation violation
162    DocumentationViolation,
163}
164
165/// Violation severity levels
166#[derive(Debug, Clone)]
167pub enum ViolationSeverity {
168    Low,
169    Medium,
170    High,
171    Critical,
172}
173
174/// Compliance requirement
175#[derive(Debug, Clone)]
176pub struct ComplianceRequirement {
177    /// Requirement ID
178    pub id: String,
179    /// Requirement description
180    pub description: String,
181    /// Required/optional
182    pub mandatory: bool,
183    /// Category
184    pub category: ComplianceCategory,
185}
186
187/// Compliance categories
188#[derive(Debug, Clone)]
189pub enum ComplianceCategory {
190    Security,
191    Performance,
192    API,
193    Documentation,
194    Metadata,
195    Testing,
196}
197
198/// Performance benchmarker
199#[derive(Debug)]
200pub struct PerformanceBenchmarker<A: Float> {
201    /// Benchmark configuration
202    config: BenchmarkConfig,
203    /// Standard benchmarks
204    benchmarks: Vec<Box<dyn PerformanceBenchmark<A>>>,
205    /// Baseline results
206    baselines: HashMap<String, BenchmarkBaseline>,
207}
208
209/// Performance benchmark trait
210pub trait PerformanceBenchmark<A: Float>: Debug {
211    /// Run benchmark
212    fn run(&self, plugin: &mut dyn OptimizerPlugin<A>) -> BenchmarkResult<A>;
213
214    /// Get benchmark name
215    fn name(&self) -> &str;
216
217    /// Get benchmark type
218    fn benchmark_type(&self) -> BenchmarkType;
219
220    /// Get expected baseline
221    fn expected_baseline(&self) -> Option<BenchmarkBaseline>;
222}
223
224/// Benchmark types
225#[derive(Debug, Clone)]
226pub enum BenchmarkType {
227    /// Throughput benchmark
228    Throughput,
229    /// Latency benchmark
230    Latency,
231    /// Memory usage benchmark
232    Memory,
233    /// Convergence speed benchmark
234    Convergence,
235    /// Scalability benchmark
236    Scalability,
237}
238
239/// Benchmark baseline
240#[derive(Debug, Clone)]
241pub struct BenchmarkBaseline {
242    /// Expected value
243    pub expected_value: f64,
244    /// Tolerance (percentage)
245    pub tolerance: f64,
246    /// Units
247    pub units: String,
248}
249
250/// Complete validation results
251#[derive(Debug, Clone)]
252pub struct ValidationResults<A: Float> {
253    /// Overall validation passed
254    pub validation_passed: bool,
255    /// Test suite results
256    pub suite_results: Vec<SuiteResult>,
257    /// Compliance results
258    pub compliance_results: Vec<ComplianceResult>,
259    /// Performance benchmark results
260    pub benchmark_results: Vec<BenchmarkResult<A>>,
261    /// Overall score (0.0 to 1.0), excluding any unverified category.
262    /// `None` when every category was unverified -- there is no evidence
263    /// to score, so this must never be reported as a passing number.
264    pub overall_score: Option<f64>,
265    /// Validation timestamp
266    pub timestamp: std::time::SystemTime,
267    /// Total validation time
268    pub total_time: Duration,
269}
270
271// Built-in test suites
272
273/// Functionality test suite
274#[derive(Debug)]
275pub struct FunctionalityTestSuite<A: Float> {
276    config: ValidationConfig,
277    _phantom: std::marker::PhantomData<A>,
278}
279
280/// Numerical accuracy test suite
281#[derive(Debug)]
282pub struct NumericalAccuracyTestSuite<A: Float> {
283    config: ValidationConfig,
284    _phantom: std::marker::PhantomData<A>,
285}
286
287/// Thread safety test suite
288#[derive(Debug)]
289pub struct ThreadSafetyTestSuite<A: Float + std::fmt::Debug> {
290    config: ValidationConfig,
291    _phantom: std::marker::PhantomData<A>,
292}
293
294impl<A: Float + std::fmt::Debug + Send + Sync> ThreadSafetyTestSuite<A> {
295    /// Create a new thread safety test suite
296    pub fn new(config: ValidationConfig) -> Self {
297        Self {
298            config,
299            _phantom: std::marker::PhantomData,
300        }
301    }
302}
303
304impl<A: Float + std::fmt::Debug + Send + Sync + 'static> ThreadSafetyTestSuite<A> {
305    /// Concurrent step smoke test: clone the plugin, share it behind
306    /// `Arc<Mutex<_>>` across several threads, and drive many `step()`
307    /// calls concurrently. `OptimizerPlugin<A>: Send + Sync` is already a
308    /// supertrait bound, so this is always runnable -- there is no
309    /// "capability unavailable" case to fall back to `Unverified` for here.
310    fn test_concurrent_steps(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
311        use std::sync::atomic::{AtomicBool, Ordering};
312        use std::sync::{Arc, Mutex};
313        let start_time = Instant::now();
314
315        const NUM_THREADS: usize = 4;
316        const STEPS_PER_THREAD: usize = 25;
317        // Dimension comes from the configured test data sizes rather than a
318        // literal, so a caller that asked for a different workload gets it.
319        let dim = self
320            .config
321            .test_data_sizes
322            .iter()
323            .copied()
324            .find(|size| *size > 0)
325            .unwrap_or(8);
326        let dim = dim.min(4096);
327
328        let shared: Arc<Mutex<Box<dyn OptimizerPlugin<A>>>> =
329            Arc::new(Mutex::new(plugin.clone_plugin()));
330        {
331            let mut guard = shared.lock().unwrap_or_else(|e| e.into_inner());
332            if let Err(e) = guard.initialize(&[dim]) {
333                return TestResult {
334                    passed: false,
335                    message: format!("initialize failed before concurrency test: {e}"),
336                    execution_time: start_time.elapsed(),
337                    data: HashMap::new(),
338                };
339            }
340        }
341
342        let params: Array1<A> =
343            Array1::from_iter((0..dim).map(|i| A::from(1.0 + i as f64).unwrap_or_else(A::one)));
344        let gradients: Array1<A> =
345            Array1::from_iter((0..dim).map(|_| A::from(0.01).unwrap_or_else(A::zero)));
346        let saw_error = Arc::new(AtomicBool::new(false));
347
348        let mut handles = Vec::with_capacity(NUM_THREADS);
349        for _ in 0..NUM_THREADS {
350            let shared = Arc::clone(&shared);
351            let saw_error = Arc::clone(&saw_error);
352            let params = params.clone();
353            let gradients = gradients.clone();
354            handles.push(std::thread::spawn(move || {
355                for _ in 0..STEPS_PER_THREAD {
356                    let mut guard = shared.lock().unwrap_or_else(|e| e.into_inner());
357                    match guard.step(&params, &gradients) {
358                        Ok(result) => {
359                            if result.iter().any(|v| !v.is_finite()) {
360                                saw_error.store(true, Ordering::SeqCst);
361                            }
362                        }
363                        Err(_) => saw_error.store(true, Ordering::SeqCst),
364                    }
365                }
366            }));
367        }
368
369        let mut any_panicked = false;
370        for handle in handles {
371            if handle.join().is_err() {
372                any_panicked = true;
373            }
374        }
375
376        let passed = !any_panicked && !saw_error.load(Ordering::SeqCst);
377        let message = if any_panicked {
378            "A worker thread panicked while calling step() concurrently through Arc<Mutex<_>>"
379                .to_string()
380        } else if passed {
381            format!(
382                "{NUM_THREADS} threads completed {STEPS_PER_THREAD} concurrent step() calls \
383                 each through a shared Arc<Mutex<_>> instance with no panics and finite output"
384            )
385        } else {
386            "Concurrent step() calls produced an error or a non-finite result".to_string()
387        };
388
389        TestResult {
390            passed,
391            message,
392            execution_time: start_time.elapsed(),
393            data: HashMap::new(),
394        }
395    }
396}
397
398impl<A: Float + std::fmt::Debug + Send + Sync + 'static> ValidationTestSuite<A>
399    for ThreadSafetyTestSuite<A>
400{
401    fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult {
402        let start_time = Instant::now();
403        let result = self.test_concurrent_steps(plugin);
404        let passed = result.passed;
405
406        SuiteResult {
407            suite_name: "Thread Safety".to_string(),
408            test_results: vec![result],
409            suite_passed: passed,
410            execution_time: start_time.elapsed(),
411            summary: TestSummary {
412                total_tests: 1,
413                passed_tests: passed as usize,
414                failed_tests: (!passed) as usize,
415                skipped_tests: 0,
416                success_rate: if passed { 1.0 } else { 0.0 },
417            },
418            verified: true,
419        }
420    }
421
422    fn name(&self) -> &str {
423        "Thread Safety Tests"
424    }
425
426    fn description(&self) -> &str {
427        "Tests for thread safety and concurrent access"
428    }
429
430    fn test_count(&self) -> usize {
431        1
432    }
433}
434
435/// Memory management test suite
436#[derive(Debug)]
437pub struct MemoryTestSuite<A: Float + std::fmt::Debug> {
438    config: ValidationConfig,
439    _phantom: std::marker::PhantomData<A>,
440}
441
442impl<A: Float + std::fmt::Debug + Send + Sync> MemoryTestSuite<A> {
443    /// Create a new memory test suite
444    pub fn new(config: ValidationConfig) -> Self {
445        Self {
446            config,
447            _phantom: std::marker::PhantomData,
448        }
449    }
450}
451
452impl<A: Float + std::fmt::Debug + Send + Sync> MemoryTestSuite<A> {
453    /// Sample the plugin's self-reported `memory_usage()` across many steps
454    /// and flag runs where the peak reported usage keeps climbing in the
455    /// second half relative to the first -- a crude but real growth
456    /// heuristic. Plugins that never override `memory_usage()` report a
457    /// constant `0`, which is a real (if uninformative) measurement -- not
458    /// a fabricated pass -- and the flat sequence correctly reads as "no
459    /// growth observed".
460    fn test_memory_growth(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
461        let start_time = Instant::now();
462        const SAMPLES: usize = 50;
463        // Dimension from the configured workload rather than a literal.
464        let dim = self
465            .config
466            .test_data_sizes
467            .iter()
468            .copied()
469            .find(|size| *size > 0)
470            .unwrap_or(16)
471            .min(4096);
472
473        if !self.config.check_memory_leaks {
474            return TestResult {
475                passed: true,
476                message: "memory leak detection disabled by ValidationConfig::check_memory_leaks"
477                    .to_string(),
478                execution_time: start_time.elapsed(),
479                data: HashMap::new(),
480            };
481        }
482
483        if let Err(e) = plugin.initialize(&[dim]) {
484            return TestResult {
485                passed: false,
486                message: format!("initialize failed before memory growth probe: {e}"),
487                execution_time: start_time.elapsed(),
488                data: HashMap::new(),
489            };
490        }
491
492        let mut params: Array1<A> = Array1::from_elem(dim, A::one());
493        let gradients: Array1<A> = Array1::from_elem(dim, A::from(0.01).unwrap_or_else(A::zero));
494        let baseline = plugin.memory_usage().current_usage;
495        let mut samples = Vec::with_capacity(SAMPLES);
496
497        for _ in 0..SAMPLES {
498            match plugin.step(&params, &gradients) {
499                Ok(next) => params = next,
500                Err(e) => {
501                    return TestResult {
502                        passed: false,
503                        message: format!("step failed during memory growth probe: {e}"),
504                        execution_time: start_time.elapsed(),
505                        data: HashMap::new(),
506                    };
507                }
508            }
509            samples.push(plugin.memory_usage().current_usage);
510        }
511
512        let half = SAMPLES / 2;
513        let first_half_peak = samples[..half].iter().copied().max().unwrap_or(0);
514        let second_half_peak = samples[half..].iter().copied().max().unwrap_or(0);
515        let growth_factor = if first_half_peak == 0 {
516            if second_half_peak == 0 {
517                1.0
518            } else {
519                f64::INFINITY
520            }
521        } else {
522            second_half_peak as f64 / first_half_peak as f64
523        };
524        // Allow modest growth (e.g. lazily-allocated optimizer state
525        // settling in) but flag sustained, unbounded growth across the run.
526        let passed = growth_factor <= 1.5;
527
528        TestResult {
529            passed,
530            message: format!(
531                "self-reported current_usage over {SAMPLES} steps: baseline={baseline}B \
532                 first_half_peak={first_half_peak}B second_half_peak={second_half_peak}B \
533                 growth_factor={growth_factor:.2}"
534            ),
535            execution_time: start_time.elapsed(),
536            data: HashMap::new(),
537        }
538    }
539}
540
541impl<A: Float + std::fmt::Debug + Send + Sync> ValidationTestSuite<A> for MemoryTestSuite<A> {
542    fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult {
543        let start_time = Instant::now();
544        let result = self.test_memory_growth(plugin);
545        let passed = result.passed;
546
547        SuiteResult {
548            suite_name: "Memory Management".to_string(),
549            test_results: vec![result],
550            suite_passed: passed,
551            execution_time: start_time.elapsed(),
552            summary: TestSummary {
553                total_tests: 1,
554                passed_tests: passed as usize,
555                failed_tests: (!passed) as usize,
556                skipped_tests: 0,
557                success_rate: if passed { 1.0 } else { 0.0 },
558            },
559            verified: true,
560        }
561    }
562
563    fn name(&self) -> &str {
564        "Memory Management Tests"
565    }
566
567    fn description(&self) -> &str {
568        "Tests for memory allocation and management"
569    }
570
571    fn test_count(&self) -> usize {
572        1
573    }
574}
575
576pub mod convergence;
577pub use convergence::{ConvergenceTestSuite, TestProblem};
578
579// Built-in compliance checkers
580
581/// API compliance checker
582#[derive(Debug)]
583pub struct ApiComplianceChecker;
584
585/// Security compliance checker
586#[derive(Debug)]
587pub struct SecurityComplianceChecker;
588
589/// Performance compliance checker
590#[derive(Debug)]
591pub struct PerformanceComplianceChecker;
592
593/// Documentation compliance checker
594#[derive(Debug)]
595pub struct DocumentationComplianceChecker;
596
597// Built-in performance benchmarks
598
599/// Throughput benchmark
600#[derive(Debug)]
601pub struct ThroughputBenchmark<A: Float> {
602    problemsize: usize,
603    iterations: usize,
604    _phantom: std::marker::PhantomData<A>,
605}
606
607impl<A: Float + Send + Sync> ThroughputBenchmark<A> {
608    /// Create a new throughput benchmark
609    pub fn new(problemsize: usize, iterations: usize) -> Self {
610        Self {
611            problemsize,
612            iterations,
613            _phantom: std::marker::PhantomData,
614        }
615    }
616}
617
618impl<A: Float + Debug + Send + Sync> PerformanceBenchmark<A> for ThroughputBenchmark<A> {
619    fn run(&self, plugin: &mut dyn OptimizerPlugin<A>) -> BenchmarkResult<A> {
620        let start_time = Instant::now();
621        let dim = self.problemsize.max(1);
622
623        if let Err(e) = plugin.initialize(&[dim]) {
624            let mut metrics = HashMap::new();
625            metrics.insert("error".to_string(), 0.0);
626            return BenchmarkResult {
627                name: format!("Throughput (initialize failed: {e})"),
628                score: 0.0,
629                metrics,
630                execution_time: start_time.elapsed(),
631                memory_usage: 0,
632                data: HashMap::new(),
633                verified: false,
634            };
635        }
636
637        let params: Array1<A> = Array1::from_iter(
638            (0..dim).map(|i| A::from(1.0 + (i % 7) as f64 * 0.1).unwrap_or_else(A::one)),
639        );
640        let gradients: Array1<A> = Array1::from_iter(
641            (0..dim).map(|i| A::from(0.01 + (i % 5) as f64 * 0.001).unwrap_or_else(A::zero)),
642        );
643
644        let run_start = Instant::now();
645        let mut current = params;
646        let mut completed = 0usize;
647        for _ in 0..self.iterations {
648            match plugin.step(&current, &gradients) {
649                Ok(next) => {
650                    current = next;
651                    completed += 1;
652                }
653                Err(_) => break,
654            }
655        }
656        let elapsed_secs = run_start.elapsed().as_secs_f64();
657        let ops_per_sec = if elapsed_secs > 0.0 {
658            completed as f64 / elapsed_secs
659        } else {
660            completed as f64
661        };
662
663        // Normalize against the baseline into [0, 1] rather than surfacing
664        // the raw ops/sec magnitude -- a magnitude in the hundreds
665        // previously dominated the [0,1] overall score regardless of what
666        // the functional tests found.
667        let score = self
668            .expected_baseline()
669            .map(|baseline| {
670                (ops_per_sec / baseline.expected_value.max(f64::EPSILON)).clamp(0.0, 1.0)
671            })
672            .unwrap_or(0.0);
673
674        let mut metrics = HashMap::new();
675        metrics.insert("ops_per_sec".to_string(), ops_per_sec);
676        metrics.insert("completed_iterations".to_string(), completed as f64);
677
678        BenchmarkResult {
679            name: "Throughput".to_string(),
680            score,
681            metrics,
682            execution_time: start_time.elapsed(),
683            memory_usage: plugin.memory_usage().current_usage,
684            data: HashMap::new(),
685            verified: true,
686        }
687    }
688
689    fn name(&self) -> &str {
690        "Throughput Benchmark"
691    }
692
693    fn benchmark_type(&self) -> BenchmarkType {
694        BenchmarkType::Throughput
695    }
696
697    fn expected_baseline(&self) -> Option<BenchmarkBaseline> {
698        Some(BenchmarkBaseline {
699            expected_value: 50.0,
700            tolerance: 10.0,
701            units: "ops/sec".to_string(),
702        })
703    }
704}
705
706/// Latency benchmark
707#[derive(Debug)]
708pub struct LatencyBenchmark<A: Float> {
709    problemsize: usize,
710    _phantom: std::marker::PhantomData<A>,
711}
712
713impl<A: Float + Send + Sync> LatencyBenchmark<A> {
714    /// Create a new latency benchmark
715    pub fn new(problemsize: usize) -> Self {
716        Self {
717            problemsize,
718            _phantom: std::marker::PhantomData,
719        }
720    }
721}
722
723impl<A: Float + Debug + Send + Sync> PerformanceBenchmark<A> for LatencyBenchmark<A> {
724    fn run(&self, plugin: &mut dyn OptimizerPlugin<A>) -> BenchmarkResult<A> {
725        let start_time = Instant::now();
726        let dim = self.problemsize.max(1);
727
728        if let Err(e) = plugin.initialize(&[dim]) {
729            return BenchmarkResult {
730                name: format!("Latency (initialize failed: {e})"),
731                score: 0.0,
732                metrics: HashMap::new(),
733                execution_time: start_time.elapsed(),
734                memory_usage: 0,
735                data: HashMap::new(),
736                verified: false,
737            };
738        }
739
740        let params: Array1<A> = Array1::from_iter(
741            (0..dim).map(|i| A::from(1.0 + (i % 7) as f64 * 0.1).unwrap_or_else(A::one)),
742        );
743        let gradients: Array1<A> =
744            Array1::from_iter((0..dim).map(|_| A::from(0.01).unwrap_or_else(A::zero)));
745
746        const SAMPLES: usize = 50;
747        let run_start = Instant::now();
748        let mut current = params;
749        let mut completed = 0usize;
750        for _ in 0..SAMPLES {
751            match plugin.step(&current, &gradients) {
752                Ok(next) => {
753                    current = next;
754                    completed += 1;
755                }
756                Err(_) => break,
757            }
758        }
759        let elapsed = run_start.elapsed();
760        let avg_latency_ms = if completed > 0 {
761            elapsed.as_secs_f64() * 1000.0 / completed as f64
762        } else {
763            f64::INFINITY
764        };
765
766        // Lower latency is better: normalize as baseline/actual, clamped to
767        // [0, 1] so a plugin faster than the baseline scores 1.0 rather than
768        // an unbounded value that would dominate the overall score.
769        let score = self
770            .expected_baseline()
771            .map(|baseline| {
772                if avg_latency_ms.is_finite() && avg_latency_ms > 0.0 {
773                    (baseline.expected_value / avg_latency_ms).clamp(0.0, 1.0)
774                } else {
775                    0.0
776                }
777            })
778            .unwrap_or(0.0);
779
780        let mut metrics = HashMap::new();
781        metrics.insert("avg_latency_ms".to_string(), avg_latency_ms);
782
783        BenchmarkResult {
784            name: "Latency".to_string(),
785            score,
786            metrics,
787            execution_time: start_time.elapsed(),
788            memory_usage: plugin.memory_usage().current_usage,
789            data: HashMap::new(),
790            verified: true,
791        }
792    }
793
794    fn name(&self) -> &str {
795        "Latency Benchmark"
796    }
797
798    fn benchmark_type(&self) -> BenchmarkType {
799        BenchmarkType::Latency
800    }
801
802    fn expected_baseline(&self) -> Option<BenchmarkBaseline> {
803        Some(BenchmarkBaseline {
804            expected_value: 20.0,
805            tolerance: 5.0,
806            units: "ms".to_string(),
807        })
808    }
809}
810
811/// Memory efficiency benchmark
812#[derive(Debug)]
813pub struct MemoryBenchmark<A: Float> {
814    problemsize: usize,
815    _phantom: std::marker::PhantomData<A>,
816}
817
818impl<A: Float + Send + Sync> MemoryBenchmark<A> {
819    /// Create a new memory benchmark
820    pub fn new(problemsize: usize) -> Self {
821        Self {
822            problemsize,
823            _phantom: std::marker::PhantomData,
824        }
825    }
826}
827
828impl<A: Float + Debug + Send + Sync> PerformanceBenchmark<A> for MemoryBenchmark<A> {
829    fn run(&self, plugin: &mut dyn OptimizerPlugin<A>) -> BenchmarkResult<A> {
830        let start_time = Instant::now();
831        let dim = self.problemsize.max(1);
832
833        if let Err(e) = plugin.initialize(&[dim]) {
834            return BenchmarkResult {
835                name: format!("Memory (initialize failed: {e})"),
836                score: 0.0,
837                metrics: HashMap::new(),
838                execution_time: start_time.elapsed(),
839                memory_usage: 0,
840                data: HashMap::new(),
841                verified: false,
842            };
843        }
844
845        let params: Array1<A> = Array1::from_iter(
846            (0..dim).map(|i| A::from(1.0 + (i % 7) as f64 * 0.1).unwrap_or_else(A::one)),
847        );
848        let gradients: Array1<A> =
849            Array1::from_iter((0..dim).map(|_| A::from(0.01).unwrap_or_else(A::zero)));
850
851        let mut current = params;
852        for _ in 0..20 {
853            match plugin.step(&current, &gradients) {
854                Ok(next) => current = next,
855                Err(_) => break,
856            }
857        }
858
859        let usage = plugin.memory_usage();
860        let usage_mb = usage.current_usage as f64 / (1024.0 * 1024.0);
861
862        // Lower memory is better: normalize as baseline/actual. A plugin
863        // that never overrides `memory_usage()` reports `0` -- that is a
864        // real measurement of "nothing self-reported", not evidence of
865        // either compliance or violation, so it scores neutrally (1.0)
866        // rather than being penalized for a metric it never populated.
867        let score = self
868            .expected_baseline()
869            .map(|baseline| {
870                if usage_mb > 0.0 {
871                    (baseline.expected_value / usage_mb).clamp(0.0, 1.0)
872                } else {
873                    1.0
874                }
875            })
876            .unwrap_or(0.0);
877
878        let mut metrics = HashMap::new();
879        metrics.insert("memory_usage_mb".to_string(), usage_mb);
880
881        BenchmarkResult {
882            name: "Memory".to_string(),
883            score,
884            metrics,
885            execution_time: start_time.elapsed(),
886            memory_usage: usage.current_usage,
887            data: HashMap::new(),
888            verified: true,
889        }
890    }
891
892    fn name(&self) -> &str {
893        "Memory Benchmark"
894    }
895
896    fn benchmark_type(&self) -> BenchmarkType {
897        BenchmarkType::Memory
898    }
899
900    fn expected_baseline(&self) -> Option<BenchmarkBaseline> {
901        Some(BenchmarkBaseline {
902            expected_value: 100.0,
903            tolerance: 20.0,
904            units: "MB".to_string(),
905        })
906    }
907}
908
909impl<A: Float + Debug + Send + Sync + 'static> PluginValidationFramework<A> {
910    /// Create a new validation framework
911    pub fn new(config: ValidationConfig) -> Self {
912        let mut framework = Self {
913            config: config.clone(),
914            test_suites: Vec::new(),
915            compliance_checkers: Vec::new(),
916            benchmarker: PerformanceBenchmarker::new(BenchmarkConfig::default()),
917            results: ValidationResults::new(),
918        };
919
920        // Add default test suites
921        framework.add_default_test_suites();
922        framework.add_default_compliance_checkers();
923        framework.add_default_benchmarks();
924
925        framework
926    }
927
928    /// Run complete validation on a plugin
929    pub fn validate_plugin(&mut self, plugin: &mut dyn OptimizerPlugin<A>) -> ValidationResults<A> {
930        let start_time = Instant::now();
931        let mut suite_results = Vec::new();
932        let mut compliance_results = Vec::new();
933        let mut benchmark_results = Vec::new();
934
935        // Run test suites
936        for testsuite in &self.test_suites {
937            let result = testsuite.run_tests(plugin);
938            suite_results.push(result);
939        }
940
941        // Run compliance checks
942        let plugininfo = plugin.plugin_info();
943        for checker in &self.compliance_checkers {
944            let result = checker.check_compliance(&plugininfo);
945            compliance_results.push(result);
946        }
947
948        // Run performance benchmarks
949        let bench_results = self.benchmarker.run_all_benchmarks(plugin);
950        benchmark_results.extend(bench_results);
951
952        // Calculate overall score, excluding any unverified category from
953        // both the numerator and the weight sum -- an unrun check must never
954        // read as a pass.
955        let overall_score =
956            self.calculate_overall_score(&suite_results, &compliance_results, &benchmark_results);
957
958        // Determine if validation passed. `None` means nothing could be
959        // verified at all -- there is no evidence to certify against, so
960        // the gate cannot pass on zero evidence. Otherwise only the
961        // *verified* suites/checkers must agree, mirroring the score.
962        let validation_passed = match overall_score {
963            Some(score) => {
964                score >= 0.8 // 80% threshold
965                    && suite_results
966                        .iter()
967                        .filter(|r| r.verified)
968                        .all(|r| r.suite_passed)
969                    && compliance_results
970                        .iter()
971                        .filter(|r| r.verified)
972                        .all(|r| r.compliant)
973            }
974            None => false,
975        };
976
977        let results = ValidationResults {
978            validation_passed,
979            suite_results,
980            compliance_results,
981            benchmark_results,
982            overall_score,
983            timestamp: std::time::SystemTime::now(),
984            total_time: start_time.elapsed(),
985        };
986        // Retain the run so a caller can re-read it without re-validating.
987        // Until 0.3.2 `results` was initialised in `new` and never written or
988        // read, so the framework's own record of what it had certified did not
989        // exist.
990        self.results = results.clone();
991        results
992    }
993
994    /// The most recent [`Self::validate_plugin`] run, or a
995    /// never-validated placeholder (`validation_passed: false`,
996    /// `overall_score: None`) if none has happened.
997    pub fn last_results(&self) -> &ValidationResults<A> {
998        &self.results
999    }
1000
1001    /// Add custom test suite
1002    pub fn add_test_suite(&mut self, testsuite: Box<dyn ValidationTestSuite<A>>) {
1003        self.test_suites.push(testsuite);
1004    }
1005
1006    /// Add custom compliance checker
1007    pub fn add_compliance_checker(&mut self, checker: Box<dyn ComplianceChecker>) {
1008        self.compliance_checkers.push(checker);
1009    }
1010
1011    /// Add custom benchmark
1012    pub fn add_benchmark(&mut self, benchmark: Box<dyn PerformanceBenchmark<A>>) {
1013        self.benchmarker.add_benchmark(benchmark);
1014    }
1015
1016    fn add_default_test_suites(&mut self) {
1017        self.test_suites
1018            .push(Box::new(FunctionalityTestSuite::new(self.config.clone())));
1019        self.test_suites
1020            .push(Box::new(NumericalAccuracyTestSuite::new(
1021                self.config.clone(),
1022            )));
1023
1024        if self.config.check_thread_safety {
1025            self.test_suites
1026                .push(Box::new(ThreadSafetyTestSuite::new(self.config.clone())));
1027        }
1028
1029        if self.config.check_memory_leaks {
1030            self.test_suites
1031                .push(Box::new(MemoryTestSuite::new(self.config.clone())));
1032        }
1033
1034        if self.config.check_convergence {
1035            self.test_suites
1036                .push(Box::new(ConvergenceTestSuite::new(self.config.clone())));
1037        }
1038    }
1039
1040    fn add_default_compliance_checkers(&mut self) {
1041        self.compliance_checkers
1042            .push(Box::new(ApiComplianceChecker));
1043        self.compliance_checkers
1044            .push(Box::new(SecurityComplianceChecker));
1045        self.compliance_checkers
1046            .push(Box::new(PerformanceComplianceChecker));
1047        self.compliance_checkers
1048            .push(Box::new(DocumentationComplianceChecker));
1049    }
1050
1051    fn add_default_benchmarks(&mut self) {
1052        for &size in &self.config.test_data_sizes {
1053            self.benchmarker
1054                .add_benchmark(Box::new(ThroughputBenchmark::new(size, 100)));
1055            self.benchmarker
1056                .add_benchmark(Box::new(LatencyBenchmark::new(size)));
1057            self.benchmarker
1058                .add_benchmark(Box::new(MemoryBenchmark::new(size)));
1059        }
1060    }
1061
1062    /// Aggregate suite/compliance/benchmark results into a single [0, 1]
1063    /// score, excluding any category that was not actually verified from
1064    /// both the numerator and the weight sum. An unverified category
1065    /// (a suite that could not run, a checker that could not decide) must
1066    /// never contribute as though it had passed -- and if *nothing* could
1067    /// be verified there is no evidence to score at all, hence `None`
1068    /// rather than a fabricated `0.0` that a caller might read as "checked
1069    /// and failed" instead of "not checked".
1070    fn calculate_overall_score(
1071        &self,
1072        suite_results: &[SuiteResult],
1073        compliance_results: &[ComplianceResult],
1074        benchmark_results: &[BenchmarkResult<A>],
1075    ) -> Option<f64> {
1076        let mut total_score = 0.0;
1077        let mut weight_sum = 0.0;
1078
1079        // Test suite scores (50% weight)
1080        let verified_suites: Vec<&SuiteResult> =
1081            suite_results.iter().filter(|r| r.verified).collect();
1082        if !verified_suites.is_empty() {
1083            let suite_score = verified_suites
1084                .iter()
1085                .map(|r| r.summary.success_rate)
1086                .sum::<f64>()
1087                / verified_suites.len() as f64;
1088            total_score += suite_score * 0.5;
1089            weight_sum += 0.5;
1090        }
1091
1092        // Compliance scores (30% weight)
1093        let verified_compliance: Vec<&ComplianceResult> =
1094            compliance_results.iter().filter(|r| r.verified).collect();
1095        if !verified_compliance.is_empty() {
1096            let compliance_score = verified_compliance
1097                .iter()
1098                .map(|r| r.compliance_score)
1099                .sum::<f64>()
1100                / verified_compliance.len() as f64;
1101            total_score += compliance_score * 0.3;
1102            weight_sum += 0.3;
1103        }
1104
1105        // Performance scores (20% weight). Individual benchmark scores are
1106        // already normalized into [0, 1] against their baseline (see
1107        // ThroughputBenchmark/LatencyBenchmark/MemoryBenchmark::run), so
1108        // this average stays commensurable with the other two categories
1109        // instead of a raw ops/sec or MB magnitude dominating the mean.
1110        let verified_benchmarks: Vec<&BenchmarkResult<A>> =
1111            benchmark_results.iter().filter(|r| r.verified).collect();
1112        if !verified_benchmarks.is_empty() {
1113            let perf_score = verified_benchmarks.iter().map(|r| r.score).sum::<f64>()
1114                / verified_benchmarks.len() as f64;
1115            total_score += perf_score * 0.2;
1116            weight_sum += 0.2;
1117        }
1118
1119        if weight_sum > 0.0 {
1120            Some((total_score / weight_sum).clamp(0.0, 1.0))
1121        } else {
1122            None
1123        }
1124    }
1125}
1126
1127// Implementation of test suites
1128
1129impl<A: Float + Debug + Send + Sync + 'static> FunctionalityTestSuite<A> {
1130    fn new(config: ValidationConfig) -> Self {
1131        Self {
1132            config,
1133            _phantom: std::marker::PhantomData,
1134        }
1135    }
1136}
1137
1138impl<A: Float + Debug + Send + Sync + 'static> ValidationTestSuite<A>
1139    for FunctionalityTestSuite<A>
1140{
1141    fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult {
1142        let start_time = Instant::now();
1143        let mut test_results = Vec::new();
1144
1145        // Test 1: Basic step functionality
1146        let result1 = self.test_basic_step(plugin);
1147        test_results.push(result1);
1148
1149        // Test 2: Parameter initialization
1150        let result2 = self.test_initialization(plugin);
1151        test_results.push(result2);
1152
1153        // Test 3: State management
1154        let result3 = self.test_state_management(plugin);
1155        test_results.push(result3);
1156
1157        // Test 4: Configuration handling
1158        let result4 = self.test_configuration(plugin);
1159        test_results.push(result4);
1160
1161        let passed_tests = test_results.iter().filter(|r| r.passed).count();
1162        let total_tests = test_results.len();
1163
1164        SuiteResult {
1165            suite_name: self.name().to_string(),
1166            test_results,
1167            suite_passed: passed_tests == total_tests,
1168            execution_time: start_time.elapsed(),
1169            summary: TestSummary {
1170                total_tests,
1171                passed_tests,
1172                failed_tests: total_tests - passed_tests,
1173                skipped_tests: 0,
1174                success_rate: passed_tests as f64 / total_tests as f64,
1175            },
1176            verified: true,
1177        }
1178    }
1179
1180    fn name(&self) -> &str {
1181        "Functionality Tests"
1182    }
1183
1184    fn description(&self) -> &str {
1185        "Tests basic optimizer functionality and API compliance"
1186    }
1187
1188    fn test_count(&self) -> usize {
1189        4
1190    }
1191}
1192
1193impl<A: Float + Debug + Send + Sync + 'static> FunctionalityTestSuite<A> {
1194    fn test_basic_step(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1195        let start_time = Instant::now();
1196
1197        // Create test data. `A::from` cannot fail for these literals in any
1198        // real float type, but a plugin generic over an exotic `A` must not
1199        // abort the whole validation run, so a failed conversion reports a
1200        // failed test instead of panicking.
1201        let literal = |value: f64| A::from(value);
1202        let (Some(p0), Some(p1), Some(g0), Some(g1)) =
1203            (literal(1.0), literal(2.0), literal(0.1), literal(0.2))
1204        else {
1205            return TestResult {
1206                passed: false,
1207                message: "the element type cannot represent the test literals".to_string(),
1208                execution_time: start_time.elapsed(),
1209                data: HashMap::new(),
1210            };
1211        };
1212        let params = Array1::from_vec(vec![p0, p1]);
1213        let gradients = Array1::from_vec(vec![g0, g1]);
1214
1215        match plugin.step(&params, &gradients) {
1216            Ok(result) => {
1217                if result.len() == params.len() {
1218                    // A step must actually move the parameters. "Moved" is
1219                    // judged against the configured `numerical_tolerance` rather
1220                    // than a hardcoded epsilon, which is what that setting is
1221                    // for.
1222                    let moved = result.iter().zip(params.iter()).any(|(&after, &before)| {
1223                        (after - before)
1224                            .abs()
1225                            .to_f64()
1226                            .is_some_and(|delta| delta > self.config.numerical_tolerance)
1227                    });
1228                    TestResult {
1229                        passed: moved,
1230                        message: if moved {
1231                            "Basic step test passed".to_string()
1232                        } else {
1233                            format!(
1234                                "step left every parameter within numerical_tolerance {:.3e}, so \
1235                                 no optimization happened",
1236                                self.config.numerical_tolerance
1237                            )
1238                        },
1239                        execution_time: start_time.elapsed(),
1240                        data: HashMap::new(),
1241                    }
1242                } else {
1243                    TestResult {
1244                        passed: false,
1245                        message: "Step result has incorrect dimensions".to_string(),
1246                        execution_time: start_time.elapsed(),
1247                        data: HashMap::new(),
1248                    }
1249                }
1250            }
1251            Err(e) => TestResult {
1252                passed: false,
1253                message: format!("Step function failed: {}", e),
1254                execution_time: start_time.elapsed(),
1255                data: HashMap::new(),
1256            },
1257        }
1258    }
1259
1260    fn test_initialization(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1261        let start_time = Instant::now();
1262
1263        match plugin.initialize(&[10, 20]) {
1264            Ok(()) => TestResult {
1265                passed: true,
1266                message: "Initialization test passed".to_string(),
1267                execution_time: start_time.elapsed(),
1268                data: HashMap::new(),
1269            },
1270            Err(e) => TestResult {
1271                passed: false,
1272                message: format!("Initialization failed: {}", e),
1273                execution_time: start_time.elapsed(),
1274                data: HashMap::new(),
1275            },
1276        }
1277    }
1278
1279    fn test_state_management(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1280        let start_time = Instant::now();
1281
1282        // Test getting and setting state
1283        match (plugin.get_state(), plugin.reset()) {
1284            (Ok(_), Ok(())) => TestResult {
1285                passed: true,
1286                message: "State management test passed".to_string(),
1287                execution_time: start_time.elapsed(),
1288                data: HashMap::new(),
1289            },
1290            (Err(e), _) => TestResult {
1291                passed: false,
1292                message: format!("Failed to get state: {}", e),
1293                execution_time: start_time.elapsed(),
1294                data: HashMap::new(),
1295            },
1296            (_, Err(e)) => TestResult {
1297                passed: false,
1298                message: format!("Failed to reset: {}", e),
1299                execution_time: start_time.elapsed(),
1300                data: HashMap::new(),
1301            },
1302        }
1303    }
1304
1305    fn test_configuration(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1306        let start_time = Instant::now();
1307
1308        let config = plugin.get_config();
1309        match plugin.set_config(config) {
1310            Ok(()) => TestResult {
1311                passed: true,
1312                message: "Configuration test passed".to_string(),
1313                execution_time: start_time.elapsed(),
1314                data: HashMap::new(),
1315            },
1316            Err(e) => TestResult {
1317                passed: false,
1318                message: format!("Configuration test failed: {}", e),
1319                execution_time: start_time.elapsed(),
1320                data: HashMap::new(),
1321            },
1322        }
1323    }
1324}
1325
1326// Similar implementations for other test suites would follow...
1327
1328impl<A: Float + Debug + Send + Sync + 'static> NumericalAccuracyTestSuite<A> {
1329    fn new(config: ValidationConfig) -> Self {
1330        Self {
1331            config,
1332            _phantom: std::marker::PhantomData,
1333        }
1334    }
1335
1336    /// A config roundtrip should preserve the learning rate within
1337    /// `numerical_tolerance` -- catches plugins that silently lose
1338    /// precision (or drop fields) between `get_config`/`set_config`.
1339    fn test_config_roundtrip(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1340        let start_time = Instant::now();
1341        let original = plugin.get_config();
1342        if let Err(e) = plugin.set_config(original.clone()) {
1343            return TestResult {
1344                passed: false,
1345                message: format!("set_config failed during roundtrip: {e}"),
1346                execution_time: start_time.elapsed(),
1347                data: HashMap::new(),
1348            };
1349        }
1350        let after = plugin.get_config();
1351        let diff = (after.learning_rate - original.learning_rate).abs();
1352        let passed = diff <= self.config.numerical_tolerance;
1353        TestResult {
1354            passed,
1355            message: format!(
1356                "learning_rate roundtrip |diff|={diff:.3e} tolerance={:.3e}",
1357                self.config.numerical_tolerance
1358            ),
1359            execution_time: start_time.elapsed(),
1360            data: HashMap::new(),
1361        }
1362    }
1363
1364    /// A well-conditioned finite step must not silently produce NaN/inf.
1365    fn test_step_output_finite(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
1366        let start_time = Instant::now();
1367        const DIM: usize = 6;
1368
1369        if let Err(e) = plugin.initialize(&[DIM]) {
1370            return TestResult {
1371                passed: false,
1372                message: format!("initialize failed before numerical accuracy probe: {e}"),
1373                execution_time: start_time.elapsed(),
1374                data: HashMap::new(),
1375            };
1376        }
1377
1378        let params: Array1<A> = Array1::from_iter(
1379            (0..DIM).map(|i| A::from(1.0 + i as f64 * 0.1).unwrap_or_else(A::one)),
1380        );
1381        let gradients: Array1<A> = Array1::from_iter(
1382            (0..DIM).map(|i| A::from(0.05 - i as f64 * 0.005).unwrap_or_else(A::zero)),
1383        );
1384
1385        match plugin.step(&params, &gradients) {
1386            Ok(result) => {
1387                let all_finite = result.iter().all(|v| v.is_finite());
1388                TestResult {
1389                    passed: all_finite,
1390                    message: if all_finite {
1391                        "step() output is finite for well-conditioned input".to_string()
1392                    } else {
1393                        "step() produced a non-finite value for finite, well-conditioned input"
1394                            .to_string()
1395                    },
1396                    execution_time: start_time.elapsed(),
1397                    data: HashMap::new(),
1398                }
1399            }
1400            Err(e) => TestResult {
1401                passed: false,
1402                message: format!("step() failed: {e}"),
1403                execution_time: start_time.elapsed(),
1404                data: HashMap::new(),
1405            },
1406        }
1407    }
1408}
1409
1410impl<A: Float + Debug + Send + Sync + 'static> ValidationTestSuite<A>
1411    for NumericalAccuracyTestSuite<A>
1412{
1413    fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult {
1414        let start_time = Instant::now();
1415        let test_results = vec![
1416            self.test_config_roundtrip(plugin),
1417            self.test_step_output_finite(plugin),
1418        ];
1419
1420        let passed_tests = test_results.iter().filter(|r| r.passed).count();
1421        let total_tests = test_results.len();
1422
1423        SuiteResult {
1424            suite_name: self.name().to_string(),
1425            test_results,
1426            suite_passed: passed_tests == total_tests,
1427            execution_time: start_time.elapsed(),
1428            summary: TestSummary {
1429                total_tests,
1430                passed_tests,
1431                failed_tests: total_tests - passed_tests,
1432                skipped_tests: 0,
1433                success_rate: passed_tests as f64 / total_tests as f64,
1434            },
1435            verified: true,
1436        }
1437    }
1438
1439    fn name(&self) -> &str {
1440        "Numerical Accuracy Tests"
1441    }
1442
1443    fn description(&self) -> &str {
1444        "Tests numerical precision and accuracy of optimization steps"
1445    }
1446
1447    fn test_count(&self) -> usize {
1448        2
1449    }
1450}
1451
1452// Implementation placeholders for other components...
1453
1454impl<A: Float + Send + Sync> PerformanceBenchmarker<A> {
1455    fn new(config: BenchmarkConfig) -> Self {
1456        Self {
1457            config,
1458            benchmarks: Vec::new(),
1459            baselines: HashMap::new(),
1460        }
1461    }
1462
1463    fn add_benchmark(&mut self, benchmark: Box<dyn PerformanceBenchmark<A>>) {
1464        self.benchmarks.push(benchmark);
1465    }
1466
1467    /// Register a baseline a benchmark's `execution_time` must stay within.
1468    pub fn set_baseline(&mut self, benchmark_name: String, baseline: BenchmarkBaseline) {
1469        self.baselines.insert(benchmark_name, baseline);
1470    }
1471
1472    /// Registered baselines, keyed by benchmark name.
1473    pub fn baselines(&self) -> &HashMap<String, BenchmarkBaseline> {
1474        &self.baselines
1475    }
1476
1477    /// The benchmark configuration in force.
1478    pub fn config(&self) -> &BenchmarkConfig {
1479        &self.config
1480    }
1481
1482    /// Run every registered benchmark `config.runs` times after
1483    /// `config.warmup_iterations` discarded warmup runs, keeping the best score
1484    /// per benchmark, and check each against its registered baseline.
1485    ///
1486    /// Until 0.3.2 this ran each benchmark exactly once and ignored both
1487    /// `config` and `baselines` entirely: `runs`, `warmup_iterations` and every
1488    /// registered baseline were stored and never read, so a benchmark that blew
1489    /// past its declared budget still reported whatever score it computed.
1490    fn run_all_benchmarks(
1491        &mut self,
1492        plugin: &mut dyn OptimizerPlugin<A>,
1493    ) -> Vec<BenchmarkResult<A>> {
1494        let runs = self.config.runs.max(1);
1495        let warmup = self.config.warmup_iterations;
1496        let mut results = Vec::with_capacity(self.benchmarks.len());
1497
1498        for bench in &self.benchmarks {
1499            for _ in 0..warmup {
1500                let _ = bench.run(plugin);
1501            }
1502            let mut best: Option<BenchmarkResult<A>> = None;
1503            for _ in 0..runs {
1504                let candidate = bench.run(plugin);
1505                best = match best {
1506                    Some(current) if current.score >= candidate.score => Some(current),
1507                    _ => Some(candidate),
1508                };
1509            }
1510            let Some(mut result) = best else { continue };
1511
1512            if let Some(baseline) = self.baselines.get(&result.name) {
1513                let measured = result.execution_time.as_secs_f64();
1514                let ceiling = baseline.expected_value * (1.0 + baseline.tolerance / 100.0);
1515                let within = measured <= ceiling;
1516                result
1517                    .metrics
1518                    .insert("baseline_expected".to_string(), baseline.expected_value);
1519                result
1520                    .metrics
1521                    .insert("baseline_ceiling".to_string(), ceiling);
1522                result.metrics.insert(
1523                    "baseline_within_tolerance".to_string(),
1524                    if within { 1.0 } else { 0.0 },
1525                );
1526                if !within {
1527                    // A benchmark that misses its declared budget must not keep
1528                    // a passing score.
1529                    result.score = 0.0;
1530                }
1531            }
1532            results.push(result);
1533        }
1534
1535        results
1536    }
1537}
1538
1539impl<A: Float + Send + Sync> ValidationResults<A> {
1540    fn new() -> Self {
1541        Self {
1542            validation_passed: false,
1543            suite_results: Vec::new(),
1544            compliance_results: Vec::new(),
1545            benchmark_results: Vec::new(),
1546            overall_score: None,
1547            timestamp: std::time::SystemTime::now(),
1548            total_time: Duration::from_secs(0),
1549        }
1550    }
1551}
1552
1553// Default implementations
1554
1555impl Default for ValidationConfig {
1556    fn default() -> Self {
1557        Self {
1558            strict_mode: false,
1559            numerical_tolerance: 1e-10,
1560            performance_tolerance: 20.0,
1561            max_test_duration: Duration::from_secs(300),
1562            check_memory_leaks: true,
1563            check_thread_safety: false,
1564            check_convergence: true,
1565            random_seed: 42,
1566            test_data_sizes: vec![10, 100, 1000],
1567        }
1568    }
1569}
1570
1571// Placeholder implementations for compliance checkers
1572
1573impl ComplianceChecker for ApiComplianceChecker {
1574    fn check_compliance(&self, plugininfo: &PluginInfo) -> ComplianceResult {
1575        let mut violations = Vec::new();
1576        let mut score = 1.0;
1577
1578        if plugininfo.name.trim().is_empty() {
1579            violations.push(ComplianceViolation {
1580                violation_type: ViolationType::ApiViolation,
1581                description: "Plugin name is empty".to_string(),
1582                severity: ViolationSeverity::Critical,
1583                suggested_fix: Some("Provide a non-empty plugin name".to_string()),
1584            });
1585            score -= 0.4;
1586        }
1587
1588        if plugininfo.version.trim().is_empty() {
1589            violations.push(ComplianceViolation {
1590                violation_type: ViolationType::ApiViolation,
1591                description: "Plugin version is empty".to_string(),
1592                severity: ViolationSeverity::High,
1593                suggested_fix: Some("Provide a semantic version string".to_string()),
1594            });
1595            score -= 0.3;
1596        }
1597
1598        if plugininfo.supported_types.is_empty() {
1599            violations.push(ComplianceViolation {
1600                violation_type: ViolationType::ApiViolation,
1601                description: "Plugin declares no supported data types".to_string(),
1602                severity: ViolationSeverity::Medium,
1603                suggested_fix: Some(
1604                    "Declare at least one entry in `supported_types` (e.g. DataType::F64)"
1605                        .to_string(),
1606                ),
1607            });
1608            score -= 0.2;
1609        }
1610
1611        if plugininfo.min_sdk_version.trim().is_empty() {
1612            violations.push(ComplianceViolation {
1613                violation_type: ViolationType::ApiViolation,
1614                description: "Plugin declares no minimum SDK version".to_string(),
1615                severity: ViolationSeverity::Low,
1616                suggested_fix: Some(
1617                    "Set `min_sdk_version` to the SDK version targeted".to_string(),
1618                ),
1619            });
1620            score -= 0.1;
1621        }
1622
1623        ComplianceResult {
1624            compliant: violations.is_empty(),
1625            violations,
1626            warnings: Vec::new(),
1627            compliance_score: score.max(0.0),
1628            verified: true,
1629        }
1630    }
1631
1632    fn name(&self) -> &str {
1633        "API Compliance"
1634    }
1635
1636    fn requirements(&self) -> Vec<ComplianceRequirement> {
1637        vec![
1638            ComplianceRequirement {
1639                id: "api-1".to_string(),
1640                description: "Plugin must declare a non-empty name".to_string(),
1641                mandatory: true,
1642                category: ComplianceCategory::API,
1643            },
1644            ComplianceRequirement {
1645                id: "api-2".to_string(),
1646                description: "Plugin must declare a non-empty version".to_string(),
1647                mandatory: true,
1648                category: ComplianceCategory::API,
1649            },
1650            ComplianceRequirement {
1651                id: "api-3".to_string(),
1652                description: "Plugin must declare at least one supported data type".to_string(),
1653                mandatory: true,
1654                category: ComplianceCategory::API,
1655            },
1656        ]
1657    }
1658}
1659
1660impl ComplianceChecker for SecurityComplianceChecker {
1661    /// Inspects the declarative `PluginInfo` metadata this checker is given
1662    /// (license presence, dependency version bounds). Static/dynamic code
1663    /// inspection (unsafe blocks, filesystem/network access, signature
1664    /// verification) is a different trust boundary handled by
1665    /// `plugin::loader::SecurityManager`/`CodeScanner` at load time, which
1666    /// this checker does not have access to -- it must not claim to have
1667    /// verified what it cannot see.
1668    fn check_compliance(&self, plugininfo: &PluginInfo) -> ComplianceResult {
1669        let mut violations = Vec::new();
1670        let mut score: f64 = 1.0;
1671
1672        if plugininfo.license.trim().is_empty() {
1673            violations.push(ComplianceViolation {
1674                violation_type: ViolationType::SecurityViolation,
1675                description: "Plugin declares no license; provenance cannot be assessed"
1676                    .to_string(),
1677                severity: ViolationSeverity::Medium,
1678                suggested_fix: Some("Declare an SPDX license identifier".to_string()),
1679            });
1680            score -= 0.3;
1681        }
1682
1683        for dep in &plugininfo.dependencies {
1684            let version_req = dep.version.trim();
1685            if version_req.is_empty() || version_req == "*" {
1686                violations.push(ComplianceViolation {
1687                    violation_type: ViolationType::SecurityViolation,
1688                    description: format!(
1689                        "Dependency '{}' has an unbounded version requirement ('{}'); this \
1690                         lets any future release -- including a compromised one -- be pulled \
1691                         in transparently",
1692                        dep.name, dep.version
1693                    ),
1694                    severity: ViolationSeverity::High,
1695                    suggested_fix: Some("Pin dependencies to a bounded version range".to_string()),
1696                });
1697                score -= 0.2;
1698            }
1699        }
1700
1701        let compliant = !violations.iter().any(|v| {
1702            matches!(
1703                v.severity,
1704                ViolationSeverity::Critical | ViolationSeverity::High
1705            )
1706        });
1707
1708        ComplianceResult {
1709            compliant,
1710            violations,
1711            warnings: vec![
1712                "Security compliance here covers declared metadata only; code-level scanning \
1713                 and signature verification happen separately in PluginLoader::SecurityManager"
1714                    .to_string(),
1715            ],
1716            compliance_score: score.max(0.0),
1717            verified: true,
1718        }
1719    }
1720
1721    fn name(&self) -> &str {
1722        "Security Compliance"
1723    }
1724
1725    fn requirements(&self) -> Vec<ComplianceRequirement> {
1726        vec![
1727            ComplianceRequirement {
1728                id: "sec-1".to_string(),
1729                description: "Plugin should declare a license".to_string(),
1730                mandatory: false,
1731                category: ComplianceCategory::Security,
1732            },
1733            ComplianceRequirement {
1734                id: "sec-2".to_string(),
1735                description: "Dependencies must not use unbounded version requirements".to_string(),
1736                mandatory: true,
1737                category: ComplianceCategory::Security,
1738            },
1739        ]
1740    }
1741}
1742
1743impl ComplianceChecker for PerformanceComplianceChecker {
1744    /// `check_compliance` only receives `PluginInfo` metadata -- it has no
1745    /// access to benchmark measurements, so performance conformance is not
1746    /// decidable here. `PerformanceBenchmarker` (see `ThroughputBenchmark`,
1747    /// `LatencyBenchmark`, `MemoryBenchmark`) already contributes real,
1748    /// measured performance to the overall score under its own weight, so
1749    /// this checker reports `Unverified` rather than a second, fabricated
1750    /// opinion.
1751    fn check_compliance(&self, _plugininfo: &PluginInfo) -> ComplianceResult {
1752        ComplianceResult {
1753            compliant: false,
1754            violations: Vec::new(),
1755            warnings: vec![
1756                "Performance compliance is not decidable from PluginInfo alone; see the \
1757                 benchmark suite (ThroughputBenchmark/LatencyBenchmark/MemoryBenchmark) instead"
1758                    .to_string(),
1759            ],
1760            compliance_score: 0.0,
1761            verified: false,
1762        }
1763    }
1764
1765    fn name(&self) -> &str {
1766        "Performance Compliance"
1767    }
1768
1769    fn requirements(&self) -> Vec<ComplianceRequirement> {
1770        vec![ComplianceRequirement {
1771            id: "perf-1".to_string(),
1772            description: "Performance must meet the declared benchmark baseline (see \
1773                           PerformanceBenchmarker)"
1774                .to_string(),
1775            mandatory: false,
1776            category: ComplianceCategory::Performance,
1777        }]
1778    }
1779}
1780
1781impl ComplianceChecker for DocumentationComplianceChecker {
1782    fn check_compliance(&self, plugininfo: &PluginInfo) -> ComplianceResult {
1783        let mut violations = Vec::new();
1784        let mut score = 1.0;
1785
1786        if plugininfo.description.len() < 10 {
1787            violations.push(ComplianceViolation {
1788                violation_type: ViolationType::DocumentationViolation,
1789                description: "Plugin description is too short".to_string(),
1790                severity: ViolationSeverity::Medium,
1791                suggested_fix: Some("Provide a more detailed description".to_string()),
1792            });
1793            score -= 0.2;
1794        }
1795
1796        if plugininfo.author.is_empty() {
1797            violations.push(ComplianceViolation {
1798                violation_type: ViolationType::MissingMetadata,
1799                description: "Author information is missing".to_string(),
1800                severity: ViolationSeverity::Low,
1801                suggested_fix: Some("Add author information".to_string()),
1802            });
1803            score -= 0.1;
1804        }
1805
1806        ComplianceResult {
1807            compliant: violations.is_empty(),
1808            violations,
1809            warnings: Vec::new(),
1810            compliance_score: score.max(0.0),
1811            verified: true,
1812        }
1813    }
1814
1815    fn name(&self) -> &str {
1816        "Documentation Compliance"
1817    }
1818
1819    fn requirements(&self) -> Vec<ComplianceRequirement> {
1820        Vec::new()
1821    }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826    use super::*;
1827
1828    #[test]
1829    fn test_validation_config_default() {
1830        let config = ValidationConfig::default();
1831        assert!(!config.strict_mode);
1832        assert!(config.check_memory_leaks);
1833        assert!(config.check_convergence);
1834    }
1835
1836    #[test]
1837    fn test_validation_framework_creation() {
1838        let config = ValidationConfig::default();
1839        let framework = PluginValidationFramework::<f64>::new(config);
1840        assert!(!framework.test_suites.is_empty());
1841        assert!(!framework.compliance_checkers.is_empty());
1842    }
1843
1844    #[test]
1845    fn test_documentation_compliance_checker() {
1846        let checker = DocumentationComplianceChecker;
1847
1848        let info = PluginInfo {
1849            description: "Short".to_string(),
1850            author: "".to_string(),
1851            ..Default::default()
1852        };
1853
1854        let result = checker.check_compliance(&info);
1855        assert!(!result.compliant);
1856        assert_eq!(result.violations.len(), 2);
1857    }
1858}