use std::sync::{Arc, Weak};
use crate::db::{
analyze_file, file_scopes, infer_function, infer_scope, AnalyzeOutput, FunctionInferenceResult,
MirDatabase, ScopeInferenceResult,
};
use crate::{AnalysisSession, PhpVersion};
const LRU_CAP: usize = 4096;
const STORM: usize = LRU_CAP + 300;
fn storm_source(n: usize) -> String {
let mut s = String::from("<?php\n");
for i in 0..n {
s.push_str(&format!(
"function f{i}(int $x): int {{ return $x + 1; }}\n"
));
}
s
}
fn storm_session() -> (AnalysisSession, Arc<str>) {
let mut session = AnalysisSession::new(PhpVersion::LATEST);
let path: Arc<str> = Arc::from("/memo_bounds/storm.php");
session.ingest_file(path.clone(), Arc::from(storm_source(STORM)));
(session, path)
}
fn alive_count<T>(weaks: &[Weak<T>]) -> usize {
weaks.iter().filter(|w| w.upgrade().is_some()).count()
}
#[test]
fn infer_function_memos_bounded_under_rename_storm() {
let (mut session, path) = storm_session();
let weaks: Vec<Weak<FunctionInferenceResult>> = {
let db = session.snapshot_db();
let file = db.lookup_source_file(path.as_ref()).unwrap();
(0..STORM)
.map(|i| {
let result = infer_function(&db, file, Arc::from(format!("f{i}")))
.clone()
.unwrap_or_else(|| panic!("f{i} not found"));
Arc::downgrade(&result)
})
.collect()
};
assert_eq!(alive_count(&weaks), STORM, "memos live before eviction");
session.set_file_text(Arc::from("/memo_bounds/other.php"), Arc::from("<?php\n"));
let alive = alive_count(&weaks);
assert!(
alive <= LRU_CAP,
"infer_function memo table not bounded: {alive} values alive after \
{STORM} distinct keys (lru = {LRU_CAP})"
);
assert!(alive > 0, "eviction dropped everything — lru misconfigured");
}
#[test]
fn analyze_file_memos_bounded_under_file_storm() {
const FILE_CAP: usize = 16384;
const FILE_STORM: usize = FILE_CAP + 300;
let mut session = AnalysisSession::new(PhpVersion::LATEST);
let paths: Vec<Arc<str>> = (0..FILE_STORM)
.map(|i| Arc::<str>::from(format!("/memo_bounds/files/f{i}.php")))
.collect();
for (i, path) in paths.iter().enumerate() {
session.set_file_text(
path.clone(),
Arc::from(format!(
"<?php\nfunction ff{i}(int $x): int {{ return $x + {i}; }}\n"
)),
);
}
let weaks: Vec<Weak<AnalyzeOutput>> = {
let db = session.snapshot_db();
paths
.iter()
.map(|path| {
let file = db.lookup_source_file(path.as_ref()).unwrap();
Arc::downgrade(analyze_file(&db, file))
})
.collect()
};
assert_eq!(
alive_count(&weaks),
FILE_STORM,
"memos live before eviction"
);
session.set_file_text(Arc::from("/memo_bounds/other.php"), Arc::from("<?php\n"));
let alive = alive_count(&weaks);
assert!(
alive <= FILE_CAP,
"analyze_file memo table not bounded: {alive} values alive after \
{FILE_STORM} distinct files (lru = {FILE_CAP})"
);
assert!(alive > 0, "eviction dropped everything — lru misconfigured");
}
#[test]
fn infer_scope_memos_bounded_under_rename_storm() {
let (mut session, path) = storm_session();
let weaks: Vec<Weak<ScopeInferenceResult>> = {
let db = session.snapshot_db();
let file = db.lookup_source_file(path.as_ref()).unwrap();
let scopes = file_scopes(&db, file);
assert!(scopes.len() > LRU_CAP, "fixture must overflow the lru cap");
scopes
.iter()
.map(|key| Arc::downgrade(infer_scope(&db, file, key.clone())))
.collect()
};
assert_eq!(
alive_count(&weaks),
weaks.len(),
"memos live before eviction"
);
session.set_file_text(Arc::from("/memo_bounds/other.php"), Arc::from("<?php\n"));
let alive = alive_count(&weaks);
assert!(
alive <= LRU_CAP,
"infer_scope memo table not bounded: {alive} values alive after \
{} distinct keys (lru = {LRU_CAP})",
weaks.len()
);
assert!(alive > 0, "eviction dropped everything — lru misconfigured");
}