Refactor + tests (#6)

- Minor codebase clean-up
- Slight reorganisation of examples + tests so that we don't break stuff by accident
- CI tests
This commit was merged in pull request #6.
This commit is contained in:
2026-04-10 18:16:49 +01:00
committed by GitHub
parent 363446821e
commit f0019619b3
74 changed files with 1672 additions and 930 deletions

View File

@@ -0,0 +1,8 @@
Using BDD-based Bounded Model Checking
Formula (((EF(E< proc0.allowed >X(proc0.in)) AND EF(E< proc1.allowed >X(proc1.in))) AND EF(E< proc2.allowed >X(proc2.in))) AND EF(E< proc3.allowed >X(proc3.in))) holds
Using BDD-based Bounded Model Checking
Formula EF((((proc0.approach AND proc1.approach) AND proc2.approach) AND proc3.approach)) holds
Using BDD-based Bounded Model Checking
Formula AG((proc0.in IMPLIES K[proc0](((~proc1.in AND ~proc2.in) AND ~proc3.in)))) holds
Using BDD-based Bounded Model Checking
Formula AG((proc0.in IMPLIES C[proc0 proc1 proc2 proc3](((~proc1.in AND ~proc2.in) AND ~proc3.in)))) holds

View File

@@ -0,0 +1,58 @@
# Context entities: out allowed
# Reactions:
. proc = "proc0":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc1":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc2":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc3":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
# Context Automaton States:
= Init state: init
* init
* green
* red
* T
# Context Automaton Transitions:
* [init -> green]: { proc0={ out } proc1={ out } proc2={ out } proc3={ out } }
* [green -> red]: { proc0={ allowed } } proc0.req
* [green -> red]: { proc1={ allowed } } proc1.req
* [green -> red]: { proc2={ allowed } } proc2.req
* [green -> red]: { proc3={ allowed } } proc3.req
* [green -> green]: { proc0={ } } (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)
* [green -> green]: { proc1={ } } (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)
* [green -> green]: { proc2={ } } (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)
* [green -> green]: { proc3={ } } (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)
* [red -> green]: { proc0={ } } proc0.leave
* [red -> green]: { proc1={ } } proc1.leave
* [red -> green]: { proc2={ } } proc2.leave
* [red -> green]: { proc3={ } } proc3.leave
* [red -> red]: { proc0={ } } (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)
* [red -> red]: { proc1={ } } (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)
* [red -> red]: { proc2={ } } (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)
* [red -> red]: { proc3={ } } (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)
* [green -> T]: { } ~((((((((true OR proc0.req) OR proc1.req) OR proc2.req) OR proc3.req) OR (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)) OR (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)) OR (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req)) OR (((~proc0.req AND ~proc1.req) AND ~proc2.req) AND ~proc3.req))
* [red -> T]: { } ~((((((((true OR proc0.leave) OR proc1.leave) OR proc2.leave) OR proc3.leave) OR (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)) OR (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)) OR (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave)) OR (((~proc0.leave AND ~proc1.leave) AND ~proc2.leave) AND ~proc3.leave))
* [T -> T]: { }

View File

@@ -0,0 +1,80 @@
{ proc0={ req } proc1={ approach } proc2={ req } proc3={ out leave } }
{ proc0={ req } proc1={ req } proc2={ approach } proc3={ out leave } }
{ proc0={ req } proc1={ req } proc2={ req } proc3={ out leave } }
{ proc0={ approach } proc1={ approach } proc2={ req } proc3={ out leave } }
{ proc0={ approach } proc1={ req } proc2={ req } proc3={ out leave } }
{ proc0={ approach } proc1={ approach } proc2={ req in } proc3={ approach } }
{ proc0={ approach } proc1={ req } proc2={ out leave } proc3={ req } }
{ proc0={ approach } proc1={ req } proc2={ req in } proc3={ approach } }
{ proc0={ req } proc1={ approach } proc2={ approach } proc3={ out leave } }
{ proc0={ approach } proc1={ approach } proc2={ approach } proc3={ out leave } }
{ proc0={ req } proc1={ approach } proc2={ req in } proc3={ req } }
{ proc0={ approach } proc1={ req } proc2={ approach } proc3={ out leave } }
{ proc0={ req } proc1={ approach } proc2={ req } proc3={ req in } }
{ proc0={ approach } proc1={ req } proc2={ req in } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ out leave } proc3={ approach } }
{ proc0={ req in } proc1={ approach } proc2={ req } proc3={ req } }
{ proc0={ approach } proc1={ approach } proc2={ approach } proc3={ req in } }
{ proc0={ req } proc1={ req } proc2={ out leave } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ out leave } proc3={ req } }
{ proc0={ req } proc1={ req } proc2={ req in } proc3={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ out leave } proc3={ req } }
{ proc0={ req } proc1={ req } proc2={ req in } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ req in } proc3={ approach } }
{ proc0={ approach } proc1={ req } proc2={ out leave } proc3={ approach } }
{ proc0={ req } proc1={ req } proc2={ out leave } proc3={ approach } }
{ proc0={ } proc1={ } proc2={ } proc3={ } }
{ proc0={ approach } proc1={ approach } proc2={ out leave } proc3={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ req in } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ approach } proc3={ req in } }
{ proc0={ req in } proc1={ req } proc2={ approach } proc3={ req } }
{ proc0={ req in } proc1={ approach } proc2={ approach } proc3={ approach } }
{ proc0={ req } proc1={ req in } proc2={ req } proc3={ req } }
{ proc0={ req in } proc1={ req } proc2={ approach } proc3={ approach } }
{ proc0={ req } proc1={ req } proc2={ req } proc3={ req in } }
{ proc0={ approach } proc1={ req } proc2={ req } proc3={ req in } }
{ proc0={ approach } proc1={ approach } proc2={ req } proc3={ req in } }
{ proc0={ out leave } proc1={ req } proc2={ approach } proc3={ approach } }
{ proc0={ req in } proc1={ req } proc2={ req } proc3={ req } }
{ proc0={ out leave } proc1={ req } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ req } proc2={ approach } proc3={ req in } }
{ proc0={ req in } proc1={ approach } proc2={ approach } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ approach } proc3={ req } }
{ proc0={ req in } proc1={ approach } proc2={ req } proc3={ approach } }
{ proc0={ out leave } proc1={ approach } proc2={ req } proc3={ req } }
{ proc0={ approach } proc1={ req } proc2={ approach } proc3={ req in } }
{ proc0={ out leave } proc1={ req } proc2={ approach } proc3={ req } }
{ proc0={ req } proc1={ out leave } proc2={ req } proc3={ req } }
{ proc0={ req } proc1={ approach } proc2={ req } proc3={ req } }
{ proc0={ out leave } proc1={ req } proc2={ req } proc3={ req } }
{ proc0={ approach } proc1={ req in } proc2={ approach } proc3={ approach } }
{ proc0={ out leave } proc1={ approach } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ req } proc2={ req } proc3={ req } }
{ proc0={ approach } proc1={ out leave } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ req } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ out leave } proc2={ req } proc3={ req } }
{ proc0={ req } proc1={ out leave } proc2={ approach } proc3={ req } }
{ proc0={ approach } proc1={ out leave } proc2={ approach } proc3={ req } }
{ proc0={ approach } proc1={ req in } proc2={ req } proc3={ req } }
{ proc0={ out leave } proc1={ approach } proc2={ req } proc3={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ req } proc3={ req } }
{ proc0={ out leave } proc1={ approach } proc2={ approach } proc3={ req } }
{ proc0={ req in } proc1={ req } proc2={ req } proc3={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ approach } proc3={ req } }
{ proc0={ req } proc1={ req } proc2={ approach } proc3={ req } }
{ proc0={ approach } proc1={ req } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ approach } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ req in } proc2={ approach } proc3={ req } }
{ proc0={ approach } proc1={ approach } proc2={ req } proc3={ approach } }
{ proc0={ approach } proc1={ req in } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ approach } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ req in } proc2={ approach } proc3={ req } }
{ proc0={ approach } proc1={ req } proc2={ approach } proc3={ req } }
{ proc0={ req } proc1={ req in } proc2={ approach } proc3={ approach } }
{ proc0={ req } proc1={ out leave } proc2={ approach } proc3={ approach } }
{ proc0={ req } proc1={ req } proc2={ approach } proc3={ approach } }
{ proc0={ approach } proc1={ out leave } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ out leave } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ req in } proc2={ req } proc3={ approach } }
{ proc0={ req } proc1={ req } proc2={ req } proc3={ approach } }

View File

@@ -0,0 +1,8 @@
Using BDD-based Bounded Model Checking
Formula ((EF(E< proc0.allowed >X(proc0.in)) AND EF(E< proc1.allowed >X(proc1.in))) AND EF(E< proc2.allowed >X(proc2.in))) holds
Using BDD-based Bounded Model Checking
Formula EF(((proc0.approach AND proc1.approach) AND proc2.approach)) holds
Using BDD-based Bounded Model Checking
Formula AG((proc0.in IMPLIES K[proc0]((~proc1.in AND ~proc2.in)))) holds
Using BDD-based Bounded Model Checking
Formula AG((proc0.in IMPLIES C[proc0 proc1 proc2]((~proc1.in AND ~proc2.in)))) holds

View File

@@ -0,0 +1,47 @@
# Context entities: out allowed
# Reactions:
. proc = "proc0":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc1":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc2":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
# Context Automaton States:
= Init state: init
* init
* green
* red
* T
# Context Automaton Transitions:
* [init -> green]: { proc0={ out } proc1={ out } proc2={ out } }
* [green -> red]: { proc0={ allowed } } proc0.req
* [green -> red]: { proc1={ allowed } } proc1.req
* [green -> red]: { proc2={ allowed } } proc2.req
* [green -> green]: { proc0={ } } ((~proc0.req AND ~proc1.req) AND ~proc2.req)
* [green -> green]: { proc1={ } } ((~proc0.req AND ~proc1.req) AND ~proc2.req)
* [green -> green]: { proc2={ } } ((~proc0.req AND ~proc1.req) AND ~proc2.req)
* [red -> green]: { proc0={ } } proc0.leave
* [red -> green]: { proc1={ } } proc1.leave
* [red -> green]: { proc2={ } } proc2.leave
* [red -> red]: { proc0={ } } ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave)
* [red -> red]: { proc1={ } } ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave)
* [red -> red]: { proc2={ } } ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave)
* [green -> T]: { } ~((((((true OR proc0.req) OR proc1.req) OR proc2.req) OR ((~proc0.req AND ~proc1.req) AND ~proc2.req)) OR ((~proc0.req AND ~proc1.req) AND ~proc2.req)) OR ((~proc0.req AND ~proc1.req) AND ~proc2.req))
* [red -> T]: { } ~((((((true OR proc0.leave) OR proc1.leave) OR proc2.leave) OR ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave)) OR ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave)) OR ((~proc0.leave AND ~proc1.leave) AND ~proc2.leave))
* [T -> T]: { }

View File

@@ -0,0 +1,23 @@
# Reactions:
. proc = "proc0":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc1":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })
. proc = "proc2":
* (R={ out },I={ },P={ approach })
* (R={ approach },I={ req },P={ req })
* (R={ allowed },I={ },P={ in })
* (R={ in },I={ },P={ out leave })
* (R={ req },I={ in },P={ req })

View File

@@ -0,0 +1,32 @@
{ proc0={ req in } proc1={ req } proc2={ req } }
{ proc0={ req in } proc1={ req } proc2={ approach } }
{ proc0={ req } proc1={ req in } proc2={ approach } }
{ proc0={ req in } proc1={ approach } proc2={ req } }
{ proc0={ req in } proc1={ approach } proc2={ approach } }
{ proc0={ req } proc1={ req in } proc2={ req } }
{ proc0={ approach } proc1={ req in } proc2={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ req in } }
{ proc0={ approach } proc1={ req in } proc2={ req } }
{ proc0={ approach } proc1={ out leave } proc2={ req } }
{ proc0={ req } proc1={ approach } proc2={ out leave } }
{ proc0={ req } proc1={ req } proc2={ req in } }
{ proc0={ approach } proc1={ req } proc2={ req } }
{ proc0={ approach } proc1={ req } proc2={ req in } }
{ proc0={ out leave } proc1={ req } proc2={ req } }
{ proc0={ req } proc1={ approach } proc2={ req in } }
{ proc0={ req } proc1={ out leave } proc2={ approach } }
{ proc0={ approach } proc1={ out leave } proc2={ approach } }
{ proc0={ req } proc1={ req } proc2={ out leave } }
{ proc0={ req } proc1={ out leave } proc2={ req } }
{ proc0={ } proc1={ } proc2={ } }
{ proc0={ req } proc1={ approach } proc2={ req } }
{ proc0={ out leave } proc1={ req } proc2={ approach } }
{ proc0={ approach } proc1={ req } proc2={ out leave } }
{ proc0={ approach } proc1={ approach } proc2={ req } }
{ proc0={ req } proc1={ req } proc2={ approach } }
{ proc0={ out leave } proc1={ approach } proc2={ req } }
{ proc0={ approach } proc1={ req } proc2={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ approach } }
{ proc0={ approach } proc1={ approach } proc2={ out leave } }
{ proc0={ out leave } proc1={ approach } proc2={ approach } }
{ proc0={ req } proc1={ approach } proc2={ approach } }

View File

@@ -0,0 +1,17 @@
# Context entities: e1 e4
# Reactions:
. proc = "m":
* (R={ e1 e4 },I={ e2 },P={ e1 e2 })
* (R={ e2 },I={ e4 },P={ e1 e4 e3 })
* (R={ e1 e3 },I={ e2 },P={ e1 e2 })
* (R={ e3 },I={ e2 },P={ e1 })
# Context Automaton States:
= Init state: s0
* s0
* s1
# Context Automaton Transitions:
* [s0 -> s1]: { m={ e1 e4 } }
* [s1 -> s1]: { m={ } }
* [s1 -> s1]: { m={ e4 } }

View File

@@ -0,0 +1,8 @@
# Reactions:
. proc = "m":
* (R={ e1 e4 },I={ e2 },P={ e1 e2 })
* (R={ e2 },I={ e4 },P={ e1 e4 e3 })
* (R={ e1 e3 },I={ e2 },P={ e1 e2 })
* (R={ e3 },I={ e2 },P={ e1 })

View File

@@ -0,0 +1,3 @@
{ m={ e1 e4 e3 } }
{ m={ e1 e2 } }
{ m={ } }

85
tests/run_all_tests.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Runs all ReactICS regression tests (BDD + SMT + generators)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REACTICS="$ROOT_DIR/reactics-bdd/reactics"
GENERATORS="$ROOT_DIR/examples/bdd/generators"
cd "$ROOT_DIR"
failed=0
# ---------------------------------------------------------------
echo "========== BDD Tests =========="
echo
bash "$SCRIPT_DIR/run_tests.sh" || failed=1
# ---------------------------------------------------------------
echo
echo "========== SMT Tests (pytest) =========="
echo
python3 -m pytest "$SCRIPT_DIR/test_smt.py" -v || failed=1
# ---------------------------------------------------------------
echo
echo "========== Generator Tests =========="
echo
gen_pass=0
gen_fail=0
check_gen() {
local name="$1"
local gen_cmd="$2"
local property="$3"
local expected="$4"
local tmpfile
tmpfile=$(mktemp /tmp/reactics_gen_XXXXXX.drs)
eval "$gen_cmd" > "$tmpfile" 2>&1
local result
result=$("$REACTICS" -c "$property" "$tmpfile" 2>&1 | grep -oE "(holds|does not hold)" || true)
rm -f "$tmpfile"
if [[ "$result" == "$expected" ]]; then
echo " PASS: $name"
((gen_pass++))
else
echo " FAIL: $name (expected '$expected', got '$result')"
((gen_fail++))
failed=1
fi
}
echo "[gen_tgc]"
check_gen "tgc(3) f1" "python3 $GENERATORS/gen_tgc.py 3" f1 "holds"
check_gen "tgc(3) f3" "python3 $GENERATORS/gen_tgc.py 3" f3 "holds"
check_gen "tgc(4) f2" "python3 $GENERATORS/gen_tgc.py 4" f2 "holds"
echo
echo "[gen_asm]"
check_gen "asm(3) f1" "python3 $GENERATORS/gen_asm.py 3" f1 "holds"
check_gen "asm(3) f2" "python3 $GENERATORS/gen_asm.py 3" f2 "holds"
echo
echo "[gen_drs]"
check_gen "drs(2,2,2,a) f0" "python3 $GENERATORS/gen_drs.py 2 2 2 a" f0 "does not hold"
check_gen "drs(2,2,2,b) f0" "python3 $GENERATORS/gen_drs.py 2 2 2 b" f0 "holds"
echo
echo "Generator results: $gen_pass passed, $gen_fail failed"
# ---------------------------------------------------------------
echo
echo "================================"
if [[ $failed -eq 0 ]]; then
echo "All test suites passed."
else
echo "Some tests FAILED."
exit 1
fi

69
tests/run_tests.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Regression test harness for ReactICS BDD module
# Compares current output against saved expected output
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REACTICS="$ROOT_DIR/reactics-bdd/reactics"
EXPECTED_DIR="$SCRIPT_DIR/expected"
PASS=0
FAIL=0
ERRORS=""
run_test() {
local name="$1"
local expected_file="$2"
shift 2
local actual
actual=$("$@" 2>&1) || true
local expected
expected=$(cat "$expected_file")
if [[ "$actual" == "$expected" ]]; then
echo " PASS: $name"
((PASS++))
else
echo " FAIL: $name"
diff <(echo "$expected") <(echo "$actual") | head -20
((FAIL++))
ERRORS="$ERRORS\n - $name"
fi
}
echo "Running ReactICS regression tests..."
echo
# --- tgc.drs tests ---
echo "[tgc]"
run_test "tgc print" "$EXPECTED_DIR/tgc_print.txt" "$REACTICS" -P examples/bdd/tgc.drs
run_test "tgc reactions" "$EXPECTED_DIR/tgc_reactions.txt" "$REACTICS" -r examples/bdd/tgc.drs
run_test "tgc states" "$EXPECTED_DIR/tgc_states.txt" "$REACTICS" -s examples/bdd/tgc.drs
run_test "tgc mc (f1-f4)" "$EXPECTED_DIR/tgc_mc.txt" \
bash -c 'for f in f1 f2 f3 f4; do '"$REACTICS"' -c $f examples/bdd/tgc.drs 2>&1; done'
echo
echo "[trivial]"
run_test "trivial print" "$EXPECTED_DIR/trivial_print.txt" "$REACTICS" -P reactics-bdd/in/trivial.drs
run_test "trivial reactions" "$EXPECTED_DIR/trivial_reactions.txt" "$REACTICS" -r reactics-bdd/in/trivial.drs
run_test "trivial states" "$EXPECTED_DIR/trivial_states.txt" "$REACTICS" -s reactics-bdd/in/trivial.drs
echo
echo "[tgc4]"
run_test "tgc4 print" "$EXPECTED_DIR/tgc4_print.txt" "$REACTICS" -P examples/bdd/tgc4.drs
run_test "tgc4 states" "$EXPECTED_DIR/tgc4_states.txt" "$REACTICS" -s examples/bdd/tgc4.drs
run_test "tgc4 mc (f1-f4)" "$EXPECTED_DIR/tgc4_mc.txt" \
bash -c 'for f in f1 f2 f3 f4; do '"$REACTICS"' -c $f examples/bdd/tgc4.drs 2>&1; done'
echo
echo "================================"
echo "Results: $PASS passed, $FAIL failed"
if [[ $FAIL -gt 0 ]]; then
echo -e "Failures:$ERRORS"
exit 1
else
echo "All tests passed."
fi

501
tests/test_smt.py Normal file
View File

@@ -0,0 +1,501 @@
"""
Regression tests for the ReactICS SMT module.
Tests the Python API directly: reaction system construction,
context automata, and SMT-based verification (reachability + rsLTL).
"""
import sys
import os
import io
from contextlib import redirect_stdout
# Add reactics-smt to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "reactics-smt"))
from z3 import sat, unsat
from rs import (
ReactionSystem,
ContextAutomaton,
ReactionSystemWithConcentrations,
ContextAutomatonWithConcentrations,
ReactionSystemWithConcentrationsParam,
ReactionSystemWithAutomaton,
)
from smt import SmtCheckerRS, SmtCheckerRSC, SmtCheckerRSCParam
from logics import Formula_rsLTL, BagDescription
from rsltl_shortcuts import ltl_F, bag_entity, param_entity, param_And
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def run_silent(fn, *args, **kwargs):
"""Run a function with stdout suppressed, return its result."""
with redirect_stdout(io.StringIO()):
return fn(*args, **kwargs)
# ---------------------------------------------------------------------------
# Data model tests
# ---------------------------------------------------------------------------
class TestReactionSystem:
def test_add_entities(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
assert rs.background_set == ["a", "b"]
assert rs.get_entity_id("a") == 0
assert rs.get_entity_id("b") == 1
assert rs.get_entity_name(0) == "a"
def test_add_reaction(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
rs.add_bg_set_entity("c")
rs.add_reaction(["a"], ["b"], ["c"])
assert len(rs.reactions) == 1
reactants, inhibitors, products = rs.reactions[0]
assert reactants == [0]
assert inhibitors == [1]
assert products == [2]
def test_reactions_by_product(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
rs.add_reaction(["a"], [], ["b"])
rbp = rs.get_reactions_by_product()
assert 1 in rbp # entity id for "b"
assert len(rbp[1]) == 1
def test_duplicate_entity_raises(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
try:
rs.add_bg_set_entity("a")
assert False, "Should have raised"
except RuntimeError:
pass
class TestReactionSystemWithConcentrations:
def test_add_entity_with_concentration(self):
rs = ReactionSystemWithConcentrations()
rs.add_bg_set_entity(("x", 3))
rs.add_bg_set_entity(("y", 5))
assert rs.background_set == ["x", "y"]
assert rs.get_max_concentration_level(0) == 3
assert rs.get_max_concentration_level(1) == 5
def test_add_reaction_with_concentrations(self):
rs = ReactionSystemWithConcentrations()
rs.add_bg_set_entity(("a", 2))
rs.add_bg_set_entity(("b", 1))
rs.add_reaction([("a", 1)], [], [("b", 1)])
assert len(rs.reactions) == 1
def test_meta_reactions(self):
rs = ReactionSystemWithConcentrations()
rs.add_bg_set_entity(("e", 3))
rs.add_bg_set_entity(("inc", 1))
rs.add_reaction_inc("e", "inc", [("e", 1)], [("e", 3)])
e_id = rs.get_entity_id("e")
assert e_id in rs.meta_reactions
assert rs.meta_reactions[e_id][0][0] == "inc"
class TestContextAutomaton:
def test_states_and_transitions(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
ca = ContextAutomaton(rs)
ca.add_init_state("s0")
ca.add_state("s1")
assert ca.get_init_state_id() == 0
assert ca.get_state_id("s1") == 1
ca.add_transition("s0", ["a"], "s1")
ca.add_transition("s1", [], "s1")
assert len(ca.transitions) == 2
def test_invalid_transition_raises(self):
rs = ReactionSystem()
rs.add_bg_set_entity("a")
ca = ContextAutomaton(rs)
ca.add_init_state("s0")
try:
ca.add_transition("s0", ["a"], "nonexistent")
assert False, "Should have raised"
except RuntimeError:
pass
class TestContextAutomatonWithConcentrations:
def test_transitions_with_concentrations(self):
rs = ReactionSystemWithConcentrations()
rs.add_bg_set_entity(("a", 2))
rs.add_bg_set_entity(("b", 1))
ca = ContextAutomatonWithConcentrations(rs)
ca.add_init_state("s0")
ca.add_state("s1")
ca.add_transition("s0", [("a", 2)], "s1")
ca.add_transition("s1", [], "s1")
assert len(ca.transitions) == 2
# ---------------------------------------------------------------------------
# Formula construction tests
# ---------------------------------------------------------------------------
class TestFormulas:
def test_bag_description(self):
b = BagDescription.f_entity("x")
assert str(b) == "x"
b_gt = b > 0
assert ">" in str(b_gt)
def test_rsltl_formula(self):
f = Formula_rsLTL.f_F(
BagDescription.f_TRUE(),
BagDescription.f_entity("x") > 0,
)
assert "F" in str(f)
def test_combined_formula(self):
f = Formula_rsLTL.f_G(
BagDescription.f_TRUE(),
Formula_rsLTL.f_Implies(
BagDescription.f_entity("a") == 1,
Formula_rsLTL.f_F(
BagDescription.f_TRUE(),
BagDescription.f_entity("b") > 0,
),
),
)
assert "G" in str(f)
assert "F" in str(f)
# ---------------------------------------------------------------------------
# Verification tests: SmtCheckerRS (basic, no concentrations)
# ---------------------------------------------------------------------------
def _build_simple_rs():
"""A minimal RS for testing: a -> b -> c with context automaton."""
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
rs.add_bg_set_entity("c")
rs.add_reaction(["a"], [], ["b"])
rs.add_reaction(["b"], [], ["c"])
ca = ContextAutomaton(rs)
ca.add_init_state("s0")
ca.add_state("s1")
ca.add_transition("s0", ["a"], "s1")
ca.add_transition("s1", [], "s1")
return ReactionSystemWithAutomaton(rs, ca)
class TestSmtCheckerRS:
def test_reachability_sat(self):
rsa = _build_simple_rs()
checker = SmtCheckerRS(rsa)
run_silent(
checker.check_reachability,
["c"],
print_witness=False,
print_time=False,
print_mem=False,
)
# After finding SAT, solver state should be satisfiable
assert checker.solver.check() == sat
def test_reachability_unreachable(self):
"""Entity 'a' is never produced, so it shouldn't be reachable as a final state."""
rs = ReactionSystem()
rs.add_bg_set_entity("a")
rs.add_bg_set_entity("b")
rs.add_reaction(["a"], [], ["b"])
ca = ContextAutomaton(rs)
ca.add_init_state("s0")
ca.add_state("s1")
# Context provides 'a' initially, then nothing
ca.add_transition("s0", ["a"], "s1")
ca.add_transition("s1", [], "s1")
rsa = ReactionSystemWithAutomaton(rs, ca)
checker = SmtCheckerRS(rsa)
# 'b' is reachable (produced from 'a')
run_silent(
checker.check_reachability,
["b"],
print_witness=False,
print_time=False,
print_mem=False,
)
assert checker.solver.check() == sat
# ---------------------------------------------------------------------------
# Verification tests: SmtCheckerRSC (with concentrations)
# ---------------------------------------------------------------------------
def _build_chain_rsc(chain_len=2, max_conc=2):
"""Build a chain reaction system with concentrations."""
r = ReactionSystemWithConcentrations()
r.add_bg_set_entity(("inc", 1))
r.add_bg_set_entity(("dec", 1))
for i in range(1, chain_len + 1):
r.add_bg_set_entity(("e_" + str(i), max_conc))
for i in range(1, chain_len + 1):
ent = "e_" + str(i)
r.add_reaction_inc(ent, "inc", [(ent, 1)], [(ent, max_conc)])
r.add_reaction_dec(ent, "dec", [(ent, 1)], [])
if i < chain_len:
r.add_reaction([(ent, max_conc)], [], [("e_" + str(i + 1), 1)])
r.add_reaction(
[("e_" + str(chain_len), max_conc)],
[("dec", 1)],
[("e_" + str(chain_len), max_conc)],
)
c = ContextAutomatonWithConcentrations(r)
c.add_init_state("init")
c.add_state("working")
c.add_transition("init", [("e_1", 1), ("inc", 1)], "working")
c.add_transition("working", [("inc", 1)], "working")
return ReactionSystemWithAutomaton(r, c)
class TestSmtCheckerRSC:
def test_chain_reachability_sat(self):
"""Chain(2,2): e_2=2 should be reachable at level 3."""
rc = _build_chain_rsc(2, 2)
checker = SmtCheckerRSC(rc)
prop = ([("e_2", 2)], [])
run_silent(
checker.check_reachability,
prop,
print_witness=False,
print_time=False,
print_mem=False,
max_level=10,
)
assert checker.solver.check() == sat
assert checker.current_level == 3
def test_chain_reachability_longer(self):
"""Chain(3,2): e_3=2 should be reachable at level 5."""
rc = _build_chain_rsc(3, 2)
checker = SmtCheckerRSC(rc)
prop = ([("e_3", 2)], [])
run_silent(
checker.check_reachability,
prop,
print_witness=False,
print_time=False,
print_mem=False,
max_level=10,
)
assert checker.solver.check() == sat
assert checker.current_level == 5
def test_rsltl_finally(self):
"""Check rsLTL F formula on chain(2,2)."""
rc = _build_chain_rsc(2, 2)
checker = SmtCheckerRSC(rc)
form = Formula_rsLTL.f_F(
BagDescription.f_entity("inc") > 0,
BagDescription.f_entity("e_2") >= 2,
)
run_silent(
checker.check_rsltl,
formula=form,
print_witness=False,
print_time=False,
print_mem=False,
)
assert checker.solver.check() == sat
assert checker.current_level == 3
def test_rsltl_finally_fast(self):
"""Chain(2,2), F(e_1==2): should be SAT at level 1."""
rc = _build_chain_rsc(2, 2)
checker = SmtCheckerRSC(rc)
form = Formula_rsLTL.f_F(
BagDescription.f_entity("inc") > 0,
BagDescription.f_entity("e_1") == 2,
)
run_silent(
checker.check_rsltl,
formula=form,
print_witness=False,
print_time=False,
print_mem=False,
)
assert checker.solver.check() == sat
assert checker.current_level == 1
def test_rsltl_next_until(self):
"""Chain(2,2), X[TRUE](e_1 > 0 U[inc > 0] e_2 > 0): SAT at level 2."""
rc = _build_chain_rsc(2, 2)
checker = SmtCheckerRSC(rc)
form = Formula_rsLTL.f_X(
BagDescription.f_TRUE(),
Formula_rsLTL.f_U(
BagDescription.f_entity("inc") > 0,
BagDescription.f_entity("e_1") > 0,
BagDescription.f_entity("e_2") > 0,
),
)
run_silent(
checker.check_rsltl,
formula=form,
print_witness=False,
print_time=False,
print_mem=False,
)
assert checker.solver.check() == sat
assert checker.current_level == 2
# ---------------------------------------------------------------------------
# Verification tests: SmtCheckerRSC on heat shock response
# ---------------------------------------------------------------------------
def _build_heat_shock():
"""Heat shock response model."""
stress_temp = 42
max_temp = 50
r = ReactionSystemWithConcentrations()
for e in [("hsp", 1), ("hsf", 1), ("hsf2", 1), ("hsf3", 1), ("hse", 1),
("mfp", 1), ("prot", 1), ("hsf3:hse", 1), ("hsp:mfp", 1),
("hsp:hsf", 1), ("temp", max_temp), ("heat", 1), ("cool", 1)]:
r.add_bg_set_entity(e)
r.add_reaction([("hsf", 1)], [("hsp", 1)], [("hsf3", 1)])
r.add_reaction([("hsf", 1), ("hsp", 1), ("mfp", 1)], [], [("hsf3", 1)])
r.add_reaction([("hsf3", 1)], [("hse", 1), ("hsp", 1)], [("hsf", 1)])
r.add_reaction([("hsf3", 1), ("hsp", 1), ("mfp", 1)], [("hse", 1)], [("hsf", 1)])
r.add_reaction([("hsf3", 1), ("hse", 1)], [("hsp", 1)], [("hsf3:hse", 1)])
r.add_reaction([("hsp", 1), ("hsf3", 1), ("mfp", 1), ("hse", 1)], [], [("hsf3:hse", 1)])
r.add_reaction([("hse", 1)], [("hsf3", 1)], [("hse", 1)])
r.add_reaction([("hsp", 1), ("hsf3", 1), ("hse", 1)], [("mfp", 1)], [("hse", 1)])
r.add_reaction([("hsf3:hse", 1)], [("hsp", 1)], [("hsp", 1), ("hsf3:hse", 1)])
r.add_reaction([("hsp", 1), ("mfp", 1), ("hsf3:hse", 1)], [], [("hsp", 1), ("hsf3:hse", 1)])
r.add_reaction([("hsf", 1), ("hsp", 1)], [("mfp", 1)], [("hsp:hsf", 1)])
r.add_reaction([("hsp:hsf", 1), ("temp", stress_temp)], [], [("hsf", 1), ("hsp", 1)])
r.add_reaction([("hsp:hsf", 1)], [("temp", stress_temp)], [("hsp:hsf", 1)])
r.add_reaction([("hsp", 1), ("hsf3:hse", 1)], [("mfp", 1)], [("hse", 1), ("hsp:hsf", 1)])
r.add_reaction([("temp", stress_temp), ("prot", 1)], [], [("mfp", 1), ("prot", 1)])
r.add_reaction([("prot", 1)], [("temp", stress_temp)], [("prot", 1)])
r.add_reaction([("hsp", 1), ("mfp", 1)], [], [("hsp:mfp", 1)])
r.add_reaction([("mfp", 1)], [("hsp", 1)], [("mfp", 1)])
r.add_reaction([("hsp:mfp", 1)], [], [("hsp", 1), ("prot", 1)])
r.add_reaction_inc("temp", "heat", [("temp", 1)], [("temp", max_temp)])
r.add_reaction_dec("temp", "cool", [("temp", 1)], [])
r.add_permanency("temp", [("heat", 1), ("cool", 1)])
c = ContextAutomatonWithConcentrations(r)
c.add_init_state("0")
c.add_state("1")
c.add_transition("0", [("hsf", 1), ("prot", 1), ("hse", 1), ("temp", 35)], "1")
c.add_transition("1", [("cool", 1)], "1")
c.add_transition("1", [("heat", 1)], "1")
c.add_transition("1", [], "1")
return ReactionSystemWithAutomaton(r, c)
class TestHeatShockResponse:
def test_mfp_reachable(self):
"""mfp should be reachable at level 9."""
rc = _build_heat_shock()
checker = SmtCheckerRSC(rc)
prop = ([("mfp", 1)], [])
run_silent(
checker.check_reachability,
prop,
print_witness=False,
print_time=False,
print_mem=False,
max_level=40,
)
assert checker.solver.check() == sat
assert checker.current_level == 9
# ---------------------------------------------------------------------------
# Verification tests: SmtCheckerRSCParam (parametric)
# ---------------------------------------------------------------------------
def _build_parametric_overview():
"""Parametric example from doc/overview.py."""
prs = ReactionSystemWithConcentrationsParam()
for ec in [("a", 3), ("b", 2), ("c", 1), ("h", 1)]:
prs.add_bg_set_entity(ec)
lda = prs.get_param("lda")
prs.add_reaction([("a", 1)], [("h", 1)], [("b", 2)])
prs.add_reaction(lda, [("h", 1)], [("c", 1)])
ca = ContextAutomatonWithConcentrations(prs)
ca.add_init_state("0")
ca.add_state("1")
ca.add_transition("0", [("a", 3)], "1")
ca.add_transition("1", [], "1")
ca.add_transition("1", [("h", 1)], "1")
return ReactionSystemWithAutomaton(prs, ca), lda
class TestSmtCheckerRSCParam:
def test_parametric_sat(self):
"""Parametric overview: F[h==0](c>0) should be SAT at level 2."""
rc, lda = _build_parametric_overview()
pc = param_entity(lda, "a") == 0
f = ltl_F(bag_entity("h") == 0, "c")
checker = SmtCheckerRSCParam(rc, optimise=True)
run_silent(
checker.check_rsltl,
formulae_list=[f],
param_constr=pc,
)
# The parametric checker uses path-based solving;
# after SAT the solver should be satisfiable
assert checker.solver.check() == sat
# ---------------------------------------------------------------------------
# RS translation tests (RSC -> RS)
# ---------------------------------------------------------------------------
class TestRSCtoRSTranslation:
def test_translated_system_reachability(self):
"""Chain(2,2) translated to plain RS: e_2#2 should be reachable."""
rc = _build_chain_rsc(2, 2)
orc = rc.get_ordinary_reaction_system_with_automaton()
checker = SmtCheckerRS(orc)
run_silent(
checker.check_reachability,
["e_2#2"],
print_witness=False,
print_time=False,
print_mem=False,
)
assert checker.solver.check() == sat