mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(runner): give the finalize trace record its own step index
This commit is contained in:
1 parent
25fc351360
commit
670892bdb7
2 files changed
+106
-6
No files matched your search
@@ -284,14 +284,17 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finalize each evaluator once the loop ends so liveness obligations that
|
// Finalize each evaluator once the loop ends so liveness obligations that
|
||||||
// never discharged (an unbounded eventually that never fired, a strong
|
// never discharged (an eventually that never fired) are reported as
|
||||||
// next with no successor) are reported as violations rather than silently
|
// violations rather than silently left pending. Properties already
|
||||||
// left pending. Properties already violated mid-run are not re-reported.
|
// violated mid-run are not re-reported. The synthetic record gets its own
|
||||||
|
// step index so no two trace lines share one; witnesses still attribute
|
||||||
|
// the violation to the step that spawned the obligation.
|
||||||
if ended := options.Verifier.Finalize(); len(ended) > 0 {
|
if ended := options.Verifier.Finalize(); len(ended) > 0 {
|
||||||
witnesses := collectWitnesses(options.Verifier, ended, logger, stepIndex)
|
finalIndex := stepIndex + 1
|
||||||
summary.Violations = append(summary.Violations, violationRecords(ended, witnesses, stepIndex)...)
|
witnesses := collectWitnesses(options.Verifier, ended, logger, finalIndex)
|
||||||
|
summary.Violations = append(summary.Violations, violationRecords(ended, witnesses, finalIndex)...)
|
||||||
finalStep := trace.Step{
|
finalStep := trace.Step{
|
||||||
Index: stepIndex,
|
Index: finalIndex,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Violations: ended,
|
Violations: ended,
|
||||||
Witnesses: witnesses,
|
Witnesses: witnesses,
|
||||||
|
|||||||
@@ -336,6 +336,103 @@ globalThis.actions = actions(() => []);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunner_AlwaysNextLeavesNoEndOfRunViolation(t *testing.T) {
|
||||||
|
// always(next(p)) ends every run with a pending deferred check. That
|
||||||
|
// residue is vacuous (no successor state to check), so neither the
|
||||||
|
// summary nor the trace may report an end-of-run violation.
|
||||||
|
const spec = `
|
||||||
|
import { actions, always, next } from "@sanderling/spec";
|
||||||
|
globalThis.properties = {
|
||||||
|
nextHolds: always(next(() => true)),
|
||||||
|
};
|
||||||
|
globalThis.actions = actions(() => []);
|
||||||
|
`
|
||||||
|
state := newHarnessWithSpec(t, spec)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
summary, err := Run(ctx, Options{
|
||||||
|
Duration: time.Hour,
|
||||||
|
IdleTimeout: 20 * time.Millisecond,
|
||||||
|
MaxSteps: 3,
|
||||||
|
Driver: state.mock,
|
||||||
|
Verifier: state.verifier,
|
||||||
|
TraceWriter: state.writer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if len(summary.Violations) != 0 {
|
||||||
|
t.Errorf("expected no violations, got %v", summary.Violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunner_FinalizeRecordUsesDistinctStepIndex(t *testing.T) {
|
||||||
|
// An eventually that never fires is reported at run end through a
|
||||||
|
// synthetic trace record. That record must carry its own step index so no
|
||||||
|
// two trace lines share one (duplicate indices made the replay UI select
|
||||||
|
// two rows at once).
|
||||||
|
const spec = `
|
||||||
|
import { actions, eventually } from "@sanderling/spec";
|
||||||
|
globalThis.properties = {
|
||||||
|
neverFires: eventually(() => false),
|
||||||
|
};
|
||||||
|
globalThis.actions = actions(() => []);
|
||||||
|
`
|
||||||
|
state := newHarnessWithSpec(t, spec)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
summary, err := Run(ctx, Options{
|
||||||
|
Duration: time.Hour,
|
||||||
|
IdleTimeout: 20 * time.Millisecond,
|
||||||
|
MaxSteps: 3,
|
||||||
|
Driver: state.mock,
|
||||||
|
Verifier: state.verifier,
|
||||||
|
TraceWriter: state.writer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if !containsProperty(summary.Violations, "neverFires") {
|
||||||
|
t.Fatalf("expected neverFires in violations: %v", summary.Violations)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
type traceLine struct {
|
||||||
|
Step int `json:"step"`
|
||||||
|
Violations []string `json:"violations"`
|
||||||
|
}
|
||||||
|
seen := map[int]bool{}
|
||||||
|
finalizeStep := 0
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
||||||
|
for scanner.Scan() {
|
||||||
|
var line traceLine
|
||||||
|
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
|
||||||
|
t.Fatalf("trace line decode: %v", err)
|
||||||
|
}
|
||||||
|
if seen[line.Step] {
|
||||||
|
t.Errorf("duplicate step index %d in trace", line.Step)
|
||||||
|
}
|
||||||
|
seen[line.Step] = true
|
||||||
|
if slices.Contains(line.Violations, "neverFires") {
|
||||||
|
finalizeStep = line.Step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
t.Fatalf("scan trace: %v", err)
|
||||||
|
}
|
||||||
|
if finalizeStep != summary.Steps+1 {
|
||||||
|
t.Errorf("finalize record step = %d, want %d (steps+1)", finalizeStep, summary.Steps+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunner_ThrowingPredicateIsLoggedNotPanic(t *testing.T) {
|
func TestRunner_ThrowingPredicateIsLoggedNotPanic(t *testing.T) {
|
||||||
const throwingSpec = `
|
const throwingSpec = `
|
||||||
import { actions, always, Tap } from "@sanderling/spec";
|
import { actions, always, Tap } from "@sanderling/spec";
|
||||||
|
|||||||
Reference in new issue
Block a user