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 env_vars: BTreeMap::new(),
372 parent_run_id: None,
373 sizes: RecordedSizes::default(),
374 status: RecordedRunStatus::Incomplete,
375 }
376 }
377
378 #[test]
379 fn test_empty_index() {
380 let index = RunIdIndex::new(&[]);
381 assert!(index.is_empty());
382 assert_eq!(index.len(), 0);
383 }
384
385 #[test]
386 fn test_single_entry() {
387 let runs = vec![make_run(ReportUuid::from_u128(
388 0x550e8400_e29b_41d4_a716_446655440000,
389 ))];
390 let index = RunIdIndex::new(&runs);
391
392 assert_eq!(index.len(), 1);
393
394 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(1));
396
397 let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
398 assert_eq!(prefix.prefix, "5");
399 assert_eq!(prefix.rest, "50e8400-e29b-41d4-a716-446655440000");
400 assert_eq!(prefix.full(), "550e8400-e29b-41d4-a716-446655440000");
401 }
402
403 #[test]
404 fn test_shared_prefix() {
405 let runs = vec![
407 make_run(ReportUuid::from_u128(
408 0x55551111_0000_0000_0000_000000000000,
409 )),
410 make_run(ReportUuid::from_u128(
411 0x55552222_0000_0000_0000_000000000000,
412 )),
413 ];
414 let index = RunIdIndex::new(&runs);
415
416 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(5));
418 assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(5));
419
420 let prefix0 = index.shortest_unique_prefix(runs[0].run_id).unwrap();
421 assert_eq!(prefix0.prefix, "55551");
422 assert_eq!(prefix0.rest, "111-0000-0000-0000-000000000000");
423
424 let prefix1 = index.shortest_unique_prefix(runs[1].run_id).unwrap();
425 assert_eq!(prefix1.prefix, "55552");
426 }
427
428 #[test]
429 fn test_asymmetric_neighbors() {
430 let runs = vec![
433 make_run(ReportUuid::from_u128(
434 0x11110000_0000_0000_0000_000000000000,
435 )),
436 make_run(ReportUuid::from_u128(
437 0x11120000_0000_0000_0000_000000000000,
438 )),
439 make_run(ReportUuid::from_u128(
440 0x22220000_0000_0000_0000_000000000000,
441 )),
442 ];
443 let index = RunIdIndex::new(&runs);
444
445 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(4));
447 assert_eq!(index.shortest_unique_prefix_len(runs[1].run_id), Some(4));
448 assert_eq!(index.shortest_unique_prefix_len(runs[2].run_id), Some(1));
450 }
451
452 #[test]
453 fn test_prefix_crosses_dash() {
454 let runs = vec![
456 make_run(ReportUuid::from_u128(
457 0x12345678_9000_0000_0000_000000000000,
458 )),
459 make_run(ReportUuid::from_u128(
460 0x12345678_9111_0000_0000_000000000000,
461 )),
462 ];
463 let index = RunIdIndex::new(&runs);
464
465 assert_eq!(index.shortest_unique_prefix_len(runs[0].run_id), Some(10));
466
467 let prefix = index.shortest_unique_prefix(runs[0].run_id).unwrap();
469 assert_eq!(prefix.prefix, "12345678-90");
470 assert_eq!(prefix.rest, "00-0000-0000-000000000000");
471 }
472
473 #[test]
474 fn test_resolve_prefix() {
475 let runs = vec![
476 make_run(ReportUuid::from_u128(
477 0xabcdef00_1234_5678_9abc_def012345678,
478 )),
479 make_run(ReportUuid::from_u128(
480 0x22222222_2222_2222_2222_222222222222,
481 )),
482 make_run(ReportUuid::from_u128(
483 0x23333333_3333_3333_3333_333333333333,
484 )),
485 ];
486 let index = RunIdIndex::new(&runs);
487
488 assert_eq!(index.resolve_prefix("abc").unwrap(), runs[0].run_id);
490 assert_eq!(index.resolve_prefix("22").unwrap(), runs[1].run_id);
491
492 assert_eq!(index.resolve_prefix("ABC").unwrap(), runs[0].run_id);
494 assert_eq!(index.resolve_prefix("AbC").unwrap(), runs[0].run_id);
495
496 assert_eq!(index.resolve_prefix("abcdef00-").unwrap(), runs[0].run_id);
498 assert_eq!(index.resolve_prefix("abcdef00-12").unwrap(), runs[0].run_id);
499
500 let err = index.resolve_prefix("2").unwrap_err();
502 assert!(matches!(
503 err,
504 PrefixResolutionError::Ambiguous { count: 2, .. }
505 ));
506
507 let err = index.resolve_prefix("9").unwrap_err();
509 assert!(matches!(err, PrefixResolutionError::NotFound));
510
511 let err = index.resolve_prefix("xyz").unwrap_err();
513 assert!(matches!(err, PrefixResolutionError::InvalidPrefix));
514 }
515
516 #[test]
517 fn test_not_in_index() {
518 let runs = vec![make_run(ReportUuid::from_u128(
519 0x11111111_1111_1111_1111_111111111111,
520 ))];
521 let index = RunIdIndex::new(&runs);
522
523 let other = ReportUuid::from_u128(0x22222222_2222_2222_2222_222222222222);
524 assert_eq!(index.shortest_unique_prefix_len(other), None);
525 assert_eq!(index.shortest_unique_prefix(other), None);
526 }
527
528 #[test]
529 fn test_hex_len_to_string_index() {
530 assert_eq!(hex_len_to_string_index(0), 0);
532 assert_eq!(hex_len_to_string_index(8), 8);
533 assert_eq!(hex_len_to_string_index(9), 10);
535 assert_eq!(hex_len_to_string_index(13), 15);
536 assert_eq!(hex_len_to_string_index(17), 20);
537 assert_eq!(hex_len_to_string_index(21), 25);
538 assert_eq!(hex_len_to_string_index(32), 36);
540 }
541
542 #[test]
543 fn test_run_id_selector_default() {
544 assert_eq!(RunIdSelector::default(), RunIdSelector::Latest);
545 }
546
547 #[test]
548 fn test_run_id_selector_from_str() {
549 assert_eq!(
551 "latest".parse::<RunIdSelector>().unwrap(),
552 RunIdSelector::Latest
553 );
554
555 assert_eq!(
557 "abc123".parse::<RunIdSelector>().unwrap(),
558 RunIdSelector::Prefix("abc123".to_owned())
559 );
560 assert_eq!(
561 "550e8400-e29b-41d4".parse::<RunIdSelector>().unwrap(),
562 RunIdSelector::Prefix("550e8400-e29b-41d4".to_owned())
563 );
564 assert_eq!(
565 "ABCDEF".parse::<RunIdSelector>().unwrap(),
566 RunIdSelector::Prefix("ABCDEF".to_owned())
567 );
568 assert_eq!(
569 "0".parse::<RunIdSelector>().unwrap(),
570 RunIdSelector::Prefix("0".to_owned())
571 );
572
573 assert!("Latest".parse::<RunIdSelector>().is_err());
575 assert!("LATEST".parse::<RunIdSelector>().is_err());
576
577 assert!("xyz".parse::<RunIdSelector>().is_err());
579 assert!("abc_123".parse::<RunIdSelector>().is_err());
580 assert!("hello".parse::<RunIdSelector>().is_err());
581
582 assert!("".parse::<RunIdSelector>().is_err());
584 }
585
586 #[test]
587 fn test_run_id_selector_display() {
588 assert_eq!(RunIdSelector::Latest.to_string(), "latest");
589 assert_eq!(
590 RunIdSelector::Prefix("abc123".to_owned()).to_string(),
591 "abc123"
592 );
593 }
594
595 #[test]
596 fn test_run_id_or_archive_selector_default() {
597 assert_eq!(
598 RunIdOrRecordingSelector::default(),
599 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
600 );
601 }
602
603 #[test]
604 fn test_run_id_or_archive_selector_from_str() {
605 assert_eq!(
607 "latest".parse::<RunIdOrRecordingSelector>().unwrap(),
608 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest)
609 );
610
611 assert_eq!(
613 "abc123".parse::<RunIdOrRecordingSelector>().unwrap(),
614 RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned()))
615 );
616
617 assert_eq!(
619 "nextest-run-abc123.zip"
620 .parse::<RunIdOrRecordingSelector>()
621 .unwrap(),
622 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("nextest-run-abc123.zip"))
623 );
624 assert_eq!(
625 "/path/to/archive.zip"
626 .parse::<RunIdOrRecordingSelector>()
627 .unwrap(),
628 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
629 );
630 assert_eq!(
631 "../relative/path.zip"
632 .parse::<RunIdOrRecordingSelector>()
633 .unwrap(),
634 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../relative/path.zip"))
635 );
636
637 assert_eq!(
640 "/proc/self/fd/11"
641 .parse::<RunIdOrRecordingSelector>()
642 .unwrap(),
643 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/proc/self/fd/11"))
644 );
645 assert_eq!(
646 "/dev/fd/5".parse::<RunIdOrRecordingSelector>().unwrap(),
647 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/dev/fd/5"))
648 );
649 assert_eq!(
650 "./my-recording"
651 .parse::<RunIdOrRecordingSelector>()
652 .unwrap(),
653 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("./my-recording"))
654 );
655 assert_eq!(
656 "../path/to/file"
657 .parse::<RunIdOrRecordingSelector>()
658 .unwrap(),
659 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("../path/to/file"))
660 );
661 #[cfg(windows)]
663 {
664 assert_eq!(
665 r"C:\path\to\file"
666 .parse::<RunIdOrRecordingSelector>()
667 .unwrap(),
668 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from(r"C:\path\to\file"))
669 );
670 }
671
672 assert!("xyz".parse::<RunIdOrRecordingSelector>().is_err());
675 assert!("hello".parse::<RunIdOrRecordingSelector>().is_err());
676 assert!("latets".parse::<RunIdOrRecordingSelector>().is_err());
678 assert!("latestt".parse::<RunIdOrRecordingSelector>().is_err());
679 assert!("recording".parse::<RunIdOrRecordingSelector>().is_err());
681 }
682
683 #[test]
684 fn test_run_id_or_archive_selector_display() {
685 assert_eq!(
686 RunIdOrRecordingSelector::RunId(RunIdSelector::Latest).to_string(),
687 "latest"
688 );
689 assert_eq!(
690 RunIdOrRecordingSelector::RunId(RunIdSelector::Prefix("abc123".to_owned())).to_string(),
691 "abc123"
692 );
693 assert_eq!(
694 RunIdOrRecordingSelector::RecordingPath(Utf8PathBuf::from("/path/to/archive.zip"))
695 .to_string(),
696 "/path/to/archive.zip"
697 );
698 }
699}