use std::sync::{Arc, Weak};
use crate::db::{
analyze_file, file_scopes, infer_function, infer_scope, AnalyzeOutput, FunctionInferenceResult,
MirDatabase,
};
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 analyze_file_revalidates_after_its_scopes_are_evicted() {
let mut session = AnalysisSession::new(PhpVersion::LATEST);
let mut small = String::from("<?php\n");
for i in 0..20 {
small.push_str(&format!(
"function s{i}($x) {{ return s{}($x); }}\n",
(i + 1) % 20
));
}
let small_path: Arc<str> = Arc::from("/memo_bounds/small.php");
let big_path: Arc<str> = Arc::from("/memo_bounds/big.php");
session.ingest_file(small_path.clone(), Arc::from(small));
session.ingest_file(big_path.clone(), Arc::from(storm_source(STORM)));
let analyze = |session: &AnalysisSession, path: &Arc<str>| {
session.snapshot().read(|db| {
let file = db.lookup_source_file(path.as_ref()).unwrap();
let scopes = file_scopes(db, file);
for key in scopes.iter() {
infer_scope(db, file, key.clone());
}
analyze_file(db, file).clone()
})
};
analyze(&session, &small_path).expect("small pass");
analyze(&session, &big_path).expect("big pass");
session.set_file_text(Arc::from("/memo_bounds/other.php"), Arc::from("<?php\n"));
analyze(&session, &small_path).expect("small pass after eviction");
}