@@ -748,11 +748,18 @@ impl ExecutionCache {
748748
749749#[ cfg( test) ]
750750mod tests {
751+ use std:: { ffi:: OsStr , sync:: Arc } ;
752+
751753 use rusqlite:: Connection ;
754+ use rustc_hash:: FxHashMap ;
752755 use tempfile:: TempDir ;
753756 use vite_path:: AbsolutePathBuf ;
757+ use vite_task_graph:: config:: user:: { EnabledCacheConfig , UserCacheConfig } ;
758+ use vite_task_plan:: { plan_request:: SyntheticPlanRequest , plan_synthetic} ;
759+ use wincode:: { config:: DEFAULT_PREALLOCATION_SIZE_LIMIT , error:: WriteError } ;
754760
755761 use super :: * ;
762+ use crate :: { collections:: HashMap , session:: execute:: fingerprint:: PathFingerprint } ;
756763
757764 fn temp_dir ( ) -> ( TempDir , AbsolutePathBuf ) {
758765 let tmp = TempDir :: new ( ) . unwrap ( ) ;
@@ -764,6 +771,211 @@ mod tests {
764771 Connection :: open ( db. as_path ( ) ) . unwrap ( )
765772 }
766773
774+ fn synthetic_cache_metadata ( workspace : & Arc < AbsolutePath > ) -> CacheMetadata {
775+ let program = Arc :: < OsStr > :: from ( std:: env:: current_exe ( ) . unwrap ( ) . into_os_string ( ) ) ;
776+ plan_synthetic (
777+ workspace,
778+ workspace,
779+ SyntheticPlanRequest {
780+ program,
781+ args : Arc :: from ( [ ] ) ,
782+ cache_config : UserCacheConfig :: with_config ( EnabledCacheConfig {
783+ env : None ,
784+ untracked_env : None ,
785+ input : None ,
786+ output : None ,
787+ } ) ,
788+ envs : Arc :: new ( FxHashMap :: default ( ) ) ,
789+ } ,
790+ Arc :: from ( [ Str :: from ( "cache-regression-test" ) ] ) ,
791+ )
792+ . unwrap ( )
793+ . cache_metadata
794+ . unwrap ( )
795+ }
796+
797+ #[ tokio:: test]
798+ async fn cache_entry_larger_than_default_preallocation_limit_roundtrips ( ) {
799+ let ( _tmp, dir) = temp_dir ( ) ;
800+ let cache = ExecutionCache :: load_from_path ( & dir) . unwrap ( ) ;
801+ let entry_size = size_of :: < ( RelativePathBuf , PathFingerprint ) > ( ) ;
802+ let entry_count = DEFAULT_PREALLOCATION_SIZE_LIMIT / entry_size + 1 ;
803+ let needed = entry_count * entry_size;
804+
805+ let mut inferred_inputs = HashMap :: with_capacity ( entry_count) ;
806+ for index in 0 ..entry_count {
807+ inferred_inputs. insert (
808+ RelativePathBuf :: new ( vite_str:: format!( "input-{index}" ) ) . unwrap ( ) ,
809+ PathFingerprint :: FileContentHash ( index as u64 ) ,
810+ ) ;
811+ }
812+ let value = CacheEntryValue {
813+ post_run_fingerprint : PostRunFingerprint {
814+ inferred_inputs,
815+ ..PostRunFingerprint :: default ( )
816+ } ,
817+ std_outputs : Arc :: from ( [ ] ) ,
818+ duration : Duration :: ZERO ,
819+ globbed_inputs : BTreeMap :: new ( ) ,
820+ output_archive : None ,
821+ } ;
822+
823+ assert ! ( needed > DEFAULT_PREALLOCATION_SIZE_LIMIT ) ;
824+ assert_eq ! (
825+ <TaskCacheConfig as ConfigCore >:: PREALLOCATION_SIZE_LIMIT ,
826+ Some ( TASK_CACHE_PREALLOCATION_SIZE_LIMIT ) ,
827+ ) ;
828+ assert ! ( matches!(
829+ wincode:: serialize( & value) ,
830+ Err ( WriteError :: PreallocationSizeLimit {
831+ needed: error_needed,
832+ limit: DEFAULT_PREALLOCATION_SIZE_LIMIT ,
833+ } ) if error_needed == needed
834+ ) ) ;
835+
836+ cache. upsert ( CacheTable :: CacheEntries , & 0_u8 , & value) . await . unwrap ( ) ;
837+ let CacheRead :: Found ( row) = cache
838+ . get_key_by_value :: < u8 , CacheEntryValue > ( CacheTable :: CacheEntries , & 0_u8 )
839+ . await
840+ . unwrap ( )
841+ else {
842+ panic ! ( "large cache entry did not round-trip" ) ;
843+ } ;
844+ assert_eq ! ( row. value. post_run_fingerprint. inferred_inputs. len( ) , entry_count) ;
845+ assert_eq ! (
846+ row. value. post_run_fingerprint. inferred_inputs. get(
847+ & RelativePathBuf :: new( vite_str:: format!( "input-{}" , entry_count - 1 ) ) . unwrap( ) ,
848+ ) ,
849+ Some ( & PathFingerprint :: FileContentHash ( ( entry_count - 1 ) as u64 ) ) ,
850+ ) ;
851+ }
852+
853+ #[ tokio:: test]
854+ async fn unreadable_cache_entry_is_a_miss_and_only_that_row_is_deleted ( ) {
855+ let ( _tmp, dir) = temp_dir ( ) ;
856+ let cache = ExecutionCache :: load_from_path ( & dir) . unwrap ( ) ;
857+ let key = serialize_cache ( & 0_u8 ) . unwrap ( ) ;
858+ let neighbor_key = serialize_cache ( & 1_u8 ) . unwrap ( ) ;
859+ {
860+ let conn = cache. conn . lock ( ) . await ;
861+ conn. execute (
862+ "INSERT INTO cache_entries (key, value) VALUES (?1, ?2), (?3, ?4)" ,
863+ rusqlite:: params![ key, vec![ 0xff_u8 ] , neighbor_key, vec![ 0xfe_u8 ] ] ,
864+ )
865+ . unwrap ( ) ;
866+ }
867+
868+ assert ! ( matches!(
869+ cache
870+ . get_key_by_value:: <u8 , CacheEntryValue >( CacheTable :: CacheEntries , & 0_u8 )
871+ . await
872+ . unwrap( ) ,
873+ CacheRead :: Unreadable
874+ ) ) ;
875+
876+ let rows: Vec < Vec < u8 > > = {
877+ let conn = cache. conn . lock ( ) . await ;
878+ conn. prepare ( "SELECT key FROM cache_entries ORDER BY key" )
879+ . unwrap ( )
880+ . query_map ( [ ] , |row| row. get ( 0 ) )
881+ . unwrap ( )
882+ . collect :: < rusqlite:: Result < _ > > ( )
883+ . unwrap ( )
884+ } ;
885+ assert_eq ! ( rows, vec![ neighbor_key] ) ;
886+ }
887+
888+ #[ tokio:: test]
889+ async fn unreadable_task_fingerprint_is_a_miss_and_is_deleted ( ) {
890+ let ( _tmp, dir) = temp_dir ( ) ;
891+ let cache = ExecutionCache :: load_from_path ( & dir) . unwrap ( ) ;
892+ let key = serialize_cache ( & 0_u8 ) . unwrap ( ) ;
893+ {
894+ let conn = cache. conn . lock ( ) . await ;
895+ conn. execute (
896+ "INSERT INTO task_fingerprints (key, value) VALUES (?1, 'not a blob')" ,
897+ [ & key] ,
898+ )
899+ . unwrap ( ) ;
900+ }
901+
902+ assert ! ( matches!(
903+ cache
904+ . get_key_by_value:: <u8 , CacheEntryKey >( CacheTable :: TaskFingerprints , & 0_u8 )
905+ . await
906+ . unwrap( ) ,
907+ CacheRead :: Unreadable
908+ ) ) ;
909+ let remaining: u32 = cache
910+ . conn
911+ . lock ( )
912+ . await
913+ . query_one ( "SELECT COUNT(*) FROM task_fingerprints" , [ ] , |row| row. get ( 0 ) )
914+ . unwrap ( ) ;
915+ assert_eq ! ( remaining, 0 ) ;
916+ }
917+
918+ #[ tokio:: test]
919+ async fn cleanup_does_not_delete_a_concurrently_replaced_value ( ) {
920+ let ( _tmp, dir) = temp_dir ( ) ;
921+ let cache = ExecutionCache :: load_from_path ( & dir) . unwrap ( ) ;
922+ let key = serialize_cache ( & 0_u8 ) . unwrap ( ) ;
923+ {
924+ let conn = cache. conn . lock ( ) . await ;
925+ conn. execute (
926+ "INSERT INTO cache_entries (key, value) VALUES (?1, ?2)" ,
927+ rusqlite:: params![ key, Value :: Real ( 1.0 ) ] ,
928+ )
929+ . unwrap ( ) ;
930+ }
931+
932+ let deleted = cache
933+ . delete_if_unchanged ( CacheTable :: CacheEntries , & key, & Value :: Integer ( 1 ) )
934+ . await
935+ . unwrap ( ) ;
936+ assert_eq ! ( deleted, 0 ) ;
937+ let value: Value = cache
938+ . conn
939+ . lock ( )
940+ . await
941+ . query_one ( "SELECT value FROM cache_entries WHERE key=?1" , [ & key] , |row| row. get ( 0 ) )
942+ . unwrap ( ) ;
943+ assert_eq ! ( value, Value :: Real ( 1.0 ) ) ;
944+ }
945+
946+ #[ tokio:: test]
947+ async fn unreadable_entry_and_dangling_fingerprint_do_not_block_execution ( ) {
948+ let ( _tmp, dir) = temp_dir ( ) ;
949+ let workspace = Arc :: < AbsolutePath > :: from ( dir. clone ( ) ) ;
950+ let cache = ExecutionCache :: load_from_path ( & dir) . unwrap ( ) ;
951+ let metadata = synthetic_cache_metadata ( & workspace) ;
952+ let cache_key = CacheEntryKey :: from_metadata ( & metadata) ;
953+ let key_blob = serialize_cache ( & cache_key) . unwrap ( ) ;
954+ {
955+ let conn = cache. conn . lock ( ) . await ;
956+ conn. execute ( "INSERT INTO cache_entries (key, value) VALUES (?1, X'FF')" , [ & key_blob] )
957+ . unwrap ( ) ;
958+ }
959+ cache. upsert_task_fingerprint ( & metadata. execution_cache_key , & cache_key) . await . unwrap ( ) ;
960+
961+ assert ! ( matches!(
962+ cache. try_hit( & metadata, & BTreeMap :: new( ) , & dir) . await . unwrap( ) ,
963+ Err ( CacheMiss :: Unreadable )
964+ ) ) ;
965+ assert ! ( matches!(
966+ cache. try_hit( & metadata, & BTreeMap :: new( ) , & dir) . await . unwrap( ) ,
967+ Err ( CacheMiss :: NotFound )
968+ ) ) ;
969+
970+ let conn = cache. conn . lock ( ) . await ;
971+ let entries: u32 =
972+ conn. query_one ( "SELECT COUNT(*) FROM cache_entries" , [ ] , |row| row. get ( 0 ) ) . unwrap ( ) ;
973+ let fingerprints: u32 =
974+ conn. query_one ( "SELECT COUNT(*) FROM task_fingerprints" , [ ] , |row| row. get ( 0 ) ) . unwrap ( ) ;
975+ drop ( conn) ;
976+ assert_eq ! ( ( entries, fingerprints) , ( 0 , 0 ) ) ;
977+ }
978+
767979 /// Reopening the same cache directory keeps existing entries: the tables are
768980 /// created with `IF NOT EXISTS`, so a second open never wipes the database.
769981 #[ test]
0 commit comments