mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
* feat(ltl): add Now/Next/Eventually/Implies/Or/And/Not formulas Replace the fold-with-latch evaluator with a residual-formula reducer. Each Observe() instantiates a fresh obligation from the root (stripping an outer Always), reduces each pending obligation against current state, latches Violated on first failure, and surfaces Pending verdicts for deferred obligations. Existing Always/Pure/Thunk tests continue to pass. * feat(ltl): support relative duration for eventually().within() * feat(proto): add Swipe, PressKey, RecentLogs RPCs * feat(verifier,runner): formula handles, new action kinds, rich state - verifier: add formula-spec registry; bindNow/bindNext/bindEventually with chainable .implies/.or/.and/.not and .within(n,unit) on eventually; bindFrom for uniform sampling. bindAlways keeps accepting plain predicates. - verifier: store lastTree, lastAction, step time, logs, exceptions on the Verifier; SnapshotInput replaces the (snapshots, tree) pair. stateObject now produces state.lastAction/time/logs/exceptions matching the TS State type. - verifier: make taps/swipes/waitOnce/pressKey built-in generators actually fire; taps picks a clickable, enabled element from the last hierarchy. - agent: add exceptions field to Message wire format. - driver: add Swipe/PressKey/RecentLogs to Driver interface; wire maestro client and mock driver. LogEntry exposed for runner consumption. - runner: apply Swipe/PressKey/Wait actions; collect logcat and exceptions; pass lastAction and step time into PushSnapshot. * feat(spec-api): LTL operators, new actions, richer State - ltl.ts exports now/next/eventually; always overload accepts a Formula - types.ts: Formula gains implies/or/and/not; EventuallyFormula adds .within; State gains lastAction/time/logs/exceptions; Swipe/PressKey/Wait action types - actions.ts: Swipe/PressKey/Wait/from constructors; waitOnce + pressKey default generators - tests exercise the chaining, sampling, and new actions through a recorded fake runtime * feat(sidecar): add swipe, pressKey, recentLogs RPC handlers * feat(sdk-android): capture uncaught exceptions Install a default uncaught handler on Uatu.start, chained with any existing handler so Android's crash reporter still runs. Expose Uatu.reportError for callers to forward caught throwables. A bounded circular buffer (default 50) drains into each STATE message's new exceptions field. Protocol.kt serializes/deserializes the field, matching the Go wire format added to internal/agent/protocol.go. * feat(spec-api): add @uatu/spec/defaults/properties bundle * feat(sample-app): exercise new LTL operators + defaults spec.ts now imports eventually/next/now/from from @uatu/spec and noUncaughtExceptions from @uatu/spec/defaults/properties. It declares three properties that exercise the new surface: - accountCountNonNegative: plain always() safety - addAccountAdvances: always(now(x).implies(next(y))) - eventuallyLoggedIn: eventually(p).within(30, "seconds") - noUncaughtExceptions: imported default The weighted actions root uses from() for random phone/name sampling and entries for taps/swipes/waitOnce/pressKey built-ins. SampleApplication gains a debug hook gated on the system property uatu.inject_error so the e2e run can synthesize an Uatu.reportError and verify noUncaughtExceptions violates. cmd/uatu/test_run.go adds a subpath alias so specs importing "@uatu/spec/defaults/properties" resolve against the in-tree source when running from the uatu checkout. The spec-integration tests swap the old click-counter fixtures for the new login hierarchy. * feat(trace): record swipe/key/wait details + exceptions trace.Step gains an Exceptions array so the trace captures the class/message/stackTrace for each SDK-reported throwable in a step. trace.Action gains FromX/FromY/ToX/ToY/Key/DurationMillis so the full payload of Swipe/PressKey/Wait actions is visible in trace.jsonl. sample-app's debug error hook now gates on ApplicationInfo.DEBUGGABLE instead of a system property (adb setprop fails on non-rooted emulators).
229 lines
5.6 KiB
Go
229 lines
5.6 KiB
Go
package ltl
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type Verdict int
|
|
|
|
const (
|
|
VerdictHolds Verdict = iota
|
|
VerdictViolated
|
|
VerdictPending
|
|
)
|
|
|
|
func (v Verdict) String() string {
|
|
switch v {
|
|
case VerdictHolds:
|
|
return "holds"
|
|
case VerdictViolated:
|
|
return "violated"
|
|
case VerdictPending:
|
|
return "pending"
|
|
default:
|
|
return fmt.Sprintf("verdict(%d)", int(v))
|
|
}
|
|
}
|
|
|
|
// Evaluator reduces a formula across observed steps using residual-formula
|
|
// semantics. Each step either resolves pending obligations (to holds or
|
|
// violated) or carries them forward as residuals. Once a single obligation
|
|
// violates, the overall verdict latches to Violated.
|
|
type Evaluator struct {
|
|
root Formula
|
|
pending []Formula
|
|
violated bool
|
|
}
|
|
|
|
func NewEvaluator(formula Formula) *Evaluator {
|
|
return &Evaluator{root: formula}
|
|
}
|
|
|
|
// Observe evaluates the formula against the current state and returns the
|
|
// running verdict. Uses the real wall clock for deadline-bound operators;
|
|
// callers that need reproducible time should use ObserveAt.
|
|
func (e *Evaluator) Observe() Verdict {
|
|
return e.ObserveAt(time.Now())
|
|
}
|
|
|
|
// ObserveAt is like Observe but takes the current step time explicitly.
|
|
func (e *Evaluator) ObserveAt(now time.Time) Verdict {
|
|
if e.violated {
|
|
return VerdictViolated
|
|
}
|
|
|
|
fresh := rootObligation(e.root)
|
|
obligations := append(e.pending, fresh)
|
|
e.pending = e.pending[:0]
|
|
|
|
for _, obligation := range obligations {
|
|
result := reduce(obligation, now)
|
|
switch result.status {
|
|
case statusHolds:
|
|
// drop
|
|
case statusViolated:
|
|
e.violated = true
|
|
e.pending = nil
|
|
return VerdictViolated
|
|
case statusPending:
|
|
e.pending = append(e.pending, result.formula)
|
|
}
|
|
}
|
|
|
|
if len(e.pending) > 0 {
|
|
return VerdictPending
|
|
}
|
|
return VerdictHolds
|
|
}
|
|
|
|
// rootObligation returns the formula to instantiate at each step. An outer
|
|
// Always is stripped so its inner is re-evaluated every step; any other root
|
|
// formula is itself re-instantiated each step (matching the v0.1 semantics
|
|
// where a bare Thunk is re-observed on every call).
|
|
func rootObligation(root Formula) Formula {
|
|
if always, ok := root.(AlwaysFormula); ok {
|
|
return always.Inner
|
|
}
|
|
return root
|
|
}
|
|
|
|
type residualStatus int
|
|
|
|
const (
|
|
statusHolds residualStatus = iota
|
|
statusViolated
|
|
statusPending
|
|
)
|
|
|
|
type reduceResult struct {
|
|
status residualStatus
|
|
formula Formula
|
|
}
|
|
|
|
func holds() reduceResult { return reduceResult{status: statusHolds} }
|
|
func violated() reduceResult { return reduceResult{status: statusViolated} }
|
|
func pending(f Formula) reduceResult {
|
|
return reduceResult{status: statusPending, formula: f}
|
|
}
|
|
|
|
func reduce(formula Formula, now time.Time) reduceResult {
|
|
switch concrete := formula.(type) {
|
|
case PureFormula:
|
|
if concrete.Value {
|
|
return holds()
|
|
}
|
|
return violated()
|
|
|
|
case ThunkFormula:
|
|
if concrete.Func() {
|
|
return holds()
|
|
}
|
|
return violated()
|
|
|
|
case NowFormula:
|
|
return reduce(concrete.Inner, now)
|
|
|
|
case NextFormula:
|
|
// Next defers the inner obligation to the following step without
|
|
// evaluating it now.
|
|
return pending(concrete.Inner)
|
|
|
|
case EventuallyFormula:
|
|
// First-reduction deadline resolution: if the formula was built with
|
|
// a relative duration, fix the absolute deadline to (now + duration)
|
|
// so subsequent reductions compare against a stable value.
|
|
if !concrete.HasDeadline && concrete.Duration > 0 {
|
|
concrete.Deadline = now.Add(concrete.Duration)
|
|
concrete.HasDeadline = true
|
|
}
|
|
innerResult := reduce(concrete.Inner, now)
|
|
if innerResult.status == statusHolds {
|
|
return holds()
|
|
}
|
|
if concrete.HasStepBound && concrete.StepBound <= 1 {
|
|
return violated()
|
|
}
|
|
if concrete.HasDeadline && !now.Before(concrete.Deadline) {
|
|
return violated()
|
|
}
|
|
next := concrete
|
|
if concrete.HasStepBound {
|
|
next.StepBound = concrete.StepBound - 1
|
|
}
|
|
return pending(next)
|
|
|
|
case ImpliesFormula:
|
|
antecedent := reduce(concrete.Antecedent, now)
|
|
switch antecedent.status {
|
|
case statusHolds:
|
|
return reduce(concrete.Consequent, now)
|
|
case statusViolated:
|
|
return holds()
|
|
case statusPending:
|
|
return pending(ImpliesFormula{
|
|
Antecedent: antecedent.formula,
|
|
Consequent: concrete.Consequent,
|
|
})
|
|
}
|
|
|
|
case OrFormula:
|
|
left := reduce(concrete.Left, now)
|
|
right := reduce(concrete.Right, now)
|
|
if left.status == statusHolds || right.status == statusHolds {
|
|
return holds()
|
|
}
|
|
if left.status == statusViolated && right.status == statusViolated {
|
|
return violated()
|
|
}
|
|
if left.status == statusViolated {
|
|
return pending(right.formula)
|
|
}
|
|
if right.status == statusViolated {
|
|
return pending(left.formula)
|
|
}
|
|
return pending(OrFormula{Left: left.formula, Right: right.formula})
|
|
|
|
case AndFormula:
|
|
left := reduce(concrete.Left, now)
|
|
right := reduce(concrete.Right, now)
|
|
if left.status == statusViolated || right.status == statusViolated {
|
|
return violated()
|
|
}
|
|
if left.status == statusHolds && right.status == statusHolds {
|
|
return holds()
|
|
}
|
|
if left.status == statusHolds {
|
|
return pending(right.formula)
|
|
}
|
|
if right.status == statusHolds {
|
|
return pending(left.formula)
|
|
}
|
|
return pending(AndFormula{Left: left.formula, Right: right.formula})
|
|
|
|
case NotFormula:
|
|
inner := reduce(concrete.Inner, now)
|
|
switch inner.status {
|
|
case statusHolds:
|
|
return violated()
|
|
case statusViolated:
|
|
return holds()
|
|
case statusPending:
|
|
return pending(NotFormula{Inner: inner.formula})
|
|
}
|
|
|
|
case AlwaysFormula:
|
|
innerResult := reduce(concrete.Inner, now)
|
|
if innerResult.status == statusViolated {
|
|
return violated()
|
|
}
|
|
next := AlwaysFormula{Inner: concrete.Inner}
|
|
if innerResult.status == statusHolds {
|
|
return pending(next)
|
|
}
|
|
return pending(AndFormula{Left: innerResult.formula, Right: next})
|
|
}
|
|
|
|
panic(fmt.Sprintf("ltl: unsupported formula type %T", formula))
|
|
}
|