Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic file loaders #214

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
<!-- next-header -->
## [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

### Internal
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
234 changes: 156 additions & 78 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,134 @@ 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)]
pub struct TryCmdLoaders(BTreeMap<std::ffi::OsString, TryCmdLoader>);

// Rust <1.70 cannot derive(Debug) for function pointers.
// TODO remove once MSRV >= 1.70.
impl std::fmt::Debug for TryCmdLoaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner = f
.debug_map()
.entries(self.0.keys().map(|k| (k, "<function>")))
.finish()?;

f.debug_struct("TryCmdLoaders").field("0", &inner).finish()
}
}

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(crate) struct TryCmd {
pub(crate) steps: Vec<Step>,
pub(crate) fs: Filesystem,
pub struct TryCmd {
pub steps: Vec<Step>,
pub fs: Filesystem,
}

impl TryCmd {
pub(crate) fn load(path: &std::path::Path) -> Result<Self, crate::Error> {
let mut sequence = if let Some(ext) = path.extension() {
if ext == std::ffi::OsStr::new("toml") {
let raw = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
let one_shot = OneShot::parse_toml(&raw)?;
let mut sequence: Self = one_shot.into();
let is_binary = match sequence.steps[0].binary {
true => snapbox::DataFormat::Binary,
false => snapbox::DataFormat::Text,
};
/// Construct an instance from a TOML file.
pub 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))?;
let one_shot = OneShot::parse_toml(&raw)?;
let mut sequence: Self = one_shot.into();
let is_binary = match sequence.steps[0].binary {
true => snapbox::DataFormat::Binary,
false => snapbox::DataFormat::Text,
};

if sequence.steps[0].stdin.is_none() {
let stdin_path = path.with_extension("stdin");
let stdin = if stdin_path.exists() {
// No `map_text` as we will trust what the user inputted
Some(crate::Data::read_from(&stdin_path, Some(is_binary))?)
} else {
None
};
sequence.steps[0].stdin = stdin;
}
if sequence.steps[0].stdin.is_none() {
let stdin_path = path.with_extension("stdin");
let stdin = if stdin_path.exists() {
// No `map_text` as we will trust what the user inputted
Some(crate::Data::read_from(&stdin_path, Some(is_binary))?)
} else {
None
};
sequence.steps[0].stdin = stdin;
}

if sequence.steps[0].expected_stdout.is_none() {
let stdout_path = path.with_extension("stdout");
let stdout = if stdout_path.exists() {
Some(
crate::Data::read_from(&stdout_path, Some(is_binary))?
.normalize(NormalizePaths)
.normalize(NormalizeNewlines),
)
} else {
None
};
sequence.steps[0].expected_stdout = stdout;
}
if sequence.steps[0].expected_stdout.is_none() {
let stdout_path = path.with_extension("stdout");
let stdout = if stdout_path.exists() {
Some(
crate::Data::read_from(&stdout_path, Some(is_binary))?
.normalize(NormalizePaths)
.normalize(NormalizeNewlines),
)
} else {
None
};
sequence.steps[0].expected_stdout = stdout;
}

if sequence.steps[0].expected_stderr.is_none() {
let stderr_path = path.with_extension("stderr");
let stderr = if stderr_path.exists() {
Some(
crate::Data::read_from(&stderr_path, Some(is_binary))?
.normalize(NormalizePaths)
.normalize(NormalizeNewlines),
)
} else {
None
};
sequence.steps[0].expected_stderr = stderr;
}
if sequence.steps[0].expected_stderr.is_none() {
let stderr_path = path.with_extension("stderr");
let stderr = if stderr_path.exists() {
Some(
crate::Data::read_from(&stderr_path, Some(is_binary))?
.normalize(NormalizePaths)
.normalize(NormalizeNewlines),
)
} else {
None
};
sequence.steps[0].expected_stderr = stderr;
}

sequence
} else if ext == std::ffi::OsStr::new("trycmd") || ext == std::ffi::OsStr::new("md") {
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)?
Ok(sequence)
}

/// Construct an instance from a .trycmd file.
pub 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)
}

/// 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() {
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 Expand Up @@ -566,22 +630,34 @@ impl From<OneShot> for TryCmd {
}
}

/// A command invocation and its expected result.
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub(crate) struct Step {
pub(crate) id: Option<String>,
pub(crate) bin: Option<Bin>,
pub(crate) args: Vec<String>,
pub(crate) env: Env,
pub(crate) stdin: Option<crate::Data>,
pub(crate) stderr_to_stdout: bool,
pub(crate) expected_status_source: Option<usize>,
pub(crate) expected_status: Option<CommandStatus>,
pub(crate) expected_stdout_source: Option<std::ops::Range<usize>>,
pub(crate) expected_stdout: Option<crate::Data>,
pub(crate) expected_stderr_source: Option<std::ops::Range<usize>>,
pub(crate) expected_stderr: Option<crate::Data>,
pub(crate) binary: bool,
pub(crate) timeout: Option<std::time::Duration>,
pub struct Step {
/// Uniquely identifies this step from others.
pub id: Option<String>,
/// The program that will be executed.
pub bin: Option<Bin>,
/// Arguments to the executed program.
pub args: Vec<String>,
/// Environment variables for the execution.
pub env: Env,
/// Process stdin
pub stdin: Option<crate::Data>,
/// Whether to redirect stderr to stdout.
pub stderr_to_stdout: bool,
pub expected_status_source: Option<usize>,
/// Expected command status result.
pub expected_status: Option<CommandStatus>,
pub expected_stdout_source: Option<std::ops::Range<usize>>,
/// Expected process stdout content.
pub expected_stdout: Option<crate::Data>,
pub expected_stderr_source: Option<std::ops::Range<usize>>,
/// Expected process stderr content.
pub expected_stderr: Option<crate::Data>,
/// Whether process output is binary (as opposed to text).
pub binary: bool,
/// How long to wait for process to exit before timing out.
pub timeout: Option<std::time::Duration>,
}

impl Step {
Expand Down Expand Up @@ -756,10 +832,12 @@ impl serde::ser::Serialize for JoinedArgs {
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Filesystem {
pub(crate) cwd: Option<std::path::PathBuf>,
/// Current working directory.
pub cwd: Option<std::path::PathBuf>,
/// Sandbox base
pub(crate) base: Option<std::path::PathBuf>,
pub(crate) sandbox: Option<bool>,
pub base: Option<std::path::PathBuf>,
/// Whether to create a sandboxed copy of the base at run-time.
pub sandbox: Option<bool>,
}

impl Filesystem {
Expand Down Expand Up @@ -789,15 +867,15 @@ impl Filesystem {
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Env {
#[serde(default)]
pub(crate) inherit: Option<bool>,
pub inherit: Option<bool>,
#[serde(default)]
pub(crate) add: BTreeMap<String, String>,
pub add: BTreeMap<String, String>,
#[serde(default)]
pub(crate) remove: Vec<String>,
pub remove: Vec<String>,
}

impl Env {
pub(crate) fn update(&mut self, other: &Self) {
pub fn update(&mut self, other: &Self) {
if self.inherit.is_none() {
self.inherit = other.inherit;
}
Expand Down