use super::is_cacheable;
use std::fs;
use std::path::Path;
fn entry_type(dir: &Path, name: &str) -> fs::FileType {
fs::read_dir(dir)
.expect("read the fixture directory")
.filter_map(Result::ok)
.find(|entry| entry.file_name() == name)
.unwrap_or_else(|| panic!("no entry named {name} in the fixture"))
.file_type()
.expect("read the entry's file type")
}
#[test]
fn an_ordinary_file_is_cacheable() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("styles.css"), b"body{}").unwrap();
assert!(is_cacheable(&entry_type(root.path(), "styles.css")));
}
#[test]
fn an_empty_file_is_cacheable() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("empty.txt"), b"").unwrap();
assert!(
is_cacheable(&entry_type(root.path(), "empty.txt")),
"a zero-length file is still a file; refusing it would be an accident, not a policy"
);
}
#[test]
fn a_directory_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
fs::create_dir(root.path().join("assets")).unwrap();
assert!(
!is_cacheable(&entry_type(root.path(), "assets")),
"a directory is descended, never cached"
);
}
#[test]
fn a_symlink_to_a_file_inside_the_root_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("real.css"), b"body{}").unwrap();
std::os::unix::fs::symlink(root.path().join("real.css"), root.path().join("link.css")).unwrap();
assert!(
!is_cacheable(&entry_type(root.path(), "link.css")),
"a symlink is refused even pointing inside the root, so the walk never follows one"
);
}
#[test]
fn a_symlink_to_a_file_outside_the_root_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("secret"), b"not yours").unwrap();
std::os::unix::fs::symlink(outside.path().join("secret"), root.path().join("escape")).unwrap();
assert!(
!is_cacheable(&entry_type(root.path(), "escape")),
"a symlink out of the root must never be cached"
);
}
#[test]
fn a_symlink_to_a_directory_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
std::os::unix::fs::symlink(outside.path(), root.path().join("elsewhere")).unwrap();
assert!(
!is_cacheable(&entry_type(root.path(), "elsewhere")),
"a symlinked directory is neither cached nor descended"
);
}
#[test]
fn a_fifo_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
let fifo = root.path().join("pipe");
let c_path = std::ffi::CString::new(fifo.as_os_str().as_encoded_bytes()).unwrap();
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
assert_eq!(rc, 0, "could not create the fifo fixture");
assert!(
!is_cacheable(&entry_type(root.path(), "pipe")),
"opening a fifo during the walk would hang construction"
);
}
#[test]
fn a_unix_socket_is_not_cacheable() {
let root = tempfile::tempdir().unwrap();
let socket = root.path().join("sock");
let _listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
assert!(
!is_cacheable(&entry_type(root.path(), "sock")),
"a socket is not a file to read"
);
}
#[test]
fn the_walk_does_not_descend_a_symlinked_directory() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("secret"), b"not yours").unwrap();
std::os::unix::fs::symlink(outside.path(), root.path().join("elsewhere")).unwrap();
fs::write(root.path().join("real.css"), b"body{}").unwrap();
let found = super::cacheable_entries(root.path());
assert_eq!(found.len(), 1, "expected only the real file, got: {found:?}");
assert!(found[0].ends_with("real.css"));
assert!(
!found.iter().any(|p| p.ends_with("secret")),
"the walk followed a symlinked directory and enumerated a file outside the root"
);
}
#[test]
fn the_walk_descends_real_directories() {
let root = tempfile::tempdir().unwrap();
fs::create_dir_all(root.path().join("a/b")).unwrap();
fs::write(root.path().join("top.css"), b"1").unwrap();
fs::write(root.path().join("a/mid.css"), b"2").unwrap();
fs::write(root.path().join("a/b/deep.css"), b"3").unwrap();
let mut names: Vec<String> = super::cacheable_entries(root.path())
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(names, vec!["deep.css", "mid.css", "top.css"]);
}
#[test]
fn the_walk_enumerates_only_regular_files() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("keep.css"), b"1").unwrap();
fs::create_dir(root.path().join("dir")).unwrap();
std::os::unix::fs::symlink(root.path().join("keep.css"), root.path().join("link")).unwrap();
let socket = root.path().join("sock");
let _listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
let found = super::cacheable_entries(root.path());
assert_eq!(found.len(), 1, "expected only keep.css, got: {found:?}");
assert!(found[0].ends_with("keep.css"));
}
#[test]
fn an_unreadable_directory_is_skipped_rather_than_fatal() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("readable.css"), b"1").unwrap();
let blocked = root.path().join("blocked");
fs::create_dir(&blocked).unwrap();
fs::write(blocked.join("hidden.css"), b"2").unwrap();
fs::set_permissions(&blocked, fs::Permissions::from_mode(0o000)).unwrap();
let found = super::cacheable_entries(root.path());
fs::set_permissions(&blocked, fs::Permissions::from_mode(0o755)).unwrap();
assert!(
found.iter().any(|p| p.ends_with("readable.css")),
"the readable file should still be enumerated: {found:?}"
);
}
#[test]
fn the_walk_stops_at_its_ceiling() {
let root = tempfile::tempdir().unwrap();
for n in 0..10 {
fs::write(root.path().join(format!("f{n}.css")), b"x").unwrap();
}
let found = super::cacheable_entries_bounded(root.path(), 3);
assert_eq!(found.len(), 3, "the ceiling did not truncate the walk: {found:?}");
}
#[test]
fn the_walk_returns_a_deterministic_order() {
let root = tempfile::tempdir().unwrap();
for name in ["zebra.css", "alpha.css", "middle.css"] {
fs::write(root.path().join(name), b"x").unwrap();
}
let found = super::cacheable_entries(root.path());
let mut sorted = found.clone();
sorted.sort();
assert_eq!(found, sorted, "enumeration was not in sorted order: {found:?}");
let truncated = super::cacheable_entries_bounded(root.path(), 2);
let mut expected = truncated.clone();
expected.sort();
assert_eq!(truncated, expected, "truncated output was not sorted: {truncated:?}");
assert_eq!(truncated.len(), 2);
}
#[test]
fn population_holds_the_cacheable_files_keyed_relative_to_the_root() {
let root = tempfile::tempdir().unwrap();
fs::create_dir(root.path().join("assets")).unwrap();
fs::write(root.path().join("index.html"), b"<html>").unwrap();
fs::write(root.path().join("assets/app.css"), b"body{}").unwrap();
std::os::unix::fs::symlink(root.path().join("index.html"), root.path().join("link.html"))
.unwrap();
let cache = super::populate(root.path(), 1 << 20);
assert_eq!(cache.len(), 2, "expected exactly the two real files");
assert_eq!(cache.bytes_held(), b"<html>".len() + b"body{}".len());
assert!(cache.get(Path::new("index.html")).is_some());
assert!(
cache.get(Path::new("assets/app.css")).is_some(),
"a nested file must be keyed by its path relative to the root, not its basename"
);
assert!(
cache.get(Path::new("link.html")).is_none(),
"a symlink must not be cached"
);
assert!(!cache.truncated());
}
#[test]
fn a_cached_entry_holds_the_files_contents_and_metadata() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("app.css"), b"body{color:red}").unwrap();
let cache = super::populate(root.path(), 1 << 20);
let entry = cache.get(Path::new("app.css")).expect("the file should be cached");
assert_eq!(&entry.bytes[..], b"body{color:red}");
assert_eq!(
entry.metadata.len(),
b"body{color:red}".len() as u64,
"the stored metadata must describe the file, since the ETag is derived from it"
);
}
#[cfg(target_os = "linux")]
#[test]
fn a_non_utf8_filename_is_keyed_without_loss() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let root = tempfile::tempdir().unwrap();
let first = OsStr::from_bytes(b"a\xff.css");
let second = OsStr::from_bytes(b"a\xfe.css");
fs::write(root.path().join(first), b"first").unwrap();
fs::write(root.path().join(second), b"second").unwrap();
let cache = super::populate(root.path(), 1 << 20);
assert_eq!(cache.len(), 2, "a lossy key would have collapsed these into one");
assert_eq!(
&cache.get(Path::new(first)).expect("first").bytes[..],
b"first",
"the wrong file's bytes would be served if keys were lossy"
);
assert_eq!(&cache.get(Path::new(second)).expect("second").bytes[..], b"second");
}
#[test]
fn the_budget_truncates_a_deterministic_prefix() {
let root = tempfile::tempdir().unwrap();
for name in ["c.css", "a.css", "b.css"] {
fs::write(root.path().join(name), vec![b'x'; 100]).unwrap();
}
let cache = super::populate(root.path(), 250);
assert!(cache.truncated(), "the budget was exceeded and should be reported");
assert_eq!(cache.len(), 2);
assert!(
cache.get(Path::new("a.css")).is_some() && cache.get(Path::new("b.css")).is_some(),
"sorted enumeration means the prefix is a.css and b.css, on any filesystem"
);
assert!(cache.bytes_held() <= 250, "the budget was exceeded: {}", cache.bytes_held());
}
#[test]
fn a_zero_budget_caches_nothing() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("app.css"), b"body{}").unwrap();
let cache = super::populate(root.path(), 0);
assert_eq!(cache.len(), 0);
assert!(cache.truncated());
}
#[test]
fn a_precompressed_sibling_is_recorded() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("app.css"), b"body{}").unwrap();
fs::write(root.path().join("app.css.br"), b"brotli").unwrap();
fs::write(root.path().join("plain.css"), b"body{}").unwrap();
let cache = super::populate(root.path(), 1 << 20);
assert!(
cache.get(Path::new("app.css")).unwrap().has_precompressed_sibling,
"app.css has a .br sibling and must be marked"
);
assert!(
!cache.get(Path::new("plain.css")).unwrap().has_precompressed_sibling,
"plain.css has no sibling and must not be marked"
);
assert_eq!(cache.with_siblings(), 1);
}
#[test]
fn a_fifo_under_the_root_does_not_hang_population() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("app.css"), b"body{}").unwrap();
let fifo = root.path().join("pipe");
let c_path = std::ffi::CString::new(fifo.as_os_str().as_encoded_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
let root_path = root.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let cache = super::populate(&root_path, 1 << 20);
let _ = tx.send(cache.len());
});
let cached = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("population hung — a fifo was opened");
assert_eq!(cached, 1, "only app.css should be cached");
}
#[test]
fn the_budget_stops_rather_than_packing() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("a.css"), vec![b'x'; 100]).unwrap();
fs::write(root.path().join("b.css"), vec![b'x'; 200]).unwrap();
fs::write(root.path().join("c.css"), vec![b'x'; 50]).unwrap();
let cache = super::populate(root.path(), 250);
assert_eq!(cache.len(), 1, "expected only a.css, got {} entries", cache.len());
assert!(cache.get(Path::new("a.css")).is_some());
assert!(
cache.get(Path::new("c.css")).is_none(),
"c.css fits in the remaining budget, but packing it would make the cached set depend \
on file sizes rather than on sorted order"
);
assert!(cache.truncated());
}