Core tests make a throwaway blob store with blobs::scratch

blobs, store and portable each built a BlobStore in a temp directory their own
way: pid+tag twice, a uuid once. blobs::scratch(tag) is that, with a counter, so
two tests can never share a directory even if they pick the same tag. The
desktop's and ffi's temp-dir helpers stay, one per crate: sharing them would
need a test-util feature on the core crate.

DRY pass #2, batch 2, F9 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:24:52 -04:00
co-authored by Claude Opus 5.5
parent 3ea7c817bf
commit 2dec89bf8c
3 changed files with 24 additions and 25 deletions
+19 -14
View File
@@ -215,18 +215,23 @@ pub fn serve(path: &str, query: Option<&str>) -> (u16, String, Vec<u8>) {
(200, content_type_for(&claimed), bytes)
}
/// A blob store in a fresh throwaway directory, for any core test that needs one.
/// No tempfile dependency for one fixture: the process id and a counter keep
/// concurrent tests and runs apart, and `tag` says whose it was.
#[cfg(test)]
pub(crate) fn scratch(tag: &str) -> BlobStore {
use std::sync::atomic::{AtomicU32, Ordering};
static SEQ: AtomicU32 = AtomicU32::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("ts-blobs-{}-{n}-{tag}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
BlobStore::new(dir).expect("scratch blob store")
}
#[cfg(test)]
mod tests {
use super::*;
/// A blob store in a throwaway directory. No tempfile dependency for one test
/// fixture — the process id keeps concurrent runs apart.
fn store(tag: &str) -> BlobStore {
let dir = std::env::temp_dir().join(format!("ts-blobs-{}-{tag}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
BlobStore::new(dir).expect("store")
}
/// sha256("hello") — a fixed vector, so a broken digest can't agree with itself.
const HELLO: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
@@ -237,7 +242,7 @@ mod tests {
#[test]
fn stores_and_reads_back() {
let store = store("roundtrip");
let store = scratch("roundtrip");
assert!(!store.has(HELLO));
store.store(HELLO, b"hello").expect("store");
assert!(store.has(HELLO));
@@ -248,7 +253,7 @@ mod tests {
fn refuses_bytes_that_dont_match_the_hash() {
// A corrupted or substituted transfer must never be filed under a name that
// claims it's genuine.
let store = store("mismatch");
let store = scratch("mismatch");
let err = store.store(HELLO, b"goodbye").expect_err("must reject");
assert!(err.contains("integrity"), "got {err}");
assert!(!store.has(HELLO), "nothing should have been written");
@@ -257,7 +262,7 @@ mod tests {
#[test]
fn rejects_a_hash_that_could_escape_the_directory() {
// The hash arrives from a server response and becomes a filename.
let store = store("traversal");
let store = scratch("traversal");
assert!(store.path("../../etc/passwd").is_none());
assert!(store.store("../../etc/passwd", b"x").is_err());
assert!(store.path("").is_none());
@@ -267,7 +272,7 @@ mod tests {
#[test]
fn accepts_an_uppercase_hash() {
// The wire format isn't guaranteed to be lowercase; the filename is.
let store = store("case");
let store = scratch("case");
store
.store(&HELLO.to_ascii_uppercase(), b"hello")
.expect("store");
@@ -329,14 +334,14 @@ mod tests {
#[test]
fn put_files_bytes_under_their_own_hash() {
let store = store("put");
let store = scratch("put");
assert_eq!(store.put(b"hello").expect("put"), HELLO);
assert_eq!(store.read(HELLO).as_deref(), Some(&b"hello"[..]));
}
#[test]
fn missing_blob_reads_as_none() {
let store = store("missing");
let store = scratch("missing");
assert!(store.read(HELLO).is_none());
assert!(!store.has(HELLO));
}