Skip to content

Commit a5e544a

Browse files
wan9chicodex
andcommitted
test(cache): cover large and unreadable entries
Co-authored-by: GPT-5.6 Codex <codex@openai.com>
1 parent ffed460 commit a5e544a

1 file changed

Lines changed: 212 additions & 0 deletions

File tree

  • crates/vite_task/src/session/cache

crates/vite_task/src/session/cache/mod.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,11 +748,18 @@ impl ExecutionCache {
748748

749749
#[cfg(test)]
750750
mod 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

Comments
 (0)