@@ -4,6 +4,8 @@ use std::path::Path;
44use std:: path:: PathBuf ;
55
66use aether_path:: FilePath ;
7+ use filetime:: set_file_mtime;
8+ use filetime:: FileTime ;
79use oak_db:: Db ;
810use oak_db:: DbInputs ;
911use oak_db:: OakDatabase ;
@@ -69,6 +71,12 @@ fn remove_watched_file(db: &mut OakDatabase, path: FilePath) {
6971 ) ;
7072}
7173
74+ /// The global salsa revision. Bumps once per input setter that actually
75+ /// fires, so a delta of zero across an operation means it wrote no inputs.
76+ fn salsa_revision ( db : & OakDatabase ) -> salsa:: Revision {
77+ salsa:: plumbing:: current_revision ( db)
78+ }
79+
7280/// Sync helper: force a fresh full rescan of `root`. Equivalent to the
7381/// production trigger of a DESCRIPTION watcher event hitting the root.
7482fn rescan_workspace_root ( db : & mut OakDatabase , root : Root ) {
@@ -626,3 +634,60 @@ fn test_apply_watcher_events_tolerates_non_package_description() {
626634 let root = db. workspace_roots ( ) . roots ( & db) [ 0 ] ;
627635 assert ! ( root. packages( & db) . is_empty( ) ) ;
628636}
637+
638+ #[ test]
639+ fn test_noop_rescan_does_not_bump_salsa_revision ( ) {
640+ // Salsa has no backdating for inputs: setting an input to the value it
641+ // already holds still bumps the global revision and invalidates every
642+ // query downstream of it. So a rescan that finds nothing new on disk must
643+ // not touch any input. We observe the revision directly. A clean rescan
644+ // leaves it flat, a structural change moves it.
645+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
646+ write_package ( & tmp. path ( ) . join ( "pkg" ) , "pkg" , & [ ( "a.R" , "x <- 1\n " ) ] ) ;
647+ fs:: create_dir_all ( tmp. path ( ) . join ( "pkg/tests" ) ) . unwrap ( ) ;
648+ fs:: write ( tmp. path ( ) . join ( "pkg/tests/test-foo.R" ) , "t\n " ) . unwrap ( ) ;
649+ fs:: write ( tmp. path ( ) . join ( "top.R" ) , "y <- 2\n " ) . unwrap ( ) ;
650+ let mut db = OakDatabase :: new ( ) ;
651+ set_workspace_paths ( & mut db, & [ tmp. path ( ) . to_path_buf ( ) ] , & HashSet :: new ( ) ) ;
652+ let root = db. workspace_roots ( ) . roots ( & db) [ 0 ] ;
653+
654+ // Nothing changed on disk, so the rescan reuses every `Package` / `File`
655+ // entity and finds every input field already equal.
656+ let before = salsa_revision ( & db) ;
657+ rescan_workspace_root ( & mut db, root) ;
658+ assert_eq ! ( salsa_revision( & db) , before) ;
659+
660+ // A new file under pkg/R/ is a real structural change, so it does move the
661+ // revision. This proves the assertion above isn't vacuously true.
662+ fs:: write ( tmp. path ( ) . join ( "pkg/R/b.R" ) , "z <- 3\n " ) . unwrap ( ) ;
663+ rescan_workspace_root ( & mut db, root) ;
664+ assert ! ( salsa_revision( & db) > before) ;
665+ }
666+
667+ #[ test]
668+ fn test_watcher_reblip_same_mtime_does_not_bump_salsa_revision ( ) {
669+ // Watchers coalesce and re-emit events, so one save can deliver a Changed
670+ // event whose re-stat matches the mtime we already stored.
671+ // `add_watched_file` guards the `File::revision` write against that, so a
672+ // duplicate event leaves the revision (and thus `source_text`) alone.
673+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
674+ write_package ( & tmp. path ( ) . join ( "pkg" ) , "pkg" , & [ ( "a.R" , "x <- 1\n " ) ] ) ;
675+ let mut db = OakDatabase :: new ( ) ;
676+ set_workspace_paths ( & mut db, & [ tmp. path ( ) . to_path_buf ( ) ] , & HashSet :: new ( ) ) ;
677+
678+ let fs_path = tmp. path ( ) . join ( "pkg/R/a.R" ) ;
679+ let path = FilePath :: from_path_buf ( fs_path. clone ( ) ) . unwrap ( ) ;
680+
681+ // Duplicate event, nothing rewritten: same mtime, so the guard skips.
682+ let before = salsa_revision ( & db) ;
683+ add_watched_file ( & mut db, path. clone ( ) ) ;
684+ assert_eq ! ( salsa_revision( & db) , before) ;
685+
686+ // A genuine save carries a distinct mtime, so the revision bumps and the
687+ // next `source_text` re-reads. Set the mtime explicitly rather than trust
688+ // the filesystem's mtime granularity to notice a back-to-back write.
689+ fs:: write ( & fs_path, "x <- 2\n " ) . unwrap ( ) ;
690+ set_file_mtime ( & fs_path, FileTime :: from_unix_time ( 1_000_000_000 , 0 ) ) . unwrap ( ) ;
691+ add_watched_file ( & mut db, path) ;
692+ assert ! ( salsa_revision( & db) > before) ;
693+ }
0 commit comments