mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(oracle-reduction): replay stored traces under four reduced oracles
re-evaluates each trace offline under the full engine, a crash-only detector, a single-state check and a single-step property triple, and reports what each refutes: the oracles vary while the traces stay fixed, which separates a defect an oracle cannot express from one an explorer never reached. a disagreement with the verdicts a run recorded exits nonzero rather than being counted as a finding.
This commit is contained in:
1 parent
a0a8c9c710
commit
a45ba76d8e
7 files changed
+2182
No files matched your search
@@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/trace"
|
||||
"github.com/priyanshujain/sanderling/internal/verifier"
|
||||
)
|
||||
|
||||
const maxStepBytes = 64 * 1024 * 1024
|
||||
|
||||
// loadedRun is one run directory: its meta, the steps in file order, and the
|
||||
// synthetic end-of-run record the runner writes when Finalize convicted a
|
||||
// liveness obligation.
|
||||
type loadedRun struct {
|
||||
Directory string
|
||||
Meta trace.Meta
|
||||
Steps []trace.Step
|
||||
Finalize *trace.Step
|
||||
}
|
||||
|
||||
// loadRun reads a run directory and refuses anything E3 cannot replay. A step
|
||||
// written before the format change carries no element depths, so its hierarchy
|
||||
// decodes with a nil root and every selector resolves to nothing: the refusal
|
||||
// has to name the version rather than let the replay report an empty screen.
|
||||
func loadRun(directory string) (loadedRun, error) {
|
||||
metaBody, err := os.ReadFile(filepath.Join(directory, "meta.json"))
|
||||
if err != nil {
|
||||
return loadedRun{}, fmt.Errorf("read meta: %w", err)
|
||||
}
|
||||
var meta trace.Meta
|
||||
if err := json.Unmarshal(metaBody, &meta); err != nil {
|
||||
return loadedRun{}, fmt.Errorf("decode meta: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath.Join(directory, "trace.jsonl"))
|
||||
if err != nil {
|
||||
return loadedRun{}, fmt.Errorf("open trace: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
run := loadedRun{Directory: directory, Meta: meta}
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 0, 1024*1024), maxStepBytes)
|
||||
line := 0
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
if len(scanner.Bytes()) == 0 {
|
||||
continue
|
||||
}
|
||||
var step trace.Step
|
||||
if err := json.Unmarshal(scanner.Bytes(), &step); err != nil {
|
||||
return loadedRun{}, fmt.Errorf(
|
||||
"decode step on line %d: %w",
|
||||
line,
|
||||
err,
|
||||
)
|
||||
}
|
||||
if step.TraceVersion != trace.TraceVersion {
|
||||
return loadedRun{}, fmt.Errorf(
|
||||
"step %d is trace_version %d, and E3 replays version %d only: "+
|
||||
"an older step stores no element depths, so its hierarchy decodes "+
|
||||
"with a nil root and every selector resolves to nothing on replay",
|
||||
step.Index, step.TraceVersion, trace.TraceVersion)
|
||||
}
|
||||
run.Steps = append(run.Steps, step)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return loadedRun{}, fmt.Errorf("read trace: %w", err)
|
||||
}
|
||||
if len(run.Steps) == 0 {
|
||||
return loadedRun{}, fmt.Errorf("trace has no steps")
|
||||
}
|
||||
if last := run.Steps[len(run.Steps)-1]; len(run.Steps) > 1 &&
|
||||
last.Hierarchy == nil &&
|
||||
len(last.Violations) > 0 {
|
||||
run.Finalize = &last
|
||||
run.Steps = run.Steps[:len(run.Steps)-1]
|
||||
}
|
||||
if meta.Seed == 0 {
|
||||
return loadedRun{}, fmt.Errorf(
|
||||
"meta records no seed, so the run's bundle cannot be reproduced",
|
||||
)
|
||||
}
|
||||
return run, nil
|
||||
}
|
||||
|
||||
// finalizeIndex is the step index the runner gives its end-of-run record: one
|
||||
// past the last step it wrote.
|
||||
func (r loadedRun) finalizeIndex() int {
|
||||
return r.Steps[len(r.Steps)-1].Index + 1
|
||||
}
|
||||
|
||||
// extractorFold reconstructs every extractor's value at each step from the
|
||||
// recorded per-step diffs. A run's trace stores what changed, so the value an
|
||||
// extractor held at a step is the last change at or before it; an extractor
|
||||
// that never changed held JSON null throughout, which is what an unwritten
|
||||
// diff means.
|
||||
func extractorFold(
|
||||
steps []trace.Step,
|
||||
names []string,
|
||||
) ([]map[int]json.RawMessage, error) {
|
||||
index := make(map[string]int, len(names))
|
||||
for position, name := range names {
|
||||
index[name] = position
|
||||
}
|
||||
current := make(map[int]json.RawMessage, len(names))
|
||||
for position := range names {
|
||||
current[position] = json.RawMessage("null")
|
||||
}
|
||||
folded := make([]map[int]json.RawMessage, len(steps))
|
||||
for step := range steps {
|
||||
for name, change := range steps[step].ExtractorChanges {
|
||||
position, ok := index[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"step %d records extractor %q, which the spec does not register; "+
|
||||
"the trace and the spec are not the same bundle",
|
||||
steps[step].Index,
|
||||
name,
|
||||
)
|
||||
}
|
||||
current[position] = change.Curr
|
||||
}
|
||||
snapshot := make(map[int]json.RawMessage, len(current))
|
||||
for position, value := range current {
|
||||
snapshot[position] = value
|
||||
}
|
||||
folded[step] = snapshot
|
||||
}
|
||||
return folded, nil
|
||||
}
|
||||
|
||||
// lastActionFor rebuilds the action the runner had applied before the next
|
||||
// step observed. An action the runner chose but never dispatched left
|
||||
// state.lastAction null, and the recorded skip reason is what says so.
|
||||
func lastActionFor(step trace.Step) *verifier.Action {
|
||||
if step.NextAction == nil || step.ActionSkipped != "" {
|
||||
return nil
|
||||
}
|
||||
recorded := *step.NextAction
|
||||
action := verifier.Action{
|
||||
Kind: verifier.ActionKind(recorded.Kind),
|
||||
On: recorded.Selector,
|
||||
Text: recorded.Text,
|
||||
X: recorded.X,
|
||||
Y: recorded.Y,
|
||||
FromX: recorded.FromX,
|
||||
FromY: recorded.FromY,
|
||||
ToX: recorded.ToX,
|
||||
ToY: recorded.ToY,
|
||||
Key: recorded.Key,
|
||||
DurationMillis: recorded.DurationMillis,
|
||||
}
|
||||
return &action
|
||||
}
|
||||
|
||||
func traceLogs(entries []trace.LogEntry) []verifier.LogEntry {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
logs := make([]verifier.LogEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
logs = append(logs, verifier.LogEntry{
|
||||
UnixMillis: entry.UnixMillis,
|
||||
Level: entry.Level,
|
||||
Tag: entry.Tag,
|
||||
Message: entry.Message,
|
||||
})
|
||||
}
|
||||
return logs
|
||||
}
|
||||
|
||||
func traceExceptions(entries []trace.Exception) []verifier.Exception {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
exceptions := make([]verifier.Exception, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
exceptions = append(exceptions, verifier.Exception{
|
||||
Class: entry.Class,
|
||||
Message: entry.Message,
|
||||
StackTrace: entry.StackTrace,
|
||||
UnixMillis: entry.UnixMillis,
|
||||
})
|
||||
}
|
||||
return exceptions
|
||||
}
|
||||
|
||||
// discoverRuns finds every run directory at or below root, a run directory
|
||||
// being one holding both meta.json and trace.jsonl.
|
||||
func discoverRuns(root string) ([]string, error) {
|
||||
var directories []string
|
||||
err := filepath.Walk(
|
||||
root,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(path, "trace.jsonl")); statErr != nil {
|
||||
return nil
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(path, "meta.json")); statErr != nil {
|
||||
return nil
|
||||
}
|
||||
directories = append(directories, path)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(directories)
|
||||
return directories, nil
|
||||
}
|
||||
Reference in new issue
Block a user