use super::*;
use expect_test::expect;
use serde_json::json;
fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let from = entry.path();
let to = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir_all(&from, &to)?;
} else {
std::fs::copy(&from, &to)?;
}
}
Ok(())
}
fn fixture_path(name: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
#[tokio::test]
async fn warm_start_serves_symbols_correctly() {
let workspace = tempfile::tempdir().expect("workspace tempdir");
let cache_dir = tempfile::tempdir().expect("cache tempdir");
copy_dir_all(&fixture_path("psr4-mini"), workspace.path()).expect("copy fixture");
let opts = json!({
"cachePath": cache_dir.path().to_str().unwrap(),
"diagnostics": {"enabled": false},
});
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts.clone()).await;
s.wait_for_index_ready().await;
let syms = s.snapshot_workspace_symbols("User").await;
expect![[r#"Class User @ src/Model/User.php:4"#]].assert_eq(&syms);
}
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts).await;
s.wait_for_index_ready().await;
let syms = s.snapshot_workspace_symbols("User").await;
expect![[r#"Class User @ src/Model/User.php:4"#]].assert_eq(&syms);
}
}
#[tokio::test]
async fn warm_start_detects_changed_file_content() {
let workspace = tempfile::tempdir().expect("workspace tempdir");
let cache_dir = tempfile::tempdir().expect("cache tempdir");
copy_dir_all(&fixture_path("psr4-mini"), workspace.path()).expect("copy fixture");
let opts = json!({
"cachePath": cache_dir.path().to_str().unwrap(),
"diagnostics": {"enabled": false},
});
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts.clone()).await;
s.wait_for_index_ready().await;
let syms = s.snapshot_workspace_symbols("Widget").await;
expect![[r#"<no symbols>"#]].assert_eq(&syms);
}
let user_php = workspace.path().join("src/Model/User.php");
std::fs::write(
&user_php,
"<?php\nnamespace App\\Model;\n\nclass Widget {}\n",
)
.expect("overwrite User.php");
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts).await;
s.wait_for_index_ready().await;
let syms = s.snapshot_workspace_symbols("Widget").await;
expect![[r#"Class Widget @ src/Model/User.php:3"#]].assert_eq(&syms);
}
}
#[tokio::test]
async fn did_save_writes_a_cache_entry_findable_by_key() {
let workspace = tempfile::tempdir().expect("workspace tempdir");
let cache_dir = tempfile::tempdir().expect("cache tempdir");
copy_dir_all(&fixture_path("psr4-mini"), workspace.path()).expect("copy fixture");
let opts = json!({
"cachePath": cache_dir.path().to_str().unwrap(),
"diagnostics": {"enabled": false},
});
let mut s = TestServer::with_root_and_options(workspace.path(), opts).await;
s.wait_for_index_ready().await;
let user_content = "<?php\nnamespace App\\Model;\n\nclass User { public int $id; }\n";
let user_php = workspace.path().join("src/Model/User.php");
std::fs::write(&user_php, user_content).expect("write User.php");
let uri = s.uri("src/Model/User.php");
s.client()
.notify(
"textDocument/didSave",
serde_json::json!({ "textDocument": { "uri": uri } }),
)
.await;
let cache = php_lsp::cache::WorkspaceCache::with_dir(cache_dir.path().to_path_buf());
let key = php_lsp::cache::WorkspaceCache::key_for(&uri, user_content);
let mut cached: Option<php_lsp::file_index::FileIndex> = None;
for _ in 0..100 {
cached = cache.read(&key);
if cached.is_some() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
assert!(
cached.is_some(),
"did_save should have written a FileIndex cache entry keyed by (uri, saved content)"
);
let classes: Vec<&str> = cached
.as_ref()
.unwrap()
.classes
.iter()
.map(|c| c.name.as_ref())
.collect();
assert_eq!(
classes,
vec!["User"],
"cached FileIndex should reflect the saved content, not the original fixture"
);
}
#[tokio::test]
async fn did_save_cache_is_found_by_subsequent_scan() {
let workspace = tempfile::tempdir().expect("workspace tempdir");
let cache_dir = tempfile::tempdir().expect("cache tempdir");
copy_dir_all(&fixture_path("psr4-mini"), workspace.path()).expect("copy fixture");
let opts = json!({
"cachePath": cache_dir.path().to_str().unwrap(),
"diagnostics": {"enabled": false},
});
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts.clone()).await;
s.wait_for_index_ready().await;
let user_content = "<?php\nnamespace App\\Model;\n\nclass User { public int $id; }\n";
let user_php = workspace.path().join("src/Model/User.php");
std::fs::write(&user_php, user_content).expect("write User.php");
let uri = s.uri("src/Model/User.php");
s.client()
.notify(
"textDocument/didSave",
serde_json::json!({ "textDocument": { "uri": uri } }),
)
.await;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts).await;
s.wait_for_index_ready().await;
let syms = s.snapshot_workspace_symbols("User").await;
expect![[r#"Class User @ src/Model/User.php:3"#]].assert_eq(&syms);
}
}
#[tokio::test]
async fn warm_start_replays_reference_postings_from_first_session() {
let workspace = tempfile::tempdir().expect("workspace tempdir");
let cache_dir = tempfile::tempdir().expect("cache tempdir");
let widget = "<?php\nclass Widget {\n public function spin(): void {}\n}\n";
let caller = "<?php\n$w = new Widget();\n$w->spin();\n";
std::fs::write(workspace.path().join("widget.php"), widget).expect("write widget.php");
std::fs::write(workspace.path().join("caller.php"), caller).expect("write caller.php");
let opts = |warm_analysis: bool| {
json!({
"cachePath": cache_dir.path().to_str().unwrap(),
"diagnostics": {"enabled": false},
"phpVersion": "8.3",
"warmAnalysis": warm_analysis,
})
};
let caller_uri = {
let mut s = TestServer::with_root_and_options(workspace.path(), opts(true)).await;
s.wait_for_index_ready().await;
assert!(
s.wait_for_warm_sweeps(1).await,
"warm sweep did not complete"
);
s.uri("caller.php")
};
let session_dir = cache_dir.path().join("session");
let php_v = "8.3"
.parse::<mir_analyzer::PhpVersion>()
.expect("valid version")
.cache_byte();
let mir_cache = mir_analyzer::cache::AnalysisCache::open(&session_dir, php_v, 0);
let (_, ref_locs) = mir_cache
.get(&caller_uri, &mir_analyzer::cache::hash_content(caller))
.expect("warm sweep must persist an AnalysisCache entry for caller.php");
assert!(
!ref_locs.is_empty(),
"persisted entry must carry caller.php's reference postings"
);
{
let mut s = TestServer::with_root_and_options(workspace.path(), opts(false)).await;
s.wait_for_index_ready().await;
s.open("widget.php", widget).await;
let resp = s.references("widget.php", 2, 20, false).await;
let out = render_locations(&resp, &s.uri(""));
expect![[r#"caller.php:2:4-2:8"#]].assert_eq(&out);
}
}