use super::*;
use scirs2_core::ndarray::Array1;
#[derive(Debug, Clone)]
struct TestOptimizer<A: Float> {
lr: A,
initialized: bool,
}
impl<A: Float + Debug + Send + Sync + 'static> OptimizerPlugin<A> for TestOptimizer<A> {
fn step(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {
let out: Array1<A> = params
.iter()
.zip(gradients.iter())
.map(|(&p, &g)| p - g * self.lr)
.collect();
Ok(out)
}
fn name(&self) -> &str {
"test-sgd"
}
fn version(&self) -> &str {
"1.0.0"
}
fn plugin_info(&self) -> PluginInfo {
PluginInfo {
name: "test-sgd".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities::default()
}
fn initialize(&mut self, _paramshape: &[usize]) -> Result<()> {
self.initialized = true;
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.initialized = false;
Ok(())
}
fn get_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn set_config(&mut self, _config: OptimizerConfig) -> Result<()> {
Ok(())
}
fn get_state(&self) -> Result<OptimizerState> {
Ok(OptimizerState::default())
}
fn set_state(&mut self, _state: OptimizerState) -> Result<()> {
Ok(())
}
fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<A>> {
Box::new(self.clone())
}
}
#[derive(Debug)]
struct TestFactory;
impl PluginFactoryWrapper for TestFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
Ok(Box::new(TestOptimizer::<f64> {
lr: 0.1,
initialized: false,
}))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "test-sgd".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[derive(Debug)]
struct PanicFactory;
impl PluginFactoryWrapper for PanicFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
panic!("intentional panic inside factory create_f32");
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
panic!("intentional panic inside factory create_f64");
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "panic-plugin".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[derive(Debug, Clone)]
struct PanicOnCloneOptimizer;
impl OptimizerPlugin<f64> for PanicOnCloneOptimizer {
fn step(&mut self, params: &Array1<f64>, _gradients: &Array1<f64>) -> Result<Array1<f64>> {
Ok(params.clone())
}
fn name(&self) -> &str {
"panic-on-clone"
}
fn version(&self) -> &str {
"1.0.0"
}
fn plugin_info(&self) -> PluginInfo {
PluginInfo {
name: "panic-on-clone".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities::default()
}
fn initialize(&mut self, _paramshape: &[usize]) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
fn get_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn set_config(&mut self, _config: OptimizerConfig) -> Result<()> {
Ok(())
}
fn get_state(&self) -> Result<OptimizerState> {
Ok(OptimizerState::default())
}
fn set_state(&mut self, _state: OptimizerState) -> Result<()> {
Ok(())
}
fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<f64>> {
panic!("intentional panic inside clone_plugin");
}
}
#[derive(Debug)]
struct PanicOnCloneFactory;
impl PluginFactoryWrapper for PanicOnCloneFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
Ok(Box::new(PanicOnCloneOptimizer))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "panic-on-clone".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[test]
fn f64_cache_insert_survives_a_panicking_clone_plugin() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(PanicOnCloneFactory)
.expect("registration should succeed");
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = registry.create_optimizer::<f64>("panic-on-clone", OptimizerConfig::default());
std::panic::set_hook(prev_hook);
assert!(
result.is_err(),
"a panicking clone_plugin during cache population must surface as Err, not unwind"
);
registry
.register_plugin(TestFactory)
.expect("registry must still accept registrations after a caught clone panic");
let opt = registry
.create_optimizer::<f64>("test-sgd", OptimizerConfig::default())
.expect("registry must still create optimizers after a caught clone panic");
assert_eq!(opt.name(), "test-sgd");
}
#[derive(Debug)]
struct PanicOnSecondCloneOptimizer {
remaining_successful_clones: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl OptimizerPlugin<f64> for PanicOnSecondCloneOptimizer {
fn step(&mut self, params: &Array1<f64>, _gradients: &Array1<f64>) -> Result<Array1<f64>> {
Ok(params.clone())
}
fn name(&self) -> &str {
"panic-on-second-clone"
}
fn version(&self) -> &str {
"1.0.0"
}
fn plugin_info(&self) -> PluginInfo {
PluginInfo {
name: "panic-on-second-clone".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities::default()
}
fn initialize(&mut self, _paramshape: &[usize]) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
fn get_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn set_config(&mut self, _config: OptimizerConfig) -> Result<()> {
Ok(())
}
fn get_state(&self) -> Result<OptimizerState> {
Ok(OptimizerState::default())
}
fn set_state(&mut self, _state: OptimizerState) -> Result<()> {
Ok(())
}
fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<f64>> {
let previous = self
.remaining_successful_clones
.fetch_update(
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
|n| n.checked_sub(1),
)
.unwrap_or(0);
if previous == 0 {
panic!("intentional panic: no successful clones remaining");
}
Box::new(PanicOnSecondCloneOptimizer {
remaining_successful_clones: self.remaining_successful_clones.clone(),
})
}
}
#[derive(Debug)]
struct PanicOnSecondCloneFactory {
remaining_successful_clones: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl PluginFactoryWrapper for PanicOnSecondCloneFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
Ok(Box::new(PanicOnSecondCloneOptimizer {
remaining_successful_clones: self.remaining_successful_clones.clone(),
}))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "panic-on-second-clone".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[test]
fn f64_cache_hit_panic_evicts_the_poisoned_entry_instead_of_trapping_the_name() {
let registry = PluginRegistry::new(RegistryConfig {
validate_on_registration: false,
..RegistryConfig::default()
});
let remaining = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(1));
registry
.register_plugin(PanicOnSecondCloneFactory {
remaining_successful_clones: remaining.clone(),
})
.expect("registration should succeed");
let cfg = OptimizerConfig::default();
let _first = registry
.create_optimizer::<f64>("panic-on-second-clone", cfg.clone())
.expect(
"first (miss) call must succeed: only its cache-population clone runs, \
and that one is the single successful clone",
);
assert_eq!(
mutex_lock(®istry.cache).len(),
1,
"sanity: the instance was cached after the first call"
);
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let second = registry.create_optimizer::<f64>("panic-on-second-clone", cfg);
std::panic::set_hook(prev_hook);
assert!(
second.is_err(),
"a panicking clone_plugin on a cache hit must surface as Err, not unwind"
);
assert_eq!(
mutex_lock(®istry.cache).len(),
0,
"the poisoned entry must be evicted, not left permanently cached"
);
registry
.register_plugin(TestFactory)
.expect("registry must still accept registrations after the caught hit-path panic");
let opt = registry
.create_optimizer::<f64>("test-sgd", OptimizerConfig::default())
.expect("registry must still create optimizers after the caught hit-path panic");
assert_eq!(opt.name(), "test-sgd");
}
#[test]
fn static_plugin_roundtrip_creates_and_steps() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(TestFactory)
.expect("registration should succeed");
let mut opt = registry
.create_optimizer::<f64>("test-sgd", OptimizerConfig::default())
.expect("f64 optimizer should be creatable");
opt.initialize(&[3]).expect("initialize");
let params = Array1::from(vec![1.0_f64, 2.0, 3.0]);
let grads = Array1::from(vec![0.5_f64, 0.5, 0.5]);
let updated = opt.step(¶ms, &grads).expect("step should succeed");
assert!((updated[0] - 0.95).abs() < 1e-9);
assert!((updated[1] - 1.95).abs() < 1e-9);
assert!((updated[2] - 2.95).abs() < 1e-9);
let opt32 = registry
.create_optimizer::<f32>("test-sgd", OptimizerConfig::default())
.expect("f32 optimizer should be creatable");
assert_eq!(opt32.name(), "test-sgd");
}
#[test]
fn create_optimizer_reliably_updates_usage_statistics() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(TestFactory)
.expect("registration should succeed");
for expected_count in 1..=5usize {
let _ = registry
.create_optimizer::<f64>("test-sgd", OptimizerConfig::default())
.expect("optimizer should be creatable");
let factories = read_lock(®istry.factories);
let registration = factories.get("test-sgd").expect("plugin must still exist");
assert_eq!(
registration.load_count, expected_count,
"load_count must increment exactly once per successful create_optimizer call"
);
assert!(
registration.last_used.is_some(),
"last_used must be set after a successful create_optimizer call"
);
}
}
#[derive(Debug)]
struct GpuTestFactory;
impl PluginFactoryWrapper for GpuTestFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
Ok(Box::new(TestOptimizer::<f64> {
lr: 0.1,
initialized: false,
}))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "gpu-sgd".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities {
gpu_support: true,
..PluginCapabilities::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[test]
fn search_plugins_filters_by_required_capabilities() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(TestFactory) .expect("registration should succeed");
registry
.register_plugin(GpuTestFactory) .expect("registration should succeed");
let all = registry.search_plugins(PluginQuery::default());
assert_eq!(all.total_count, 2, "sanity: both plugins are registered");
let gpu_only = registry.search_plugins(PluginQuery {
required_capabilities: vec!["gpu_support".to_string()],
..PluginQuery::default()
});
assert_eq!(
gpu_only.total_count, 1,
"only the GPU-capable plugin should match"
);
assert_eq!(gpu_only.plugins[0].name, "gpu-sgd");
let unknown = registry.search_plugins(PluginQuery {
required_capabilities: vec!["quantum_teleportation".to_string()],
..PluginQuery::default()
});
assert_eq!(unknown.total_count, 0);
}
#[test]
fn panicking_factory_is_caught_and_registry_survives() {
let config = RegistryConfig {
validate_on_registration: false,
..RegistryConfig::default()
};
let registry = PluginRegistry::new(config);
registry
.register_plugin(PanicFactory)
.expect("registration should succeed");
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = registry.create_optimizer::<f64>("panic-plugin", OptimizerConfig::default());
std::panic::set_hook(prev_hook);
assert!(
result.is_err(),
"a panicking factory must surface as Err, not unwind out of create_optimizer"
);
registry
.register_plugin(TestFactory)
.expect("registry must still accept registrations after a caught panic");
let opt = registry
.create_optimizer::<f64>("test-sgd", OptimizerConfig::default())
.expect("registry must still create optimizers after a caught panic");
assert_eq!(opt.name(), "test-sgd");
}
#[test]
fn lock_helpers_recover_from_poisoning() {
use std::sync::{Arc, Mutex, RwLock};
let rw = Arc::new(RwLock::new(5_i32));
let rw_clone = Arc::clone(&rw);
let _ = std::thread::spawn(move || {
let _guard = rw_clone.write().unwrap_or_else(|e| e.into_inner());
panic!("poison the rwlock");
})
.join();
assert!(rw.read().is_err(), "precondition: the RwLock is poisoned");
assert_eq!(*read_lock(&rw), 5);
*write_lock(&rw) = 7;
assert_eq!(*read_lock(&rw), 7);
let mx = Arc::new(Mutex::new(1_i32));
let mx_clone = Arc::clone(&mx);
let _ = std::thread::spawn(move || {
let _guard = mx_clone.lock().unwrap_or_else(|e| e.into_inner());
panic!("poison the mutex");
})
.join();
assert!(mx.lock().is_err(), "precondition: the Mutex is poisoned");
*mutex_lock(&mx) += 10;
assert_eq!(*mutex_lock(&mx), 11);
}
#[derive(Debug)]
struct NamedTestFactory {
plugin_name: &'static str,
}
impl PluginFactoryWrapper for NamedTestFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
Ok(Box::new(TestOptimizer::<f64> {
lr: 0.1,
initialized: false,
}))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: self.plugin_name.to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[derive(Debug)]
struct CountingFactory {
create_f64_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl PluginFactoryWrapper for CountingFactory {
fn create_f32(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>> {
Ok(Box::new(TestOptimizer::<f32> {
lr: 0.1,
initialized: false,
}))
}
fn create_f64(&self, _config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>> {
self.create_f64_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Box::new(TestOptimizer::<f64> {
lr: 0.1,
initialized: false,
}))
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: "counting-sgd".to_string(),
version: "1.0.0".to_string(),
..PluginInfo::default()
}
}
fn validate_config(&self, _config: &OptimizerConfig) -> Result<()> {
Ok(())
}
fn default_config(&self) -> OptimizerConfig {
OptimizerConfig::default()
}
fn config_schema(&self) -> ConfigSchema {
ConfigSchema {
fields: HashMap::new(),
required_fields: Vec::new(),
version: "1.0.0".to_string(),
}
}
fn supports_type(&self, _datatype: &DataType) -> bool {
true
}
}
#[test]
fn f64_cache_hit_avoids_recreation_and_bumps_stats() {
let create_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let registry = PluginRegistry::new(RegistryConfig {
validate_on_registration: false,
..RegistryConfig::default()
});
registry
.register_plugin(CountingFactory {
create_f64_calls: create_calls.clone(),
})
.expect("registration should succeed");
let cfg = OptimizerConfig::default();
let _first = registry
.create_optimizer::<f64>("counting-sgd", cfg.clone())
.expect("first create should succeed");
assert_eq!(
create_calls.load(std::sync::atomic::Ordering::SeqCst),
1,
"the first call for a name must reach the factory"
);
let after_first = registry.get_cache_stats();
assert_eq!(
after_first.misses, 1,
"first call for a name is always a miss"
);
assert_eq!(after_first.hits, 0);
let _second = registry
.create_optimizer::<f64>("counting-sgd", cfg)
.expect("second create should succeed");
assert_eq!(
create_calls.load(std::sync::atomic::Ordering::SeqCst),
1,
"a cache hit must NOT re-invoke the factory's create_f64 -- this is the \
actual 'avoids recreation' claim, checked independently of the cache's \
own hit/miss counters"
);
let after_second = registry.get_cache_stats();
assert_eq!(
after_second.hits, 1,
"same name + identical config must be a cache hit"
);
assert_eq!(after_second.misses, 1, "miss count must not grow on a hit");
}
#[test]
fn f64_cache_treats_a_config_change_as_a_miss_not_a_stale_hit() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(TestFactory)
.expect("registration should succeed");
let cfg_a = OptimizerConfig {
learning_rate: 0.01,
..OptimizerConfig::default()
};
let cfg_b = OptimizerConfig {
learning_rate: 0.02,
..OptimizerConfig::default()
};
let _ = registry
.create_optimizer::<f64>("test-sgd", cfg_a)
.expect("first create should succeed");
let _ = registry
.create_optimizer::<f64>("test-sgd", cfg_b)
.expect("second create with a different config should succeed");
let stats = registry.get_cache_stats();
assert_eq!(
stats.misses, 2,
"a config change for the same name must never be served from a stale cache entry"
);
assert_eq!(stats.hits, 0);
}
#[test]
fn f64_caching_disabled_bypasses_the_cache_entirely() {
let registry = PluginRegistry::new(RegistryConfig {
enable_caching: false,
..RegistryConfig::default()
});
registry
.register_plugin(TestFactory)
.expect("registration should succeed");
let cfg = OptimizerConfig::default();
let _ = registry
.create_optimizer::<f64>("test-sgd", cfg.clone())
.expect("first create should succeed");
let _ = registry
.create_optimizer::<f64>("test-sgd", cfg)
.expect("second create should succeed");
let stats = registry.get_cache_stats();
assert_eq!(stats.hits, 0);
assert_eq!(
stats.misses, 0,
"PluginCache must not be touched at all when enable_caching is false"
);
assert_eq!(mutex_lock(®istry.cache).len(), 0);
}
#[test]
fn f32_creation_never_touches_the_f64_only_cache() {
let registry = PluginRegistry::new(RegistryConfig::default());
registry
.register_plugin(TestFactory)
.expect("registration should succeed");
let cfg = OptimizerConfig::default();
let _ = registry
.create_optimizer::<f32>("test-sgd", cfg.clone())
.expect("first create should succeed");
let _ = registry
.create_optimizer::<f32>("test-sgd", cfg)
.expect("second create should succeed");
let stats = registry.get_cache_stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(mutex_lock(®istry.cache).len(), 0);
}
#[test]
fn f64_cache_evicts_lru_entry_once_max_size_is_exceeded() {
let registry = PluginRegistry::new(RegistryConfig {
max_cache_size: 2,
..RegistryConfig::default()
});
registry
.register_plugin(NamedTestFactory {
plugin_name: "cache-a",
})
.expect("register a");
registry
.register_plugin(NamedTestFactory {
plugin_name: "cache-b",
})
.expect("register b");
registry
.register_plugin(NamedTestFactory {
plugin_name: "cache-c",
})
.expect("register c");
let cfg = OptimizerConfig::default();
let _ = registry
.create_optimizer::<f64>("cache-a", cfg.clone())
.expect("create a");
let _ = registry
.create_optimizer::<f64>("cache-b", cfg.clone())
.expect("create b");
assert_eq!(mutex_lock(®istry.cache).len(), 2);
let _ = registry
.create_optimizer::<f64>("cache-c", cfg.clone())
.expect("create c");
assert_eq!(
mutex_lock(®istry.cache).len(),
2,
"cache must never exceed max_cache_size"
);
assert_eq!(registry.get_cache_stats().evictions, 1);
let misses_before = registry.get_cache_stats().misses;
let _ = registry
.create_optimizer::<f64>("cache-a", cfg)
.expect("recreate a after eviction");
assert_eq!(
registry.get_cache_stats().misses,
misses_before + 1,
"cache-a must have actually been evicted from `instances`, not just counted as evicted"
);
}