Skip to content

Commit

Permalink
Support registering custom file loaders
Browse files Browse the repository at this point in the history
Before, there was a static mapping of supported file extensions
to the functions that loaded them. It wasn't possible to define
your own file formats or to customize the loading of the built-in
extensions by providing your own code. This commit changes that.

File loading now takes a mapping of file extensions to functions to
load them. This mapping is consulted to determine how to load each
encountered file.

The public `TestCases` struct gains a new public API to register
a custom loader for a file extension.

This feature enables end-users to leverage the power of the `trycmd`
runner without being constrained to the built-in semantics for TOML
and `trycmd` file handling.
  • Loading branch information
indygreg committed Jun 4, 2023
1 parent e28e3c0 commit b9efa38
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] - ReleaseDate

- Structs in `trycmd::schema` are now `pub` instead of `pub(crate)`. Their fields are also `pub` instead of `pub(crate)`.
- Custom file loading support.
- `TestCases::file_extension_loader()` allows registering a function for loading a file.
- You can now define your own file formats or bring modified code for loading `toml` and `trycmd` files.

## [0.14.16] - 2023-04-13

Expand Down
27 changes: 26 additions & 1 deletion src/cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct TestCases {
bins: std::cell::RefCell<crate::BinRegistry>,
substitutions: std::cell::RefCell<snapbox::Substitutions>,
has_run: std::cell::Cell<bool>,
file_loaders: std::cell::RefCell<crate::schema::TryCmdLoaders>,
}

impl TestCases {
Expand Down Expand Up @@ -106,6 +107,25 @@ impl TestCases {
self
}

/// Define a function used to load a filesystem path into a test.
///
/// `extension` is the file extension to register the loader for, without
/// the leading dot. e.g. `toml`, `json`, or `trycmd`.
///
/// By default there are loaders for `toml`, `trycmd`, and `md` extensions.
/// Calling this function with those extensions will overwrite the default
/// loaders.
pub fn file_extension_loader(
&self,
extension: impl Into<std::ffi::OsString>,
loader: crate::schema::TryCmdLoader,
) -> &Self {
self.file_loaders
.borrow_mut()
.insert(extension.into(), loader);
self
}

/// Add a variable for normalizing output
///
/// Variable names must be
Expand Down Expand Up @@ -162,7 +182,12 @@ impl TestCases {
mode.initialize().unwrap();

let runner = self.runner.borrow_mut().prepare();
runner.run(&mode, &self.bins.borrow(), &self.substitutions.borrow());
runner.run(
&self.file_loaders.borrow(),
&mode,
&self.bins.borrow(),
&self.substitutions.borrow(),
);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl Runner {

pub(crate) fn run(
&self,
loaders: &crate::schema::TryCmdLoaders,
mode: &Mode,
bins: &crate::BinRegistry,
substitutions: &snapbox::Substitutions,
Expand All @@ -46,7 +47,7 @@ impl Runner {
.cases
.par_iter()
.flat_map(|c| {
let results = c.run(mode, bins, substitutions);
let results = c.run(loaders, mode, bins, substitutions);

let stderr = stderr();
let mut stderr = stderr.lock();
Expand Down Expand Up @@ -137,6 +138,7 @@ impl Case {

pub(crate) fn run(
&self,
loaders: &crate::schema::TryCmdLoaders,
mode: &Mode,
bins: &crate::BinRegistry,
substitutions: &snapbox::Substitutions,
Expand All @@ -153,7 +155,7 @@ impl Case {
return vec![Err(output)];
}

let mut sequence = match crate::schema::TryCmd::load(&self.path) {
let mut sequence = match crate::schema::TryCmd::load(loaders, &self.path) {
Ok(sequence) => sequence,
Err(e) => {
let output = Output::step(self.path.clone(), "setup".into());
Expand Down
52 changes: 47 additions & 5 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ use snapbox::{NormalizeNewlines, NormalizePaths};
use std::collections::BTreeMap;
use std::collections::VecDeque;

/// A function that turns a filesystem path into a [TryCmd].
pub type TryCmdLoader = fn(&std::path::Path) -> Result<TryCmd, crate::Error>;

/// Mapping of file extension to function that can load it.
#[derive(Clone, Debug)]
pub struct TryCmdLoaders(BTreeMap<std::ffi::OsString, TryCmdLoader>);

impl Default for TryCmdLoaders {
fn default() -> Self {
let mut res = BTreeMap::new();

res.insert("toml".to_string().into(), TryCmd::load_toml as _);
res.insert("trycmd".to_string().into(), TryCmd::load_trycmd as _);
res.insert("md".to_string().into(), TryCmd::load_trycmd as _);

Self(res)
}
}

impl std::ops::Deref for TryCmdLoaders {
type Target =
BTreeMap<std::ffi::OsString, fn(&std::path::Path) -> Result<TryCmd, crate::Error>>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::DerefMut for TryCmdLoaders {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

/// Represents an executable set of commands and their environment.
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct TryCmd {
Expand All @@ -14,6 +48,7 @@ pub struct TryCmd {
}

impl TryCmd {
/// Construct an instance from a TOML file.
pub(crate) fn load_toml(path: &std::path::Path) -> Result<Self, crate::Error> {
let raw = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
Expand Down Expand Up @@ -66,19 +101,26 @@ impl TryCmd {
Ok(sequence)
}

/// Construct an instance from a .trycmd file.
pub(crate) fn load_trycmd(path: &std::path::Path) -> Result<Self, crate::Error> {
let raw = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
let normalized = snapbox::utils::normalize_lines(&raw);
Self::parse_trycmd(&normalized)
}

pub(crate) fn load(path: &std::path::Path) -> Result<Self, crate::Error> {
/// Construct an instance from a path, using the file extension to determine the type.
pub(crate) fn load(
loaders: &TryCmdLoaders,
path: &std::path::Path,
) -> Result<Self, crate::Error> {
let mut sequence = if let Some(ext) = path.extension() {
if ext == std::ffi::OsStr::new("toml") {
Self::load_toml(path)?
} else if ext == std::ffi::OsStr::new("trycmd") || ext == std::ffi::OsStr::new("md") {
Self::load_trycmd(path)?
let loader = loaders
.iter()
.find_map(|(x, loader)| if ext == x { Some(loader) } else { None });

if let Some(loader) = loader {
loader(path)?
} else {
return Err(format!("Unsupported extension: {}", ext.to_string_lossy()).into());
}
Expand Down

0 comments on commit b9efa38

Please sign in to comment.