1use std::collections::BTreeMap;
12use std::fmt;
13use std::time::Duration;
14
15pub use reverie::DispatchStats;
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::host_input::HostInputChange;
22use crate::host_input::HostInputSet;
23use crate::pid::DetTid;
24use crate::syscall_histogram::SyscallHistogramSummary;
25use crate::time::LogicalTime;
26
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
35#[serde(deny_unknown_fields)]
36pub struct PathEvidence {
37 pub schema: u8,
38 pub guest_rpc_observed: bool,
39 pub ptrace_fallback_sites: usize,
40 pub trusted_shared_object_sites: usize,
41 pub trusted_shared_objects: Vec<String>,
42}
43
44impl PathEvidence {
45 pub const SCHEMA: u8 = 1;
46}
47
48#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
54pub struct TimesliceStats {
55 pub count: u64,
57 pub sum_ns: u64,
59 pub min_ns: u64,
61 pub max_ns: u64,
63}
64
65impl TimesliceStats {
66 pub fn record(&mut self, ns: u64) {
68 if self.count == 0 {
69 self.min_ns = ns;
70 self.max_ns = ns;
71 } else {
72 self.min_ns = self.min_ns.min(ns);
73 self.max_ns = self.max_ns.max(ns);
74 }
75 self.sum_ns += ns;
76 self.count += 1;
77 }
78
79 pub fn merge(&mut self, other: &TimesliceStats) {
81 if other.count == 0 {
82 return;
83 }
84 if self.count == 0 {
85 *self = *other;
86 return;
87 }
88 self.min_ns = self.min_ns.min(other.min_ns);
89 self.max_ns = self.max_ns.max(other.max_ns);
90 self.sum_ns += other.sum_ns;
91 self.count += other.count;
92 }
93
94 pub fn mean_ns(&self) -> u64 {
96 self.sum_ns.checked_div(self.count).unwrap_or(0)
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.count == 0
102 }
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
110pub struct PreemptionSummary {
111 pub max_timeslice_ns: Option<u64>,
113 #[serde(default)]
116 pub timer_enabled: Option<bool>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub stopless_slice_ends: Option<u64>,
122}
123
124impl fmt::Display for PreemptionSummary {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self.max_timeslice_ns {
127 Some(maximum) => write!(f, "Configured maximum timeslice: {maximum} virtual ns")?,
128 None => write!(f, "Configured maximum timeslice: disabled")?,
129 };
130 let timer = match self.timer_enabled {
131 Some(false) => "none",
132 Some(true) => "enabled",
133 None => "unknown",
134 };
135 write!(f, "; timer: {timer}")?;
136 if let Some(ends) = self.stopless_slice_ends {
137 write!(
138 f,
139 "; slice ends taken without a stop (no other thread could run): {ends}"
140 )?;
141 }
142 Ok(())
143 }
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone, Default)]
148pub struct RunSummary {
149 pub sched_turns: u64,
151
152 pub schedevent_replayed: u64,
154 pub schedevent_recorded: u64,
156 pub schedevent_desynced: u64,
158
159 pub desync_descrip: Option<String>,
161
162 pub reprio_descrip: Option<String>,
164
165 pub threads_descrip: String,
167
168 pub num_processes: u64,
170 pub num_threads: u64,
172
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub syscalls: Option<u64>,
176
177 pub virttime_elapsed: u64,
179 pub virttime_final: u64,
181
182 pub realtime_elapsed: Option<Duration>,
184
185 pub timeslice_stats: TimesliceStats,
188
189 pub per_thread_timeslice: Vec<(DetTid, TimesliceStats)>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub preemption: Option<PreemptionSummary>,
201
202 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub dispatch_stats: Option<DispatchStats>,
209
210 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub replay_departure: Option<crate::replay_departure::Summary>,
215
216 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub syscall_histogram: Option<SyscallHistogramSummary>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub host_inputs: Option<HostInputSet>,
231
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub host_inputs_changed: Vec<HostInputChange>,
238 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
243 pub guest_deadlock: bool,
244
245 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub virtual_timeout_ns: Option<u64>,
251
252 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
256 pub exclusive_pairs: BTreeMap<String, u64>,
257
258 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub store_exclusives: Option<StoreExclusiveCounts>,
265
266 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub store_exclusives_unavailable: Option<String>,
270
271 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub guest_ids: Option<crate::pid::GuestIds>,
277
278 #[serde(default, skip_serializing_if = "is_zero")]
285 pub host_id_tasks: u64,
286}
287
288fn is_zero(count: &u64) -> bool {
289 *count == 0
290}
291
292#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
295pub struct StoreExclusiveCounts {
296 pub failed: u64,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub executed: Option<u64>,
301 pub fully_counted: bool,
305}
306
307impl fmt::Display for StoreExclusiveCounts {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 let StoreExclusiveCounts {
310 failed,
311 executed,
312 fully_counted,
313 } = self;
314 let executed = executed.map(|executed| counted(executed, "store-exclusive"));
315 match (*failed, executed) {
316 (0, Some(executed)) => write!(f, "{executed} executed, none failed")?,
317 (0, None) => write!(f, "No store-exclusive failed")?,
318 (failed, executed) => {
319 let failed = counted(failed, "store-exclusive failure");
320 write!(f, "{failed}; replay may diverge")?;
321 if let Some(executed) = executed {
322 write!(f, " ({executed} executed)")?;
323 }
324 }
325 }
326 if !fully_counted {
327 write!(
328 f,
329 "; not counted for the whole run (multiplexed, or the task ran on CPUs whose \
330 PMU does not carry these events)"
331 )?;
332 }
333 Ok(())
334 }
335}
336
337fn counted(count: u64, noun: &str) -> String {
339 if count == 1 {
340 format!("1 {noun}")
341 } else {
342 format!("{count} {noun}s")
343 }
344}
345
346impl fmt::Display for RunSummary {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 self.fmt_summary(f, true, self.reprio_descrip.as_deref())
350 }
351}
352
353pub struct RunSummaryInfo<'a> {
357 summary: &'a RunSummary,
358 reprio_description: Option<&'a str>,
359}
360
361impl fmt::Display for RunSummaryInfo<'_> {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 self.summary.fmt_summary(f, false, self.reprio_description)
364 }
365}
366
367impl RunSummary {
368 pub fn syscall_histogram_is_consistent(&self) -> Option<bool> {
371 let histogram = self.syscall_histogram.as_ref()?;
372 Some(Some(histogram.total_calls()) == self.syscalls)
373 }
374
375 pub fn info<'a>(&'a self, reprio_description: Option<&'a str>) -> RunSummaryInfo<'a> {
376 RunSummaryInfo {
377 summary: self,
378 reprio_description,
379 }
380 }
381
382 fn fmt_summary(
383 &self,
384 f: &mut fmt::Formatter<'_>,
385 include_report_metadata: bool,
386 reprio_description: Option<&str>,
387 ) -> fmt::Result {
388 let RunSummary {
389 sched_turns,
390 schedevent_replayed,
391 schedevent_recorded,
392 schedevent_desynced,
393 desync_descrip,
394 reprio_descrip: _,
395 num_processes,
396 num_threads,
397 syscalls,
398 virttime_elapsed,
399 virttime_final,
400 realtime_elapsed,
401 threads_descrip,
402 timeslice_stats,
403 per_thread_timeslice,
404 preemption,
405 dispatch_stats: _,
406 syscall_histogram: _,
407 host_inputs: _,
408 host_inputs_changed: _,
409 exclusive_pairs,
410 store_exclusives,
411 store_exclusives_unavailable,
412 guest_deadlock,
413 guest_ids,
414 host_id_tasks,
415 virtual_timeout_ns,
416 replay_departure: _,
417 } = self;
418 writeln!(f, "Final thread-tree was: {}", threads_descrip)?;
419 writeln!(
420 f,
421 "There were {} group leaders of {} thread(s) total.",
422 num_processes, num_threads
423 )?;
424 if let Some(syscalls) = syscalls {
425 writeln!(f, "Guest threads completed {syscalls} syscalls.")?;
426 }
427 if *guest_deadlock {
428 writeln!(
429 f,
430 "Deadlock under Hermit's model: no remaining thread can be woken by anything Hermit models."
431 )?;
432 }
433 if let Some(bound) = virtual_timeout_ns {
434 writeln!(
435 f,
436 "The run's virtual clock passed --virtual-timeout={bound}: no scheduler turn was granted after it."
437 )?;
438 }
439 if include_report_metadata {
440 writeln!(
441 f,
442 "Internally, the hermit scheduler ran {} turns, recorded {} events, replayed {} events ({} desynced)",
443 sched_turns, schedevent_recorded, schedevent_replayed, schedevent_desynced,
444 )?;
445 } else {
446 writeln!(
447 f,
448 "Internally, the hermit scheduler ran {} turns, recorded {} events ({} desynced)",
449 sched_turns, schedevent_recorded, schedevent_desynced,
450 )?;
451 }
452
453 if include_report_metadata && let Some(preemption) = preemption {
454 writeln!(f, "{preemption}")?;
455 }
456 if include_report_metadata && let Some(guest_ids) = guest_ids {
457 writeln!(f, "Guest process IDs: {guest_ids}")?;
458 if *host_id_tasks > 0 {
459 writeln!(
460 f,
461 " {host_id_tasks} host-ID report(s) accepted; chosen process IDs are not guaranteed"
462 )?;
463 }
464 }
465
466 if include_report_metadata {
467 if let Some(counts) = store_exclusives {
468 writeln!(f, "{counts}")?;
469 }
470 if let Some(reason) = store_exclusives_unavailable {
471 writeln!(f, "Store-exclusives were not counted: {reason}")?;
472 }
473 }
474 if !exclusive_pairs.is_empty() {
475 writeln!(
476 f,
477 "Store-exclusive (LL/SC) pairs in executable mappings of {} file(s):",
478 exclusive_pairs.len()
479 )?;
480 for (path, pairs) in exclusive_pairs {
481 writeln!(f, " {path}: {pairs}")?;
482 }
483 }
484
485 if let Some(txt) = desync_descrip {
486 write!(f, "{}", txt)?;
487 }
488 if let Some(txt) = reprio_description {
489 write!(f, "{}", txt)?;
490 }
491
492 writeln!(
493 f,
494 "Final virtual global (cpu) time: {}",
495 LogicalTime::from_nanos(*virttime_final)
496 )?;
497 writeln!(
498 f,
499 "Elapsed virtual global (cpu) time: {}",
500 LogicalTime::from_nanos(*virttime_elapsed)
501 )?;
502
503 if timeslice_stats.is_empty() {
504 writeln!(f, "Timeslice stats: none recorded")?;
505 } else {
506 writeln!(
507 f,
508 "Timeslice stats: min={}ns max={}ns mean={}ns count={}",
509 timeslice_stats.min_ns,
510 timeslice_stats.max_ns,
511 timeslice_stats.mean_ns(),
512 timeslice_stats.count,
513 )?;
514 if per_thread_timeslice.len() > 1 {
517 for (dettid, st) in per_thread_timeslice {
518 if st.is_empty() {
519 continue;
520 }
521 writeln!(
522 f,
523 " timeslice thread {}: min={}ns max={}ns mean={}ns count={}",
524 dettid,
525 st.min_ns,
526 st.max_ns,
527 st.mean_ns(),
528 st.count,
529 )?;
530 }
531 }
532 }
533
534 if let Some(rt) = realtime_elapsed {
535 writeln!(f, "Nondeterministic realtime elapsed: {:?}", rt)?
536 };
537
538 Ok(())
539 }
540}
541
542#[cfg(test)]
554mod tests {
555 use std::collections::BTreeMap;
556
557 use super::PathEvidence;
558 use super::PreemptionSummary;
559 use super::RunSummary;
560 use super::StoreExclusiveCounts;
561 use super::TimesliceStats;
562
563 #[test]
564 fn replay_diagnostics_are_optional_json_and_never_change_compared_display() {
565 use crate::replay_departure::Finding;
566 use crate::replay_departure::Source;
567 use crate::replay_departure::Summary;
568 let plain = RunSummary::default();
569 let json = serde_json::to_value(&plain).unwrap();
570 assert!(json.get("replay_departure").is_none());
571 let decoded: RunSummary = serde_json::from_value(json.clone()).unwrap();
572 assert!(decoded.replay_departure.is_none());
573 let mut marked = plain.clone();
574 marked.replay_departure = Some(Summary {
575 count: 2,
576 first: Finding {
577 source: Source::ThreadFiling,
578 thread: crate::pid::DetTid::from_raw(3),
579 previous: crate::time::LogicalTime::from_nanos(101),
580 observed: crate::time::LogicalTime::from_nanos(100),
581 slice: Some(7),
582 recorded_event_count: Some(19),
583 },
584 });
585 assert_eq!(marked.to_string(), plain.to_string());
586 assert_eq!(marked.info(None).to_string(), plain.info(None).to_string());
587 let mut marked_json = serde_json::to_value(&marked).unwrap();
588 let roundtrip: RunSummary = serde_json::from_value(marked_json.clone()).unwrap();
589 assert_eq!(roundtrip.replay_departure, marked.replay_departure);
590 assert_eq!(
591 marked_json
592 .as_object_mut()
593 .unwrap()
594 .remove("replay_departure"),
595 Some(serde_json::to_value(marked.replay_departure.unwrap()).unwrap())
596 );
597 assert_eq!(
598 marked_json, json,
599 "every existing JSON field stays identical"
600 );
601 }
602
603 #[test]
604 fn path_evidence_json_is_exact() {
605 let evidence = PathEvidence {
606 schema: PathEvidence::SCHEMA,
607 guest_rpc_observed: true,
608 ptrace_fallback_sites: 0,
609 trusted_shared_object_sites: 1,
610 trusted_shared_objects: vec!["/usr/lib/libc.so.6".into()],
611 };
612 let json = serde_json::to_string(&evidence).unwrap();
613 assert_eq!(
614 json,
615 r#"{"schema":1,"guest_rpc_observed":true,"ptrace_fallback_sites":0,"trusted_shared_object_sites":1,"trusted_shared_objects":["/usr/lib/libc.so.6"]}"#
616 );
617 assert_eq!(
618 serde_json::from_str::<PathEvidence>(&json).unwrap(),
619 evidence
620 );
621
622 let unknown = json.replace(
623 r#""trusted_shared_objects""#,
624 r#""unexpected":0,"trusted_shared_objects""#,
625 );
626 let error = serde_json::from_str::<PathEvidence>(&unknown).unwrap_err();
627 assert!(error.to_string().contains("unknown field `unexpected`"));
628
629 let missing = json.replace(r#""guest_rpc_observed":true,"#, "");
630 let error = serde_json::from_str::<PathEvidence>(&missing).unwrap_err();
631 assert!(
632 error
633 .to_string()
634 .contains("missing field `guest_rpc_observed`")
635 );
636 }
637
638 #[test]
639 fn timeslice_stats_empty() {
640 let s = TimesliceStats::default();
641 assert!(s.is_empty());
642 assert_eq!(s.count, 0);
643 assert_eq!(s.mean_ns(), 0); }
645
646 #[test]
647 fn timeslice_stats_record() {
648 let mut s = TimesliceStats::default();
649 s.record(10);
650 s.record(30);
651 s.record(20);
652 assert!(!s.is_empty());
653 assert_eq!(s.count, 3);
654 assert_eq!(s.min_ns, 10);
655 assert_eq!(s.max_ns, 30);
656 assert_eq!(s.sum_ns, 60);
657 assert_eq!(s.mean_ns(), 20);
658 }
659
660 #[test]
661 fn timeslice_stats_merge() {
662 let mut a = TimesliceStats::default();
663 a.record(10);
664 a.record(40);
665 let mut b = TimesliceStats::default();
666 b.record(5);
667 b.record(100);
668 a.merge(&b);
669 assert_eq!(a.count, 4);
670 assert_eq!(a.min_ns, 5);
671 assert_eq!(a.max_ns, 100);
672 assert_eq!(a.sum_ns, 155);
673
674 let before = a;
676 a.merge(&TimesliceStats::default());
677 assert_eq!(a, before);
678
679 let mut empty = TimesliceStats::default();
681 empty.merge(&b);
682 assert_eq!(empty, b);
683 }
684
685 #[test]
686 fn older_summary_json_keeps_an_unrecorded_syscall_total_absent() {
687 let value = serde_json::to_value(RunSummary::default()).unwrap();
688 let mut object = value.as_object().unwrap().clone();
689 object.remove("syscalls");
690 let parsed: RunSummary = serde_json::from_value(object.into()).unwrap();
691 assert_eq!(parsed.syscalls, None);
692
693 let measured = RunSummary {
694 syscalls: Some(0),
695 ..Default::default()
696 };
697 let value = serde_json::to_value(&measured).unwrap();
698 assert_eq!(value["syscalls"], 0);
699 let parsed: RunSummary = serde_json::from_value(value).unwrap();
700 assert_eq!(parsed.syscalls, Some(0));
701 }
702
703 #[test]
704 fn older_summary_json_keeps_preemption_configuration_unknown() {
705 let value = serde_json::to_value(RunSummary::default()).unwrap();
706 assert!(value.get("preemption").is_none());
707 let parsed: RunSummary = serde_json::from_value(value).unwrap();
708 assert_eq!(parsed.preemption, None);
709
710 let mut value = serde_json::to_value(&parsed).unwrap();
711 value["preemption"] = serde_json::json!({ "max_timeslice_ns": 200_000_000 });
712 let parsed: RunSummary = serde_json::from_value(value).unwrap();
713 assert_eq!(
714 parsed.preemption,
715 Some(PreemptionSummary {
716 max_timeslice_ns: Some(200_000_000),
717 timer_enabled: None,
718 stopless_slice_ends: None,
719 })
720 );
721
722 let mut value = serde_json::to_value(&parsed).unwrap();
723 value["preemption"] = serde_json::Value::Null;
724 let parsed: RunSummary = serde_json::from_value(value).unwrap();
725 assert_eq!(parsed.preemption, None);
726 }
727
728 #[test]
729 fn summary_json_distinguishes_disabled_and_positive_preemption() {
730 for (max_timeslice_ns, timer_enabled, expected) in [
731 (
732 None,
733 Some(false),
734 serde_json::json!({ "max_timeslice_ns": null, "timer_enabled": false }),
735 ),
736 (
737 Some(200_000_000),
738 Some(false),
739 serde_json::json!({ "max_timeslice_ns": 200_000_000, "timer_enabled": false }),
740 ),
741 (
742 Some(12_340),
743 Some(true),
744 serde_json::json!({ "max_timeslice_ns": 12_340, "timer_enabled": true }),
745 ),
746 (
747 Some(200_000_000),
748 None,
749 serde_json::json!({ "max_timeslice_ns": 200_000_000, "timer_enabled": null }),
750 ),
751 ] {
752 let summary = RunSummary {
753 preemption: Some(PreemptionSummary {
754 max_timeslice_ns,
755 timer_enabled,
756 stopless_slice_ends: None,
757 }),
758 ..Default::default()
759 };
760 let value = serde_json::to_value(&summary).unwrap();
761 assert_eq!(value["preemption"], expected);
762 let parsed: RunSummary = serde_json::from_value(value).unwrap();
763 assert_eq!(parsed.preemption, summary.preemption);
764 }
765 }
766
767 #[test]
768 fn full_summary_shows_effective_preemption_without_changing_guest_info() {
769 let baseline = RunSummary {
770 syscalls: Some(7),
771 ..Default::default()
772 };
773 let baseline_info = baseline.info(None).to_string();
774 assert!(baseline_info.contains("Guest threads completed 7 syscalls."));
775 assert!(
776 !baseline
777 .to_string()
778 .contains("Configured maximum timeslice:")
779 );
780 for (max_timeslice_ns, timer_enabled, expected) in [
781 (
782 None,
783 Some(false),
784 "Configured maximum timeslice: disabled; timer: none",
785 ),
786 (
787 Some(12_340),
788 Some(true),
789 "Configured maximum timeslice: 12340 virtual ns; timer: enabled",
790 ),
791 (
792 Some(200_000_000),
793 Some(false),
794 "Configured maximum timeslice: 200000000 virtual ns; timer: none",
795 ),
796 (
797 Some(200_000_000),
798 None,
799 "Configured maximum timeslice: 200000000 virtual ns; timer: unknown",
800 ),
801 ] {
802 let preemption = PreemptionSummary {
803 max_timeslice_ns,
804 timer_enabled,
805 stopless_slice_ends: None,
806 };
807 assert_eq!(preemption.to_string(), expected);
808 let summary = RunSummary {
809 preemption: Some(preemption),
810 ..baseline.clone()
811 };
812 assert!(summary.to_string().contains(expected));
813 assert_eq!(summary.info(None).to_string(), baseline_info);
814 }
815 }
816
817 #[test]
818 fn a_count_of_slice_ends_without_a_stop_is_shown_when_known() {
819 let preemption = PreemptionSummary {
820 max_timeslice_ns: Some(1000),
821 timer_enabled: Some(true),
822 stopless_slice_ends: Some(42),
823 };
824 assert_eq!(
825 preemption.to_string(),
826 "Configured maximum timeslice: 1000 virtual ns; timer: enabled; \
827 slice ends taken without a stop (no other thread could run): 42"
828 );
829 let value = serde_json::to_value(preemption).unwrap();
830 assert_eq!(value["stopless_slice_ends"], 42);
831 let unknown = PreemptionSummary {
832 stopless_slice_ends: None,
833 ..preemption
834 };
835 assert!(
836 serde_json::to_value(unknown)
837 .unwrap()
838 .get("stopless_slice_ends")
839 .is_none()
840 );
841 }
842
843 #[test]
844 fn store_exclusive_pairs_are_in_both_views_and_counts_only_in_the_full_report() {
845 let baseline = RunSummary {
846 syscalls: Some(7),
847 ..Default::default()
848 };
849 let value = serde_json::to_value(&baseline).unwrap();
850 for key in [
851 "exclusive_pairs",
852 "store_exclusives",
853 "store_exclusives_unavailable",
854 ] {
855 assert!(value.get(key).is_none(), "{key}");
856 }
857 assert!(!baseline.to_string().contains("tore-exclusive"));
858
859 let pairs = RunSummary {
860 exclusive_pairs: BTreeMap::from([
861 ("/lib64/libc.so.6".to_owned(), 38),
862 ("/bin/guest".to_owned(), 2),
863 ]),
864 ..baseline.clone()
865 };
866 let listing = "Store-exclusive (LL/SC) pairs in executable mappings of 2 file(s):\n \
867 /bin/guest: 2\n /lib64/libc.so.6: 38\n";
868 assert!(pairs.info(None).to_string().contains(listing));
869 assert!(pairs.to_string().contains(listing));
870
871 for (counts, line, json) in [
872 (
873 StoreExclusiveCounts {
874 failed: 20,
875 executed: Some(10_000_020),
876 fully_counted: true,
877 },
878 "20 store-exclusive failures; replay may diverge (10000020 store-exclusives \
879 executed)\n",
880 serde_json::json!({"failed": 20, "executed": 10_000_020, "fully_counted": true}),
881 ),
882 (
883 StoreExclusiveCounts {
884 failed: 0,
885 executed: Some(2),
886 fully_counted: true,
887 },
888 "2 store-exclusives executed, none failed\n",
889 serde_json::json!({"failed": 0, "executed": 2, "fully_counted": true}),
890 ),
891 (
892 StoreExclusiveCounts {
893 failed: 0,
894 executed: Some(1),
895 fully_counted: true,
896 },
897 "1 store-exclusive executed, none failed\n",
898 serde_json::json!({"failed": 0, "executed": 1, "fully_counted": true}),
899 ),
900 (
901 StoreExclusiveCounts {
902 failed: 3,
903 executed: None,
904 fully_counted: true,
905 },
906 "3 store-exclusive failures; replay may diverge\n",
907 serde_json::json!({"failed": 3, "fully_counted": true}),
908 ),
909 (
910 StoreExclusiveCounts {
911 failed: 0,
912 executed: None,
913 fully_counted: true,
914 },
915 "No store-exclusive failed\n",
916 serde_json::json!({"failed": 0, "fully_counted": true}),
917 ),
918 (
919 StoreExclusiveCounts {
920 failed: 1,
921 executed: Some(9),
922 fully_counted: false,
923 },
924 "1 store-exclusive failure; replay may diverge (9 store-exclusives executed); \
925 not counted for the whole run (multiplexed, or the task ran on CPUs whose PMU \
926 does not carry these events)\n",
927 serde_json::json!({"failed": 1, "executed": 9, "fully_counted": false}),
928 ),
929 ] {
930 let counted = RunSummary {
931 store_exclusives: Some(counts),
932 ..pairs.clone()
933 };
934 assert!(counted.to_string().contains(line), "{counted}");
935 assert_eq!(counted.info(None).to_string(), pairs.info(None).to_string());
936 let value = serde_json::to_value(&counted).unwrap();
937 assert_eq!(value["store_exclusives"], json);
938 let parsed: RunSummary = serde_json::from_value(value).unwrap();
939 assert_eq!(parsed.store_exclusives, Some(counts));
940 assert_eq!(parsed.exclusive_pairs, pairs.exclusive_pairs);
941 }
942
943 let unavailable = RunSummary {
944 store_exclusives_unavailable: Some("no PMU".to_owned()),
945 ..pairs.clone()
946 };
947 assert!(
948 unavailable
949 .to_string()
950 .contains("Store-exclusives were not counted: no PMU\n")
951 );
952 assert_eq!(
953 unavailable.info(None).to_string(),
954 pairs.info(None).to_string()
955 );
956 }
957}