use std::cmp::Ordering;
use std::fmt;
use serde::{Deserialize, Serialize};
use super::constants::{OBJECTS_SEGMENT, STORAGE_VERSION};
use super::identifiers::{MachineKey, TargetTriple};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Engine {
Criterion,
Callgrind,
AllocTracker,
AllTheTime,
}
impl Engine {
pub const ALL: [Self; 4] = [
Self::Callgrind,
Self::Criterion,
Self::AllocTracker,
Self::AllTheTime,
];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Criterion => "criterion",
Self::Callgrind => "callgrind",
Self::AllocTracker => "alloc_tracker",
Self::AllTheTime => "all_the_time",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name.to_ascii_lowercase().as_str() {
"criterion" => Some(Self::Criterion),
"callgrind" => Some(Self::Callgrind),
"alloc_tracker" => Some(Self::AllocTracker),
"all_the_time" => Some(Self::AllTheTime),
_ => None,
}
}
}
impl Ord for Engine {
fn cmp(&self, other: &Self) -> Ordering {
self.as_str().cmp(other.as_str())
}
}
impl PartialOrd for Engine {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Engine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct DiscriminantSet {
pub engine: Engine,
pub target_triple: TargetTriple,
pub machine_key: MachineKey,
}
impl DiscriminantSet {
#[must_use]
pub fn new(engine: Engine, target_triple: &TargetTriple, machine_key: &MachineKey) -> Self {
Self {
engine,
target_triple: TargetTriple::from(sanitize_segment(target_triple.as_str())),
machine_key: MachineKey::from(sanitize_segment(machine_key.as_str())),
}
}
#[must_use]
pub fn partition_prefix(&self, project: &str) -> String {
let project = sanitize_segment(project);
let engine = self.engine.as_str();
let triple = &self.target_triple;
let machine_key = &self.machine_key;
format!("{STORAGE_VERSION}/{project}/{OBJECTS_SEGMENT}/{engine}/{triple}/{machine_key}/")
}
#[must_use]
pub fn clean_key(&self, project: &str, commit: &str) -> String {
let prefix = self.partition_prefix(project);
let commit = sanitize_segment(commit);
format!("{prefix}{commit}/clean.json")
}
#[must_use]
pub fn dirty_key(&self, project: &str, commit: &str, observation_unix: i64) -> String {
let prefix = self.partition_prefix(project);
let commit = sanitize_segment(commit);
format!("{prefix}{commit}/dirty-{observation_unix}.json")
}
#[must_use]
pub fn bless_key(&self, project: &str, commit: &str, issued_unix: i64) -> String {
let prefix = self.partition_prefix(project);
let commit = sanitize_segment(commit);
format!("{prefix}{commit}/bless-{issued_unix}.json")
}
#[must_use]
pub fn commit_prefix(&self, project: &str, commit: &str) -> String {
let prefix = self.partition_prefix(project);
let commit = sanitize_segment(commit);
format!("{prefix}{commit}/")
}
}
impl fmt::Display for DiscriminantSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}/{}/{}",
self.engine, self.target_triple, self.machine_key
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObjectKind {
Clean,
Dirty {
observation_unix: i64,
},
Bless {
issued_unix: i64,
},
}
impl ObjectKind {
fn from_file_segment(file: &str) -> Option<Self> {
if file == "clean.json" {
return Some(Self::Clean);
}
if let Some(seconds) = file
.strip_prefix("dirty-")
.and_then(|rest| rest.strip_suffix(".json"))
{
return seconds
.parse()
.ok()
.map(|observation_unix| Self::Dirty { observation_unix });
}
if let Some(seconds) = file
.strip_prefix("bless-")
.and_then(|rest| rest.strip_suffix(".json"))
{
return seconds
.parse()
.ok()
.map(|issued_unix| Self::Bless { issued_unix });
}
None
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StorageKey {
pub project: String,
pub set: DiscriminantSet,
pub commit: String,
pub kind: ObjectKind,
}
impl StorageKey {
#[must_use]
pub fn is_clean(&self) -> bool {
matches!(self.kind, ObjectKind::Clean)
}
#[must_use]
pub fn is_dirty(&self) -> bool {
matches!(self.kind, ObjectKind::Dirty { .. })
}
#[must_use]
pub fn is_bless(&self) -> bool {
matches!(self.kind, ObjectKind::Bless { .. })
}
#[must_use]
pub fn bless_key(&self, issued_unix: i64) -> String {
self.set.bless_key(&self.project, &self.commit, issued_unix)
}
}
#[must_use]
pub fn parse_key(key: &str) -> Option<StorageKey> {
let parts: Vec<&str> = key.split('/').collect();
let [
version,
project,
objects,
engine,
target_triple,
machine_key,
commit,
file,
] = parts.as_slice()
else {
return None;
};
if *version != STORAGE_VERSION || *objects != OBJECTS_SEGMENT {
return None;
}
if parts.iter().any(|segment| segment.is_empty()) {
return None;
}
let engine = Engine::from_name(engine)?;
let kind = ObjectKind::from_file_segment(file)?;
Some(StorageKey {
project: (*project).to_owned(),
set: DiscriminantSet {
engine,
target_triple: TargetTriple::from(*target_triple),
machine_key: MachineKey::from(*machine_key),
},
commit: (*commit).to_owned(),
kind,
})
}
#[must_use]
pub fn sanitize_segment(raw: &str) -> String {
let mangled: String = raw
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
if mangled.is_empty() || mangled.chars().all(|c| c == '.') {
return "_".to_owned();
}
mangled
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn every_engine_partitions_by_machine_key() {
let set = DiscriminantSet::new(
Engine::AllocTracker,
&TargetTriple::from("x86_64-pc-windows-msvc"),
&MachineKey::from("abc123"),
);
assert_eq!(
set.partition_prefix("folo"),
"v1/folo/objects/alloc_tracker/x86_64-pc-windows-msvc/abc123/"
);
}
#[test]
fn all_the_time_partitions_by_machine_key() {
let set = DiscriminantSet::new(
Engine::AllTheTime,
&TargetTriple::from("x86_64-pc-windows-msvc"),
&MachineKey::from("abc123"),
);
assert_eq!(
set.partition_prefix("folo"),
"v1/folo/objects/all_the_time/x86_64-pc-windows-msvc/abc123/"
);
}
#[test]
fn machine_key_appears_in_partition() {
let set = DiscriminantSet::new(
Engine::Criterion,
&TargetTriple::from("x86_64-pc-windows-msvc"),
&MachineKey::from("abc123"),
);
assert_eq!(
set.partition_prefix("folo"),
"v1/folo/objects/criterion/x86_64-pc-windows-msvc/abc123/"
);
}
#[test]
fn partition_prefix_does_not_match_a_sibling_machine_key() {
let triple = TargetTriple::from("x86_64-pc-windows-msvc");
let ci = DiscriminantSet::new(Engine::Criterion, &triple, &MachineKey::from("ci"));
let pool = DiscriminantSet::new(Engine::Criterion, &triple, &MachineKey::from("ci-pool"));
assert!(
!pool
.clean_key("folo", "abc123")
.starts_with(&ci.partition_prefix("folo")),
"{} must not fall under {}",
pool.clean_key("folo", "abc123"),
ci.partition_prefix("folo")
);
assert!(
ci.clean_key("folo", "abc123")
.starts_with(&ci.partition_prefix("folo"))
);
}
#[test]
fn display_formats_engine_triple_and_machine_key() {
let set = DiscriminantSet::new(
Engine::Criterion,
&TargetTriple::from("x86_64-pc-windows-msvc"),
&MachineKey::from("abc123"),
);
assert_eq!(set.to_string(), "criterion/x86_64-pc-windows-msvc/abc123");
}
#[test]
fn clean_key_is_named_by_commit() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("abc123"),
);
assert_eq!(
set.clean_key("folo", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/abc123/\
deadbeefdeadbeefdeadbeefdeadbeefdeadbeef/clean.json"
);
}
#[test]
fn dirty_key_is_named_by_commit_and_observation_time() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("abc123"),
);
assert_eq!(
set.dirty_key(
"folo",
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1_700_000_000
),
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/abc123/\
deadbeefdeadbeefdeadbeefdeadbeefdeadbeef/dirty-1700000000.json"
);
}
#[test]
fn bless_key_targets_the_commit_directory() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("m1"),
);
assert_eq!(
set.bless_key("folo", "abc123", 1_700_000_000),
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/bless-1700000000.json"
);
}
#[test]
fn commit_prefix_enumerates_one_commit_directory() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("m1"),
);
assert_eq!(
set.commit_prefix("folo", "dead/beef"),
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/dead_beef/"
);
}
#[test]
fn engine_display_matches_as_str() {
assert_eq!(Engine::Criterion.to_string(), "criterion");
assert_eq!(Engine::Callgrind.to_string(), "callgrind");
assert_eq!(Engine::AllocTracker.to_string(), "alloc_tracker");
assert_eq!(Engine::AllTheTime.to_string(), "all_the_time");
}
#[test]
fn engine_from_name_roundtrips() {
for engine in Engine::ALL {
assert_eq!(Engine::from_name(engine.as_str()), Some(engine));
}
assert_eq!(Engine::from_name("dhat"), None);
}
#[test]
fn engine_from_name_is_case_insensitive() {
assert_eq!(Engine::from_name("Callgrind"), Some(Engine::Callgrind));
assert_eq!(Engine::from_name("ALL_THE_TIME"), Some(Engine::AllTheTime));
}
#[test]
fn sanitize_segment_keeps_safe_characters() {
assert_eq!(
sanitize_segment("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(sanitize_segment("my.project-1"), "my.project-1");
}
#[test]
fn sanitize_segment_folds_case() {
assert_eq!(sanitize_segment("Team-App"), "team-app");
assert_eq!(
sanitize_segment("X86_64-PC-Windows-MSVC"),
"x86_64-pc-windows-msvc"
);
}
#[test]
fn sanitize_segment_replaces_separators_and_specials() {
assert_eq!(sanitize_segment("team/app"), "team_app");
assert_eq!(sanitize_segment(r"team\app"), "team_app");
assert_eq!(sanitize_segment("weird:name"), "weird_name");
assert_eq!(sanitize_segment("with space"), "with_space");
assert_eq!(sanitize_segment("café"), "caf_");
}
#[test]
fn sanitize_segment_maps_empty_and_dot_only_to_underscore() {
assert_eq!(sanitize_segment(""), "_");
assert_eq!(sanitize_segment("."), "_");
assert_eq!(sanitize_segment(".."), "_");
}
#[test]
fn new_sanitizes_partition_components() {
let set = DiscriminantSet::new(
Engine::Criterion,
&TargetTriple::from("weird/triple"),
&MachineKey::from("machine/one"),
);
assert_eq!(
set.partition_prefix("team/app"),
"v1/team_app/objects/criterion/weird_triple/machine_one/"
);
let prefix = set.partition_prefix("team/app");
assert!(prefix.ends_with('/'), "{prefix}");
assert_eq!(prefix.trim_end_matches('/').split('/').count(), 6);
}
#[test]
fn clean_key_sanitizes_the_commit() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("m1"),
);
let object = set.clean_key("folo", "dead/beef");
assert_eq!(
object,
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/dead_beef/clean.json"
);
assert_eq!(object.split('/').count(), 8);
}
#[test]
fn dirty_key_sanitizes_the_commit() {
let set = DiscriminantSet::new(
Engine::Callgrind,
&TargetTriple::from("x86_64-unknown-linux-gnu"),
&MachineKey::from("m1"),
);
let object = set.dirty_key("folo", "dead/beef", 1_700_000_000);
assert_eq!(
object,
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/dead_beef/dirty-1700000000.json"
);
assert_eq!(object.split('/').count(), 8);
}
#[test]
fn parse_key_decomposes_a_clean_key() {
let parsed =
parse_key("v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/clean.json")
.unwrap();
assert_eq!(parsed.project, "folo");
assert_eq!(parsed.set.engine, Engine::Callgrind);
assert_eq!(parsed.set.target_triple, "x86_64-unknown-linux-gnu");
assert_eq!(parsed.set.machine_key, "m1");
assert_eq!(parsed.commit, "abc123");
assert!(parsed.is_clean());
assert!(!parsed.is_dirty());
assert!(!parsed.is_bless());
}
#[test]
fn parse_key_recognizes_a_dirty_snapshot() {
let parsed = parse_key(
"v1/folo/objects/criterion/x86_64-pc-windows-msvc/m1/abc123/dirty-1700000000.json",
)
.unwrap();
assert!(parsed.is_dirty());
assert!(!parsed.is_clean());
assert_eq!(
parsed.kind,
ObjectKind::Dirty {
observation_unix: 1_700_000_000
}
);
assert_eq!(parsed.set.target_triple, "x86_64-pc-windows-msvc");
assert_eq!(parsed.set.machine_key, "m1");
}
#[test]
fn parse_key_recognizes_a_blessing_sidecar() {
let parsed = parse_key(
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/bless-1700000000.json",
)
.unwrap();
assert!(parsed.is_bless());
assert!(!parsed.is_dirty());
assert!(!parsed.is_clean());
assert_eq!(
parsed.kind,
ObjectKind::Bless {
issued_unix: 1_700_000_000
}
);
assert_eq!(parsed.commit, "abc123");
}
#[test]
fn bless_key_targets_the_sets_commit_directory() {
let parsed =
parse_key("v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/clean.json")
.unwrap();
assert_eq!(
parsed.bless_key(1_700_000_000),
"v1/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/bless-1700000000.json"
);
}
#[test]
fn parse_key_rejects_malformed_keys() {
assert!(parse_key("v2/folo/objects/callgrind/t/m/c/f.json").is_none());
assert!(parse_key("v1/folo/callgrind/t/m/c/f.json").is_none());
assert!(parse_key("v1/folo/data/callgrind/t/m/c/f.json").is_none());
assert!(parse_key("v1/folo/objects/callgrind/t/m/f.json").is_none());
assert!(parse_key("v1/folo/objects/callgrind/t/m/c/sub/f.json").is_none());
assert!(parse_key("v1/folo/objects/callgrind/t//c/f.json").is_none());
assert!(parse_key("v1/folo/objects/callgrind/t/m/c/").is_none());
assert!(parse_key("v1/folo/objects/dhat/t/m/c/clean.json").is_none());
assert!(parse_key("v1/folo/objects/callgrind/t/m/c/notes.json").is_none());
assert!(
parse_key("v2/folo/objects/callgrind/x86_64-unknown-linux-gnu/m1/abc123/clean.json")
.is_none()
);
assert!(
parse_key("v1/folo/data/callgrind/x86_64-unknown-linux-gnu/m1/abc123/clean.json")
.is_none()
);
}
#[test]
fn engine_orders_by_identifier() {
let mut by_engine = Engine::ALL.to_vec();
by_engine.sort();
let ordered_names: Vec<&str> = by_engine.iter().map(|engine| engine.as_str()).collect();
let mut by_name = Engine::ALL.map(Engine::as_str).to_vec();
by_name.sort_unstable();
assert_eq!(ordered_names, by_name);
assert!(Engine::Callgrind < Engine::Criterion);
assert_eq!(
Engine::Callgrind.partial_cmp(&Engine::Criterion),
Some(Ordering::Less)
);
}
#[test]
fn engine_serde_matches_as_str() {
for engine in Engine::ALL {
let json = serde_json::to_string(&engine).unwrap();
assert_eq!(json, format!("\"{}\"", engine.as_str()));
assert_eq!(
serde_json::from_str::<Engine>(&json).unwrap(),
engine,
"engine {engine} must round-trip through its wire form"
);
}
}
}