pub mod entry;
pub mod key;
pub mod store;
use std::path::Path;
use lanekeep_core::ContentHash;
pub use entry::Entry;
pub use key::{CacheKey, FORMAT_VERSION, GrammarKey, RunKey};
pub use store::Store;
#[must_use]
pub fn hash_bytes(bytes: &[u8]) -> ContentHash {
ContentHash::new(*blake3::hash(bytes).as_bytes())
}
#[must_use]
pub fn validate(entry: &Entry, root: &Path) -> bool {
entry.dependencies.iter().all(|read| {
let current = std::fs::read(root.join(read.path.as_str()))
.ok()
.map(|bytes| hash_bytes(&bytes));
match (read.hash, current) {
(Some(recorded), Some(now)) => recorded == now,
(None, None) => true,
_ => false,
}
})
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use lanekeep_core::FilePath;
use lanekeep_core::tracked::TrackedRead;
use super::*;
struct Project {
dir: PathBuf,
}
impl Project {
fn new(name: &str, files: &[(&str, &str)]) -> Self {
let dir = std::env::temp_dir()
.join(format!("lanekeep-validate-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("creates dir");
let project = Self { dir };
for (path, contents) in files {
project.write(path, contents);
}
project
}
fn write(&self, path: &str, contents: &str) {
let full = self.dir.join(path);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).expect("creates parent");
}
std::fs::write(full, contents).expect("writes");
}
}
impl Drop for Project {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn entry_depending_on(reads: Vec<TrackedRead>) -> Entry {
Entry {
dependencies: reads,
..Entry::default()
}
}
#[test]
fn an_entry_with_no_dependencies_is_always_valid() {
let project = Project::new("none", &[]);
assert!(validate(&Entry::default(), &project.dir));
}
#[test]
fn an_unchanged_dependency_holds() {
let project = Project::new("unchanged", &[("package.json", "{}")]);
let entry = entry_depending_on(vec![TrackedRead::found(
FilePath::new("package.json"),
hash_bytes(b"{}"),
)]);
assert!(validate(&entry, &project.dir));
}
#[test]
fn a_changed_dependency_invalidates() {
let project = Project::new("changed", &[("package.json", "{\"type\":\"module\"}")]);
let entry = entry_depending_on(vec![TrackedRead::found(
FilePath::new("package.json"),
hash_bytes(b"{}"),
)]);
assert!(!validate(&entry, &project.dir));
}
#[test]
fn a_vanished_dependency_invalidates() {
let project = Project::new("vanished", &[]);
let entry = entry_depending_on(vec![TrackedRead::found(
FilePath::new("package.json"),
hash_bytes(b"{}"),
)]);
assert!(!validate(&entry, &project.dir));
}
#[test]
fn an_absent_dependency_that_is_still_absent_holds() {
let project = Project::new("still-absent", &[]);
let entry = entry_depending_on(vec![TrackedRead::absent(FilePath::new("tsconfig.json"))]);
assert!(validate(&entry, &project.dir));
}
#[test]
fn a_dependency_that_appeared_invalidates() {
let project = Project::new("appeared", &[("tsconfig.json", "{}")]);
let entry = entry_depending_on(vec![TrackedRead::absent(FilePath::new("tsconfig.json"))]);
assert!(!validate(&entry, &project.dir));
}
#[test]
fn one_changed_dependency_among_many_invalidates() {
let project = Project::new(
"one-of-many",
&[("a.json", "{}"), ("b.json", "changed"), ("c.json", "{}")],
);
let entry = entry_depending_on(vec![
TrackedRead::found(FilePath::new("a.json"), hash_bytes(b"{}")),
TrackedRead::found(FilePath::new("b.json"), hash_bytes(b"{}")),
TrackedRead::found(FilePath::new("c.json"), hash_bytes(b"{}")),
]);
assert!(!validate(&entry, &project.dir));
}
#[test]
fn a_directory_where_a_file_was_invalidates() {
let project = Project::new("directory", &[]);
std::fs::create_dir_all(project.dir.join("package.json")).expect("creates dir");
let entry = entry_depending_on(vec![TrackedRead::found(
FilePath::new("package.json"),
hash_bytes(b"{}"),
)]);
assert!(!validate(&entry, &project.dir));
}
#[test]
fn identical_bytes_hash_identically() {
assert_eq!(hash_bytes(b"hello"), hash_bytes(b"hello"));
assert_ne!(hash_bytes(b"hello"), hash_bytes(b"hellp"));
}
}