Verification: formal checks, tests, faults and logs
How archec checks that the model is right: the state machines and contracts it proves, the requirements that cannot hold together, the tests with faults, their coverage, and the real system's logs read against the model.
Formal checks: state machines and contracts#
The linter reads each state machine as a graph (a definition's with the states and transitions of the ones it extends, as Arche gets it):
| code | what it finds |
|---|---|
unreachable-state |
a state no path reaches from the initial state. Entering a composite starts in its pseudo initial child (else its first; one per region), and a transition that never fires leads nowhere. goto("X") in the code reaches X. |
deadlock-state |
a reachable state with no way out, nor from a state around it, that is not pseudo final. It is a warning when the machine has a final state, and a hint when it has none, since a latched fault that only a reset leaves is often meant. A state every state falls into (* -> Fault) is a safe state. A component whose code calls goto(…) is not checked. |
dead-transition |
a trigger that is a message no input port of the component receives (receives, its handlers, its interfaces' messages; a port that says none takes anything), or a guard that is false |
shadowed-transition |
a transition that an earlier one beats every time: same state, same trigger, and the earlier one has no guard |
nondeterministic (hint) |
two guarded transitions from one state on one trigger whose guards may both hold. else, g / not g, distinct values and disjoint ranges (x < 0 / x >= 0) count as exclusive. |
A guard is read only when it is a plain comparison; one written in words is left alone.
A contract says what a component assumes of what it receives and guarantees of what it sends:
component BrakeEcu {
in cmd: can-fd receives BRAKE_CMD
out status: can-fd sends BRAKE_STATUS
contract {
assume cmd.force in 0..1000N
assume cmd.mode != 2
guarantee status.applied within 20ms after cmd
}
}
A clause names a port's message field (cmd.force, or cmd.BRAKE_CMD.force) or a variable, compared
with in lo..hi, ==, !=, <, <=, >, >=, or bare (status.applied, not fault). Clauses can be
joined by and, opened with always or eventually, and closed with within 20ms [after port]. An
interface or a port type can hold a contract too. Its clauses name the message or the field, and they hold
on every port that takes part in it.
At each link a.p -> b.q, what a guarantees on p must imply what b assumes on q:
- Ranges and equalities on the same field. A guarantee
in 0..1500Nagainst an assumptionin 0..1000Nis acontract-unmetwarning that quotes both sides. - Units. Time units are converted. Others are not: N against kN is a
contract-unknownhint, and N against ms is unmet. - Deadlines. A guarantee
within 20msmeets an assumptionwithin 50ms. When both name anafterevent, a link must join the two ports.
An assumption with nothing guaranteed on its field is contract-unknown, and so is a clause archec cannot
read (an or, a call). Arche has no place for contracts, so they stay in the model. archec sysml writes
them as a requirement of assume constraints and require constraints.
Requirements that cannot hold together#
archec verify --conflicts looks for requirements no implementation can satisfy together, as NASA's FRET
does, with nothing to install. It reads the formal properties and the EARS sentences over each part's
states. At each tick the environment chooses which message each part receives; the implementation then
puts each part in any of its states (the machine's transitions are one implementation, not a limit),
starting from its initial one. When no choice of states survives every input, the requirements conflict:
✗ conflict R1, R2 cannot hold together [req-conflict]
at 10ms, when brakes receives watchdog, every state the parts can be in breaks R1 or R2
0 brakes in Idle
10ms brakes receives watchdog → whatever the parts do:
brakes in Idle breaks R2
brakes in Braking breaks R2
brakes in Holding breaks R2
brakes in Fault breaks R1
main.arche:31 R1: never brakes in Fault
main.arche:32 R2: if brakes receives watchdog then brakes shall be in Fault
Each conflict is minimal: leave out any one requirement and the rest can hold. It is a safety game on a
finite graph (the parts' states and, per requirement, the earliest deadline it awaits), solved exactly. What
is bounded: time is counted in ticks, the deadlines' common divisor (made coarser past 16 ticks per
deadline, each rounded up; --tick sets it); eventually and leads to with no within are read as within
--horizon ticks (8); a response that sends a message is not read.
archec verify --meaning [REQ…] shows what a property says, on two timelines, one where it holds and one
where it fails (the specification from archec doc --html shows them under each requirement):
R3 (brakes receives BRAKE_CMD) leads to brakes in Braking within 50ms
Each time brakes receives BRAKE_CMD, brakes in Braking holds then or at most 50ms later.
A = brakes receives BRAKE_CMD
B = brakes in Braking
holds:
A ··▲·······▲·····
B ·····██·····█···
└───┘ 50ms
fails:
A ··▲·······▲·····
B ············█···
└───┘ 50ms ✗ B not in time
eventually and leads to take a deadline, within 50ms. NuSMV, untimed, checks them without it; the
monitors (archec gen --monitors), archec replay and --conflicts check it.
For a model checker, archec gen --target uppaal writes uppaal/model.xml and uppaal/model.q, and
--target nusmv writes nusmv/model.smv (see Code generation).
Testing with faults#
A test case can break things on purpose, in Arche's simulation, to check that the model copes:
failure-mode FM-RX "Receiver hangs" { of Car.brakes; effect "No more braking"; inject "freeze brakes" }
case TC-CAN-LOSS "Brake CAN lost" {
at 20ms fault drop adcu.can_brake // every message through that link end is lost
within 60ms expect brakes.state == "SafeStop"
at 100ms fault up adcu.can_brake // works again
at 120ms fault FM-RX // a failure mode's `inject`
}
| fault | what the simulation does |
|---|---|
drop part.port / up part.port |
messages through that link end are lost (traced as dropped, "fault: …") / no longer |
delay part.port 5ms |
each message through it arrives 5 ms later |
stuck part.port f=0, g=True |
messages through it carry those field values |
freeze part / thaw part |
the part (and what is inside it) stops: its code, timers and inbox; what it held resumes when thawed |
clear [target] |
ends the target's faults, or all of them |
FM-ID |
the failure mode's inject |
A step takes fault … too (at its start). The linter checks the fault and its target (bad-fault, unresolved).
Each one compiles to an observer call (fault_drop("adcu.can_brake")…) that Arche's engine runs.
Test coverage#
archec test main.arche --coverage (and Run with Coverage in VS Code's Test Explorer) says what the runs
exercised of the model they ran on:
Coverage (car.yaml):
states 6/8 75 %
not covered: adcu: MinimalRisk
transitions 5/10 50 %
not covered: adcu: Autonomous -> MinimalRisk
messages 4/10 40 %
not covered: HEARTBEAT: adcu.can_brake -> brakes.can
functions 9/9 100 %
requirements 4/4 100 %
States entered, transitions taken, each message on each link that carries it, the functions called, and the
requirements a test verifies whose cases ran. The whole report is kept in .archec/coverage.json. In the
editor, each state, transition, function and link is marked run or not run at its line (VS Code 1.88+).
Arche's simulator records it (arche-cli test --coverage <file>).
Runtime verification: the real system's logs#
archec replay checks the model's requirements on what the real system did: a CAN log (candump, Vector
ASC or BLF), a ROS 2 bag (MCAP), or JSON lines, each event decoded by the model.
cd examples/brake-by-wire
archec replay main.arche logs/run-late.log logs/states-late.jsonl --junit replay.xml
✗ REQ-BRK-1 1 violation(s), the first at +0.070000 s (log time 1762790400.070000): brakes receives BRAKE_CMD at 50ms, not brakes in Braking 20ms later
✓ REQ-BRK-2 1 trigger(s)
✗ REQ-BRK-3 1 violation(s), the first at +0.150000 s (log time 1762790400.150000): brakes in Fault
✗ REQ-BRK-4 1 violation(s), the first at +0.050000 s (log time 1762790400.050000): BRAKE_CMD.force = 15000
✗ stale-after ctrl.status BRAKE_STATUS 1 violation(s), the first at +0.125000 s …: none for 70ms
A CAN id is the message the deployment's route … { id 0x100 } puts there, its bytes decoded by the same
layout as the DBC and the generated C and Rust; a ROS 2 topic is the message of its type. What is checked:
EARS requirements as monitors, formal properties, a metric that names a message field against its
limit, contracts, chain budgets, ranges, periods and stale-after. A bus log does not show states: a
JSON line {"t": …, "part": "brakes", "state": "Braking"} does (from a firmware trace, say), and what needs
one is otherwise reported as not checkable. A response still awaited when the log ends is inconclusive.
--json and --junit are for CI, as with archec ci. --scenario replay.arche --from ctrl turns the log into a
test case: what ctrl sent, sent to the model at the same times, and the states the real parts went through,
expected of the model's; archec test then runs the model on the real inputs. See docs/commands.md.