blob: aa5f641e6235f8e839a8bbbbaf55b0d842be5d32 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
serde::{Deserialize, Serialize},
std::cmp::Ordering,
std::collections::HashMap,
std::ffi::OsString,
std::fs::{DirEntry, File},
std::path::PathBuf,
thiserror::Error,
};
/// Manager for a directory used to store test results. `DirectoryManager` vends
/// paths to new subdirectories and automatically removes old directories.
///
/// `DirectoryManager` maintains two lists of subdirectories, a list of "unsaved"
/// subdirectories and a list of "saved" subdirectories.
/// Unsaved subdirectories are conceptually in a bounded queue indexed in order of
/// increasing age. When the queue is full the oldest subdirectory is deleted.
/// Saved subdirectories are identified by name and are not subject to automatic
/// deletion, although they may be deleted manually.
/// New subdirectories are unsaved by default and may be saved with |save_directory|.
pub(crate) struct DirectoryManager {
root: PathBuf,
max_entries: usize,
time_supplier: Box<dyn TimeSupplier>,
// Mapping from directory name to saved name.
saved_directories: HashMap<OsString, String>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DirectoryId {
Index(u32),
Name(String),
}
#[derive(Error, Debug)]
pub(crate) enum DirectoryError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("error serializing to disk: {0}")]
Serializer(#[from] serde_json::error::Error),
#[error("directory matching the id does not exist")]
IdNotFound(DirectoryId),
#[error("another directory is already saved with name '{0}")]
NameInUse(String),
}
const SAVED_DIRECTORIES_FILE: &str = "saved_directories.json";
type Result<T> = std::result::Result<T, DirectoryError>;
impl DirectoryManager {
/// Create a new DirectoryManager to manage directories in |root|. Ensures the
/// |root| directory exists.
pub fn new(root: PathBuf, max_entries: usize) -> Result<Self> {
Self::new_with_time_supplier(root, max_entries, RealTimeSupplier)
}
fn new_with_time_supplier<T: TimeSupplier + 'static>(
root: PathBuf,
max_entries: usize,
time_supplier: T,
) -> Result<Self> {
std::fs::DirBuilder::new().recursive(true).create(&root)?;
let saved_directories = read_saved_directories(&root).unwrap_or(HashMap::new());
Ok(Self { root, max_entries, time_supplier: Box::new(time_supplier), saved_directories })
}
/// Create a new output directory, deleting old directories as necessary.
pub fn new_directory(&mut self) -> Result<PathBuf> {
let mut unsaved_entries = self
.entries_ordered()?
.into_iter()
.filter(|(_, dir)| !self.saved_directories.contains_key(&dir.dir_name))
.collect::<Vec<_>>();
while unsaved_entries.len() >= self.max_entries {
let (_id, oldest_entry) = unsaved_entries.pop().unwrap();
std::fs::remove_dir_all(self.root.join(oldest_entry.dir_name))?;
}
let new_directory = self.root.join(timestamp(self.time_supplier.now()));
std::fs::DirBuilder::new().create(&new_directory)?;
Ok(new_directory)
}
/// Get the id and path to the most recent run directory, if any exist.
pub fn latest_directory(&self) -> Result<Option<PathBuf>> {
let entries = self.entries_ordered()?;
Ok(entries.into_iter().next().map(|(_, dir)| self.root.join(dir.dir_name)))
}
/// Get a path to the directory matching the given |id|, if one exists.
pub fn get_by_id(&self, id: DirectoryId) -> Result<Option<PathBuf>> {
Ok(self
.entries_ordered()?
.into_iter()
.find(|entry| entry.0 == id)
.map(|(_, entry)| self.root.join(entry.dir_name)))
}
/// List present directories in order of increasing age.
pub fn entries_ordered(&self) -> Result<Vec<(DirectoryId, RunDirectory)>> {
let mut directories = std::fs::read_dir(&self.root)?
.filter_map::<RunDirectory, _>(|result| match result {
Ok(dir_entry) if dir_entry.file_name() == SAVED_DIRECTORIES_FILE => None,
Ok(dir_entry) => Some(dir_entry.into()),
Err(_) => None,
})
.collect::<Vec<_>>();
directories.sort_by(cmp_run_directories_increasing_age);
let mut current_index = 0;
Ok(directories
.into_iter()
.map(|entry| match self.saved_directories.get(&entry.dir_name) {
Some(save_name) => (DirectoryId::Name(save_name.clone()), entry),
None => {
let index = current_index;
current_index += 1;
(DirectoryId::Index(index), entry)
}
})
.collect())
}
/// Mark the unsaved directory at |index| as saved and assign it the name |save_name|.
pub fn save_directory(&mut self, index: u32, save_name: String) -> Result<()> {
if self.saved_directories.values().any(|saved_name| *saved_name == save_name) {
return Err(DirectoryError::NameInUse(save_name));
}
let entries = self.entries_ordered()?;
let (_id, directory) = entries
.into_iter()
.find(|(id, _)| *id == DirectoryId::Index(index))
.ok_or_else(|| DirectoryError::IdNotFound(DirectoryId::Index(index)))?;
self.saved_directories.insert(directory.dir_name, save_name);
write_saved_directories(&self.root, &self.saved_directories)
}
/// Manually delete the directory referenced by |id|.
pub fn delete(&mut self, id: DirectoryId) -> Result<()> {
let entries = self.entries_ordered()?;
match entries.into_iter().find(|entry| entry.0 == id) {
Some((_, directory)) => {
std::fs::remove_dir_all(self.root.join(&directory.dir_name))?;
if self.saved_directories.remove(&directory.dir_name).is_some() {
// update list of saved directories if the deleted entry was saved
write_saved_directories(&self.root, &self.saved_directories)?;
}
Ok(())
}
None => Err(DirectoryError::IdNotFound(id)),
}
}
}
pub struct RunDirectory {
/// Name of the directory.
pub dir_name: OsString,
/// Timestamp, if any, parsed from the name.
pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
}
impl From<DirEntry> for RunDirectory {
fn from(dir_entry: DirEntry) -> Self {
let dir_name = dir_entry.path().iter().last().unwrap().to_os_string();
Self { timestamp: dir_name.to_str().and_then(|s| parse_timestamp(s)), dir_name }
}
}
fn cmp_run_directories_increasing_age(left: &RunDirectory, right: &RunDirectory) -> Ordering {
cmp_run_directories(left, right).reverse()
}
fn cmp_run_directories(left: &RunDirectory, right: &RunDirectory) -> Ordering {
// Prefer to order by timestamp. Entries missing timestamp are 'less' than those with a
// timestamp, and are otherwise sorted by name.
match (left.timestamp, right.timestamp) {
(Some(left_time), Some(right_time)) => left_time.cmp(&right_time),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => left.dir_name.cmp(&right.dir_name),
}
}
// serde won't serialize a HashMap with OsString as a key, so we serialize as a vector instead.
#[derive(Serialize, Deserialize)]
struct SerializableSavedDirectory {
dir_name: OsString,
saved_name: String,
}
fn read_saved_directories(root: &PathBuf) -> Result<HashMap<OsString, String>> {
let dir_file = File::open(root.join(SAVED_DIRECTORIES_FILE))?;
let saved_list: Vec<SerializableSavedDirectory> = serde_json::from_reader(dir_file)?;
Ok(saved_list
.into_iter()
.map(|serializable| (serializable.dir_name, serializable.saved_name))
.collect())
}
fn write_saved_directories(
root: &PathBuf,
saved_directories: &HashMap<OsString, String>,
) -> Result<()> {
let saved_list: Vec<SerializableSavedDirectory> = saved_directories
.iter()
.map(|(dir_name, saved_name)| SerializableSavedDirectory {
dir_name: dir_name.clone(),
saved_name: saved_name.clone(),
})
.collect();
let mut dir_file = File::create(root.join(SAVED_DIRECTORIES_FILE))?;
Ok(serde_json::to_writer_pretty(&mut dir_file, &saved_list)?)
}
/// UTC timestamp, down to millis.
const DIRECTORY_TIMESTAMP_FORMAT: &str = "%Y%m%dT%H%M%S%3f";
fn timestamp(time: chrono::DateTime<chrono::Utc>) -> String {
format!("{}", time.format(DIRECTORY_TIMESTAMP_FORMAT))
}
fn parse_timestamp(timestamp: &str) -> Option<chrono::DateTime<chrono::Utc>> {
let naive_time =
chrono::NaiveDateTime::parse_from_str(timestamp, DIRECTORY_TIMESTAMP_FORMAT).ok()?;
Some(chrono::DateTime::from_utc(naive_time, chrono::Utc))
}
/// Trait for injecting fake times in tests.
trait TimeSupplier {
fn now(&self) -> chrono::DateTime<chrono::Utc>;
}
/// Time supplier that reads the real system time.
struct RealTimeSupplier;
impl TimeSupplier for RealTimeSupplier {
fn now(&self) -> chrono::DateTime<chrono::Utc> {
chrono::Utc::now()
}
}
#[cfg(test)]
mod test {
use {
super::*, std::cell::RefCell, std::convert::TryInto, std::fs::File, std::io::Write,
std::path::Path,
};
const MAX_DIR_ENTRIES: usize = 7;
const SAVE_NAME: &str = "an interesting test run";
/// Supplies a test with fake time that is guaranteed to advance for each invocation of now().
/// This is necessary as our directories are timestamped down to the millisecond. While this is
/// a high enough resolution for test runs, under test if we rapidly create directories, time
/// may not have updated and we could get errors with name collisions.
struct FakeMonotonicTimeSupplier {
last_time: RefCell<chrono::DateTime<chrono::Utc>>,
}
impl TimeSupplier for FakeMonotonicTimeSupplier {
fn now(&self) -> chrono::DateTime<chrono::Utc> {
let mut last_time = self.last_time.borrow_mut();
let new_time = *last_time + chrono::Duration::seconds(1);
*last_time = new_time;
new_time
}
}
fn new_manager(root: &Path) -> DirectoryManager {
DirectoryManager::new_with_time_supplier(
root.to_path_buf(),
MAX_DIR_ENTRIES,
FakeMonotonicTimeSupplier { last_time: RefCell::new(chrono::Utc::now()) },
)
.expect("create directory manager")
}
#[test]
fn create_dir() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let new_directory = manager.new_directory().expect("create new directory");
assert!(new_directory.exists());
}
#[test]
fn create_dir_removes_old_dirs() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let oldest_directory = manager.new_directory().expect("create new directory");
assert!(oldest_directory.exists());
// Add a file. The manager should still be able to clean up the directory.
let mut file = File::create(oldest_directory.join("file.txt")).expect("create file");
file.write_all("hello".as_bytes()).expect("write to file");
drop(file);
let subsequent_directories =
std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(MAX_DIR_ENTRIES)
.collect::<Vec<_>>();
assert!(!oldest_directory.exists());
assert!(subsequent_directories.iter().all(|path| path.exists()));
}
fn test_with_three_directories<F>(test_fn: F)
where
F: Fn([PathBuf; 3], DirectoryManager) -> (),
{
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let mut directories =
std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(3)
.collect::<Vec<_>>();
directories.reverse();
let directories: [PathBuf; 3] = directories.try_into().unwrap();
for (index, directory) in directories.iter().enumerate() {
assert_eq!(
manager.get_by_id(DirectoryId::Index(index as u32)).expect("get by id"),
Some(directory.clone())
);
assert!(directory.exists());
}
test_fn(directories, manager);
}
#[test]
fn save_directories_updates_indices() {
test_with_three_directories(|directories, mut manager| {
// saving the most recent directory moves ids of earlier directories up
manager.save_directory(0, SAVE_NAME.to_string()).expect("save directory");
assert_eq!(
manager.get_by_id(DirectoryId::Name(SAVE_NAME.to_string())).expect("get by id"),
Some(directories[0].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Index(0)).expect("get by id"),
Some(directories[1].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Index(1)).expect("get by id"),
Some(directories[2].clone())
);
});
test_with_three_directories(|directories, mut manager| {
// saving the middle directory moves id of earliest directory
manager.save_directory(1, SAVE_NAME.to_string()).expect("save directory");
assert_eq!(
manager.get_by_id(DirectoryId::Index(0)).expect("get by id"),
Some(directories[0].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Name(SAVE_NAME.to_string())).expect("get by id"),
Some(directories[1].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Index(1)).expect("get by id"),
Some(directories[2].clone())
);
});
test_with_three_directories(|directories, mut manager| {
// saving the oldest directory does not affect ids of earlier directories
manager.save_directory(2, SAVE_NAME.to_string()).expect("save directory");
assert_eq!(
manager.get_by_id(DirectoryId::Index(0)).expect("get by id"),
Some(directories[0].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Index(1)).expect("get by id"),
Some(directories[1].clone())
);
assert_eq!(
manager.get_by_id(DirectoryId::Name(SAVE_NAME.to_string())).expect("get by id"),
Some(directories[2].clone())
);
});
}
#[test]
fn saved_dir_list_persisted() {
const SAVE_NAME_1: &str = "save-1";
const SAVE_NAME_2: &str = "save-2";
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let saved_directory_1 = manager.new_directory().expect("create new directory");
assert!(saved_directory_1.exists());
manager.save_directory(0, SAVE_NAME_1.to_string()).expect("save directory");
let saved_directory_2 = manager.new_directory().expect("create new directory");
let mut unsaved_directories =
std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(MAX_DIR_ENTRIES - 1)
.collect::<Vec<_>>();
unsaved_directories.reverse();
manager
.save_directory((MAX_DIR_ENTRIES - 1) as u32, SAVE_NAME_2.to_string())
.expect("save directory");
drop(manager);
// A new manager on the same directory should have the same list of saved directories
let new_manager = new_manager(root.path());
assert_eq!(
new_manager.get_by_id(DirectoryId::Name(SAVE_NAME_1.to_string())).expect("get by id"),
Some(saved_directory_1.clone())
);
assert!(saved_directory_1.exists());
assert_eq!(
new_manager.get_by_id(DirectoryId::Name(SAVE_NAME_2.to_string())).expect("get by id"),
Some(saved_directory_2.clone())
);
assert!(saved_directory_2.exists());
for (index, directory) in unsaved_directories.into_iter().enumerate() {
assert_eq!(
new_manager.get_by_id(DirectoryId::Index(index as u32)).expect("get by id"),
Some(directory.clone())
);
assert!(directory.exists());
}
}
#[test]
fn saved_dir_is_not_removed() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let saved_directory = manager.new_directory().expect("create new directory");
assert!(saved_directory.exists());
manager.save_directory(0, SAVE_NAME.to_string()).expect("save directory");
let subsequent_directories =
std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(MAX_DIR_ENTRIES)
.collect::<Vec<_>>();
// all the directories should still exist.
assert!(saved_directory.exists());
assert!(subsequent_directories.iter().all(|path| path.exists()));
// after creating one more, the saved directory still exists, but one of the subsequent
// directories is cleaned.
let newest_directory = manager.new_directory().expect("create new directory");
assert!(newest_directory.exists());
assert!(saved_directory.exists());
assert_eq!(
subsequent_directories.iter().filter(|path| path.exists()).count(),
MAX_DIR_ENTRIES - 1,
);
}
#[test]
fn get_by_id() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
assert_eq!(manager.get_by_id(DirectoryId::Index(0)).expect("get by id"), None);
assert_eq!(
manager.get_by_id(DirectoryId::Name(SAVE_NAME.to_string())).expect("get by id"),
None
);
let mut directories =
std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(MAX_DIR_ENTRIES)
.collect::<Vec<_>>();
directories.reverse();
for (index, directory) in directories.iter().enumerate() {
assert!(directory.exists());
assert_eq!(
manager.get_by_id(DirectoryId::Index(index as u32)).expect("get by id"),
Some(directory.clone())
);
}
let newest_directory = directories.remove(0);
manager.save_directory(0, SAVE_NAME.to_string()).expect("Save directory");
assert_eq!(
manager.get_by_id(DirectoryId::Name(SAVE_NAME.to_string())).expect("get by id"),
Some(newest_directory)
);
// Since the newest directory was assigned a name, the older directories move up an index
for (index, directory) in directories.iter().enumerate() {
assert!(directory.exists());
assert_eq!(
manager.get_by_id(DirectoryId::Index(index as u32)).expect("get by id"),
Some(directory.clone())
);
}
}
#[test]
fn delete_dir() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
let saved_directory = manager.new_directory().expect("create new directory");
manager.save_directory(0, SAVE_NAME.to_string()).expect("save directory");
let unsaved_directory = manager.new_directory().expect("create new directory");
assert!(saved_directory.exists());
assert!(unsaved_directory.exists());
manager.delete(DirectoryId::Name(SAVE_NAME.to_string())).expect("delete directory");
assert!(!saved_directory.exists());
assert!(unsaved_directory.exists());
manager.delete(DirectoryId::Index(0)).expect("delete directory");
assert!(!saved_directory.exists());
assert!(!unsaved_directory.exists());
}
#[test]
fn latest_dir() {
let root = tempfile::tempdir().expect("Create temp dir");
let mut manager = new_manager(root.path());
assert_eq!(manager.latest_directory().expect("Get latest directory"), None);
let first_directory = manager.new_directory().expect("create new directory");
assert_eq!(
manager.latest_directory().expect("get latest directory"),
Some(first_directory)
);
let _ = std::iter::repeat_with(|| manager.new_directory().expect("create new directory"))
.take(MAX_DIR_ENTRIES)
.collect::<Vec<_>>();
let new_directory = manager.new_directory().expect("create new directory");
assert_eq!(
manager.latest_directory().expect("get latest directory"),
Some(new_directory.clone())
);
// after saving the latest directory it's still returned as latest.
manager.save_directory(0, SAVE_NAME.to_string()).expect("save directory");
assert_eq!(manager.latest_directory().expect("get latest directory"), Some(new_directory));
}
#[test]
fn timestamp_round_trip() {
let now = RealTimeSupplier.now();
let timestamp = timestamp(now);
let parsed = parse_timestamp(&timestamp).expect("parse timestamp");
// Timestamp is slightly lossy
let diff = parsed - now;
assert!(diff >= chrono::Duration::milliseconds(-1));
assert!(diff <= chrono::Duration::milliseconds(1));
}
}