Add a cached fs abstraction

This commit is contained in:
Omar Abdulla
2025-08-14 17:38:12 +03:00
parent f2045db0e9
commit 7247eca2e8
11 changed files with 230 additions and 19 deletions
+2
View File
@@ -10,5 +10,7 @@ rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
semver = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] }
+49
View File
@@ -0,0 +1,49 @@
//! This module implements a cached file system allowing for results to be stored in-memory rather
//! rather being queried from the file system again.
use std::fs;
use std::io::{Error, Result};
use std::path::{Path, PathBuf};
use moka::sync::Cache;
use once_cell::sync::Lazy;
pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
static READ_CACHE: Lazy<Cache<PathBuf, Vec<u8>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_CACHE.get(path.as_path()) {
Some(content) => Ok(content),
None => {
let content = fs::read(path.as_path())?;
READ_CACHE.insert(path, content.clone());
Ok(content)
}
}
}
pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
let content = read(path)?;
String::from_utf8(content).map_err(|_| {
Error::new(
std::io::ErrorKind::InvalidData,
"The contents of the file are not valid UTF8",
)
})
}
pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result<PathBuf>>>> {
static READ_DIR_CACHE: Lazy<Cache<PathBuf, Vec<PathBuf>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_DIR_CACHE.get(path.as_path()) {
Some(entries) => Ok(Box::new(entries.into_iter().map(Ok)) as Box<_>),
None => {
let entries = fs::read_dir(path.as_path())?
.flat_map(|maybe_entry| maybe_entry.map(|entry| entry.path()))
.collect();
READ_DIR_CACHE.insert(path.clone(), entries);
Ok(read_dir(path).unwrap())
}
}
}
@@ -19,6 +19,11 @@ pub struct FilesWithExtensionIterator {
/// this vector then they will be returned when the [`Iterator::next`] method is called. If not
/// then we visit one of the next directories to visit.
files_matching_allowed_extensions: Vec<PathBuf>,
/// This option controls if the the cached file system should be used or not. This could be
/// better for certain cases where the entries in the directories do not change and therefore
/// caching can be used.
use_cached_fs: bool,
}
impl FilesWithExtensionIterator {
@@ -27,6 +32,7 @@ impl FilesWithExtensionIterator {
allowed_extensions: Default::default(),
directories_to_search: vec![root_directory.as_ref().to_path_buf()],
files_matching_allowed_extensions: Default::default(),
use_cached_fs: Default::default(),
}
}
@@ -37,6 +43,11 @@ impl FilesWithExtensionIterator {
self.allowed_extensions.insert(allowed_extension.into());
self
}
pub fn with_use_cached_fs(mut self, use_cached_fs: bool) -> Self {
self.use_cached_fs = use_cached_fs;
self
}
}
impl Iterator for FilesWithExtensionIterator {
@@ -49,16 +60,19 @@ impl Iterator for FilesWithExtensionIterator {
let directory_to_search = self.directories_to_search.pop()?;
// Read all of the entries in the directory. If we failed to read this dir's entires then we
// elect to just ignore it and look in the next directory, we do that by calling the next
// method again on the iterator, which is an intentional decision that we made here instead
// of panicking.
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else {
return self.next();
let iterator = if self.use_cached_fs {
let Ok(dir_entries) = crate::cached_fs::read_dir(directory_to_search.as_path()) else {
return self.next();
};
Box::new(dir_entries) as Box<dyn Iterator<Item = std::io::Result<PathBuf>>>
} else {
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else {
return self.next();
};
Box::new(dir_entries.map(|maybe_entry| maybe_entry.map(|entry| entry.path()))) as Box<_>
};
for entry in dir_entries.flatten() {
let entry_path = entry.path();
for entry_path in iterator.flatten() {
if entry_path.is_dir() {
self.directories_to_search.push(entry_path)
} else if entry_path.is_file()
+1
View File
@@ -1,6 +1,7 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from.
pub mod cached_fs;
pub mod fs;
pub mod futures;
pub mod iterators;
+1 -1
View File
@@ -5,7 +5,6 @@
use std::{
collections::HashMap,
fs::read_to_string,
hash::Hash,
path::{Path, PathBuf},
};
@@ -16,6 +15,7 @@ use semver::Version;
use serde::{Deserialize, Serialize};
use revive_common::EVMVersion;
use revive_dt_common::cached_fs::read_to_string;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
+1
View File
@@ -714,6 +714,7 @@ async fn compile_contracts<P: Platform>(
// library.
compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.with_use_cached_fs(true)
.fold(compiler, |compiler, path| {
compiler.with_library(&path, library_ident.as_str(), *library_address)
});
+4 -4
View File
@@ -3,6 +3,7 @@ use std::{
path::{Path, PathBuf},
};
use revive_dt_common::cached_fs::read_dir;
use serde::{Deserialize, Serialize};
use crate::metadata::MetadataFile;
@@ -54,7 +55,7 @@ impl Corpus {
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
if path.is_dir() {
let dir_entry = match std::fs::read_dir(path) {
let dir_entry = match read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
@@ -62,8 +63,8 @@ pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
}
};
for entry in dir_entry {
let entry = match entry {
for path in dir_entry {
let path = match path {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
@@ -71,7 +72,6 @@ pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
+7 -3
View File
@@ -2,7 +2,7 @@ use std::{
cmp::Ordering,
collections::BTreeMap,
fmt::Display,
fs::{File, read_to_string},
fs::File,
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
@@ -11,7 +11,9 @@ use std::{
use serde::{Deserialize, Serialize};
use revive_common::EVMVersion;
use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type};
use revive_dt_common::{
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
};
use crate::{
case::Case,
@@ -259,7 +261,9 @@ impl Metadata {
Ok(Box::new(std::iter::once(metadata_file_path.clone())))
} else {
Ok(Box::new(
FilesWithExtensionIterator::new(self.directory()?).with_allowed_extension("sol"),
FilesWithExtensionIterator::new(self.directory()?)
.with_allowed_extension("sol")
.with_use_cached_fs(true),
))
}
}
+2 -1
View File
@@ -1,11 +1,12 @@
//! This crate implements concurrent handling of testing node.
use std::{
fs::read_to_string,
sync::atomic::{AtomicUsize, Ordering},
thread,
};
use revive_dt_common::cached_fs::read_to_string;
use anyhow::Context;
use revive_dt_config::Arguments;