use std::borrow::Cow;
use std::collections::HashSet;
use std::io::Write;
use crate::policy::TransitStatus;
use crate::testing::assertions::AssertionLabel;
use crate::testing::fdr::config::AcceptanceSet;
use crate::testing::specs::csp::{Event, Process, State, TransitionRelation};
use crate::trace::ConsumedTrace;
pub const INITIAL: &str = "initial";
pub const GATE_ACCEPT: &str = "gate_accept";
pub const HANDLER: &str = "handler";
pub const GATE_REJECT: &str = "gate_reject";
pub const TERMINAL: &str = "terminal";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TraceProcessMode {
AssertionsOnly,
#[cfg(feature = "instrument")]
WithObservableInstrumentation,
#[cfg(feature = "instrument")]
FullInstrumentation,
}
pub trait FdrTraceExt {
fn csp_valid(&self) -> bool;
fn terminated_in_valid_state(&self) -> bool;
fn acceptance_at(&self, state_label: &str) -> Option<AcceptanceSet>;
fn can_refuse_after(&self, state_label: &str, event_label: &str) -> bool;
fn assertion_count(&self, label: &str) -> usize;
#[cfg(feature = "instrument")]
fn project_to_observable(&self) -> Vec<String>;
#[cfg(feature = "instrument")]
fn project_to_hidden(&self) -> Vec<String>;
fn export_cspm<W: Write>(&self, writer: &mut W) -> std::io::Result<()>;
fn to_process(&self) -> Process {
self.to_process_with_mode(TraceProcessMode::AssertionsOnly)
}
fn to_process_with_mode(&self, mode: TraceProcessMode) -> Process;
}
impl FdrTraceExt for ConsumedTrace {
fn csp_valid(&self) -> bool {
if self.error.is_some() {
return false;
}
if self.gate_decision.is_none() {
return false;
}
if matches!(self.gate_decision, Some(TransitStatus::Accepted))
&& self.assertions.is_empty()
&& self.response.is_none()
{
return false; }
true
}
fn terminated_in_valid_state(&self) -> bool {
if self.error.is_some() {
return false;
}
match self.gate_decision {
Some(TransitStatus::Accepted) => {
self.response.is_some() || !self.assertions.is_empty()
}
Some(TransitStatus::Busy)
| Some(TransitStatus::Unauthorized)
| Some(TransitStatus::Forbidden)
| Some(TransitStatus::Timeout) => {
true
}
Some(TransitStatus::Request) | None => false, }
}
fn acceptance_at(&self, state_label: &str) -> Option<AcceptanceSet> {
let mut acceptance = AcceptanceSet::new();
match state_label {
INITIAL => {
acceptance.insert(Event("request"));
}
GATE_ACCEPT => {
acceptance.insert(Event("handler_enter"));
}
HANDLER => {
acceptance.insert(Event("handler_exit"));
acceptance.insert(Event("response"));
}
GATE_REJECT | TERMINAL => {
}
_ => {
return None;
}
}
Some(acceptance)
}
fn can_refuse_after(&self, state_label: &str, event_label: &str) -> bool {
if let Some(acceptance) = self.acceptance_at(state_label) {
!acceptance.iter().any(|e| e.0 == event_label)
} else {
true
}
}
fn assertion_count(&self, label: &str) -> usize {
self.assertions
.iter()
.filter(|a| matches!(&a.label, AssertionLabel::Custom(l) if l.as_ref() == label))
.count()
}
#[cfg(feature = "instrument")]
fn project_to_observable(&self) -> Vec<String> {
use crate::instrumentation::events;
self.instrument_events
.iter()
.filter(|e| {
e.urn == events::GATE_ACCEPT
|| e.urn == events::GATE_REJECT
|| e.urn == events::REQUEST_RECV
|| e.urn == events::RESPONSE_SEND
|| e.urn == events::ASSERT_LABEL
})
.filter_map(|e| e.label.as_ref().map(|s| s.to_string()))
.collect()
}
#[cfg(feature = "instrument")]
fn project_to_hidden(&self) -> Vec<String> {
use crate::instrumentation::events;
self.instrument_events
.iter()
.filter(|e| {
e.urn == events::HANDLER_ENTER
|| e.urn == events::HANDLER_EXIT
|| e.urn == events::CRYPTO_STEP
|| e.urn == events::COMPRESS_STEP
|| e.urn == events::ROUTE_STEP
|| e.urn == events::POLICY_EVAL
|| e.urn == events::PROCESS_HIDDEN
})
.filter_map(|e| e.label.as_ref().map(|s| s.to_string()))
.collect()
}
fn export_cspm<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
writeln!(writer, "-- Execution trace")?;
writeln!(writer, "Trace = <")?;
#[cfg(feature = "instrument")]
{
let observable = self.project_to_observable();
for (idx, event) in observable.iter().enumerate() {
if idx > 0 {
write!(writer, ", ")?;
}
write!(writer, "{event}")?;
}
}
writeln!(writer, ">")?;
Ok(())
}
fn to_process_with_mode(&self, mode: TraceProcessMode) -> Process {
let mut states = HashSet::new();
let mut terminal = HashSet::new();
let mut observable = HashSet::new();
let mut hidden = HashSet::new();
let mut transitions = TransitionRelation::new();
let s_initial = State(INITIAL);
let s_terminal = State(TERMINAL);
states.insert(s_initial);
states.insert(s_terminal);
terminal.insert(s_terminal);
#[derive(Clone)]
enum TraceEvent {
Assertion {
seq: usize,
label: &'static str,
},
#[cfg(feature = "instrument")]
ObservableInstrumentation {
seq: u32,
label: String,
},
#[cfg(feature = "instrument")]
HiddenInstrumentation {
seq: u32,
label: String,
},
}
let mut events: Vec<TraceEvent> = Vec::new();
for assertion in &self.assertions {
let AssertionLabel::Custom(label) = &assertion.label;
let static_label: &'static str = match label {
Cow::Borrowed(s) => s,
Cow::Owned(s) => Box::leak(s.clone().into_boxed_str()),
};
events.push(TraceEvent::Assertion { seq: assertion.seq, label: static_label });
}
#[cfg(feature = "instrument")]
{
match mode {
TraceProcessMode::AssertionsOnly => {
}
TraceProcessMode::WithObservableInstrumentation | TraceProcessMode::FullInstrumentation => {
use crate::instrumentation::events;
for event in &self.instrument_events {
if event.urn == events::GATE_ACCEPT
|| event.urn == events::GATE_REJECT
|| event.urn == events::REQUEST_RECV
|| event.urn == events::RESPONSE_SEND
|| event.urn == events::ASSERT_LABEL
{
if let Some(label) = &event.label {
events.push(TraceEvent::ObservableInstrumentation {
seq: event.seq,
label: label.clone(),
});
}
}
}
}
}
if matches!(mode, TraceProcessMode::FullInstrumentation) {
use crate::instrumentation::events;
for event in &self.instrument_events {
if event.urn == events::HANDLER_ENTER
|| event.urn == events::HANDLER_EXIT
|| event.urn == events::CRYPTO_STEP
|| event.urn == events::COMPRESS_STEP
|| event.urn == events::ROUTE_STEP
|| event.urn == events::POLICY_EVAL
|| event.urn == events::PROCESS_HIDDEN
{
if let Some(label) = &event.label {
events.push(TraceEvent::HiddenInstrumentation { seq: event.seq, label: label.clone() });
}
}
}
}
}
events.sort_by(|a, b| {
let seq_a = match a {
TraceEvent::Assertion { seq, .. } => *seq as u64,
#[cfg(feature = "instrument")]
TraceEvent::ObservableInstrumentation { seq, .. } | TraceEvent::HiddenInstrumentation { seq, .. } => *seq as u64,
};
let seq_b = match b {
TraceEvent::Assertion { seq, .. } => *seq as u64,
#[cfg(feature = "instrument")]
TraceEvent::ObservableInstrumentation { seq, .. } | TraceEvent::HiddenInstrumentation { seq, .. } => *seq as u64,
};
seq_a.cmp(&seq_b)
});
if !events.is_empty() {
let mut from_state = s_initial;
for (idx, event) in events.iter().enumerate() {
let (event, is_hidden) = match event {
TraceEvent::Assertion { label, .. } => (Event(label), false),
#[cfg(feature = "instrument")]
TraceEvent::ObservableInstrumentation { label, .. } => {
let static_label: &'static str = Box::leak(label.clone().into_boxed_str());
(Event(static_label), false)
}
#[cfg(feature = "instrument")]
TraceEvent::HiddenInstrumentation { label, .. } => {
let static_label: &'static str = Box::leak(label.clone().into_boxed_str());
(Event(static_label), true)
}
};
if is_hidden {
hidden.insert(event);
} else {
observable.insert(event);
}
let to_state = if idx == events.len() - 1 {
s_terminal
} else {
let state_name: &'static str = Box::leak(format!("trace_state_{idx}").into_boxed_str());
let to_state = State(state_name);
states.insert(to_state);
to_state
};
transitions.add(from_state, event, to_state);
from_state = to_state;
}
}
let description = match mode {
TraceProcessMode::AssertionsOnly => "Process derived from ConsumedTrace (assertion events only)",
#[cfg(feature = "instrument")]
TraceProcessMode::WithObservableInstrumentation => {
"Process derived from ConsumedTrace (assertions + observable instrumentation)"
}
#[cfg(feature = "instrument")]
TraceProcessMode::FullInstrumentation => {
"Process derived from ConsumedTrace (all events including hidden instrumentation)"
}
};
Process {
name: "TraceProcess",
initial: s_initial,
states,
terminal,
choice: HashSet::new(), observable,
hidden,
transitions,
description: Some(description),
#[cfg(feature = "testing-timing")]
timing_constraints: None,
#[cfg(feature = "testing-timing")]
timed_transitions: None,
#[cfg(feature = "testing-schedulability")]
schedulability_periods: None,
}
}
}
impl ConsumedTrace {
pub fn to_process(&self) -> Process {
<Self as FdrTraceExt>::to_process(self)
}
pub fn to_process_with_mode(&self, mode: TraceProcessMode) -> Process {
<Self as FdrTraceExt>::to_process_with_mode(self, mode)
}
}