mod support;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use scryer_db::ScryerDb;
use scryer_engine::IndexOptions;
use support::{
fingerprint::{Fingerprint, fingerprint},
fixture::{index_dir_keeping_service, index_files},
};
const MODULES: usize = 12;
fn fixture() -> Vec<(String, String)> {
let mut files = Vec::new();
for i in 0..MODULES {
let next = (i + 1) % MODULES;
let mut src = format!(
"pub fn f{i:02}() -> u32 {{\n let _ = crate::m{next:02}::f{next:02}();\n helper() + shared()\n}}\n"
);
if i % 6 == 0 {
src.push_str("pub fn helper() -> u32 {\n 1\n}\n");
}
if i == 3 {
src.push_str("pub use crate::m11::f11 as reexported;\n");
}
if i == 11 {
src.push_str("pub fn shared() -> u32 {\n f00()\n}\n");
}
files.push((format!("src/m{i:02}.rs"), src));
}
files.push((
"src/lib.rs".into(),
(0..MODULES)
.map(|i| format!("pub mod m{i:02};\n"))
.collect::<String>(),
));
files.push((
"a.py".into(),
"def first():\n return zed()\n\n\ndef again():\n return first()\n".into(),
));
files.push(("z.py".into(), "def zed():\n return 1\n".into()));
files.push((
"a.ts".into(),
"export function first() { return zed(); }\n".into(),
));
files.push((
"z.ts".into(),
"export function zed() { return 1; }\n".into(),
));
files
}
async fn index(options: IndexOptions) -> anyhow::Result<Fingerprint> {
let files = fixture();
let borrowed: Vec<(&str, &str)> = files
.iter()
.map(|(p, c)| (p.as_str(), c.as_str()))
.collect();
let (db, project) = index_files(&borrowed, options).await?;
fingerprint(&db, project).await
}
#[tokio::test]
async fn indexing_does_not_depend_on_batching_or_file_order() -> anyhow::Result<()> {
let reference = index(IndexOptions::default()).await?;
assert_eq!(
reference.references.len(),
41,
"{:#?}",
reference.references
);
assert_eq!(reference.edges.len(), 41, "{:#?}", reference.edges);
for cap in [1, 7, 500] {
for seed in [None, Some(1), Some(2)] {
let got = index(IndexOptions {
writer_batch_cap: cap,
file_order_seed: seed,
..IndexOptions::default()
})
.await?;
let missing: Vec<_> = reference
.references
.iter()
.filter(|r| !got.references.contains(r))
.collect();
assert_eq!(
got.digest(),
reference.digest(),
"cap={cap} seed={seed:?}: {} refs vs {}, {} edges vs {}; missing refs: {missing:#?}",
got.references.len(),
reference.references.len(),
got.edges.len(),
reference.edges.len(),
);
}
}
Ok(())
}
type Edit = (&'static str, Option<String>);
fn tree() -> BTreeMap<String, String> {
let mut files: BTreeMap<String, String> = fixture().into_iter().collect();
files.insert(
"src/caller.rs".into(),
"pub fn calls_later() {\n later_defined();\n}\n".into(),
);
files
}
fn edits(files: &BTreeMap<String, String>) -> Vec<Edit> {
vec![
(
"src/m03.rs",
Some(
"// header\n// more header\n".to_string()
+ &files["src/m03.rs"]
.replace("helper() + shared()", "helper() + shared() + 0"),
),
),
(
"src/m11.rs",
Some(files["src/m11.rs"].replace("pub fn shared() -> u32 {\n f00()\n}\n", "")),
),
(
"src/later.rs",
Some("pub fn later_defined() -> u32 {\n 1\n}\n".into()),
),
(
"src/m07.rs",
Some(
files["src/m07.rs"].clone() + "pub fn extra() -> u32 {\n helper() + f08()\n}\n",
),
),
("z.py", None),
]
}
fn apply(root: &Path, edit: &Edit) -> anyhow::Result<()> {
let path = root.join(edit.0);
match &edit.1 {
Some(content) => fs::write(path, content)?,
None => fs::remove_file(path)?,
}
Ok(())
}
async fn indexed_then_edited(
files: &BTreeMap<String, String>,
edits: &[Edit],
incremental_by_file: bool,
) -> anyhow::Result<Fingerprint> {
let temp = tempfile::tempdir()?;
for (path, content) in files {
let full = temp.path().join(path);
fs::create_dir_all(full.parent().unwrap())?;
fs::write(full, content)?;
}
let db = ScryerDb::new_in_memory().await?;
let (project, service, _) =
index_dir_keeping_service(&db, temp.path(), IndexOptions::default()).await?;
for edit in edits {
apply(temp.path(), edit)?;
if incremental_by_file {
if edit.1.is_some() {
service
.index_file(project, temp.path(), Path::new(edit.0))
.await?;
} else {
service.remove_file(project, Path::new(edit.0)).await?;
}
}
}
if !incremental_by_file {
service.index_project(project, temp.path()).await?;
}
fingerprint(&db, project).await
}
fn without_via(references: &[String]) -> Vec<String> {
references
.iter()
.map(|r| match (r.find('['), r.find(']')) {
(Some(open), Some(close)) => format!("{}{}", &r[..open], &r[close + 1..]),
_ => r.clone(),
})
.collect()
}
#[tokio::test]
async fn incremental_indexing_matches_a_fresh_index_of_the_edited_tree() -> anyhow::Result<()> {
let files = tree();
let edits = edits(&files);
let mut edited = files.clone();
for (path, content) in &edits {
match content {
Some(content) => edited.insert(path.to_string(), content.clone()),
None => edited.remove(*path),
};
}
let borrowed: Vec<(&str, &str)> = edited
.iter()
.map(|(p, c)| (p.as_str(), c.as_str()))
.collect();
let (db, project) = index_files(&borrowed, IndexOptions::default()).await?;
let fresh = fingerprint(&db, project).await?;
assert!(
fresh
.references
.iter()
.any(|r| r.ends_with("::crate::later::later_defined")),
"{:#?}",
fresh.references
);
assert!(!fresh.unresolved.is_empty(), "{:#?}", fresh.unresolved);
for by_file in [false, true] {
let got = indexed_then_edited(&files, &edits, by_file).await?;
assert_eq!(got.symbol_rows, fresh.symbol_rows, "by_file={by_file}");
assert_eq!(got.unresolved, fresh.unresolved, "by_file={by_file}");
assert_eq!(
without_via(&got.references),
without_via(&fresh.references),
"by_file={by_file}"
);
assert_eq!(got.edges, fresh.edges, "by_file={by_file}");
}
Ok(())
}