use std::error::Error;
use std::fmt;
use std::str::FromStr;
use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct BenchmarkId {
pub segments: NonEmpty<String>,
}
impl BenchmarkId {
#[must_use]
pub fn new(segments: NonEmpty<String>) -> Self {
Self { segments }
}
#[must_use]
pub fn qualified(&self) -> String {
self.segments
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join("/")
}
}
impl fmt::Display for BenchmarkId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.qualified())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(into = "String", try_from = "String")]
pub struct BenchmarkIdPrefix(String);
impl BenchmarkIdPrefix {
pub fn new(value: impl Into<String>) -> Result<Self, EmptyBenchmarkIdPrefix> {
let value = value.into();
if value.is_empty() {
return Err(EmptyBenchmarkIdPrefix);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for BenchmarkIdPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for BenchmarkIdPrefix {
type Err = EmptyBenchmarkIdPrefix;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<String> for BenchmarkIdPrefix {
type Error = EmptyBenchmarkIdPrefix;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<BenchmarkIdPrefix> for String {
fn from(prefix: BenchmarkIdPrefix) -> Self {
prefix.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EmptyBenchmarkIdPrefix;
impl fmt::Display for EmptyBenchmarkIdPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a benchmark-id prefix must not be empty")
}
}
impl Error for EmptyBenchmarkIdPrefix {}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use nonempty::nonempty;
use super::*;
#[test]
fn leading_segment_distinguishes_otherwise_equal_ids() {
let foo = BenchmarkId::new(nonempty![
"foo".to_owned(),
"a::bench".to_owned(),
"run".to_owned(),
]);
let bar = BenchmarkId::new(nonempty![
"bar".to_owned(),
"a::bench".to_owned(),
"run".to_owned(),
]);
assert_ne!(foo, bar);
}
#[test]
fn ids_sort_lexicographically_by_segments() {
let bar = BenchmarkId::new(nonempty!["bar".to_owned(), "z".to_owned()]);
let foo = BenchmarkId::new(nonempty!["foo".to_owned(), "a".to_owned()]);
let mut ids = vec![foo.clone(), bar.clone()];
ids.sort();
assert_eq!(ids, vec![bar, foo]);
}
#[test]
fn qualified_joins_segments() {
let id = BenchmarkId::new(nonempty![
"fast_time".to_owned(),
"a::group".to_owned(),
"capture".to_owned(),
"two_instants".to_owned(),
]);
assert_eq!(id.qualified(), "fast_time/a::group/capture/two_instants");
assert_eq!(id.to_string(), id.qualified());
}
#[test]
fn qualified_handles_a_single_segment() {
let id = BenchmarkId::new(nonempty!["a::group".to_owned()]);
assert_eq!(id.qualified(), "a::group");
}
#[test]
fn benchmark_id_prefix_rejects_an_empty_string() {
assert_eq!(BenchmarkIdPrefix::new(""), Err(EmptyBenchmarkIdPrefix));
assert_eq!("".parse::<BenchmarkIdPrefix>(), Err(EmptyBenchmarkIdPrefix));
assert_eq!(
BenchmarkIdPrefix::try_from(String::new()),
Err(EmptyBenchmarkIdPrefix)
);
}
#[test]
fn benchmark_id_prefix_keeps_a_non_empty_value() {
let prefix = BenchmarkIdPrefix::new("foo/bar").unwrap();
assert_eq!(prefix.as_str(), "foo/bar");
assert_eq!(prefix.to_string(), "foo/bar");
}
#[test]
fn benchmark_id_prefix_serializes_as_a_plain_string() {
let prefix = BenchmarkIdPrefix::new("foo/bar").unwrap();
let json = serde_json::to_string(&prefix).unwrap();
assert_eq!(json, "\"foo/bar\"");
let restored: BenchmarkIdPrefix = serde_json::from_str(&json).unwrap();
assert_eq!(restored, prefix);
}
#[test]
fn benchmark_id_prefix_deserialization_rejects_an_empty_string() {
let error = serde_json::from_str::<BenchmarkIdPrefix>("\"\"").unwrap_err();
assert!(error.to_string().contains("benchmark-id prefix"), "{error}");
}
}