use crate::hashing::*;
use failure::{bail, format_err, Fallible};
use serde::{de::DeserializeOwned, Serialize};
use std::fs::File;
use std::io::Read;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
use std::str;
pub(crate) fn cache_file_computation<
T: Serialize + DeserializeOwned,
F: FnOnce(&str) -> Fallible<T>,
>(
cache_path: &Path,
input_path: &Path,
key: &str,
f: F,
) -> Fallible<T> {
let mut file = File::open(input_path)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
let hash = hash_buf(&content);
let results_path = cached_results_path(input_path, key, hash);
let abs_path = cache_generated_file(cache_path, &results_path, |abs_path| {
str::from_utf8(&content)
.map_err(|e| {
format_err!(
"Input file {} contains invalid UTF-8 text: {}",
results_path.display(),
e
)
})
.and_then(|str_contents| f(str_contents))
.and_then(|result| {
save_results::<T>(&abs_path, &result).and(Ok(abs_path))
})
})?;
load_cached_results::<T>(&abs_path)
}
pub(crate) fn cache_object_computation<
T: Serialize + DeserializeOwned,
F: FnOnce() -> Fallible<T>,
>(
cache_path: &Path,
object_name: &str,
hash: HashCode,
key: &str,
f: F,
) -> Fallible<T> {
let results_path = cached_results_path(Path::new(object_name), key, hash);
let abs_path = cache_generated_file(cache_path, &results_path, |abs_path| {
f().and_then(|result| {
save_results::<T>(&abs_path, &result).and(Ok(abs_path))
})
})?;
load_cached_results::<T>(&abs_path)
}
pub(crate) fn get_cached_object_computation<T: Serialize + DeserializeOwned>(
cache_path: &Path,
object_name: &str,
hash: HashCode,
key: &str,
) -> Fallible<T> {
let results_path = cached_results_path(Path::new(object_name), key, hash);
let abs_path = cache_path.join(results_path);
load_cached_results::<T>(&abs_path)
}
pub(crate) fn cache_generated_file<F: FnOnce(PathBuf) -> Fallible<PathBuf>>(
cache_path: &Path,
results_path: &Path,
f: F,
) -> Fallible<PathBuf> {
let abs_path = cache_path.join(results_path);
if abs_path.exists() {
Ok(abs_path)
} else {
let abs_path = f(abs_path)?;
if abs_path.exists() {
Ok(abs_path)
} else {
bail!(
"The result file {} was not created as expected",
abs_path.display()
)
}
}
}
pub(crate) fn get_cache_path(root: &Path) -> PathBuf {
let mut root = root.to_owned();
root.push(concat!(
env!("CARGO_PKG_NAME"),
"-",
env!("CARGO_PKG_VERSION")
));
root.push("cache");
root
}
fn load_cached_results<T: Serialize + DeserializeOwned>(results_path: &Path) -> Fallible<T> {
let file = File::open(results_path)?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).map_err(std::convert::Into::into) }
fn save_results<T: Serialize + DeserializeOwned>(results_path: &Path, results: &T) -> Fallible<()> {
results_path
.parent()
.map(|p| {
std::fs::create_dir_all(p)
.map_err(|e| format_err!("Error creating output directory {}: {}", p.display(), e))
})
.unwrap_or(Ok(()))?;
let file = File::create(results_path).map_err(|e| {
format_err!(
"Error creating cached results file {}: {}",
results_path.display(),
e
)
})?;
let writer = BufWriter::new(file);
serde_json::to_writer(writer, results).map_err(|e| {
format_err!(
"Error saving cached results to {}: {}",
results_path.display(),
e
)
})
}
fn cached_results_path(input_path: &Path, key: &str, hash: HashCode) -> PathBuf {
let input_with_hash = add_hash_to_path(input_path, hash);
let input_name = input_with_hash
.file_name()
.expect("Input path is missing a file name");
PathBuf::from(input_name).join(format!("{}.json", key))
}
#[cfg(test)]
mod test {
use super::*;
use serde::Deserialize;
use std::io::Write;
#[derive(Deserialize, Serialize, Debug)]
struct TestResult {
answer: usize,
}
#[test]
fn caches_file_results() {
let key = "mylib.c";
let root_dir = tempfile::tempdir().unwrap();
let cache_dir = get_cache_path(root_dir.path());
let data_dir = root_dir.path().join("data");
let input_path = data_dir.join("input.txt");
std::fs::create_dir_all(&data_dir).unwrap();
let mut input_file = File::create(&input_path).unwrap();
write!(&mut input_file, "Fear is the mind killer").unwrap();
drop(input_file);
let mut compute_count: usize = 0;
let result: TestResult = cache_file_computation(&cache_dir, &input_path, key, |input| {
compute_count += 1;
Ok(TestResult {
answer: input.len(),
})
})
.unwrap();
assert_eq!(1, compute_count);
assert_eq!("Fear is the mind killer".len(), result.answer);
let result: TestResult = cache_file_computation(&cache_dir, &input_path, key, |input| {
compute_count += 1;
Ok(TestResult {
answer: input.len(),
})
})
.unwrap();
assert_eq!(1, compute_count);
assert_eq!("Fear is the mind killer".len(), result.answer);
let mut input_file = File::create(&input_path).unwrap();
write!(&mut input_file, "I will face my fear").unwrap();
drop(input_file);
let result: TestResult = cache_file_computation(&cache_dir, &input_path, key, |input| {
compute_count += 1;
Ok(TestResult {
answer: input.len(),
})
})
.unwrap();
assert_eq!(2, compute_count);
assert_eq!("I will face my fear".len(), result.answer);
let mut input_file = File::create(&input_path).unwrap();
write!(&mut input_file, "Fear is the mind killer").unwrap();
drop(input_file);
let result: TestResult = cache_file_computation(&cache_dir, &input_path, key, |input| {
compute_count += 1;
Ok(TestResult {
answer: input.len(),
})
})
.unwrap();
assert_eq!(2, compute_count);
assert_eq!("Fear is the mind killer".len(), result.answer);
}
#[test]
fn caches_objects_results() {
let key = "mylib.c";
let root_dir = tempfile::tempdir().unwrap();
let cache_dir = root_dir.path().join("cache");
let hash_foo1: HashCode = 5;
let hash_foo2: HashCode = 6;
let mut compute_count: usize = 0;
let result: TestResult =
cache_object_computation(&cache_dir, "foo", hash_foo1, key, || {
compute_count += 1;
Ok(TestResult {
answer: hash_foo1 as usize,
})
})
.unwrap();
assert_eq!(1, compute_count);
assert_eq!(hash_foo1 as usize, result.answer);
let result: TestResult =
cache_object_computation(&cache_dir, "foo", hash_foo1, key, || {
compute_count += 1;
Ok(TestResult {
answer: hash_foo1 as usize,
})
})
.unwrap();
assert_eq!(1, compute_count);
assert_eq!(hash_foo1 as usize, result.answer);
let result: TestResult =
cache_object_computation(&cache_dir, "foo", hash_foo2, key, || {
compute_count += 1;
Ok(TestResult {
answer: hash_foo2 as usize,
})
})
.unwrap();
assert_eq!(2, compute_count);
assert_eq!(hash_foo2 as usize, result.answer);
let result: TestResult =
cache_object_computation(&cache_dir, "foo", hash_foo1, key, || {
compute_count += 1;
Ok(TestResult {
answer: hash_foo1 as usize,
})
})
.unwrap();
assert_eq!(2, compute_count);
assert_eq!(hash_foo1 as usize, result.answer);
}
#[test]
fn caches_generated_data() {
let key = "mylib.c";
let root_dir = tempfile::tempdir().unwrap();
let cache_dir = root_dir.path().join("cache");
let hash_foo1: HashCode = 5;
let result: Fallible<TestResult> =
get_cached_object_computation(&cache_dir, "foo", hash_foo1, key);
assert!(result.is_err());
cache_object_computation(&cache_dir, "foo", hash_foo1, key, || {
Ok(TestResult {
answer: hash_foo1 as usize,
})
})
.unwrap();
let result: Fallible<TestResult> =
get_cached_object_computation(&cache_dir, "foo", hash_foo1, key);
assert!(result.is_ok());
assert_eq!(hash_foo1 as usize, result.unwrap().answer);
}
}