1use super::{format::has_zip_extension, store::RecordedRunInfo};
7use crate::errors::{InvalidRunIdOrRecordingSelector, InvalidRunIdSelector};
8use camino::Utf8PathBuf;
9use quick_junit::ReportUuid;
10use std::{fmt, str::FromStr};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum RunIdOrRecordingSelector {
25 RunId(RunIdSelector),
27 RecordingPath(Utf8PathBuf),
29}
30
31impl FromStr for RunIdOrRecordingSelector {
32 type Err = InvalidRunIdOrRecordingSelector;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 let path = Utf8PathBuf::from(s);
36 if has_zip_extension(&path) {
37 return Ok(RunIdOrRecordingSelector::RecordingPath(path));
38 }
39
40 match s.parse::<RunIdSelector>() {
41 Ok(selector) => Ok(RunIdOrRecordingSelector::RunId(selector)),
42 Err(_) => {
43 if s.contains('/') || s.contains(std::path::MAIN_SEPARATOR) {
49 Ok(RunIdOrRecordingSelector::RecordingPath(path))
50 } else {
51 Err(InvalidRunIdOrRecordingSelector {
52 input: s.to_owned(),
53 })
54 }
55 }
56 }
57 }
58}
59
60impl Default for RunIdOrRecordingSelector {
61 fn default() -> Self {
62 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
63 }
64}
65
66impl fmt::Display for RunIdOrRecordingSelector {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 RunIdOrRecordingSelector::RunId(selector) => write!(f, "{selector}"),
70 RunIdOrRecordingSelector::RecordingPath(path) => write!(f, "{path}"),
71 }
72 }
73}
74
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
81pub enum RunIdSelector {
82 #[default]
84 Latest,
85
86 Prefix(String),
90}
91
92impl FromStr for RunIdSelector {
93 type Err = InvalidRunIdSelector;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 if s == "latest" {
97 Ok(RunIdSelector::Latest)
98 } else {
99 let is_valid = !s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit() || c == '-');
101 if is_valid {
102 Ok(RunIdSelector::Prefix(s.to_owned()))
103 } else {
104 Err(InvalidRunIdSelector {
105 input: s.to_owned(),
106 })
107 }
108 }
109 }
110}
111
112impl fmt::Display for RunIdSelector {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self {
115 RunIdSelector::Latest => write!(f, "latest"),
116 RunIdSelector::Prefix(prefix) => write!(f, "{prefix}"),
117 }
118 }
119}
120
121#[derive(Clone, Debug)]
129pub struct RunIdIndex {
130 sorted_entries: Vec<RunIdIndexEntry>,
132}
133
134#[derive(Clone, Debug)]
136struct RunIdIndexEntry {
137 run_id: ReportUuid,
138 hex: String,
140}
141
142impl RunIdIndex {
143 pub fn new(runs: &[RecordedRunInfo]) -> Self {
145 let mut sorted_entries: Vec<_> = runs
146 .iter()
147 .map(|r| RunIdIndexEntry {
148 run_id: r.run_id,
149 hex: r.run_id.to_string().replace('-', "").to_lowercase(),
150 })
151 .collect();
152
153 sorted_entries.sort_by(|a, b| a.hex.cmp(&b.hex));
155 Self { sorted_entries }
156 }
157
158 pub fn shortest_unique_prefix_len(&self, run_id: ReportUuid) -> Option<usize> {
163 let pos = self
165 .sorted_entries
166 .iter()
167 .position(|entry| entry.run_id == run_id)?;
168
169 let target_hex = &self.sorted_entries[pos].hex;
170
171 let mut min_len = 1; if pos > 0 {
176 let prev_hex = &self.sorted_entries[pos - 1].hex;
177 let common = common_hex_prefix_len(target_hex, prev_hex);
178 min_len = min_len.max(common + 1);
179 }
180
181 if pos + 1 < self.sorted_entries.len() {
183 let next_hex = &self.sorted_entries[pos + 1].hex;
184 let common = common_hex_prefix_len(target_hex, next_hex);
185 min_len = min_len.max(common + 1);
186 }
187
188 Some(min_len)
189 }
190
191 pub fn shortest_unique_prefix(&self, run_id: ReportUuid) -> Option<ShortestRunIdPrefix> {
199 let prefix_len = self.shortest_unique_prefix_len(run_id)?;
200 Some(ShortestRunIdPrefix::new(run_id, prefix_len))
201 }
202
203 pub fn resolve_prefix(&self, prefix: &str) -> Result<ReportUuid, PrefixResolutionError> {
208 let normalized = prefix.replace('-', "").to_lowercase();
210 if !normalized.chars().all(|c| c.is_ascii_hexdigit()) {
211 return Err(PrefixResolutionError::InvalidPrefix);
212 }
213
214 let start = self
217 .sorted_entries
218 .partition_point(|entry| entry.hex.as_str() < normalized.as_str());
219
220 let matches: Vec<_> = self.sorted_entries[start..]
222 .iter()
223 .take_while(|entry| entry.hex.starts_with(&normalized))
224 .map(|entry| entry.run_id)
225 .collect();
226
227 match matches.len() {
228 0 => Err(PrefixResolutionError::NotFound),
229 1 => Ok(matches[0]),
230 n => {
231 let candidates = matches.into_iter().take(8).collect();
232 Err(PrefixResolutionError::Ambiguous {
233 count: n,
234 candidates,
235 })
236 }
237 }
238 }
239
240 pub fn len(&self) -> usize {
242 self.sorted_entries.len()
243 }
244
245 pub fn is_empty(&self) -> bool {
247 self.sorted_entries.is_empty()
248 }
249
250 pub fn iter(&self) -> impl Iterator<Item = ReportUuid> + '_ {
252 self.sorted_entries.iter().map(|entry| entry.run_id)
253 }
254}
255
256#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct ShortestRunIdPrefix {
262 pub prefix: String,
264 pub rest: String,
266}
267
268impl ShortestRunIdPrefix {
269 fn new(run_id: ReportUuid, hex_len: usize) -> Self {
273 let full = run_id.to_string();
274
275 let split_index = hex_len_to_string_index(hex_len);
279 let split_index = split_index.min(full.len());
280
281 let (prefix, rest) = full.split_at(split_index);
282 Self {
283 prefix: prefix.to_string(),
284 rest: rest.to_string(),
285 }
286 }
287
288 pub fn full(&self) -> String {
290 format!("{}{}", self.prefix, self.rest)
291 }
292}
293
294fn hex_len_to_string_index(hex_len: usize) -> usize {
307 let dashes = match hex_len {
309 0..=8 => 0,
310 9..=12 => 1,
311 13..=16 => 2,
312 17..=20 => 3,
313 21..=32 => 4,
314 _ => 4, };
316 hex_len + dashes
317}
318
319fn common_hex_prefix_len(a: &str, b: &str) -> usize {
321 a.chars()
322 .zip(b.chars())
323 .take_while(|(ca, cb)| ca == cb)
324 .count()
325}
326
327#[derive(Clone, Debug)]
333pub enum PrefixResolutionError {
334 NotFound,
336
337 Ambiguous {
339 count: usize,
341 candidates: Vec<ReportUuid>,
343 },
344
345 InvalidPrefix,
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::record::{RecordedRunStatus, RecordedSizes, format::STORE_FORMAT_VERSION};
353 use chrono::TimeZone;
354 use semver::Version;
355 use std::collections::BTreeMap;
356
357 fn make_run(run_id: ReportUuid) -> RecordedRunInfo {
359 let started_at = chrono::FixedOffset::east_opt(0)
360 .unwrap()
361 .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
362 .unwrap();
363 RecordedRunInfo {
364 run_id,
365 store_format_version: STORE_FORMAT_VERSION,
366 nextest_version: Version::new(0, 1, 0),
367 started_at,
368 last_written_at: started_at,
369 duration_secs: None,
370 cli_args: Vec::new(),
371 build_scope_args: Vec::new(),
372 env_vars: BTreeMap::new(),
373 parent_run_id: None,
374 sizes: RecordedSizes::default(),
375 status: RecordedRunStatus::Incomplete,
376 }
377 }
378
379 #[test]
380 fn test_empty_index() {
381 let index = RunIdIndex::new(&[]);
382 assert!(index.is_empty());
383 assert_eq!(index.len(), 0);
384 }
385
386 #[test]
387 fn test_single_entry() {
388 let runs = vec![make_run(ReportUuid::from_u128(
389 0x550e8400_e29b_41d4_a716_446655440000,
390 ))];
391 let index = RunIdIndex::new(&runs);
392
393 assert_eq!(index.len(), 1);
394
395 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(1));
397
398 let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
399 assert_eq!(prefix.prefix, "5");
400 assert_eq!(prefix.rest, "50e8400-e29b-41d4-a716-446655440000");
401 assert_eq!(prefix.full(), "550e8400-e29b-41d4-a716-446655440000");
402 }
403
404 #[test]
405 fn test_shared_prefix() {
406 let runs = vec![
408 make_run(ReportUuid::from_u128(
409 0x55551111_0000_0000_0000_000000000000,
410 )),
411 make_run(ReportUuid::from_u128(
412 0x55552222_0000_0000_0000_000000000000,
413 )),
414 ];
415 let index = RunIdIndex::new(&runs);
416
417 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(5));
419 assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(5));
420
421 let prefix0 = index.shortest_unique_prefix(runs[0].run_id).unwrap();
422 assert_eq!(prefix0.prefix, "55551");
423 assert_eq!(prefix0.rest, "111-0000-0000-0000-000000000000");
424
425 let prefix1 = index.shortest_unique_prefix(runs[1].run_id).unwrap();
426 assert_eq!(prefix1.prefix, "55552");
427 }
428
429 #[test]
430 fn test_asymmetric_neighbors() {
431 let runs = vec![
434 make_run(ReportUuid::from_u128(
435 0x11110000_0000_0000_0000_000000000000,
436 )),
437 make_run(ReportUuid::from_u128(
438 0x11120000_0000_0000_0000_000000000000,
439 )),
440 make_run(ReportUuid::from_u128(
441 0x22220000_0000_0000_0000_000000000000,
442 )),
443 ];
444 let index = RunIdIndex::new(&runs);
445
446 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(4));
448 assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(4));
449 assert_eq!(index.shortest_unique_prefix_len(runs[2].run_id), Some(1));
451 }
452
453 #[test]
454 fn test_prefix_crosses_dash() {
455 let runs = vec![
457 make_run(ReportUuid::from_u128(
458 0x12345678_9000_0000_0000_000000000000,
459 )),
460 make_run(ReportUuid::from_u128(
461 0x12345678_9111_0000_0000_000000000000,
462 )),
463 ];
464 let index = RunIdIndex::new(&runs);
465
466 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(10));
467
468 let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
470 assert_eq!(prefix.prefix, "12345678-90");
471 assert_eq!(prefix.rest, "00-0000-0000-000000000000");
472 }
473
474 #[test]
475 fn test_resolve_prefix() {
476 let runs = vec![
477 make_run(ReportUuid::from_u128(
478 0xabcdef00_1234_5678_9abc_def012345678,
479 )),
480 make_run(ReportUuid::from_u128(
481 0x22222222_2222_2222_2222_222222222222,
482 )),
483 make_run(ReportUuid::from_u128(
484 0x23333333_3333_3333_3333_333333333333,
485 )),
486 ];
487 let index = RunIdIndex::new(&runs);
488
489 assert_eq!(index.resolve_prefix("abc").unwrap(), runs[0].run_id);
491 assert_eq!(index.resolve_prefix("22").unwrap(), runs[1].run_id);
492
493 assert_eq!(index.resolve_prefix("ABC").unwrap(), runs[0].run_id);
495 assert_eq!(index.resolve_prefix("AbC").unwrap(), runs[0].run_id);
496
497 assert_eq!(index.resolve_prefix("abcdef00-").unwrap(), runs[0].run_id);
499 assert_eq!(index.resolve_prefix("abcdef00-12").unwrap(), runs[0].run_id);
500
501 let err = index.resolve_prefix("2").unwrap_err();
503 assert!(matches!(
504 err,
505 PrefixResolutionError::Ambiguous { count: 2, .. }
506 ));
507
508 let err = index.resolve_prefix("9").unwrap_err();
510 assert!(matches!(err, PrefixResolutionError::NotFound));
511
512 let err = index.resolve_prefix("xyz").unwrap_err();
514 assert!(matches!(err, PrefixResolutionError::InvalidPrefix));
515 }
516
517 #[test]
518 fn test_not_in_index() {
519 let runs = vec![make_run(ReportUuid::from_u128(
520 0x11111111_1111_1111_1111_111111111111,
521 ))];
522 let index = RunIdIndex::new(&runs);
523
524 let other = ReportUuid::from_u128(0x22222222_2222_2222_2222_222222222222);
525 assert_eq!(index.shortest_unique_prefix_len(other), None);
526 assert_eq!(index.shortest_unique_prefix(other), None);
527 }
528
529 #[test]
530 fn test_hex_len_to_string_index() {
531 assert_eq!(hex_len_to_string_index(0), 0);
533 assert_eq!(hex_len_to_string_index(8), 8);
534 assert_eq!(hex_len_to_string_index(9), 10);
536 assert_eq!(hex_len_to_string_index(13), 15);
537 assert_eq!(hex_len_to_string_index(17), 20);
538 assert_eq!(hex_len_to_string_index(21), 25);
539 assert_eq!(hex_len_to_string_index(32), 36);
541 }
542
543 #[test]
544 fn test_run_id_selector_default() {
545 assert_eq!(RunIdSelector::default(), RunIdSelector::Latest);
546 }
547
548 #[test]
549 fn test_run_id_selector_from_str() {
550 assert_eq!(
552 "latest".parse::<RunIdSelector>().unwrap(),
553 RunIdSelector::Latest
554 );
555
556 assert_eq!(
558 "abc123".parse::<RunIdSelector>().unwrap(),
559 RunIdSelector::Prefix("abc123".to_owned())
560 );
561 assert_eq!(
562 "550e8400-e29b-41d4".parse::<RunIdSelector>().unwrap(),
563 RunIdSelector::Prefix("550e8400-e29b-41d4".to_owned())
564 );
565 assert_eq!(
566 "ABCDEF".parse::<RunIdSelector>().unwrap(),
567 RunIdSelector::Prefix("ABCDEF".to_owned())
568 );
569 assert_eq!(
570 "0".parse::<RunIdSelector>().unwrap(),
571 RunIdSelector::Prefix("0".to_owned())
572 );
573
574 assert!("Latest".parse::<RunIdSelector>().is_err());
576 assert!("LATEST".parse::<RunIdSelector>().is_err());
577
578 assert!("xyz".parse::<RunIdSelector>().is_err());
580 assert!("abc_123".parse::<RunIdSelector>().is_err());
581 assert!("hello".parse::<RunIdSelector>().is_err());
582
583 assert!("".parse::<RunIdSelector>().is_err());
585 }
586
587 #[test]
588 fn test_run_id_selector_display() {
589 assert_eq!(RunIdSelector::Latest.to_string(), "latest");
590 assert_eq!(
591 RunIdSelector::Prefix("abc123".to_owned()).to_string(),
592 "abc123"
593 );
594 }
595
596 #[test]
597 fn test_run_id_or_archive_selector_default() {
598 assert_eq!(
599 RunIdOrRecordingSelector::default(),
600 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
601 );
602 }
603
604 #[test]
605 fn test_run_id_or_archive_selector_from_str() {
606 assert_eq!(
608 "latest".parse::<RunIdOrRecordingSelector>().unwrap(),
609 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
610 );
611
612 assert_eq!(
614 "abc123".parse::<RunIdOrRecordingSelector>().unwrap(),
615 RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned()))
616 );
617
618 assert_eq!(
620 "nextest-run-abc123.zip"
621 .parse::<RunIdOrRecordingSelector>()
622 .unwrap(),
623 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("nextest-run-abc123.zip"))
624 );
625 assert_eq!(
626 "/path/to/archive.zip"
627 .parse::<RunIdOrRecordingSelector>()
628 .unwrap(),
629 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
630 );
631 assert_eq!(
632 "../relative/path.zip"
633 .parse::<RunIdOrRecordingSelector>()
634 .unwrap(),
635 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../relative/path.zip"))
636 );
637
638 assert_eq!(
641 "/proc/self/fd/11"
642 .parse::<RunIdOrRecordingSelector>()
643 .unwrap(),
644 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/proc/self/fd/11"))
645 );
646 assert_eq!(
647 "/dev/fd/5".parse::<RunIdOrRecordingSelector>().unwrap(),
648 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/dev/fd/5"))
649 );
650 assert_eq!(
651 "./my-recording"
652 .parse::<RunIdOrRecordingSelector>()
653 .unwrap(),
654 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("./my-recording"))
655 );
656 assert_eq!(
657 "../path/to/file"
658 .parse::<RunIdOrRecordingSelector>()
659 .unwrap(),
660 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../path/to/file"))
661 );
662 #[cfg(windows)]
664 {
665 assert_eq!(
666 r"C:\path\to\file"
667 .parse::<RunIdOrRecordingSelector>()
668 .unwrap(),
669 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from(r"C:\path\to\file"))
670 );
671 }
672
673 assert!("xyz".parse::<RunIdOrRecordingSelector>().is_err());
676 assert!("hello".parse::<RunIdOrRecordingSelector>().is_err());
677 assert!("latets".parse::<RunIdOrRecordingSelector>().is_err());
679 assert!("latestt".parse::<RunIdOrRecordingSelector>().is_err());
680 assert!("recording".parse::<RunIdOrRecordingSelector>().is_err());
682 }
683
684 #[test]
685 fn test_run_id_or_archive_selector_display() {
686 assert_eq!(
687 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest).to_string(),
688 "latest"
689 );
690 assert_eq!(
691 RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned())).to_string(),
692 "abc123"
693 );
694 assert_eq!(
695 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
696 .to_string(),
697 "/path/to/archive.zip"
698 );
699 }
700}