use std::sync::Arc;
use super::super::Runtime;
use super::super::contract::ContractRuntimeInterface;
use super::super::runtime::RuntimeConfig;
use super::{TestSetup, get_test_module, setup_test_contract};
use freenet_stdlib::prelude::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_module_cache_tracks_bytes_and_hits() -> Result<(), Box<dyn std::error::Error>> {
let TestSetup {
contract_store,
delegate_store,
secrets_store,
contract_key: contract_key_1,
temp_dir,
} = setup_test_contract("test_contract_1").await?;
let config = RuntimeConfig {
module_cache_budget_bytes: 64 * 1024 * 1024,
..Default::default()
};
let mut runtime =
Runtime::build_with_config(contract_store, delegate_store, secrets_store, false, config)
.unwrap();
let state = WrappedState::new(vec![]);
let _result = runtime.validate_state(
&contract_key_1,
&Parameters::from([].as_ref()),
&state,
&Default::default(),
);
{
let cache = runtime.contract_modules.lock().unwrap();
assert_eq!(cache.len(), 1, "Cache should have 1 entry after first load");
assert!(
cache.total_bytes() > 0,
"Cache should track a non-zero compiled size for the loaded module"
);
assert!(
cache.total_bytes() <= cache.budget_bytes(),
"tracked bytes {} must stay within budget {}",
cache.total_bytes(),
cache.budget_bytes()
);
}
let _result = runtime.validate_state(
&contract_key_1,
&Parameters::from([].as_ref()),
&state,
&Default::default(),
);
assert_eq!(
runtime.contract_modules.lock().unwrap().len(),
1,
"Cache should still have 1 entry (cache hit)"
);
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_module_cache_tiny_budget_still_runs() -> Result<(), Box<dyn std::error::Error>> {
let TestSetup {
contract_store,
delegate_store,
secrets_store,
contract_key,
temp_dir,
} = setup_test_contract("test_contract_1").await?;
let config = RuntimeConfig {
module_cache_budget_bytes: 1,
..Default::default()
};
let mut runtime =
Runtime::build_with_config(contract_store, delegate_store, secrets_store, false, config)
.unwrap();
let state = WrappedState::new(vec![1, 2, 3, 4]);
let result = runtime.validate_state(
&contract_key,
&Parameters::from([].as_ref()),
&state,
&Default::default(),
);
assert!(
matches!(result, Ok(ValidateResult::Valid)),
"contract must still compile+execute under a tiny budget: {result:?}"
);
let cache = runtime.contract_modules.lock().unwrap();
assert_eq!(cache.len(), 1, "single oversized entry retained");
assert!(
cache.total_bytes() > cache.budget_bytes(),
"the lone resident module legitimately exceeds the 1-byte budget"
);
std::mem::drop(temp_dir);
Ok(())
}
fn distinct_keys_same_code(
contract_store: &mut super::super::ContractStore,
code: &[u8],
count: usize,
) -> Vec<ContractKey> {
let mut keys = Vec::with_capacity(count);
for i in 0..count {
let params = Parameters::from(format!("param-{i}").into_bytes());
let wrapped = WrappedContract::new(
Arc::new(ContractCode::from(code.to_vec())),
params.clone().into_owned(),
);
let key = *wrapped.key();
let container = ContractContainer::Wasm(ContractWasmAPIVersion::V1(wrapped));
contract_store
.store_contract(container)
.expect("store distinct-param contract");
contract_store
.ensure_key_indexed(&key)
.expect("index distinct-param key");
keys.push(key);
}
keys
}
#[tokio::test(flavor = "multi_thread")]
async fn test_module_cache_evicts_by_bytes_not_count() -> Result<(), Box<dyn std::error::Error>> {
let code = get_test_module("test_contract_1")?;
let TestSetup {
mut contract_store,
delegate_store,
secrets_store,
temp_dir,
..
} = setup_test_contract("test_contract_1").await?;
let keys = distinct_keys_same_code(&mut contract_store, &code, 8);
let probe_config = RuntimeConfig {
module_cache_budget_bytes: 512 * 1024 * 1024,
..Default::default()
};
let mut probe_runtime = Runtime::build_with_config(
contract_store,
delegate_store,
secrets_store,
false,
probe_config,
)
.unwrap();
let valid_state = WrappedState::new(vec![1, 2, 3, 4]);
let params0 = Parameters::from("param-0".as_bytes().to_vec());
drop(probe_runtime.validate_state(&keys[0], ¶ms0, &valid_state, &Default::default()));
let per_module = {
let cache = probe_runtime.contract_modules.lock().unwrap();
assert_eq!(cache.len(), 1);
cache.total_bytes()
};
assert!(per_module > 0, "compiled module size must be measurable");
let budget = per_module * 2 + per_module / 2; {
let mut cache = probe_runtime.contract_modules.lock().unwrap();
*cache = super::super::ModuleCache::new(budget);
}
for (i, key) in keys.iter().enumerate() {
let params = Parameters::from(format!("param-{i}").into_bytes());
drop(probe_runtime.validate_state(key, ¶ms, &valid_state, &Default::default()));
let cache = probe_runtime.contract_modules.lock().unwrap();
assert!(
cache.total_bytes() <= cache.budget_bytes(),
"after loading {} modules, total_bytes {} exceeded budget {}",
i + 1,
cache.total_bytes(),
cache.budget_bytes()
);
}
let cache = probe_runtime.contract_modules.lock().unwrap();
assert!(
cache.len() <= 3,
"byte budget should keep ~2-3 modules resident, got {}",
cache.len()
);
assert!(
cache.len() < 8,
"eviction must have dropped some of the 8 loaded modules"
);
assert!(
cache.total_bytes() <= cache.budget_bytes(),
"final total_bytes {} must be within budget {}",
cache.total_bytes(),
cache.budget_bytes()
);
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_compiled_module_size_is_in_expected_range() -> Result<(), Box<dyn std::error::Error>>
{
let TestSetup {
contract_store,
delegate_store,
secrets_store,
contract_key,
temp_dir,
} = setup_test_contract("test_contract_1").await?;
let mut runtime = Runtime::build_with_config(
contract_store,
delegate_store,
secrets_store,
false,
RuntimeConfig::default(),
)
.unwrap();
let state = WrappedState::new(vec![1, 2, 3, 4]);
drop(runtime.validate_state(
&contract_key,
&Parameters::from([].as_ref()),
&state,
&Default::default(),
));
let measured = runtime.contract_modules.lock().unwrap().total_bytes();
let default_budget = super::super::default_module_cache_budget_bytes();
println!(
"MEASURED compiled module size for test_contract_1: {measured} bytes \
({:.2} MiB); default module cache budget = {} bytes ({} MiB) \
holds ~{} such modules",
measured as f64 / (1024.0 * 1024.0),
default_budget,
default_budget / (1024 * 1024),
if measured > 0 {
default_budget / measured
} else {
0
},
);
assert!(
(10 * 1024..=16 * 1024 * 1024).contains(&measured),
"compiled module size {measured} bytes outside expected 10KiB..16MiB range; \
revisit the module cache budget if the toolchain changed"
);
std::mem::drop(temp_dir);
Ok(())
}