use std::ffi::{OsStr, OsString};
use std::fmt::Display;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::time::Duration;
use derive_more::AsRef;
use gungraun_macros::IntoInner;
use crate::{__internal, DelayKind, ExitWith, Stdin, Stdio, ValgrindTool};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BenchmarkId(String);
#[derive(Debug, Default, Clone, IntoInner, AsRef)]
pub struct BinaryBenchmarkConfig(__internal::InternalBinaryBenchmarkConfig);
#[derive(Debug, Default, PartialEq, Clone)]
pub struct BinaryBenchmarkGroup {
pub binary_benchmarks: Vec<BinaryBenchmark>,
}
#[derive(Debug, Clone)]
pub struct Bench {
pub commands: Vec<__internal::InternalCommandKind>,
pub config: Option<__internal::InternalBinaryBenchmarkConfig>,
pub id: BenchmarkId,
pub setup: __internal::InternalBinAssistantKind,
pub teardown: __internal::InternalBinAssistantKind,
}
#[derive(Debug, Clone)]
pub struct BinaryBenchmark {
pub benches: Vec<Bench>,
pub config: Option<__internal::InternalBinaryBenchmarkConfig>,
pub id: BenchmarkId,
pub setup: Option<fn()>,
pub teardown: Option<fn()>,
}
#[derive(Debug, Default, Clone, PartialEq, IntoInner, AsRef)]
pub struct Command(__internal::InternalCommand);
#[derive(Debug, Default, Clone, PartialEq, Eq, IntoInner, AsRef)]
pub struct Delay(__internal::InternalDelay);
#[derive(Debug, Clone, IntoInner, AsRef)]
pub struct Sandbox(__internal::InternalSandbox);
impl Bench {
pub fn new<T>(id: T) -> Self
where
T: Into<BenchmarkId>,
{
Self {
id: id.into(),
config: None,
commands: vec![],
setup: __internal::InternalBinAssistantKind::None,
teardown: __internal::InternalBinAssistantKind::None,
}
}
pub fn config<T>(&mut self, config: T) -> &mut Self
where
T: Into<__internal::InternalBinaryBenchmarkConfig>,
{
self.config = Some(config.into());
self
}
pub fn command<T>(&mut self, command: T) -> &mut Self
where
T: Into<__internal::InternalCommand>,
{
self.commands
.push(__internal::InternalCommandKind::Default(Box::new(
command.into(),
)));
self
}
pub fn commands<I, T>(&mut self, commands: T) -> &mut Self
where
I: Into<__internal::InternalCommand>,
T: IntoIterator<Item = I>,
{
self.commands.extend(
commands
.into_iter()
.map(|c| __internal::InternalCommandKind::Default(Box::new(c.into()))),
);
self
}
pub fn setup(&mut self, setup: fn()) -> &mut Self {
self.setup = __internal::InternalBinAssistantKind::Default(setup);
self
}
pub fn teardown(&mut self, teardown: fn()) -> &mut Self {
self.teardown = __internal::InternalBinAssistantKind::Default(teardown);
self
}
pub fn file<T>(
&mut self,
path: T,
generator: fn(String) -> Result<Command, String>,
) -> Result<&mut Self, String>
where
T: AsRef<Path>,
{
let path = path.as_ref();
let file = File::open(path).map_err(|error| {
format!(
"{}: Error opening file '{}': {error}",
self.id,
path.display(),
)
})?;
let reader = BufReader::new(file);
let mut has_lines = false;
for (index, line) in reader.lines().enumerate() {
has_lines = true;
let line = line.map_err(|error| {
format!(
"{}: Error reading line {index} in file '{}': {error}",
self.id,
path.display()
)
})?;
let command = generator(line).map_err(|error| {
format!(
"{}: Error generating command from line {index} in file '{}': {error}",
self.id,
path.display()
)
})?;
self.commands
.push(__internal::InternalCommandKind::Default(Box::new(
command.into(),
)));
}
if !has_lines {
return Err(format!("{}: Empty file '{}'", self.id, path.display()));
}
Ok(self)
}
}
impl From<&mut Self> for Bench {
fn from(value: &mut Self) -> Self {
value.clone()
}
}
impl From<&Self> for Bench {
fn from(value: &Self) -> Self {
value.clone()
}
}
impl PartialEq for Bench {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.commands == other.commands
&& self.config == other.config
&& self.setup == other.setup
&& self.teardown == other.teardown
}
}
impl BenchmarkId {
pub fn with_parameter<T, P>(id: T, parameter: P) -> Self
where
T: AsRef<str>,
P: Display,
{
Self(format!("{}_{parameter}", id.as_ref()))
}
pub fn new<T>(id: T) -> Self
where
T: Into<String>,
{
Self(id.into())
}
#[allow(clippy::missing_panics_doc)]
pub fn validate(&self) -> Result<(), String> {
const MAX_LENGTH_ID: usize = 255;
if self.0.is_empty() {
return Err("Invalid id: Cannot be empty".to_owned());
}
let mut bytes = self.0.bytes();
let first = bytes.next().unwrap();
if first.is_ascii_alphabetic() || first == b'_' {
for (index, byte) in (1..).zip(bytes) {
if index > MAX_LENGTH_ID {
return Err(format!(
"Invalid id '{}': Maximum length of {MAX_LENGTH_ID} bytes reached",
&self.0,
));
}
if byte.is_ascii() {
if !(byte.is_ascii_alphanumeric() || byte == b'_') {
return Err(format!(
"Invalid id '{}' at position {index}: Invalid character '{}'",
&self.0,
char::from(byte)
));
}
} else {
return Err(format!(
"Invalid id '{}' at position {index}: Encountered non-ascii character",
&self.0
));
}
}
} else if first.is_ascii() {
return Err(format!(
"Invalid id '{}': As first character is '{}' not allowed",
&self.0,
char::from(first)
));
} else {
return Err(format!(
"Invalid id '{}': Encountered non-ascii character as first character",
&self.0
));
}
Ok(())
}
}
impl Display for BenchmarkId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<BenchmarkId> for String {
fn from(value: BenchmarkId) -> Self {
value.0
}
}
impl<T> From<T> for BenchmarkId
where
T: AsRef<str>,
{
fn from(value: T) -> Self {
Self(value.as_ref().to_owned())
}
}
impl BinaryBenchmark {
pub fn new<T>(id: T) -> Self
where
T: Into<BenchmarkId>,
{
Self {
id: id.into(),
config: None,
benches: vec![],
setup: None,
teardown: None,
}
}
pub fn config<T>(&mut self, config: T) -> &mut Self
where
T: Into<__internal::InternalBinaryBenchmarkConfig>,
{
self.config = Some(config.into());
self
}
pub fn bench<T>(&mut self, bench: T) -> &mut Self
where
T: Into<Bench>,
{
self.benches.push(bench.into());
self
}
pub fn setup(&mut self, setup: fn()) -> &mut Self {
self.setup = Some(setup);
self
}
pub fn teardown(&mut self, teardown: fn()) -> &mut Self {
self.teardown = Some(teardown);
self
}
}
impl From<&mut Self> for BinaryBenchmark {
fn from(value: &mut Self) -> Self {
value.clone()
}
}
impl From<&Self> for BinaryBenchmark {
fn from(value: &Self) -> Self {
value.clone()
}
}
impl PartialEq for BinaryBenchmark {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.config == other.config
&& self.benches == other.benches
&& (self.setup.is_some() && other.setup.is_some()
|| self.setup.is_none() && other.setup.is_none())
&& (self.teardown.is_some() && other.teardown.is_some()
|| self.teardown.is_none() && other.teardown.is_none())
}
}
impl BinaryBenchmarkConfig {
pub fn default_tool(&mut self, tool: ValgrindTool) -> &mut Self {
self.0.default_tool = Some(tool);
self
}
pub fn valgrind_args<I, T>(&mut self, args: T) -> &mut Self
where
I: AsRef<str>,
T: IntoIterator<Item = I>,
{
self.0.valgrind_args.extend_ignore_flag(args);
self
}
pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<OsString>,
V: Into<OsString>,
{
self.0.envs.push((key.into(), Some(value.into())));
self
}
pub fn envs<K, V, T>(&mut self, envs: T) -> &mut Self
where
K: Into<OsString>,
V: Into<OsString>,
T: IntoIterator<Item = (K, V)>,
{
self.0
.envs
.extend(envs.into_iter().map(|(k, v)| (k.into(), Some(v.into()))));
self
}
pub fn pass_through_env<K>(&mut self, key: K) -> &mut Self
where
K: Into<OsString>,
{
self.0.envs.push((key.into(), None));
self
}
pub fn pass_through_envs<K, T>(&mut self, envs: T) -> &mut Self
where
K: Into<OsString>,
T: IntoIterator<Item = K>,
{
self.0
.envs
.extend(envs.into_iter().map(|k| (k.into(), None)));
self
}
pub fn env_clear(&mut self, value: bool) -> &mut Self {
self.0.env_clear = Some(value);
self
}
pub fn current_dir<T>(&mut self, value: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.0.current_dir = Some(value.into());
self
}
pub fn exit_with<T>(&mut self, value: T) -> &mut Self
where
T: Into<__internal::InternalExitWith>,
{
self.0.exit_with = Some(value.into());
self
}
pub fn tool<T>(&mut self, tool: T) -> &mut Self
where
T: Into<__internal::InternalTool>,
{
self.0.tools.update(tool.into());
self
}
pub fn tool_override<T>(&mut self, tool: T) -> &mut Self
where
T: Into<__internal::InternalTool>,
{
self.0
.tools_override
.get_or_insert_with(__internal::InternalTools::default)
.update(tool.into());
self
}
pub fn sandbox<T>(&mut self, sandbox: T) -> &mut Self
where
T: Into<__internal::InternalSandbox>,
{
self.0.sandbox = Some(sandbox.into());
self
}
pub fn output_format<T>(&mut self, output_format: T) -> &mut Self
where
T: Into<__internal::InternalOutputFormat>,
{
self.0.output_format = Some(output_format.into());
self
}
pub fn setup_parallel(&mut self, setup_parallel: bool) -> &mut Self {
self.0.setup_parallel = Some(setup_parallel);
self
}
}
impl BinaryBenchmarkGroup {
pub fn binary_benchmark<T>(&mut self, binary_benchmark: T) -> &mut Self
where
T: Into<BinaryBenchmark>,
{
self.binary_benchmarks.push(binary_benchmark.into());
self
}
pub fn binary_benchmarks<I, T>(&mut self, binary_benchmarks: T) -> &mut Self
where
I: Into<BinaryBenchmark>,
T: IntoIterator<Item = I>,
{
self.binary_benchmarks
.extend(binary_benchmarks.into_iter().map(Into::into));
self
}
}
impl Command {
pub fn new<T>(path: T) -> Self
where
T: AsRef<OsStr>,
{
Self(__internal::InternalCommand {
path: PathBuf::from(path.as_ref()),
..Default::default()
})
}
pub fn delay<T: Into<Delay>>(&mut self, delay: T) -> &mut Self {
self.0.delay = Some(delay.into().into());
self
}
pub fn setup_parallel(&mut self, setup_parallel: bool) -> &mut Self {
self.0.config.setup_parallel = Some(setup_parallel);
self
}
pub fn arg<T>(&mut self, arg: T) -> &mut Self
where
T: Into<OsString>,
{
self.0.args.push(arg.into());
self
}
pub fn args<I, T>(&mut self, args: T) -> &mut Self
where
I: Into<OsString>,
T: IntoIterator<Item = I>,
{
self.0.args.extend(args.into_iter().map(Into::into));
self
}
pub fn stdin<T>(&mut self, stdin: T) -> &mut Self
where
T: Into<Stdin>,
{
self.0.stdin = Some(stdin.into());
self
}
pub fn stdout<T>(&mut self, stdio: T) -> &mut Self
where
T: Into<Stdio>,
{
self.0.stdout = Some(stdio.into());
self
}
pub fn stderr<T>(&mut self, stdio: T) -> &mut Self
where
T: Into<Stdio>,
{
self.0.stderr = Some(stdio.into());
self
}
pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<OsString>,
V: Into<OsString>,
{
self.0.config.envs.push((key.into(), Some(value.into())));
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
self.0
.config
.envs
.extend(vars.into_iter().map(|(k, v)| (k.into(), Some(v.into()))));
self
}
pub fn current_dir<T>(&mut self, value: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.0.config.current_dir = Some(value.into());
self
}
pub fn exit_with(&mut self, exit_with: ExitWith) -> &mut Self {
self.0.config.exit_with = Some(exit_with);
self
}
#[must_use]
pub fn build(&mut self) -> Self {
self.clone()
}
}
impl From<&mut Self> for Command {
fn from(value: &mut Self) -> Self {
value.clone()
}
}
impl From<&Self> for Command {
fn from(value: &Self) -> Self {
value.clone()
}
}
impl Delay {
pub fn from_path<T>(path: T) -> Self
where
T: Into<PathBuf>,
{
Self(__internal::InternalDelay {
kind: DelayKind::PathExists(path.into()),
..Default::default()
})
}
pub fn from_tcp_socket<T>(addr: T) -> Self
where
T: Into<SocketAddr>,
{
Self(__internal::InternalDelay {
kind: DelayKind::TcpConnect(addr.into()),
..Default::default()
})
}
pub fn from_udp_request<T, U>(addr: T, req: U) -> Self
where
T: Into<SocketAddr>,
U: Into<Vec<u8>>,
{
Self(__internal::InternalDelay {
kind: DelayKind::UdpResponse(addr.into(), req.into()),
..Default::default()
})
}
pub fn new(kind: DelayKind) -> Self {
Self(__internal::InternalDelay {
kind,
..Default::default()
})
}
#[must_use]
pub fn poll<T: Into<Duration>>(mut self, duration: T) -> Self {
self.0.poll = Some(duration.into());
self
}
#[must_use]
pub fn timeout<T: Into<Duration>>(mut self, duration: T) -> Self {
self.0.timeout = Some(duration.into());
self
}
}
impl<T> From<T> for Delay
where
T: Into<Duration>,
{
fn from(duration: T) -> Self {
Self(__internal::InternalDelay {
kind: DelayKind::DurationElapse(duration.into()),
..Default::default()
})
}
}
impl Sandbox {
pub fn new(enabled: bool) -> Self {
Self(__internal::InternalSandbox {
enabled: Some(enabled),
..Default::default()
})
}
pub fn fixtures<I, T>(&mut self, paths: T) -> &mut Self
where
I: Into<PathBuf>,
T: IntoIterator<Item = I>,
{
self.0.fixtures.extend(paths.into_iter().map(Into::into));
self
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::empty("")]
#[case::simple_invalid("-")]
#[case::when_0_char_minus_1("a\x2f")]
#[case::when_9_char_plus_1("a\x3a")]
#[case::when_big_a_char_minus_1("\x40")]
#[case::when_big_z_char_plus_1("\x5b")]
#[case::when_low_a_char_minus_1("\x60")]
#[case::when_low_z_char_plus_1("\x7b")]
#[case::invalid_2nd("a-")]
#[case::invalid_3rd("ab-")]
#[case::all_invalid("---")]
#[case::number_as_first("0")]
#[case::non_ascii_1st("µ")]
#[case::non_ascii_2nd("aµ")]
#[case::non_ascii_3rd("aaµ")]
#[case::non_ascii_middle("aµa")]
fn test_benchmark_id_validate_when_error(#[case] id: &str) {
let id = BenchmarkId::new(id);
assert!(id.validate().is_err());
}
#[rstest]
#[case::lowercase_a("a")]
#[case::lowercase_b("b")]
#[case::lowercase_m("m")]
#[case::lowercase_y("y")]
#[case::lowercase_z("z")]
#[case::uppercase_a("A")]
#[case::uppercase_b("B")]
#[case::uppercase_n("N")]
#[case::uppercase_y("Y")]
#[case::uppercase_z("Z")]
#[case::zero_2nd("a0")]
#[case::one_2nd("a1")]
#[case::eight_2nd("a8")]
#[case::nine_2nd("a9")]
#[case::number_middle("b4t")]
#[case::underscore("_")]
#[case::only_underscore("___")]
#[case::underscore_last("a_")]
#[case::mixed_all("auAEwer9__2xcd")]
fn test_benchmark_id_validate(#[case] id: &str) {
let id = BenchmarkId::new(id);
id.validate().unwrap();
}
#[rstest]
#[case::empty("", "Invalid id: Cannot be empty")]
#[case::non_ascii_first(
"µ",
"Invalid id 'µ': Encountered non-ascii character as first character"
)]
#[case::multibyte_middle(
"aµ",
"Invalid id 'aµ' at position 1: Encountered non-ascii character"
)]
#[case::non_ascii_middle("a-", "Invalid id 'a-' at position 1: Invalid character '-'")]
#[case::invalid_first("-", "Invalid id '-': As first character is '-' not allowed")]
fn test_benchmark_id_validate_error_message(#[case] id: &str, #[case] message: &str) {
let id = BenchmarkId::new(id);
assert_eq!(
id.validate()
.expect_err("Validation should return an error"),
message
);
}
}