use super::{format::has_zip_extension, store::RecordedRunInfo};
use crate::errors::{InvalidRunIdOrRecordingSelector, InvalidRunIdSelector};
use camino::Utf8PathBuf;
use quick_junit::ReportUuid;
use std::{fmt, str::FromStr};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RunIdOrRecordingSelector {
RunId(RunIdSelector),
RecordingPath(Utf8PathBuf),
}
impl FromStr for RunIdOrRecordingSelector {
type Err = InvalidRunIdOrRecordingSelector;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let path = Utf8PathBuf::from(s);
if has_zip_extension(&path) {
return Ok(RunIdOrRecordingSelector::RecordingPath(path));
}
match s.parse::<RunIdSelector>() {
Ok(selector) => Ok(RunIdOrRecordingSelector::RunId(selector)),
Err(_) => {
if s.contains('/') || s.contains(std::path::MAIN_SEPARATOR) {
Ok(RunIdOrRecordingSelector::RecordingPath(path))
} else {
Err(InvalidRunIdOrRecordingSelector {
input: s.to_owned(),
})
}
}
}
}
}
impl Default for RunIdOrRecordingSelector {
fn default() -> Self {
RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
}
}
impl fmt::Display for RunIdOrRecordingSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunIdOrRecordingSelector::RunId(selector) => write!(f, "{selector}"),
RunIdOrRecordingSelector::RecordingPath(path) => write!(f, "{path}"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum RunIdSelector {
#[default]
Latest,
Prefix(String),
}
impl FromStr for RunIdSelector {
type Err = InvalidRunIdSelector;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "latest" {
Ok(RunIdSelector::Latest)
} else {
let is_valid = !s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit() || c == '-');
if is_valid {
Ok(RunIdSelector::Prefix(s.to_owned()))
} else {
Err(InvalidRunIdSelector {
input: s.to_owned(),
})
}
}
}
}
impl fmt::Display for RunIdSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunIdSelector::Latest => write!(f, "latest"),
RunIdSelector::Prefix(prefix) => write!(f, "{prefix}"),
}
}
}
#[derive(Clone, Debug)]
pub struct RunIdIndex {
sorted_entries: Vec<RunIdIndexEntry>,
}
#[derive(Clone, Debug)]
struct RunIdIndexEntry {
run_id: ReportUuid,
hex: String,
}
impl RunIdIndex {
pub fn new(runs: &[RecordedRunInfo]) -> Self {
let mut sorted_entries: Vec<_> = runs
.iter()
.map(|r| RunIdIndexEntry {
run_id: r.run_id,
hex: r.run_id.to_string().replace('-', "").to_lowercase(),
})
.collect();
sorted_entries.sort_by(|a, b| a.hex.cmp(&b.hex));
Self { sorted_entries }
}
pub fn shortest_unique_prefix_len(&self, run_id: ReportUuid) -> Option<usize> {
let pos = self
.sorted_entries
.iter()
.position(|entry| entry.run_id == run_id)?;
let target_hex = &self.sorted_entries[pos].hex;
let mut min_len = 1;
if pos > 0 {
let prev_hex = &self.sorted_entries[pos - 1].hex;
let common = common_hex_prefix_len(target_hex, prev_hex);
min_len = min_len.max(common + 1);
}
if pos + 1 < self.sorted_entries.len() {
let next_hex = &self.sorted_entries[pos + 1].hex;
let common = common_hex_prefix_len(target_hex, next_hex);
min_len = min_len.max(common + 1);
}
Some(min_len)
}
pub fn shortest_unique_prefix(&self, run_id: ReportUuid) -> Option<ShortestRunIdPrefix> {
let prefix_len = self.shortest_unique_prefix_len(run_id)?;
Some(ShortestRunIdPrefix::new(run_id, prefix_len))
}
pub fn resolve_prefix(&self, prefix: &str) -> Result<ReportUuid, PrefixResolutionError> {
let normalized = prefix.replace('-', "").to_lowercase();
if !normalized.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(PrefixResolutionError::InvalidPrefix);
}
let start = self
.sorted_entries
.partition_point(|entry| entry.hex.as_str() < normalized.as_str());
let matches: Vec<_> = self.sorted_entries[start..]
.iter()
.take_while(|entry| entry.hex.starts_with(&normalized))
.map(|entry| entry.run_id)
.collect();
match matches.len() {
0 => Err(PrefixResolutionError::NotFound),
1 => Ok(matches[0]),
n => {
let candidates = matches.into_iter().take(8).collect();
Err(PrefixResolutionError::Ambiguous {
count: n,
candidates,
})
}
}
}
pub fn len(&self) -> usize {
self.sorted_entries.len()
}
pub fn is_empty(&self) -> bool {
self.sorted_entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = ReportUuid> + '_ {
self.sorted_entries.iter().map(|entry| entry.run_id)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShortestRunIdPrefix {
pub prefix: String,
pub rest: String,
}
impl ShortestRunIdPrefix {
fn new(run_id: ReportUuid, hex_len: usize) -> Self {
let full = run_id.to_string();
let split_index = hex_len_to_string_index(hex_len);
let split_index = split_index.min(full.len());
let (prefix, rest) = full.split_at(split_index);
Self {
prefix: prefix.to_string(),
rest: rest.to_string(),
}
}
pub fn full(&self) -> String {
format!("{}{}", self.prefix, self.rest)
}
}
fn hex_len_to_string_index(hex_len: usize) -> usize {
let dashes = match hex_len {
0..=8 => 0,
9..=12 => 1,
13..=16 => 2,
17..=20 => 3,
21..=32 => 4,
_ => 4, };
hex_len + dashes
}
fn common_hex_prefix_len(a: &str, b: &str) -> usize {
a.chars()
.zip(b.chars())
.take_while(|(ca, cb)| ca == cb)
.count()
}
#[derive(Clone, Debug)]
pub enum PrefixResolutionError {
NotFound,
Ambiguous {
count: usize,
candidates: Vec<ReportUuid>,
},
InvalidPrefix,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::{RecordedRunStatus, RecordedSizes, format::STORE_FORMAT_VERSION};
use chrono::TimeZone;
use semver::Version;
use std::collections::BTreeMap;
fn make_run(run_id: ReportUuid) -> RecordedRunInfo {
let started_at = chrono::FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
.unwrap();
RecordedRunInfo {
run_id,
store_format_version: STORE_FORMAT_VERSION,
nextest_version: Version::new(0, 1, 0),
started_at,
last_written_at: started_at,
duration_secs: None,
cli_args: Vec::new(),
build_scope_args: Vec::new(),
env_vars: BTreeMap::new(),
parent_run_id: None,
sizes: RecordedSizes::default(),
status: RecordedRunStatus::Incomplete,
}
}
#[test]
fn test_empty_index() {
let index = RunIdIndex::new(&[]);
assert!(index.is_empty());
assert_eq!(index.len(), 0);
}
#[test]
fn test_single_entry() {
let runs = vec![make_run(ReportUuid::from_u128(
0x550e8400_e29b_41d4_a716_446655440000,
))];
let index = RunIdIndex::new(&runs);
assert_eq!(index.len(), 1);
assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(1));
let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
assert_eq!(prefix.prefix, "5");
assert_eq!(prefix.rest, "50e8400-e29b-41d4-a716-446655440000");
assert_eq!(prefix.full(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_shared_prefix() {
let runs = vec![
make_run(ReportUuid::from_u128(
0x55551111_0000_0000_0000_000000000000,
)),
make_run(ReportUuid::from_u128(
0x55552222_0000_0000_0000_000000000000,
)),
];
let index = RunIdIndex::new(&runs);
assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(5));
assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(5));
let prefix0 = index.shortest_unique_prefix(runs[0].run_id).unwrap();
assert_eq!(prefix0.prefix, "55551");
assert_eq!(prefix0.rest, "111-0000-0000-0000-000000000000");
let prefix1 = index.shortest_unique_prefix(runs[1].run_id).unwrap();
assert_eq!(prefix1.prefix, "55552");
}
#[test]
fn test_asymmetric_neighbors() {
let runs = vec![
make_run(ReportUuid::from_u128(
0x11110000_0000_0000_0000_000000000000,
)),
make_run(ReportUuid::from_u128(
0x11120000_0000_0000_0000_000000000000,
)),
make_run(ReportUuid::from_u128(
0x22220000_0000_0000_0000_000000000000,
)),
];
let index = RunIdIndex::new(&runs);
assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(4));
assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(4));
assert_eq!(index.shortest_unique_prefix_len(runs[2].run_id), Some(1));
}
#[test]
fn test_prefix_crosses_dash() {
let runs = vec![
make_run(ReportUuid::from_u128(
0x12345678_9000_0000_0000_000000000000,
)),
make_run(ReportUuid::from_u128(
0x12345678_9111_0000_0000_000000000000,
)),
];
let index = RunIdIndex::new(&runs);
assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(10));
let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
assert_eq!(prefix.prefix, "12345678-90");
assert_eq!(prefix.rest, "00-0000-0000-000000000000");
}
#[test]
fn test_resolve_prefix() {
let runs = vec![
make_run(ReportUuid::from_u128(
0xabcdef00_1234_5678_9abc_def012345678,
)),
make_run(ReportUuid::from_u128(
0x22222222_2222_2222_2222_222222222222,
)),
make_run(ReportUuid::from_u128(
0x23333333_3333_3333_3333_333333333333,
)),
];
let index = RunIdIndex::new(&runs);
assert_eq!(index.resolve_prefix("abc").unwrap(), runs[0].run_id);
assert_eq!(index.resolve_prefix("22").unwrap(), runs[1].run_id);
assert_eq!(index.resolve_prefix("ABC").unwrap(), runs[0].run_id);
assert_eq!(index.resolve_prefix("AbC").unwrap(), runs[0].run_id);
assert_eq!(index.resolve_prefix("abcdef00-").unwrap(), runs[0].run_id);
assert_eq!(index.resolve_prefix("abcdef00-12").unwrap(), runs[0].run_id);
let err = index.resolve_prefix("2").unwrap_err();
assert!(matches!(
err,
PrefixResolutionError::Ambiguous { count: 2, .. }
));
let err = index.resolve_prefix("9").unwrap_err();
assert!(matches!(err, PrefixResolutionError::NotFound));
let err = index.resolve_prefix("xyz").unwrap_err();
assert!(matches!(err, PrefixResolutionError::InvalidPrefix));
}
#[test]
fn test_not_in_index() {
let runs = vec![make_run(ReportUuid::from_u128(
0x11111111_1111_1111_1111_111111111111,
))];
let index = RunIdIndex::new(&runs);
let other = ReportUuid::from_u128(0x22222222_2222_2222_2222_222222222222);
assert_eq!(index.shortest_unique_prefix_len(other), None);
assert_eq!(index.shortest_unique_prefix(other), None);
}
#[test]
fn test_hex_len_to_string_index() {
assert_eq!(hex_len_to_string_index(0), 0);
assert_eq!(hex_len_to_string_index(8), 8);
assert_eq!(hex_len_to_string_index(9), 10);
assert_eq!(hex_len_to_string_index(13), 15);
assert_eq!(hex_len_to_string_index(17), 20);
assert_eq!(hex_len_to_string_index(21), 25);
assert_eq!(hex_len_to_string_index(32), 36);
}
#[test]
fn test_run_id_selector_default() {
assert_eq!(RunIdSelector::default(), RunIdSelector::Latest);
}
#[test]
fn test_run_id_selector_from_str() {
assert_eq!(
"latest".parse::<RunIdSelector>().unwrap(),
RunIdSelector::Latest
);
assert_eq!(
"abc123".parse::<RunIdSelector>().unwrap(),
RunIdSelector::Prefix("abc123".to_owned())
);
assert_eq!(
"550e8400-e29b-41d4".parse::<RunIdSelector>().unwrap(),
RunIdSelector::Prefix("550e8400-e29b-41d4".to_owned())
);
assert_eq!(
"ABCDEF".parse::<RunIdSelector>().unwrap(),
RunIdSelector::Prefix("ABCDEF".to_owned())
);
assert_eq!(
"0".parse::<RunIdSelector>().unwrap(),
RunIdSelector::Prefix("0".to_owned())
);
assert!("Latest".parse::<RunIdSelector>().is_err());
assert!("LATEST".parse::<RunIdSelector>().is_err());
assert!("xyz".parse::<RunIdSelector>().is_err());
assert!("abc_123".parse::<RunIdSelector>().is_err());
assert!("hello".parse::<RunIdSelector>().is_err());
assert!("".parse::<RunIdSelector>().is_err());
}
#[test]
fn test_run_id_selector_display() {
assert_eq!(RunIdSelector::Latest.to_string(), "latest");
assert_eq!(
RunIdSelector::Prefix("abc123".to_owned()).to_string(),
"abc123"
);
}
#[test]
fn test_run_id_or_archive_selector_default() {
assert_eq!(
RunIdOrRecordingSelector::default(),
RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
);
}
#[test]
fn test_run_id_or_archive_selector_from_str() {
assert_eq!(
"latest".parse::<RunIdOrRecordingSelector>().unwrap(),
RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
);
assert_eq!(
"abc123".parse::<RunIdOrRecordingSelector>().unwrap(),
RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned()))
);
assert_eq!(
"nextest-run-abc123.zip"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("nextest-run-abc123.zip"))
);
assert_eq!(
"/path/to/archive.zip"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
);
assert_eq!(
"../relative/path.zip"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../relative/path.zip"))
);
assert_eq!(
"/proc/self/fd/11"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/proc/self/fd/11"))
);
assert_eq!(
"/dev/fd/5".parse::<RunIdOrRecordingSelector>().unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/dev/fd/5"))
);
assert_eq!(
"./my-recording"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("./my-recording"))
);
assert_eq!(
"../path/to/file"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../path/to/file"))
);
#[cfg(windows)]
{
assert_eq!(
r"C:\path\to\file"
.parse::<RunIdOrRecordingSelector>()
.unwrap(),
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from(r"C:\path\to\file"))
);
}
assert!("xyz".parse::<RunIdOrRecordingSelector>().is_err());
assert!("hello".parse::<RunIdOrRecordingSelector>().is_err());
assert!("latets".parse::<RunIdOrRecordingSelector>().is_err());
assert!("latestt".parse::<RunIdOrRecordingSelector>().is_err());
assert!("recording".parse::<RunIdOrRecordingSelector>().is_err());
}
#[test]
fn test_run_id_or_archive_selector_display() {
assert_eq!(
RunIdOrRecordingSelector::RunId(RunIdSelector::Latest).to_string(),
"latest"
);
assert_eq!(
RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned())).to_string(),
"abc123"
);
assert_eq!(
RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
.to_string(),
"/path/to/archive.zip"
);
}
}