mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-29 22:11:17 +00:00
commit
54d0b3511a
6
.github/workflows/rust.yml
vendored
6
.github/workflows/rust.yml
vendored
@ -37,6 +37,7 @@ jobs:
|
||||
package: [
|
||||
rinja, rinja_actix, rinja_axum, rinja_derive, rinja_derive_standalone,
|
||||
rinja_parser, rinja_rocket, rinja_warp, testing, examples/actix-web-app,
|
||||
fuzzing
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -65,7 +66,8 @@ jobs:
|
||||
set -eu
|
||||
for PKG in \
|
||||
rinja rinja_actix rinja_axum rinja_derive rinja_derive_standalone \
|
||||
rinja_parser rinja_rocket rinja_warp testing examples/actix-web-app
|
||||
rinja_parser rinja_rocket rinja_warp testing examples/actix-web-app \
|
||||
fuzzing
|
||||
do
|
||||
cd "$PKG"
|
||||
echo "Testing: $PKG"
|
||||
@ -79,7 +81,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.71.0"
|
||||
toolchain: "1.71.1"
|
||||
- run: cargo check --lib -p rinja --all-features
|
||||
|
||||
Audit:
|
||||
|
19
fuzzing/Cargo.toml
Normal file
19
fuzzing/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "rinja_fuzzing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
fuzz = { path = "fuzz" }
|
||||
|
||||
arbitrary = "1.3.2"
|
||||
pretty-error-debug = "0.3.0"
|
||||
thiserror = "1.0.63"
|
||||
|
||||
[workspace]
|
||||
members = [".", "fuzz"]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
31
fuzzing/README.md
Normal file
31
fuzzing/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Rinja Fuzzing
|
||||
|
||||
First install `cargo-fuzz` and rust-nightly (once):
|
||||
|
||||
```sh
|
||||
cargo install cargo-fuzz
|
||||
rustup install nightly
|
||||
```
|
||||
|
||||
Then execute in this folder:
|
||||
|
||||
```sh
|
||||
RUST_BACKTRACE=1 nice cargo +nightly fuzz run <fuzz_target>
|
||||
```
|
||||
|
||||
`fuzz_target` is one out of `all`, `filters`, `html` or `parser`.
|
||||
|
||||
The execution won't stop, but continue until you kill it with ctrl+c.
|
||||
Or until it finds a panic.
|
||||
If the execution found a panic, then a file with the input scenario is written, e.g.
|
||||
`fuzz/artifacts/parser/crash-b91ab…`.
|
||||
To get more information about the failed scenario, run or debug this command with the given path:
|
||||
|
||||
```sh
|
||||
cargo run -- <fuzz_target> fuzz/artifacts/parser/crash-b91ab…
|
||||
```
|
||||
|
||||
Find more information about fuzzing here:
|
||||
|
||||
* `cargo fuzz help run`
|
||||
* <https://rust-fuzz.github.io/book/cargo-fuzz.html>
|
2
fuzzing/fuzz/.gitignore
vendored
Normal file
2
fuzzing/fuzz/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/artifacts/
|
||||
/corpus/
|
42
fuzzing/fuzz/Cargo.toml
Normal file
42
fuzzing/fuzz/Cargo.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "fuzz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
rinja = { path = "../../rinja" }
|
||||
rinja_parser = { path = "../../rinja_parser" }
|
||||
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
html-escape = "0.2.13"
|
||||
libfuzzer-sys = "0.4.7"
|
||||
thiserror = "1.0.63"
|
||||
|
||||
[[bin]]
|
||||
name = "all"
|
||||
path = "fuzz_targets/all.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "filters"
|
||||
path = "fuzz_targets/filters.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "html"
|
||||
path = "fuzz_targets/html.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "parser"
|
||||
path = "fuzz_targets/parser.rs"
|
||||
test = false
|
||||
doc = false
|
5
fuzzing/fuzz/fuzz_targets/all.rs
Normal file
5
fuzzing/fuzz/fuzz_targets/all.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#![no_main]
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = <fuzz::all::Scenario as fuzz::Scenario>::fuzz(data);
|
||||
});
|
5
fuzzing/fuzz/fuzz_targets/filters.rs
Normal file
5
fuzzing/fuzz/fuzz_targets/filters.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#![no_main]
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = <fuzz::filters::Scenario as fuzz::Scenario>::fuzz(data);
|
||||
});
|
5
fuzzing/fuzz/fuzz_targets/html.rs
Normal file
5
fuzzing/fuzz/fuzz_targets/html.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#![no_main]
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = <fuzz::html::Scenario as fuzz::Scenario>::fuzz(data);
|
||||
});
|
5
fuzzing/fuzz/fuzz_targets/parser.rs
Normal file
5
fuzzing/fuzz/fuzz_targets/parser.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#![no_main]
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = <fuzz::parser::Scenario as fuzz::Scenario>::fuzz(data);
|
||||
});
|
71
fuzzing/fuzz/src/all.rs
Normal file
71
fuzzing/fuzz/src/all.rs
Normal file
@ -0,0 +1,71 @@
|
||||
macro_rules! this_file {
|
||||
($(crate :: $mod:ident :: $ty:ident;)*) => {
|
||||
use std::fmt;
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Scenario<'a> {
|
||||
$($ty(crate::$mod::Scenario<'a>),)*
|
||||
}
|
||||
|
||||
impl<'a> super::Scenario<'a> for Scenario<'a> {
|
||||
type RunError = RunError;
|
||||
|
||||
fn new(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
|
||||
let mut data = Unstructured::new(data);
|
||||
let target = Arbitrary::arbitrary(&mut data)?;
|
||||
let data = Arbitrary::arbitrary_take_rest(data)?;
|
||||
|
||||
match target {
|
||||
$(Target::$ty => Ok(Self::$ty(crate::$mod::Scenario::new(data)?)),)*
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<(), Self::RunError> {
|
||||
match self {
|
||||
$(Self::$ty(scenario) => scenario.run().map_err(RunError::$ty),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Arbitrary)]
|
||||
enum Target {
|
||||
$($ty,)*
|
||||
}
|
||||
|
||||
pub enum RunError {
|
||||
$($ty(<crate::$mod::Scenario<'static> as crate::Scenario<'static>>::RunError),)*
|
||||
}
|
||||
|
||||
impl fmt::Display for RunError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
$(Self::$ty(err) => err.fmt(f),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RunError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
$(Self::$ty(err) => err.fmt(f),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RunError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
$(RunError::$ty(err) => Some(err),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this_file! {
|
||||
crate::filters::Filters;
|
||||
crate::html::Html;
|
||||
crate::parser::Parser;
|
||||
}
|
88
fuzzing/fuzz/src/filters.rs
Normal file
88
fuzzing/fuzz/src/filters.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use rinja::filters;
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
pub enum Scenario<'a> {
|
||||
Text(Text<'a>),
|
||||
}
|
||||
|
||||
impl<'a> super::Scenario<'a> for Scenario<'a> {
|
||||
type RunError = rinja::Error;
|
||||
|
||||
fn new(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
|
||||
Self::arbitrary_take_rest(Unstructured::new(data))
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<(), Self::RunError> {
|
||||
match *self {
|
||||
Self::Text(text) => run_text(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_text(filter: Text<'_>) -> Result<(), rinja::Error> {
|
||||
let Text { input, filter } = filter;
|
||||
let _ = match filter {
|
||||
TextFilter::Capitalize => filters::capitalize(input)?.to_string(),
|
||||
TextFilter::Center(a) => filters::center(input, a)?.to_string(),
|
||||
TextFilter::Indent(a) => filters::indent(input, a)?.to_string(),
|
||||
TextFilter::Linebreaks => filters::linebreaks(input)?.to_string(),
|
||||
TextFilter::LinebreaksBr => filters::linebreaksbr(input)?.to_string(),
|
||||
TextFilter::Lowercase => filters::lowercase(input)?.to_string(),
|
||||
TextFilter::ParagraphBreaks => filters::paragraphbreaks(input)?.to_string(),
|
||||
TextFilter::Safe(e) => match e {
|
||||
Escaper::Html => filters::safe(input, filters::Html)?.to_string(),
|
||||
Escaper::Text => filters::safe(input, filters::Text)?.to_string(),
|
||||
},
|
||||
TextFilter::Title => filters::title(input)?.to_string(),
|
||||
TextFilter::Trim => filters::trim(input)?.to_string(),
|
||||
TextFilter::Truncate(a) => filters::truncate(input, a)?.to_string(),
|
||||
TextFilter::Uppercase => filters::uppercase(input)?.to_string(),
|
||||
TextFilter::Urlencode => filters::urlencode(input)?.to_string(),
|
||||
TextFilter::UrlencodeStrict => filters::urlencode_strict(input)?.to_string(),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
pub struct Text<'a> {
|
||||
input: &'a str,
|
||||
filter: TextFilter,
|
||||
}
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
enum TextFilter {
|
||||
Capitalize,
|
||||
Center(usize),
|
||||
Indent(usize),
|
||||
Linebreaks,
|
||||
LinebreaksBr,
|
||||
Lowercase,
|
||||
ParagraphBreaks,
|
||||
Safe(Escaper),
|
||||
Title,
|
||||
Trim,
|
||||
Truncate(usize),
|
||||
Uppercase,
|
||||
Urlencode,
|
||||
UrlencodeStrict,
|
||||
}
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
enum Escaper {
|
||||
Html,
|
||||
Text,
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// abs
|
||||
// escape,
|
||||
// filesizeformat
|
||||
// fmt
|
||||
// format
|
||||
// into_f64
|
||||
// into_isize
|
||||
// join
|
||||
// json
|
||||
// json_pretty
|
||||
// wordcount
|
1
fuzzing/fuzz/src/html/html.rs
Symbolic link
1
fuzzing/fuzz/src/html/html.rs
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../rinja/src/html.rs
|
43
fuzzing/fuzz/src/html/mod.rs
Normal file
43
fuzzing/fuzz/src/html/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod html;
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use html_escape::decode_html_entities_to_string;
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
pub enum Scenario<'a> {
|
||||
String(&'a str),
|
||||
Char(char),
|
||||
}
|
||||
|
||||
impl<'a> super::Scenario<'a> for Scenario<'a> {
|
||||
type RunError = std::convert::Infallible;
|
||||
|
||||
fn new(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
|
||||
Self::arbitrary_take_rest(Unstructured::new(data))
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<(), Self::RunError> {
|
||||
match *self {
|
||||
Scenario::String(src) => {
|
||||
let mut dest = String::with_capacity(src.len());
|
||||
html::write_escaped_str(&mut dest, src).unwrap();
|
||||
|
||||
let mut unescaped = String::with_capacity(src.len());
|
||||
let unescaped = decode_html_entities_to_string(dest, &mut unescaped);
|
||||
assert_eq!(src, unescaped);
|
||||
}
|
||||
Scenario::Char(c) => {
|
||||
let mut dest = String::with_capacity(6);
|
||||
html::write_escaped_char(&mut dest, c).unwrap();
|
||||
|
||||
let mut src = [0; 4];
|
||||
let src = c.encode_utf8(&mut src);
|
||||
let mut unescaped = String::with_capacity(4);
|
||||
let unescaped = decode_html_entities_to_string(dest, &mut unescaped);
|
||||
assert_eq!(src, unescaped);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
99
fuzzing/fuzz/src/lib.rs
Normal file
99
fuzzing/fuzz/src/lib.rs
Normal file
@ -0,0 +1,99 @@
|
||||
pub mod all;
|
||||
pub mod filters;
|
||||
pub mod html;
|
||||
pub mod parser;
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
pub const TARGETS: &[(&str, TargetBuilder)] = &[
|
||||
("all", |data| NamedTarget::new::<all::Scenario>(data)),
|
||||
("filters", |data| {
|
||||
NamedTarget::new::<filters::Scenario>(data)
|
||||
}),
|
||||
("html", |data| NamedTarget::new::<html::Scenario>(data)),
|
||||
("parser", |data| NamedTarget::new::<parser::Scenario>(data)),
|
||||
];
|
||||
|
||||
pub type TargetBuilder = for<'a> fn(&'a [u8]) -> Result<NamedTarget<'a>, arbitrary::Error>;
|
||||
|
||||
pub trait Scenario<'a>: fmt::Debug + Sized {
|
||||
type RunError: Error + Send + 'static;
|
||||
|
||||
fn fuzz(data: &'a [u8]) -> Result<(), FuzzError<Self::RunError>> {
|
||||
Self::new(data)
|
||||
.map_err(FuzzError::New)?
|
||||
.run()
|
||||
.map_err(FuzzError::Run)
|
||||
}
|
||||
|
||||
fn new(data: &'a [u8]) -> Result<Self, arbitrary::Error>;
|
||||
fn run(&self) -> Result<(), Self::RunError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FuzzError<RunError: Error + Send + 'static> {
|
||||
#[error("could not build scenario")]
|
||||
New(#[source] arbitrary::Error),
|
||||
#[error("could not run scenario")]
|
||||
Run(#[source] RunError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct DisplayTargets;
|
||||
|
||||
impl fmt::Display for DisplayTargets {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (idx, &(name, _)) in TARGETS.iter().enumerate() {
|
||||
match idx {
|
||||
0 => write!(f, "{name}")?,
|
||||
_ => write!(f, "|{name}")?,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NamedTarget<'a>(Box<dyn RunScenario<'a> + 'a>);
|
||||
|
||||
impl NamedTarget<'_> {
|
||||
#[inline]
|
||||
pub fn run(&self) -> Result<(), Box<dyn Error + Send + 'static>> {
|
||||
self.0.run()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for NamedTarget<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.debug(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> NamedTarget<'a> {
|
||||
#[inline]
|
||||
fn new<S: Scenario<'a> + 'a>(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
|
||||
Ok(Self(Box::new(S::new(data)?)))
|
||||
}
|
||||
}
|
||||
|
||||
trait RunScenario<'a> {
|
||||
fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
|
||||
|
||||
fn run(&self) -> Result<(), Box<dyn Error + Send + 'static>>;
|
||||
}
|
||||
|
||||
impl<'a, T: Scenario<'a>> RunScenario<'a> for T {
|
||||
#[inline]
|
||||
fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.fmt(f)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn run(&self) -> Result<(), Box<dyn Error + Send + 'static>> {
|
||||
match self.run() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
42
fuzzing/fuzz/src/parser.rs
Normal file
42
fuzzing/fuzz/src/parser.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use rinja_parser::{Ast, Syntax, SyntaxBuilder};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Scenario<'a> {
|
||||
syntax: Syntax<'a>,
|
||||
src: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> super::Scenario<'a> for Scenario<'a> {
|
||||
type RunError = rinja_parser::ParseError;
|
||||
|
||||
fn new(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
|
||||
let mut data = Unstructured::new(data);
|
||||
|
||||
let syntax = if let Some(syntax) = <Option<[Option<&'a str>; 6]>>::arbitrary(&mut data)? {
|
||||
SyntaxBuilder {
|
||||
name: "test",
|
||||
block_start: syntax[0],
|
||||
block_end: syntax[1],
|
||||
expr_start: syntax[2],
|
||||
expr_end: syntax[3],
|
||||
comment_start: syntax[4],
|
||||
comment_end: syntax[5],
|
||||
}
|
||||
.to_syntax()
|
||||
.map_err(|_| arbitrary::Error::IncorrectFormat)?
|
||||
} else {
|
||||
Syntax::default()
|
||||
};
|
||||
|
||||
let src = <&str>::arbitrary_take_rest(data)?;
|
||||
|
||||
Ok(Self { syntax, src })
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<(), Self::RunError> {
|
||||
let Scenario { syntax, src } = self;
|
||||
Ast::from_str(src, None, syntax)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
58
fuzzing/src/main.rs
Normal file
58
fuzzing/src/main.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::env::args_os;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fuzz::{DisplayTargets, TARGETS};
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let mut args = args_os().fuse();
|
||||
let exe = args.next().map(PathBuf::from);
|
||||
let name = args.next().and_then(|s| s.into_string().ok());
|
||||
let path = args.next().map(PathBuf::from);
|
||||
let empty = args.next().map(|_| ());
|
||||
|
||||
let (Some(name), Some(path), None) = (name, path, empty) else {
|
||||
return Err(Error::Usage(exe));
|
||||
};
|
||||
|
||||
let scenario_builder = TARGETS
|
||||
.iter()
|
||||
.find_map(|&(scenario, func)| (scenario == name).then_some(func))
|
||||
.ok_or(Error::Target(name))?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
match OpenOptions::new().read(true).open(Path::new(&path)) {
|
||||
Ok(mut f) => {
|
||||
f.read_to_end(&mut data)
|
||||
.map_err(|err| Error::Read(err, path))?;
|
||||
}
|
||||
Err(err) => return Err(Error::Open(err, path)),
|
||||
};
|
||||
|
||||
let scenario = scenario_builder(&data).map_err(Error::Build)?;
|
||||
eprintln!("{scenario:#?}");
|
||||
scenario.run().map_err(Error::Run)?;
|
||||
println!("Success.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, pretty_error_debug::Debug)]
|
||||
enum Error {
|
||||
#[error(
|
||||
"wrong arguments supplied\nUsage: {} <{DisplayTargets}> <path>",
|
||||
.0.as_deref().unwrap_or(Path::new("rinja_fuzzing")).display(),
|
||||
)]
|
||||
Usage(Option<PathBuf>),
|
||||
#[error("unknown fuzzing target {:?}\nImplemented targets: {DisplayTargets}", .0)]
|
||||
Target(String),
|
||||
#[error("could not open input file {}", .1.display())]
|
||||
Open(#[source] std::io::Error, PathBuf),
|
||||
#[error("could not read opened input file {}", .1.display())]
|
||||
Read(#[source] std::io::Error, PathBuf),
|
||||
#[error("could not build scenario")]
|
||||
Build(#[source] arbitrary::Error),
|
||||
#[error("could not run scenario")]
|
||||
Run(#[source] Box<dyn std::error::Error + Send + 'static>),
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user