1use std::collections::BTreeMap;
39use std::collections::BTreeSet;
40use std::fmt;
41use std::str::FromStr;
42
43use reverie_syscalls::Sysno;
44use serde::Deserialize;
45use serde::Serialize;
46
47use crate::pid::DetTid;
48use crate::schedule::SyscallPhase;
49
50pub const HAPPENS_BEFORE_VERSION: u32 = 1;
52
53#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
61pub struct HappensBeforeSpec {
62 pub version: u32,
64
65 #[serde(default)]
68 pub threads: BTreeMap<String, ThreadSpec>,
69
70 #[serde(default)]
73 pub events: BTreeMap<String, EventSpec>,
74
75 #[serde(default)]
77 pub edges: Vec<EdgeSpec>,
78}
79
80#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
84pub struct ThreadSpec {
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub label: Option<String>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub dettid: Option<i32>,
92
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub spawn_ordinal: Option<u32>,
97}
98
99#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
106pub struct EventSpec {
107 pub thread: String,
110
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub syscalls: Option<u64>,
115
116 #[serde(default, alias = "rcb", skip_serializing_if = "Option::is_none")]
119 pub rcbs: Option<u64>,
120
121 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub func: Option<String>,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub file: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub line: Option<u32>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub syscall: Option<String>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub phase: Option<PhaseSpec>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub rip: Option<String>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub mark: Option<String>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub nth: Option<u64>,
158}
159
160#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)]
162#[serde(rename_all = "lowercase")]
163pub enum PhaseSpec {
164 #[serde(alias = "pre")]
166 Prehook,
167 Polling,
169 #[serde(alias = "post")]
171 Posthook,
172}
173
174impl From<PhaseSpec> for SyscallPhase {
175 fn from(p: PhaseSpec) -> Self {
176 match p {
177 PhaseSpec::Prehook => SyscallPhase::Prehook,
178 PhaseSpec::Polling => SyscallPhase::Polling,
179 PhaseSpec::Posthook => SyscallPhase::Posthook,
180 }
181 }
182}
183
184#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
186pub struct EdgeSpec {
187 pub before: String,
189 pub after: String,
191 #[serde(default)]
193 pub strength: Strength,
194}
195
196#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
198#[serde(rename_all = "lowercase")]
199pub enum Strength {
200 #[default]
203 Hard,
204 Soft,
207}
208
209#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
215pub struct ThreadRef {
216 pub label: String,
218 pub dettid: Option<DetTid>,
220 pub spawn_ordinal: Option<u32>,
222}
223
224#[derive(PartialEq, Eq, Debug, Clone, Default)]
226pub struct CodeLocation {
227 pub function: Option<String>,
229 pub file: Option<String>,
231 pub line: Option<u32>,
233}
234
235impl CodeLocation {
236 pub fn is_empty(&self) -> bool {
238 self.function.is_none() && self.file.is_none() && self.line.is_none()
239 }
240}
241
242impl fmt::Display for CodeLocation {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match (&self.function, &self.file, self.line) {
245 (Some(func), _, Some(line)) => write!(f, "{}:{}", func, line),
246 (Some(func), _, None) => write!(f, "{}", func),
247 (None, Some(file), Some(line)) => write!(f, "{}:{}", file, line),
248 (None, Some(file), None) => write!(f, "{}", file),
249 (None, None, Some(line)) => write!(f, "line {}", line),
250 (None, None, None) => write!(f, "<unlocated>"),
251 }
252 }
253}
254
255#[derive(PartialEq, Eq, Debug, Clone)]
262pub enum Position {
263 SyscallCount(u64),
265
266 Rcb(u64),
268
269 Syscall {
271 sysno: Sysno,
273 phase: Option<SyscallPhase>,
275 nth: u64,
277 },
278
279 Rip {
282 addr: Option<u64>,
285 nth: u64,
287 },
288
289 Marker {
291 name: String,
293 nth: u64,
295 },
296}
297
298impl fmt::Display for Position {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match self {
301 Position::SyscallCount(n) => write!(f, "after {} syscalls", n),
302 Position::Rcb(m) => write!(f, "at RCB {}", m),
303 Position::Syscall { sysno, phase, nth } => {
304 write!(f, "{}", sysno.name())?;
305 if let Some(p) = phase {
306 write!(f, "@{:?}", p)?;
307 }
308 write!(f, "#{}", nth)
309 }
310 Position::Rip { addr, nth } => match addr {
311 Some(a) => write!(f, "@{:#x}#{}", a, nth),
312 None => write!(f, "@<unresolved>#{}", nth),
313 },
314 Position::Marker { name, nth } => write!(f, "mark:{}#{}", name, nth),
315 }
316 }
317}
318
319#[derive(PartialEq, Eq, Debug, Clone)]
321pub struct Anchor {
322 pub name: String,
324 pub thread: ThreadRef,
326 pub position: Position,
328 pub location: CodeLocation,
330}
331
332impl fmt::Display for Anchor {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 write!(f, "{}[{}: {}", self.name, self.thread.label, self.position)?;
335 if !self.location.is_empty() {
336 write!(f, " ({})", self.location)?;
337 }
338 write!(f, "]")
339 }
340}
341
342#[derive(PartialEq, Eq, Debug, Clone)]
344pub struct HappensBeforeEdge {
345 pub before: String,
347 pub after: String,
349 pub strength: Strength,
351}
352
353#[derive(PartialEq, Eq, Debug, Clone)]
356pub struct HappensBeforeProgram {
357 pub anchors: BTreeMap<String, Anchor>,
359 pub edges: Vec<HappensBeforeEdge>,
361}
362
363impl HappensBeforeProgram {
364 pub fn unresolved_locations(&self) -> impl Iterator<Item = &Anchor> {
368 self.anchors.values().filter(|a| {
369 matches!(a.position, Position::Rip { addr: None, .. }) && !a.location.is_empty()
370 })
371 }
372
373 pub fn has_syscall_count_anchors(&self) -> bool {
378 self.anchors
379 .values()
380 .any(|a| matches!(a.position, Position::SyscallCount(_)))
381 }
382
383 pub fn unenforced_positions(&self) -> impl Iterator<Item = &Anchor> {
387 self.anchors
388 .values()
389 .filter(|a| !matches!(a.position, Position::SyscallCount(_)))
390 }
391}
392
393#[derive(PartialEq, Eq, Debug, Clone)]
399pub enum HappensBeforeError {
400 UnsupportedVersion(u32),
402 AmbiguousPosition {
404 event: String,
406 found: Vec<String>,
408 },
409 UnknownSyscall {
411 event: String,
413 name: String,
415 },
416 BadRip {
418 event: String,
420 text: String,
422 },
423 UnknownEvent {
425 which: String,
427 name: String,
429 },
430 UnknownThread {
433 event: String,
435 thread: String,
437 },
438 Cycle(Vec<String>),
440 DslSyntax {
442 line: usize,
444 message: String,
446 },
447}
448
449impl fmt::Display for HappensBeforeError {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 match self {
452 HappensBeforeError::UnsupportedVersion(v) => write!(
453 f,
454 "unsupported happens-before schema version {} (this build understands {})",
455 v, HAPPENS_BEFORE_VERSION
456 ),
457 HappensBeforeError::AmbiguousPosition { event, found } => {
458 if found.is_empty() {
459 write!(
460 f,
461 "event '{}' must specify a position: a count (syscalls/rcbs), a syscall, a \
462 rip, a mark, or a code location (func/file/line)",
463 event
464 )
465 } else {
466 write!(
467 f,
468 "event '{}' names conflicting positions {:?}; use at most one explicit \
469 position selector (a code location may accompany it)",
470 event, found
471 )
472 }
473 }
474 HappensBeforeError::UnknownSyscall { event, name } => {
475 write!(f, "event '{}' names unknown syscall '{}'", event, name)
476 }
477 HappensBeforeError::BadRip { event, text } => {
478 write!(f, "event '{}' has unparseable rip '{}'", event, text)
479 }
480 HappensBeforeError::UnknownEvent { which, name } => {
481 write!(f, "edge '{}' references unknown event '{}'", which, name)
482 }
483 HappensBeforeError::UnknownThread { event, thread } => write!(
484 f,
485 "event '{}' references unknown thread '{}'",
486 event, thread
487 ),
488 HappensBeforeError::Cycle(names) => {
489 write!(
490 f,
491 "happens-before edges contain a cycle: {}",
492 names.join(" -> ")
493 )
494 }
495 HappensBeforeError::DslSyntax { line, message } => {
496 write!(f, "DSL parse error on line {}: {}", line, message)
497 }
498 }
499 }
500}
501
502impl std::error::Error for HappensBeforeError {}
503
504impl HappensBeforeSpec {
509 pub fn from_json(s: &str) -> anyhow::Result<HappensBeforeSpec> {
511 Ok(serde_json::from_str(s)?)
512 }
513
514 pub fn to_json(&self) -> anyhow::Result<String> {
516 Ok(serde_json::to_string_pretty(self)?)
517 }
518
519 pub fn normalize(&self) -> Result<HappensBeforeProgram, HappensBeforeError> {
523 if self.version != HAPPENS_BEFORE_VERSION {
524 return Err(HappensBeforeError::UnsupportedVersion(self.version));
525 }
526
527 let mut anchors = BTreeMap::new();
528 for (name, ev) in &self.events {
529 anchors.insert(name.clone(), self.normalize_event(name, ev)?);
530 }
531
532 let mut edges = Vec::with_capacity(self.edges.len());
534 for e in &self.edges {
535 if !anchors.contains_key(&e.before) {
536 return Err(HappensBeforeError::UnknownEvent {
537 which: "before".to_string(),
538 name: e.before.clone(),
539 });
540 }
541 if !anchors.contains_key(&e.after) {
542 return Err(HappensBeforeError::UnknownEvent {
543 which: "after".to_string(),
544 name: e.after.clone(),
545 });
546 }
547 edges.push(HappensBeforeEdge {
548 before: e.before.clone(),
549 after: e.after.clone(),
550 strength: e.strength,
551 });
552 }
553
554 detect_cycle(&anchors, &edges)?;
555
556 Ok(HappensBeforeProgram { anchors, edges })
557 }
558
559 fn normalize_event(&self, name: &str, ev: &EventSpec) -> Result<Anchor, HappensBeforeError> {
561 let thread = self.resolve_thread(name, &ev.thread)?;
562
563 let location = CodeLocation {
566 function: ev.func.clone(),
567 file: ev.file.clone(),
568 line: ev.line,
569 };
570
571 let mut found: Vec<&str> = Vec::new();
578 if ev.syscalls.is_some() {
579 found.push("syscalls");
580 }
581 if ev.rcbs.is_some() {
582 found.push("rcbs");
583 }
584 if ev.syscall.is_some() {
585 found.push("syscall");
586 }
587 if ev.rip.is_some() {
588 found.push("rip");
589 }
590 if ev.mark.is_some() {
591 found.push("mark");
592 }
593 let has_code_location = !location.is_empty();
594
595 if found.len() > 1 {
599 return Err(HappensBeforeError::AmbiguousPosition {
600 event: name.to_string(),
601 found: found.iter().map(|s| s.to_string()).collect(),
602 });
603 }
604 if found.is_empty() && !has_code_location {
605 return Err(HappensBeforeError::AmbiguousPosition {
606 event: name.to_string(),
607 found: Vec::new(),
608 });
609 }
610
611 let nth = ev.nth.unwrap_or(1);
612 let position = if let Some(n) = ev.syscalls {
613 Position::SyscallCount(n)
614 } else if let Some(m) = ev.rcbs {
615 Position::Rcb(m)
616 } else if let Some(sc) = &ev.syscall {
617 let sysno = Sysno::from_str(sc).map_err(|_| HappensBeforeError::UnknownSyscall {
618 event: name.to_string(),
619 name: sc.clone(),
620 })?;
621 Position::Syscall {
622 sysno,
623 phase: ev.phase.map(Into::into),
624 nth,
625 }
626 } else if let Some(rip) = &ev.rip {
627 let addr = parse_rip(rip).ok_or_else(|| HappensBeforeError::BadRip {
628 event: name.to_string(),
629 text: rip.clone(),
630 })?;
631 Position::Rip {
632 addr: Some(addr),
633 nth,
634 }
635 } else if let Some(mark) = &ev.mark {
636 Position::Marker {
637 name: mark.clone(),
638 nth,
639 }
640 } else {
641 debug_assert!(has_code_location);
643 Position::Rip { addr: None, nth }
644 };
645
646 Ok(Anchor {
647 name: name.to_string(),
648 thread,
649 position,
650 location,
651 })
652 }
653
654 fn resolve_thread(&self, event: &str, thread: &str) -> Result<ThreadRef, HappensBeforeError> {
657 if let Some(spec) = self.threads.get(thread) {
658 Ok(ThreadRef {
659 label: spec.label.clone().unwrap_or_else(|| thread.to_string()),
660 dettid: spec.dettid.map(DetTid::from_raw),
661 spawn_ordinal: spec.spawn_ordinal,
662 })
663 } else if let Ok(raw) = thread.parse::<i32>() {
664 Ok(ThreadRef {
665 label: thread.to_string(),
666 dettid: Some(DetTid::from_raw(raw)),
667 spawn_ordinal: None,
668 })
669 } else {
670 Err(HappensBeforeError::UnknownThread {
671 event: event.to_string(),
672 thread: thread.to_string(),
673 })
674 }
675 }
676}
677
678fn parse_rip(s: &str) -> Option<u64> {
680 let s = s.trim();
681 if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
682 u64::from_str_radix(hex, 16).ok()
683 } else {
684 s.parse::<u64>().ok()
685 }
686}
687
688fn detect_cycle(
691 anchors: &BTreeMap<String, Anchor>,
692 edges: &[HappensBeforeEdge],
693) -> Result<(), HappensBeforeError> {
694 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
696 for name in anchors.keys() {
697 adj.entry(name.as_str()).or_default();
698 }
699 for e in edges {
700 adj.entry(e.before.as_str())
701 .or_default()
702 .push(e.after.as_str());
703 }
704
705 #[derive(Clone, Copy, PartialEq)]
706 enum Mark {
707 Visiting,
708 Done,
709 }
710 let mut state: BTreeMap<&str, Mark> = BTreeMap::new();
711
712 for root in adj.keys().copied() {
715 if state.contains_key(root) {
716 continue;
717 }
718 let mut stack: Vec<(&str, usize)> = vec![(root, 0)];
720 let mut path: Vec<&str> = vec![root];
721 state.insert(root, Mark::Visiting);
722
723 while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
724 let neighbors = &adj[node];
725 if *idx < neighbors.len() {
726 let next = neighbors[*idx];
727 *idx += 1;
728 match state.get(next) {
729 Some(Mark::Visiting) => {
730 let start = path.iter().position(|&n| n == next).unwrap_or(0);
732 let mut cycle: Vec<String> =
733 path[start..].iter().map(|s| s.to_string()).collect();
734 cycle.push(next.to_string());
735 return Err(HappensBeforeError::Cycle(cycle));
736 }
737 Some(Mark::Done) => {}
738 None => {
739 state.insert(next, Mark::Visiting);
740 path.push(next);
741 stack.push((next, 0));
742 }
743 }
744 } else {
745 state.insert(node, Mark::Done);
746 stack.pop();
747 path.pop();
748 }
749 }
750 }
751 Ok(())
752}
753
754impl HappensBeforeSpec {
774 pub fn from_dsl(input: &str) -> Result<HappensBeforeSpec, HappensBeforeError> {
777 let mut spec = HappensBeforeSpec {
778 version: HAPPENS_BEFORE_VERSION,
779 threads: BTreeMap::new(),
780 events: BTreeMap::new(),
781 edges: Vec::new(),
782 };
783 let mut seen_names: BTreeSet<String> = BTreeSet::new();
784
785 for (i, raw_line) in input.lines().enumerate() {
786 let lineno = i + 1;
787 let line = strip_comment(raw_line).trim();
788 if line.is_empty() {
789 continue;
790 }
791
792 let (body, strength) = if let Some(b) = line.strip_suffix("!soft") {
794 (b.trim(), Strength::Soft)
795 } else if let Some(b) = line.strip_suffix("!hard") {
796 (b.trim(), Strength::Hard)
797 } else {
798 (line, Strength::Hard)
799 };
800
801 let (lhs, rhs) = body
802 .split_once('<')
803 .ok_or_else(|| HappensBeforeError::DslSyntax {
804 line: lineno,
805 message: "expected '<' separating two events".to_string(),
806 })?;
807
808 let before = parse_dsl_side(lhs.trim(), lineno, &mut spec, &mut seen_names)?;
809 let after = parse_dsl_side(rhs.trim(), lineno, &mut spec, &mut seen_names)?;
810 spec.edges.push(EdgeSpec {
811 before,
812 after,
813 strength,
814 });
815 }
816 Ok(spec)
817 }
818}
819
820fn strip_comment(line: &str) -> &str {
823 match line.find("//") {
824 Some(idx) => &line[..idx],
825 None => line,
826 }
827}
828
829fn parse_dsl_side(
832 token: &str,
833 lineno: usize,
834 spec: &mut HappensBeforeSpec,
835 seen_names: &mut BTreeSet<String>,
836) -> Result<String, HappensBeforeError> {
837 let (thread, anchor) = token
838 .split_once(':')
839 .ok_or_else(|| HappensBeforeError::DslSyntax {
840 line: lineno,
841 message: format!("expected 'thread:anchor' in '{}'", token),
842 })?;
843 let thread = thread.trim();
844 let anchor = anchor.trim();
845 if thread.is_empty() || anchor.is_empty() {
846 return Err(HappensBeforeError::DslSyntax {
847 line: lineno,
848 message: format!("empty thread or anchor in '{}'", token),
849 });
850 }
851
852 let (anchor_body, nth) = match anchor.split_once('#') {
854 Some((a, n)) => {
855 let parsed = n
856 .parse::<u64>()
857 .map_err(|_| HappensBeforeError::DslSyntax {
858 line: lineno,
859 message: format!("bad ordinal '#{}'", n),
860 })?;
861 (a.trim(), Some(parsed))
862 }
863 None => (anchor, None),
864 };
865
866 let mut ev = EventSpec {
867 thread: thread.to_string(),
868 nth,
869 ..Default::default()
870 };
871
872 if let Some(rest) = anchor_body.strip_prefix('@') {
873 ev.rip = Some(rest.to_string());
875 } else if let Some(m) = anchor_body.strip_prefix("rcb=") {
876 ev.rcbs = Some(
877 m.parse::<u64>()
878 .map_err(|_| HappensBeforeError::DslSyntax {
879 line: lineno,
880 message: format!("bad rcb count '{}'", m),
881 })?,
882 );
883 ev.nth = None;
884 } else if let Some(n) = anchor_body.strip_prefix("sc=") {
885 ev.syscalls = Some(
886 n.parse::<u64>()
887 .map_err(|_| HappensBeforeError::DslSyntax {
888 line: lineno,
889 message: format!("bad syscall count '{}'", n),
890 })?,
891 );
892 ev.nth = None;
893 } else if let Some((sc, phase)) = anchor_body.split_once('@') {
894 ev.syscall = Some(sc.to_string());
896 ev.phase = Some(parse_dsl_phase(phase, lineno)?);
897 } else if is_syscall_name(anchor_body) {
898 ev.syscall = Some(anchor_body.to_string());
900 } else {
901 ev.func = Some(anchor_body.to_string());
903 }
904
905 let base = sanitize_name(token);
907 let mut ev_name = base.clone();
908 let mut suffix = 1;
909 while seen_names.contains(&ev_name) && spec.events.get(&ev_name) != Some(&ev) {
910 suffix += 1;
911 ev_name = format!("{}_{}", base, suffix);
912 }
913 seen_names.insert(ev_name.clone());
914 spec.events.entry(ev_name.clone()).or_insert(ev);
915
916 if thread.parse::<i32>().is_err() {
918 spec.threads
919 .entry(thread.to_string())
920 .or_insert(ThreadSpec {
921 label: Some(thread.to_string()),
922 dettid: None,
923 spawn_ordinal: None,
924 });
925 }
926
927 Ok(ev_name)
928}
929
930fn parse_dsl_phase(phase: &str, lineno: usize) -> Result<PhaseSpec, HappensBeforeError> {
931 match phase.trim().to_ascii_lowercase().as_str() {
932 "pre" | "prehook" => Ok(PhaseSpec::Prehook),
933 "post" | "posthook" => Ok(PhaseSpec::Posthook),
934 "poll" | "polling" => Ok(PhaseSpec::Polling),
935 other => Err(HappensBeforeError::DslSyntax {
936 line: lineno,
937 message: format!("unknown syscall phase '{}'", other),
938 }),
939 }
940}
941
942fn is_syscall_name(s: &str) -> bool {
944 Sysno::from_str(s).is_ok()
945}
946
947fn sanitize_name(token: &str) -> String {
949 let mut out = String::with_capacity(token.len());
950 for ch in token.chars() {
951 if ch.is_ascii_alphanumeric() || ch == '_' {
952 out.push(ch);
953 } else {
954 out.push('_');
955 }
956 }
957 if out.is_empty() {
958 out.push('e');
959 }
960 out
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 fn spec_json() -> &'static str {
968 r#"{
969 "version": 1,
970 "threads": { "writer": {"label": "writer"}, "reader": {"label": "reader"} },
971 "events": {
972 "X_342": {"thread": "writer", "func": "free_buffer", "line": 120, "nth": 342},
973 "Y_97": {"thread": "reader", "func": "read_buffer", "nth": 97},
974 "lockA": {"thread": "writer", "syscall": "futex", "phase": "posthook", "nth": 5},
975 "storeB": {"thread": "reader", "rip": "0x401f3c", "nth": 1},
976 "scA": {"thread": "writer", "syscalls": 10},
977 "rcbB": {"thread": "reader", "rcbs": 123456}
978 },
979 "edges": [
980 {"before": "X_342", "after": "Y_97", "strength": "hard"},
981 {"before": "lockA", "after": "storeB"},
982 {"before": "scA", "after": "rcbB", "strength": "soft"}
983 ]
984 }"#
985 }
986
987 #[test]
988 fn parse_and_normalize_rfc_example() {
989 let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
990 let prog = spec.normalize().unwrap();
991 assert_eq!(prog.anchors.len(), 6);
992 assert_eq!(prog.edges.len(), 3);
993
994 assert_eq!(prog.anchors["scA"].position, Position::SyscallCount(10));
996 assert_eq!(prog.anchors["rcbB"].position, Position::Rcb(123456));
997
998 match &prog.anchors["X_342"].position {
1000 Position::Rip { addr: None, nth } => assert_eq!(*nth, 342),
1001 other => panic!("expected unresolved RIP, got {:?}", other),
1002 }
1003 assert_eq!(
1004 prog.anchors["X_342"].location.function.as_deref(),
1005 Some("free_buffer")
1006 );
1007 assert_eq!(prog.anchors["X_342"].location.line, Some(120));
1008
1009 match &prog.anchors["lockA"].position {
1011 Position::Syscall { sysno, phase, nth } => {
1012 assert_eq!(*sysno, Sysno::futex);
1013 assert_eq!(*phase, Some(SyscallPhase::Posthook));
1014 assert_eq!(*nth, 5);
1015 }
1016 other => panic!("expected syscall, got {:?}", other),
1017 }
1018
1019 assert_eq!(
1021 prog.anchors["storeB"].position,
1022 Position::Rip {
1023 addr: Some(0x401f3c),
1024 nth: 1
1025 }
1026 );
1027
1028 assert_eq!(prog.edges[2].strength, Strength::Soft);
1030 assert_eq!(prog.edges[1].strength, Strength::Hard);
1032
1033 assert_eq!(prog.unresolved_locations().count(), 2);
1035 }
1036
1037 #[test]
1038 fn round_trip_json() {
1039 let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
1040 let json = spec.to_json().unwrap();
1041 let spec2 = HappensBeforeSpec::from_json(&json).unwrap();
1042 assert_eq!(spec, spec2);
1043 }
1044
1045 #[test]
1046 fn rejects_wrong_version() {
1047 let spec = HappensBeforeSpec {
1048 version: 999,
1049 ..HappensBeforeSpec::from_json(spec_json()).unwrap()
1050 };
1051 assert_eq!(
1052 spec.normalize().unwrap_err(),
1053 HappensBeforeError::UnsupportedVersion(999)
1054 );
1055 }
1056
1057 #[test]
1058 fn rejects_ambiguous_position() {
1059 let json = r#"{
1060 "version": 1,
1061 "events": { "bad": {"thread": "1", "syscalls": 3, "rcbs": 5} },
1062 "edges": []
1063 }"#;
1064 let spec = HappensBeforeSpec::from_json(json).unwrap();
1065 match spec.normalize().unwrap_err() {
1066 HappensBeforeError::AmbiguousPosition { event, found } => {
1067 assert_eq!(event, "bad");
1068 assert_eq!(found.len(), 2);
1069 }
1070 other => panic!("expected AmbiguousPosition, got {:?}", other),
1071 }
1072 }
1073
1074 #[test]
1075 fn rejects_no_position() {
1076 let json = r#"{
1077 "version": 1,
1078 "events": { "bad": {"thread": "1"} },
1079 "edges": []
1080 }"#;
1081 let spec = HappensBeforeSpec::from_json(json).unwrap();
1082 assert!(matches!(
1083 spec.normalize().unwrap_err(),
1084 HappensBeforeError::AmbiguousPosition { .. }
1085 ));
1086 }
1087
1088 #[test]
1089 fn code_location_accompanies_count() {
1090 let json = r#"{
1094 "version": 1,
1095 "events": {
1096 "w": {"thread": "1", "func": "free_buffer", "line": 342, "syscalls": 7},
1097 "r": {"thread": "1", "func": "read_buffer", "rcbs": 900}
1098 },
1099 "edges": [ {"before": "w", "after": "r"} ]
1100 }"#;
1101 let prog = HappensBeforeSpec::from_json(json)
1102 .unwrap()
1103 .normalize()
1104 .unwrap();
1105
1106 assert_eq!(prog.anchors["w"].position, Position::SyscallCount(7));
1108 assert_eq!(
1109 prog.anchors["w"].location.function.as_deref(),
1110 Some("free_buffer")
1111 );
1112 assert_eq!(prog.anchors["w"].location.line, Some(342));
1113
1114 assert_eq!(prog.anchors["r"].position, Position::Rcb(900));
1115 assert_eq!(
1116 prog.anchors["r"].location.function.as_deref(),
1117 Some("read_buffer")
1118 );
1119
1120 assert_eq!(prog.unresolved_locations().count(), 0);
1122 }
1123
1124 #[test]
1125 fn raw_dettid_thread() {
1126 let json = r#"{
1127 "version": 1,
1128 "events": { "e": {"thread": "42", "rcbs": 7} },
1129 "edges": []
1130 }"#;
1131 let prog = HappensBeforeSpec::from_json(json)
1132 .unwrap()
1133 .normalize()
1134 .unwrap();
1135 assert_eq!(prog.anchors["e"].thread.dettid, Some(DetTid::from_raw(42)));
1136 }
1137
1138 #[test]
1139 fn rejects_unknown_thread() {
1140 let json = r#"{
1141 "version": 1,
1142 "events": { "e": {"thread": "ghost", "rcbs": 7} },
1143 "edges": []
1144 }"#;
1145 let spec = HappensBeforeSpec::from_json(json).unwrap();
1146 assert!(matches!(
1147 spec.normalize().unwrap_err(),
1148 HappensBeforeError::UnknownThread { .. }
1149 ));
1150 }
1151
1152 #[test]
1153 fn rejects_unknown_event_in_edge() {
1154 let json = r#"{
1155 "version": 1,
1156 "events": { "a": {"thread": "1", "rcbs": 7} },
1157 "edges": [ {"before": "a", "after": "missing"} ]
1158 }"#;
1159 let spec = HappensBeforeSpec::from_json(json).unwrap();
1160 assert!(matches!(
1161 spec.normalize().unwrap_err(),
1162 HappensBeforeError::UnknownEvent { .. }
1163 ));
1164 }
1165
1166 #[test]
1167 fn rejects_unknown_syscall() {
1168 let json = r#"{
1169 "version": 1,
1170 "events": { "a": {"thread": "1", "syscall": "not_a_syscall"} },
1171 "edges": []
1172 }"#;
1173 let spec = HappensBeforeSpec::from_json(json).unwrap();
1174 assert!(matches!(
1175 spec.normalize().unwrap_err(),
1176 HappensBeforeError::UnknownSyscall { .. }
1177 ));
1178 }
1179
1180 #[test]
1181 fn detects_cycle() {
1182 let json = r#"{
1183 "version": 1,
1184 "events": {
1185 "a": {"thread": "1", "rcbs": 1},
1186 "b": {"thread": "1", "rcbs": 2},
1187 "c": {"thread": "1", "rcbs": 3}
1188 },
1189 "edges": [
1190 {"before": "a", "after": "b"},
1191 {"before": "b", "after": "c"},
1192 {"before": "c", "after": "a"}
1193 ]
1194 }"#;
1195 let spec = HappensBeforeSpec::from_json(json).unwrap();
1196 match spec.normalize().unwrap_err() {
1197 HappensBeforeError::Cycle(path) => {
1198 assert_eq!(path.first(), path.last());
1200 assert!(path.len() >= 4);
1201 }
1202 other => panic!("expected Cycle, got {:?}", other),
1203 }
1204 }
1205
1206 #[test]
1207 fn accepts_dag() {
1208 let json = r#"{
1209 "version": 1,
1210 "events": {
1211 "a": {"thread": "1", "rcbs": 1},
1212 "b": {"thread": "1", "rcbs": 2},
1213 "c": {"thread": "1", "rcbs": 3}
1214 },
1215 "edges": [
1216 {"before": "a", "after": "c"},
1217 {"before": "b", "after": "c"}
1218 ]
1219 }"#;
1220 let spec = HappensBeforeSpec::from_json(json).unwrap();
1221 assert!(spec.normalize().is_ok());
1222 }
1223
1224 #[test]
1225 fn dsl_desugars() {
1226 let dsl = "\
1227 // btrfs race: erase-by-key must precede the re-insert
1228 writer:free_buffer#342 < reader:read_buffer#97
1229 writer:futex@post#5 < reader:@0x401f3c#1
1230 A:rcb=123456 < B:sc=97 !soft
1231 ";
1232 let spec = HappensBeforeSpec::from_dsl(dsl).unwrap();
1233 let prog = spec.normalize().unwrap();
1234 assert_eq!(prog.edges.len(), 3);
1235 assert_eq!(prog.anchors.len(), 6);
1236
1237 let soft = &prog.edges[2];
1239 assert_eq!(soft.strength, Strength::Soft);
1240 assert_eq!(prog.anchors[&soft.before].position, Position::Rcb(123456));
1241 assert_eq!(
1242 prog.anchors[&soft.after].position,
1243 Position::SyscallCount(97)
1244 );
1245
1246 let futex = prog
1248 .anchors
1249 .values()
1250 .find(
1251 |a| matches!(a.position, Position::Syscall { sysno, .. } if sysno == Sysno::futex),
1252 )
1253 .unwrap();
1254 match &futex.position {
1255 Position::Syscall { phase, nth, .. } => {
1256 assert_eq!(*phase, Some(SyscallPhase::Posthook));
1257 assert_eq!(*nth, 5);
1258 }
1259 _ => unreachable!(),
1260 }
1261 }
1262
1263 #[test]
1264 fn dsl_rejects_missing_arrow() {
1265 let err = HappensBeforeSpec::from_dsl("writer:foo reader:bar").unwrap_err();
1266 assert!(matches!(err, HappensBeforeError::DslSyntax { line: 1, .. }));
1267 }
1268
1269 #[test]
1270 fn parse_rip_forms() {
1271 assert_eq!(parse_rip("0x401f3c"), Some(0x401f3c));
1272 assert_eq!(parse_rip("4201276"), Some(4201276));
1273 assert_eq!(parse_rip("nonsense"), None);
1274 }
1275}