///|
/// Lifecycle state of a deterministic simulation scenario.
pub enum SimulationScenarioState {
SimulationScenarioDraft
SimulationScenarioReady
SimulationScenarioRunning
SimulationScenarioPassed
SimulationScenarioFailed
}
///|
pub fn simulation_scenario_state_variants() -> Array[SimulationScenarioState] {
[
SimulationScenarioDraft,
SimulationScenarioReady,
SimulationScenarioRunning,
SimulationScenarioPassed,
SimulationScenarioFailed,
]
}
///|
/// An event scheduled in a reusable simulation scenario.
pub struct SimulationScenarioEvent {
timestamp_us : UInt64
frame : Frame
source : String
}
///|
pub fn simulation_scenario_event(
timestamp_us : UInt64,
frame : Frame,
source? : String = "scenario",
) -> SimulationScenarioEvent {
{ timestamp_us, frame, source }
}
///|
pub fn SimulationScenarioEvent::timestamp_us(
self : SimulationScenarioEvent,
) -> UInt64 {
self.timestamp_us
}
///|
pub fn SimulationScenarioEvent::frame(self : SimulationScenarioEvent) -> Frame {
self.frame
}
///|
pub fn SimulationScenarioEvent::source(
self : SimulationScenarioEvent,
) -> String {
self.source
}
///|
/// An assertion applied to a completed simulation report.
pub enum SimulationScenarioAssertion {
SimulationAssertDeliveredAtLeast(Int)
SimulationAssertDroppedAtMost(Int)
SimulationAssertDurationAtMost(UInt64)
SimulationAssertIdentifierSeen(UInt)
SimulationAssertHealth(BusHealthState)
}
///|
pub fn simulation_scenario_assertion_variants() -> Array[
SimulationScenarioAssertion,
] {
[
SimulationAssertDeliveredAtLeast(0),
SimulationAssertDroppedAtMost(0),
SimulationAssertDurationAtMost(0),
SimulationAssertIdentifierSeen(0),
SimulationAssertHealth(Healthy),
]
}
///|
/// A result produced by running a scenario.
pub struct SimulationScenarioResult {
name : String
state : SimulationScenarioState
report : SimulationReport
assertions : Array[SimulationScenarioAssertion]
failures : Array[String]
}
///|
pub fn SimulationScenarioResult::name(
self : SimulationScenarioResult,
) -> String {
self.name
}
///|
pub fn SimulationScenarioResult::state(
self : SimulationScenarioResult,
) -> SimulationScenarioState {
self.state
}
///|
pub fn SimulationScenarioResult::report(
self : SimulationScenarioResult,
) -> SimulationReport {
self.report
}
///|
pub fn SimulationScenarioResult::assertions(
self : SimulationScenarioResult,
) -> Array[SimulationScenarioAssertion] {
self.assertions.copy()
}
///|
pub fn SimulationScenarioResult::failures(
self : SimulationScenarioResult,
) -> Array[String] {
self.failures.copy()
}
///|
pub fn SimulationScenarioResult::passed(
self : SimulationScenarioResult,
) -> Bool {
self.state is SimulationScenarioPassed
}
///|
pub fn SimulationScenarioResult::to_text(
self : SimulationScenarioResult,
) -> String {
self.name +
" state=" +
simulation_scenario_state_text(self.state) +
" failures=" +
self.failures.length().to_string() +
" delivered=" +
self.report.delivered().to_string() +
" dropped=" +
self.report.dropped().to_string()
}
///|
pub fn simulation_scenario_state_text(
state : SimulationScenarioState,
) -> String {
match state {
SimulationScenarioDraft => "draft"
SimulationScenarioReady => "ready"
SimulationScenarioRunning => "running"
SimulationScenarioPassed => "passed"
SimulationScenarioFailed => "failed"
}
}
///|
/// A reusable deterministic scenario for ECU and gateway testing.
pub struct SimulationScenario {
name : String
capacity : Int
bitrate_kbps : UInt
events : Array[SimulationScenarioEvent]
assertions : Array[SimulationScenarioAssertion]
mut state : SimulationScenarioState
}
///|
pub fn new_simulation_scenario(
name : String,
capacity? : Int = 128,
bitrate_kbps? : UInt = 500,
) -> SimulationScenario {
{
name,
capacity: if capacity < 1 {
1
} else {
capacity
},
bitrate_kbps,
events: [],
assertions: [],
state: SimulationScenarioDraft,
}
}
///|
pub fn SimulationScenario::name(self : SimulationScenario) -> String {
self.name
}
///|
pub fn SimulationScenario::capacity(self : SimulationScenario) -> Int {
self.capacity
}
///|
pub fn SimulationScenario::bitrate_kbps(self : SimulationScenario) -> UInt {
self.bitrate_kbps
}
///|
pub fn SimulationScenario::state(
self : SimulationScenario,
) -> SimulationScenarioState {
self.state
}
///|
pub fn SimulationScenario::events(
self : SimulationScenario,
) -> Array[SimulationScenarioEvent] {
self.events.copy()
}
///|
pub fn SimulationScenario::assertions(
self : SimulationScenario,
) -> Array[SimulationScenarioAssertion] {
self.assertions.copy()
}
///|
pub fn SimulationScenario::schedule(
self : SimulationScenario,
timestamp_us : UInt64,
frame : Frame,
source? : String = "scenario",
) -> Bool {
if self.events.length() >= self.capacity ||
!(self.state is SimulationScenarioDraft) {
false
} else {
self.events.push(simulation_scenario_event(timestamp_us, frame, source~))
true
}
}
///|
pub fn SimulationScenario::schedule_periodic(
self : SimulationScenario,
first_timestamp_us : UInt64,
period_us : UInt64,
count : Int,
frame : Frame,
source? : String = "periodic",
) -> Bool {
if period_us == 0 || count < 1 {
false
} else {
let mut accepted = true
for index in 0.. Unit {
self.assertions.push(assertion)
}
///|
pub fn SimulationScenario::ready(self : SimulationScenario) -> Bool {
if self.events.is_empty() {
false
} else {
self.state = SimulationScenarioReady
true
}
}
///|
pub fn SimulationScenario::run(
self : SimulationScenario,
) -> SimulationScenarioResult raise SimulationError {
if self.events.is_empty() {
self.state = SimulationScenarioFailed
}
self.state = SimulationScenarioRunning
let simulation = new_simulation(self.capacity, self.bitrate_kbps)
let ordered = self.events.copy()
ordered.sort_by((left, right) => {
if left.timestamp_us() < right.timestamp_us() {
-1
} else if left.timestamp_us() > right.timestamp_us() {
1
} else {
0
}
})
for event in ordered {
simulation.schedule(
event.timestamp_us(),
event.frame(),
source=event.source(),
)
}
let end_us : UInt64 = if ordered.is_empty() {
0
} else {
ordered[ordered.length() - 1].timestamp_us() + 1
}
ignore(simulation.run_until(end_us))
let report = simulation.report(0)
let failures = simulation_check_assertions(
self.assertions,
report,
simulation.trace(),
)
self.state = if failures.is_empty() {
SimulationScenarioPassed
} else {
SimulationScenarioFailed
}
{
name: self.name,
state: self.state,
report,
assertions: self.assertions.copy(),
failures,
}
}
///|
pub fn SimulationScenario::clear(self : SimulationScenario) -> Unit {
self.events.clear()
self.assertions.clear()
self.state = SimulationScenarioDraft
}
///|
pub fn SimulationScenario::to_text(self : SimulationScenario) -> String {
self.name +
" state=" +
simulation_scenario_state_text(self.state) +
" events=" +
self.events.length().to_string() +
" assertions=" +
self.assertions.length().to_string()
}
///|
fn simulation_check_assertions(
assertions : Array[SimulationScenarioAssertion],
report : SimulationReport,
trace : Trace,
) -> Array[String] {
let failures : Array[String] = []
for assertion in assertions {
match assertion {
SimulationAssertDeliveredAtLeast(minimum) =>
if report.delivered() < minimum {
failures.push("delivered below minimum")
}
SimulationAssertDroppedAtMost(maximum) =>
if report.dropped() > maximum {
failures.push("dropped above maximum")
}
SimulationAssertDurationAtMost(maximum) =>
if report.duration_us() > maximum {
failures.push("duration above maximum")
}
SimulationAssertIdentifierSeen(identifier) => {
let seen = trace.frames().exists(frame => frame.id() == identifier)
if !seen {
failures.push("identifier not seen " + identifier.to_string())
}
}
SimulationAssertHealth(expected) => {
let health = assess_bus_health(
trace_metrics(trace),
report.dropped(),
report.duration_us(),
500,
)
if !network_health_state_same(health.state(), expected) {
failures.push("unexpected bus health")
}
}
}
}
failures
}
///|
fn network_health_state_same(
left : BusHealthState,
right : BusHealthState,
) -> Bool {
match left {
Healthy =>
match right {
Healthy => true
_ => false
}
Busy =>
match right {
Busy => true
_ => false
}
Degraded =>
match right {
Degraded => true
_ => false
}
Overloaded =>
match right {
Overloaded => true
_ => false
}
Silent =>
match right {
Silent => true
_ => false
}
}
}
///|
/// Run multiple scenarios and return only failed results.
pub fn run_simulation_scenarios(
scenarios : Array[SimulationScenario],
) -> Array[SimulationScenarioResult] raise SimulationError {
let results : Array[SimulationScenarioResult] = []
for scenario in scenarios {
results.push(scenario.run())
}
results
}
///|
/// Produce a regression summary for scenario results.
pub fn simulation_scenario_summary(
results : Array[SimulationScenarioResult],
) -> String {
let mut passed = 0
let mut failed = 0
let names : Array[String] = []
for result in results {
if result.passed() {
passed += 1
} else {
failed += 1
names.push(result.name())
}
}
"passed=" +
passed.to_string() +
" failed=" +
failed.to_string() +
" failing=" +
names.join(",")
}