use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fmt;
use std::str::FromStr;
use reverie_syscalls::Sysno;
use serde::Deserialize;
use serde::Serialize;
use crate::pid::DetTid;
use crate::schedule::SyscallPhase;
pub const HAPPENS_BEFORE_VERSION: u32 = 1;
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct HappensBeforeSpec {
pub version: u32,
#[serde(default)]
pub threads: BTreeMap<String, ThreadSpec>,
#[serde(default)]
pub events: BTreeMap<String, EventSpec>,
#[serde(default)]
pub edges: Vec<EdgeSpec>,
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct ThreadSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dettid: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawn_ordinal: Option<u32>,
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct EventSpec {
pub thread: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syscalls: Option<u64>,
#[serde(default, alias = "rcb", skip_serializing_if = "Option::is_none")]
pub rcbs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub func: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syscall: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<PhaseSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rip: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mark: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nth: Option<u64>,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PhaseSpec {
#[serde(alias = "pre")]
Prehook,
Polling,
#[serde(alias = "post")]
Posthook,
}
impl From<PhaseSpec> for SyscallPhase {
fn from(p: PhaseSpec) -> Self {
match p {
PhaseSpec::Prehook => SyscallPhase::Prehook,
PhaseSpec::Polling => SyscallPhase::Polling,
PhaseSpec::Posthook => SyscallPhase::Posthook,
}
}
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct EdgeSpec {
pub before: String,
pub after: String,
#[serde(default)]
pub strength: Strength,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Strength {
#[default]
Hard,
Soft,
}
#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
pub struct ThreadRef {
pub label: String,
pub dettid: Option<DetTid>,
pub spawn_ordinal: Option<u32>,
}
#[derive(PartialEq, Eq, Debug, Clone, Default)]
pub struct CodeLocation {
pub function: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
}
impl CodeLocation {
pub fn is_empty(&self) -> bool {
self.function.is_none() && self.file.is_none() && self.line.is_none()
}
}
impl fmt::Display for CodeLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.function, &self.file, self.line) {
(Some(func), _, Some(line)) => write!(f, "{}:{}", func, line),
(Some(func), _, None) => write!(f, "{}", func),
(None, Some(file), Some(line)) => write!(f, "{}:{}", file, line),
(None, Some(file), None) => write!(f, "{}", file),
(None, None, Some(line)) => write!(f, "line {}", line),
(None, None, None) => write!(f, "<unlocated>"),
}
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum Position {
SyscallCount(u64),
Rcb(u64),
Syscall {
sysno: Sysno,
phase: Option<SyscallPhase>,
nth: u64,
},
Rip {
addr: Option<u64>,
nth: u64,
},
Marker {
name: String,
nth: u64,
},
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Position::SyscallCount(n) => write!(f, "after {} syscalls", n),
Position::Rcb(m) => write!(f, "at RCB {}", m),
Position::Syscall { sysno, phase, nth } => {
write!(f, "{}", sysno.name())?;
if let Some(p) = phase {
write!(f, "@{:?}", p)?;
}
write!(f, "#{}", nth)
}
Position::Rip { addr, nth } => match addr {
Some(a) => write!(f, "@{:#x}#{}", a, nth),
None => write!(f, "@<unresolved>#{}", nth),
},
Position::Marker { name, nth } => write!(f, "mark:{}#{}", name, nth),
}
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Anchor {
pub name: String,
pub thread: ThreadRef,
pub position: Position,
pub location: CodeLocation,
}
impl fmt::Display for Anchor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}[{}: {}", self.name, self.thread.label, self.position)?;
if !self.location.is_empty() {
write!(f, " ({})", self.location)?;
}
write!(f, "]")
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct HappensBeforeEdge {
pub before: String,
pub after: String,
pub strength: Strength,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct HappensBeforeProgram {
pub anchors: BTreeMap<String, Anchor>,
pub edges: Vec<HappensBeforeEdge>,
}
impl HappensBeforeProgram {
pub fn unresolved_locations(&self) -> impl Iterator<Item = &Anchor> {
self.anchors.values().filter(|a| {
matches!(a.position, Position::Rip { addr: None, .. }) && !a.location.is_empty()
})
}
pub fn has_syscall_count_anchors(&self) -> bool {
self.anchors
.values()
.any(|a| matches!(a.position, Position::SyscallCount(_)))
}
pub fn unenforced_positions(&self) -> impl Iterator<Item = &Anchor> {
self.anchors
.values()
.filter(|a| !matches!(a.position, Position::SyscallCount(_)))
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum HappensBeforeError {
UnsupportedVersion(u32),
AmbiguousPosition {
event: String,
found: Vec<String>,
},
UnknownSyscall {
event: String,
name: String,
},
BadRip {
event: String,
text: String,
},
UnknownEvent {
which: String,
name: String,
},
UnknownThread {
event: String,
thread: String,
},
Cycle(Vec<String>),
DslSyntax {
line: usize,
message: String,
},
}
impl fmt::Display for HappensBeforeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HappensBeforeError::UnsupportedVersion(v) => write!(
f,
"unsupported happens-before schema version {} (this build understands {})",
v, HAPPENS_BEFORE_VERSION
),
HappensBeforeError::AmbiguousPosition { event, found } => {
if found.is_empty() {
write!(
f,
"event '{}' must specify a position: a count (syscalls/rcbs), a syscall, a \
rip, a mark, or a code location (func/file/line)",
event
)
} else {
write!(
f,
"event '{}' names conflicting positions {:?}; use at most one explicit \
position selector (a code location may accompany it)",
event, found
)
}
}
HappensBeforeError::UnknownSyscall { event, name } => {
write!(f, "event '{}' names unknown syscall '{}'", event, name)
}
HappensBeforeError::BadRip { event, text } => {
write!(f, "event '{}' has unparseable rip '{}'", event, text)
}
HappensBeforeError::UnknownEvent { which, name } => {
write!(f, "edge '{}' references unknown event '{}'", which, name)
}
HappensBeforeError::UnknownThread { event, thread } => write!(
f,
"event '{}' references unknown thread '{}'",
event, thread
),
HappensBeforeError::Cycle(names) => {
write!(
f,
"happens-before edges contain a cycle: {}",
names.join(" -> ")
)
}
HappensBeforeError::DslSyntax { line, message } => {
write!(f, "DSL parse error on line {}: {}", line, message)
}
}
}
}
impl std::error::Error for HappensBeforeError {}
impl HappensBeforeSpec {
pub fn from_json(s: &str) -> anyhow::Result<HappensBeforeSpec> {
Ok(serde_json::from_str(s)?)
}
pub fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn normalize(&self) -> Result<HappensBeforeProgram, HappensBeforeError> {
if self.version != HAPPENS_BEFORE_VERSION {
return Err(HappensBeforeError::UnsupportedVersion(self.version));
}
let mut anchors = BTreeMap::new();
for (name, ev) in &self.events {
anchors.insert(name.clone(), self.normalize_event(name, ev)?);
}
let mut edges = Vec::with_capacity(self.edges.len());
for e in &self.edges {
if !anchors.contains_key(&e.before) {
return Err(HappensBeforeError::UnknownEvent {
which: "before".to_string(),
name: e.before.clone(),
});
}
if !anchors.contains_key(&e.after) {
return Err(HappensBeforeError::UnknownEvent {
which: "after".to_string(),
name: e.after.clone(),
});
}
edges.push(HappensBeforeEdge {
before: e.before.clone(),
after: e.after.clone(),
strength: e.strength,
});
}
detect_cycle(&anchors, &edges)?;
Ok(HappensBeforeProgram { anchors, edges })
}
fn normalize_event(&self, name: &str, ev: &EventSpec) -> Result<Anchor, HappensBeforeError> {
let thread = self.resolve_thread(name, &ev.thread)?;
let location = CodeLocation {
function: ev.func.clone(),
file: ev.file.clone(),
line: ev.line,
};
let mut found: Vec<&str> = Vec::new();
if ev.syscalls.is_some() {
found.push("syscalls");
}
if ev.rcbs.is_some() {
found.push("rcbs");
}
if ev.syscall.is_some() {
found.push("syscall");
}
if ev.rip.is_some() {
found.push("rip");
}
if ev.mark.is_some() {
found.push("mark");
}
let has_code_location = !location.is_empty();
if found.len() > 1 {
return Err(HappensBeforeError::AmbiguousPosition {
event: name.to_string(),
found: found.iter().map(|s| s.to_string()).collect(),
});
}
if found.is_empty() && !has_code_location {
return Err(HappensBeforeError::AmbiguousPosition {
event: name.to_string(),
found: Vec::new(),
});
}
let nth = ev.nth.unwrap_or(1);
let position = if let Some(n) = ev.syscalls {
Position::SyscallCount(n)
} else if let Some(m) = ev.rcbs {
Position::Rcb(m)
} else if let Some(sc) = &ev.syscall {
let sysno = Sysno::from_str(sc).map_err(|_| HappensBeforeError::UnknownSyscall {
event: name.to_string(),
name: sc.clone(),
})?;
Position::Syscall {
sysno,
phase: ev.phase.map(Into::into),
nth,
}
} else if let Some(rip) = &ev.rip {
let addr = parse_rip(rip).ok_or_else(|| HappensBeforeError::BadRip {
event: name.to_string(),
text: rip.clone(),
})?;
Position::Rip {
addr: Some(addr),
nth,
}
} else if let Some(mark) = &ev.mark {
Position::Marker {
name: mark.clone(),
nth,
}
} else {
debug_assert!(has_code_location);
Position::Rip { addr: None, nth }
};
Ok(Anchor {
name: name.to_string(),
thread,
position,
location,
})
}
fn resolve_thread(&self, event: &str, thread: &str) -> Result<ThreadRef, HappensBeforeError> {
if let Some(spec) = self.threads.get(thread) {
Ok(ThreadRef {
label: spec.label.clone().unwrap_or_else(|| thread.to_string()),
dettid: spec.dettid.map(DetTid::from_raw),
spawn_ordinal: spec.spawn_ordinal,
})
} else if let Ok(raw) = thread.parse::<i32>() {
Ok(ThreadRef {
label: thread.to_string(),
dettid: Some(DetTid::from_raw(raw)),
spawn_ordinal: None,
})
} else {
Err(HappensBeforeError::UnknownThread {
event: event.to_string(),
thread: thread.to_string(),
})
}
}
}
fn parse_rip(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u64::from_str_radix(hex, 16).ok()
} else {
s.parse::<u64>().ok()
}
}
fn detect_cycle(
anchors: &BTreeMap<String, Anchor>,
edges: &[HappensBeforeEdge],
) -> Result<(), HappensBeforeError> {
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for name in anchors.keys() {
adj.entry(name.as_str()).or_default();
}
for e in edges {
adj.entry(e.before.as_str())
.or_default()
.push(e.after.as_str());
}
#[derive(Clone, Copy, PartialEq)]
enum Mark {
Visiting,
Done,
}
let mut state: BTreeMap<&str, Mark> = BTreeMap::new();
for root in adj.keys().copied() {
if state.contains_key(root) {
continue;
}
let mut stack: Vec<(&str, usize)> = vec![(root, 0)];
let mut path: Vec<&str> = vec![root];
state.insert(root, Mark::Visiting);
while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
let neighbors = &adj[node];
if *idx < neighbors.len() {
let next = neighbors[*idx];
*idx += 1;
match state.get(next) {
Some(Mark::Visiting) => {
let start = path.iter().position(|&n| n == next).unwrap_or(0);
let mut cycle: Vec<String> =
path[start..].iter().map(|s| s.to_string()).collect();
cycle.push(next.to_string());
return Err(HappensBeforeError::Cycle(cycle));
}
Some(Mark::Done) => {}
None => {
state.insert(next, Mark::Visiting);
path.push(next);
stack.push((next, 0));
}
}
} else {
state.insert(node, Mark::Done);
stack.pop();
path.pop();
}
}
}
Ok(())
}
impl HappensBeforeSpec {
pub fn from_dsl(input: &str) -> Result<HappensBeforeSpec, HappensBeforeError> {
let mut spec = HappensBeforeSpec {
version: HAPPENS_BEFORE_VERSION,
threads: BTreeMap::new(),
events: BTreeMap::new(),
edges: Vec::new(),
};
let mut seen_names: BTreeSet<String> = BTreeSet::new();
for (i, raw_line) in input.lines().enumerate() {
let lineno = i + 1;
let line = strip_comment(raw_line).trim();
if line.is_empty() {
continue;
}
let (body, strength) = if let Some(b) = line.strip_suffix("!soft") {
(b.trim(), Strength::Soft)
} else if let Some(b) = line.strip_suffix("!hard") {
(b.trim(), Strength::Hard)
} else {
(line, Strength::Hard)
};
let (lhs, rhs) = body
.split_once('<')
.ok_or_else(|| HappensBeforeError::DslSyntax {
line: lineno,
message: "expected '<' separating two events".to_string(),
})?;
let before = parse_dsl_side(lhs.trim(), lineno, &mut spec, &mut seen_names)?;
let after = parse_dsl_side(rhs.trim(), lineno, &mut spec, &mut seen_names)?;
spec.edges.push(EdgeSpec {
before,
after,
strength,
});
}
Ok(spec)
}
}
fn strip_comment(line: &str) -> &str {
match line.find("//") {
Some(idx) => &line[..idx],
None => line,
}
}
fn parse_dsl_side(
token: &str,
lineno: usize,
spec: &mut HappensBeforeSpec,
seen_names: &mut BTreeSet<String>,
) -> Result<String, HappensBeforeError> {
let (thread, anchor) = token
.split_once(':')
.ok_or_else(|| HappensBeforeError::DslSyntax {
line: lineno,
message: format!("expected 'thread:anchor' in '{}'", token),
})?;
let thread = thread.trim();
let anchor = anchor.trim();
if thread.is_empty() || anchor.is_empty() {
return Err(HappensBeforeError::DslSyntax {
line: lineno,
message: format!("empty thread or anchor in '{}'", token),
});
}
let (anchor_body, nth) = match anchor.split_once('#') {
Some((a, n)) => {
let parsed = n
.parse::<u64>()
.map_err(|_| HappensBeforeError::DslSyntax {
line: lineno,
message: format!("bad ordinal '#{}'", n),
})?;
(a.trim(), Some(parsed))
}
None => (anchor, None),
};
let mut ev = EventSpec {
thread: thread.to_string(),
nth,
..Default::default()
};
if let Some(rest) = anchor_body.strip_prefix('@') {
ev.rip = Some(rest.to_string());
} else if let Some(m) = anchor_body.strip_prefix("rcb=") {
ev.rcbs = Some(
m.parse::<u64>()
.map_err(|_| HappensBeforeError::DslSyntax {
line: lineno,
message: format!("bad rcb count '{}'", m),
})?,
);
ev.nth = None;
} else if let Some(n) = anchor_body.strip_prefix("sc=") {
ev.syscalls = Some(
n.parse::<u64>()
.map_err(|_| HappensBeforeError::DslSyntax {
line: lineno,
message: format!("bad syscall count '{}'", n),
})?,
);
ev.nth = None;
} else if let Some((sc, phase)) = anchor_body.split_once('@') {
ev.syscall = Some(sc.to_string());
ev.phase = Some(parse_dsl_phase(phase, lineno)?);
} else if is_syscall_name(anchor_body) {
ev.syscall = Some(anchor_body.to_string());
} else {
ev.func = Some(anchor_body.to_string());
}
let base = sanitize_name(token);
let mut ev_name = base.clone();
let mut suffix = 1;
while seen_names.contains(&ev_name) && spec.events.get(&ev_name) != Some(&ev) {
suffix += 1;
ev_name = format!("{}_{}", base, suffix);
}
seen_names.insert(ev_name.clone());
spec.events.entry(ev_name.clone()).or_insert(ev);
if thread.parse::<i32>().is_err() {
spec.threads
.entry(thread.to_string())
.or_insert(ThreadSpec {
label: Some(thread.to_string()),
dettid: None,
spawn_ordinal: None,
});
}
Ok(ev_name)
}
fn parse_dsl_phase(phase: &str, lineno: usize) -> Result<PhaseSpec, HappensBeforeError> {
match phase.trim().to_ascii_lowercase().as_str() {
"pre" | "prehook" => Ok(PhaseSpec::Prehook),
"post" | "posthook" => Ok(PhaseSpec::Posthook),
"poll" | "polling" => Ok(PhaseSpec::Polling),
other => Err(HappensBeforeError::DslSyntax {
line: lineno,
message: format!("unknown syscall phase '{}'", other),
}),
}
}
fn is_syscall_name(s: &str) -> bool {
Sysno::from_str(s).is_ok()
}
fn sanitize_name(token: &str) -> String {
let mut out = String::with_capacity(token.len());
for ch in token.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
out.push('e');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn spec_json() -> &'static str {
r#"{
"version": 1,
"threads": { "writer": {"label": "writer"}, "reader": {"label": "reader"} },
"events": {
"X_342": {"thread": "writer", "func": "free_buffer", "line": 120, "nth": 342},
"Y_97": {"thread": "reader", "func": "read_buffer", "nth": 97},
"lockA": {"thread": "writer", "syscall": "futex", "phase": "posthook", "nth": 5},
"storeB": {"thread": "reader", "rip": "0x401f3c", "nth": 1},
"scA": {"thread": "writer", "syscalls": 10},
"rcbB": {"thread": "reader", "rcbs": 123456}
},
"edges": [
{"before": "X_342", "after": "Y_97", "strength": "hard"},
{"before": "lockA", "after": "storeB"},
{"before": "scA", "after": "rcbB", "strength": "soft"}
]
}"#
}
#[test]
fn parse_and_normalize_rfc_example() {
let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
let prog = spec.normalize().unwrap();
assert_eq!(prog.anchors.len(), 6);
assert_eq!(prog.edges.len(), 3);
assert_eq!(prog.anchors["scA"].position, Position::SyscallCount(10));
assert_eq!(prog.anchors["rcbB"].position, Position::Rcb(123456));
match &prog.anchors["X_342"].position {
Position::Rip { addr: None, nth } => assert_eq!(*nth, 342),
other => panic!("expected unresolved RIP, got {:?}", other),
}
assert_eq!(
prog.anchors["X_342"].location.function.as_deref(),
Some("free_buffer")
);
assert_eq!(prog.anchors["X_342"].location.line, Some(120));
match &prog.anchors["lockA"].position {
Position::Syscall { sysno, phase, nth } => {
assert_eq!(*sysno, Sysno::futex);
assert_eq!(*phase, Some(SyscallPhase::Posthook));
assert_eq!(*nth, 5);
}
other => panic!("expected syscall, got {:?}", other),
}
assert_eq!(
prog.anchors["storeB"].position,
Position::Rip {
addr: Some(0x401f3c),
nth: 1
}
);
assert_eq!(prog.edges[2].strength, Strength::Soft);
assert_eq!(prog.edges[1].strength, Strength::Hard);
assert_eq!(prog.unresolved_locations().count(), 2);
}
#[test]
fn round_trip_json() {
let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
let json = spec.to_json().unwrap();
let spec2 = HappensBeforeSpec::from_json(&json).unwrap();
assert_eq!(spec, spec2);
}
#[test]
fn rejects_wrong_version() {
let spec = HappensBeforeSpec {
version: 999,
..HappensBeforeSpec::from_json(spec_json()).unwrap()
};
assert_eq!(
spec.normalize().unwrap_err(),
HappensBeforeError::UnsupportedVersion(999)
);
}
#[test]
fn rejects_ambiguous_position() {
let json = r#"{
"version": 1,
"events": { "bad": {"thread": "1", "syscalls": 3, "rcbs": 5} },
"edges": []
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
match spec.normalize().unwrap_err() {
HappensBeforeError::AmbiguousPosition { event, found } => {
assert_eq!(event, "bad");
assert_eq!(found.len(), 2);
}
other => panic!("expected AmbiguousPosition, got {:?}", other),
}
}
#[test]
fn rejects_no_position() {
let json = r#"{
"version": 1,
"events": { "bad": {"thread": "1"} },
"edges": []
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
assert!(matches!(
spec.normalize().unwrap_err(),
HappensBeforeError::AmbiguousPosition { .. }
));
}
#[test]
fn code_location_accompanies_count() {
let json = r#"{
"version": 1,
"events": {
"w": {"thread": "1", "func": "free_buffer", "line": 342, "syscalls": 7},
"r": {"thread": "1", "func": "read_buffer", "rcbs": 900}
},
"edges": [ {"before": "w", "after": "r"} ]
}"#;
let prog = HappensBeforeSpec::from_json(json)
.unwrap()
.normalize()
.unwrap();
assert_eq!(prog.anchors["w"].position, Position::SyscallCount(7));
assert_eq!(
prog.anchors["w"].location.function.as_deref(),
Some("free_buffer")
);
assert_eq!(prog.anchors["w"].location.line, Some(342));
assert_eq!(prog.anchors["r"].position, Position::Rcb(900));
assert_eq!(
prog.anchors["r"].location.function.as_deref(),
Some("read_buffer")
);
assert_eq!(prog.unresolved_locations().count(), 0);
}
#[test]
fn raw_dettid_thread() {
let json = r#"{
"version": 1,
"events": { "e": {"thread": "42", "rcbs": 7} },
"edges": []
}"#;
let prog = HappensBeforeSpec::from_json(json)
.unwrap()
.normalize()
.unwrap();
assert_eq!(prog.anchors["e"].thread.dettid, Some(DetTid::from_raw(42)));
}
#[test]
fn rejects_unknown_thread() {
let json = r#"{
"version": 1,
"events": { "e": {"thread": "ghost", "rcbs": 7} },
"edges": []
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
assert!(matches!(
spec.normalize().unwrap_err(),
HappensBeforeError::UnknownThread { .. }
));
}
#[test]
fn rejects_unknown_event_in_edge() {
let json = r#"{
"version": 1,
"events": { "a": {"thread": "1", "rcbs": 7} },
"edges": [ {"before": "a", "after": "missing"} ]
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
assert!(matches!(
spec.normalize().unwrap_err(),
HappensBeforeError::UnknownEvent { .. }
));
}
#[test]
fn rejects_unknown_syscall() {
let json = r#"{
"version": 1,
"events": { "a": {"thread": "1", "syscall": "not_a_syscall"} },
"edges": []
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
assert!(matches!(
spec.normalize().unwrap_err(),
HappensBeforeError::UnknownSyscall { .. }
));
}
#[test]
fn detects_cycle() {
let json = r#"{
"version": 1,
"events": {
"a": {"thread": "1", "rcbs": 1},
"b": {"thread": "1", "rcbs": 2},
"c": {"thread": "1", "rcbs": 3}
},
"edges": [
{"before": "a", "after": "b"},
{"before": "b", "after": "c"},
{"before": "c", "after": "a"}
]
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
match spec.normalize().unwrap_err() {
HappensBeforeError::Cycle(path) => {
assert_eq!(path.first(), path.last());
assert!(path.len() >= 4);
}
other => panic!("expected Cycle, got {:?}", other),
}
}
#[test]
fn accepts_dag() {
let json = r#"{
"version": 1,
"events": {
"a": {"thread": "1", "rcbs": 1},
"b": {"thread": "1", "rcbs": 2},
"c": {"thread": "1", "rcbs": 3}
},
"edges": [
{"before": "a", "after": "c"},
{"before": "b", "after": "c"}
]
}"#;
let spec = HappensBeforeSpec::from_json(json).unwrap();
assert!(spec.normalize().is_ok());
}
#[test]
fn dsl_desugars() {
let dsl = "\
// btrfs race: erase-by-key must precede the re-insert
writer:free_buffer#342 < reader:read_buffer#97
writer:futex@post#5 < reader:@0x401f3c#1
A:rcb=123456 < B:sc=97 !soft
";
let spec = HappensBeforeSpec::from_dsl(dsl).unwrap();
let prog = spec.normalize().unwrap();
assert_eq!(prog.edges.len(), 3);
assert_eq!(prog.anchors.len(), 6);
let soft = &prog.edges[2];
assert_eq!(soft.strength, Strength::Soft);
assert_eq!(prog.anchors[&soft.before].position, Position::Rcb(123456));
assert_eq!(
prog.anchors[&soft.after].position,
Position::SyscallCount(97)
);
let futex = prog
.anchors
.values()
.find(
|a| matches!(a.position, Position::Syscall { sysno, .. } if sysno == Sysno::futex),
)
.unwrap();
match &futex.position {
Position::Syscall { phase, nth, .. } => {
assert_eq!(*phase, Some(SyscallPhase::Posthook));
assert_eq!(*nth, 5);
}
_ => unreachable!(),
}
}
#[test]
fn dsl_rejects_missing_arrow() {
let err = HappensBeforeSpec::from_dsl("writer:foo reader:bar").unwrap_err();
assert!(matches!(err, HappensBeforeError::DslSyntax { line: 1, .. }));
}
#[test]
fn parse_rip_forms() {
assert_eq!(parse_rip("0x401f3c"), Some(0x401f3c));
assert_eq!(parse_rip("4201276"), Some(4201276));
assert_eq!(parse_rip("nonsense"), None);
}
}