mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 12:07:09 +00:00
test(ltl): property-based NNF laws
Lock double-negation identity, Always/Eventually duality with bound preservation, leaf pushdown, and not(always true) reaching Violated.
This commit is contained in:
1 parent
c965089825
commit
59113cbf9d
1 file changed
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package ltl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// leaf builds a small set of representative atomic formulas indexed by a seed.
|
||||||
|
func leafFor(seed uint8) Formula {
|
||||||
|
switch seed % 3 {
|
||||||
|
case 0:
|
||||||
|
return Pure(true)
|
||||||
|
case 1:
|
||||||
|
return Pure(false)
|
||||||
|
default:
|
||||||
|
return ThunkNamed("p", func() bool { return true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_DoubleNegationIsIdentity(t *testing.T) {
|
||||||
|
law := func(seed uint8) bool {
|
||||||
|
leaf := leafFor(seed)
|
||||||
|
doubled := nnf(Not(Not(leaf)))
|
||||||
|
direct := nnf(leaf)
|
||||||
|
return doubled.describe() == direct.describe()
|
||||||
|
}
|
||||||
|
if err := quick.Check(law, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_NotAlwaysIsEventuallyNot(t *testing.T) {
|
||||||
|
law := func(seed uint8) bool {
|
||||||
|
leaf := leafFor(seed)
|
||||||
|
negated := nnf(Not(Always(leaf)))
|
||||||
|
expected := nnf(Eventually(Not(leaf)))
|
||||||
|
return negated.describe() == expected.describe()
|
||||||
|
}
|
||||||
|
if err := quick.Check(law, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_NotEventuallyIsAlwaysNot(t *testing.T) {
|
||||||
|
law := func(seed uint8) bool {
|
||||||
|
leaf := leafFor(seed)
|
||||||
|
negated := nnf(Not(Eventually(leaf)))
|
||||||
|
expected := nnf(Always(Not(leaf)))
|
||||||
|
return negated.describe() == expected.describe()
|
||||||
|
}
|
||||||
|
if err := quick.Check(law, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_BoundedEventuallyDualKeepsBound(t *testing.T) {
|
||||||
|
negated := nnf(Not(EventuallyWithinSteps(Pure(true), 4)))
|
||||||
|
always, ok := negated.(AlwaysFormula)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected AlwaysFormula, got %T", negated)
|
||||||
|
}
|
||||||
|
if !always.HasStepBound || always.StepBound != 4 {
|
||||||
|
t.Errorf("bound not preserved: %+v", always)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_PushesNotToThunkLeaf(t *testing.T) {
|
||||||
|
formula := nnf(Always(Not(Always(ThunkNamed("p", func() bool { return true })))))
|
||||||
|
always, ok := formula.(AlwaysFormula)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected AlwaysFormula, got %T", formula)
|
||||||
|
}
|
||||||
|
eventually, ok := always.Inner.(EventuallyFormula)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected inner EventuallyFormula, got %T", always.Inner)
|
||||||
|
}
|
||||||
|
not, ok := eventually.Inner.(NotFormula)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected NotFormula leaf, got %T", eventually.Inner)
|
||||||
|
}
|
||||||
|
if _, ok := not.Inner.(ThunkFormula); !ok {
|
||||||
|
t.Errorf("expected Not to wrap a Thunk, got %T", not.Inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNF_NotAlwaysTrueViaEvaluatorReportsViolated(t *testing.T) {
|
||||||
|
evaluator := NewEvaluator(Always(Not(Always(ThunkNamed("p", func() bool { return true })))))
|
||||||
|
for index := range 4 {
|
||||||
|
if got := evaluator.ObserveAt(time.Unix(int64(index), 0)); got == VerdictViolated {
|
||||||
|
t.Fatalf("step %d latched violated prematurely", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := evaluator.Finalize(); got != VerdictViolated {
|
||||||
|
t.Errorf("Finalize = %v, want violated", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user