Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions crates/codegraph-cli/tests/full_sync_scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! A full `sync` keeps exactly the files `index` keeps.
//!
//! The watcher deliberately prunes some directories the scan indexes: the
//! watch-only defaults such as `.cache`, and every top-level `.codegraph-*`
//! directory. A full sync used to judge scope with that stricter watch policy,
//! so `sync` deleted files `index` had just indexed, and `status` then reported
//! them as pending additions — `sync` no longer equalled `index --force`.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

struct TestDir {
path: PathBuf,
}

impl TestDir {
fn new(label: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"codegraph-cli-full-sync-scope-{label}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&path).unwrap();
Self { path }
}
}

impl Drop for TestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}

fn cli(args: &[&str]) -> String {
let output = Command::new(env!("CARGO_BIN_EXE_codegraph"))
.args(args)
.env("CODEGRAPH_NO_DAEMON", "1")
.output()
.expect("run codegraph binary");
assert!(
output.status.success(),
"codegraph {args:?} failed: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}

fn touch(root: &Path, relative: &str, contents: &str) {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}

fn indexed_paths(project: &str) -> Vec<String> {
let value: serde_json::Value =
serde_json::from_str(&cli(&["files", "-p", project, "--json"])).expect("files JSON");
let mut paths: Vec<String> = value
.as_array()
.expect("files array")
.iter()
.map(|file| file["path"].as_str().expect("path").to_string())
.collect();
paths.sort();
paths
}

fn pending_total(project: &str) -> u64 {
let value: serde_json::Value =
serde_json::from_str(&cli(&["status", project, "--json"])).expect("status JSON");
let pending = &value["pendingChanges"];
["added", "modified", "removed"]
.iter()
.map(|key| pending[*key].as_u64().expect("pending count"))
.sum()
}

#[test]
fn full_sync_keeps_files_the_watcher_does_not_watch() {
let dir = TestDir::new("watch-only");
let project = dir.path.join("proj");
touch(&project, "src/app.ts", "export const app = 1;\n");
// `.cache` and `vcpkg_installed` are watch-only ignores; the scan indexes
// them. A top-level `.codegraph-*` directory that is not the index root is
// scanned too, but the watcher rejects it.
touch(&project, ".cache/cached.ts", "export const cached = 1;\n");
touch(
&project,
"vcpkg_installed/port.ts",
"export const port = 1;\n",
);
touch(
&project,
".codegraph-sources/kept.ts",
"export const kept = 1;\n",
);
let p = project.to_str().unwrap();

cli(&["init", p]);
let indexed = indexed_paths(p);
assert_eq!(
indexed,
vec![
".cache/cached.ts".to_string(),
".codegraph-sources/kept.ts".to_string(),
"src/app.ts".to_string(),
"vcpkg_installed/port.ts".to_string(),
]
);
assert_eq!(pending_total(p), 0, "a fresh index has nothing pending");

cli(&["sync", p]);
assert_eq!(indexed_paths(p), indexed, "sync must keep what index keeps");
assert_eq!(pending_total(p), 0, "status must agree with sync");

cli(&["index", "--force", p]);
assert_eq!(indexed_paths(p), indexed, "sync equals index --force");
}
13 changes: 12 additions & 1 deletion crates/codegraph-watch/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ fn sync_project_once_with_scope(
&mut store,
project_root,
candidates,
Some(&on_disk),
scope,
&include,
&exclude,
Expand Down Expand Up @@ -423,6 +424,7 @@ fn sync_changed_paths_current_scope(
&mut store,
project_root,
changed,
None,
&scope,
&include,
&exclude,
Expand All @@ -448,6 +450,7 @@ fn sync_paths_with_store(
store: &mut Store,
project_root: &Path,
paths: impl IntoIterator<Item = impl AsRef<Path>>,
scanned: Option<&HashSet<String>>,
scope: &ProjectScope,
include: &[String],
exclude: &[String],
Expand Down Expand Up @@ -484,7 +487,15 @@ fn sync_paths_with_store(
continue;
}
outcome.files_checked += 1;
let in_scope = policy.should_handle_file(&relative);
// A full sync reconciles against the scan it just ran, so the scan alone
// decides scope, exactly as for `index --force`. The watch policy is
// stricter on purpose (it never watches `.cache`, a top-level
// `.codegraph-*` directory, and the other watch-only defaults), so it may
// only judge the paths an incremental sync was handed.
let in_scope = match scanned {
Some(scanned) => scanned.contains(&relative),
None => policy.should_handle_file(&relative),
};
if !in_scope && store.file_by_path(&relative)?.is_none() {
outcome.files_ignored += 1;
on_progress(done + 1, total);
Expand Down
Loading