mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(tracecorpus): load recorded runs for offline measures
reads a run directory's meta and every step, and refuses a step whose trace_version is not the current one: an older step stores no element depths, so its hierarchy decodes with a nil root and a structural hash over it is the empty string for every screen. Discover walks a tree for the directories holding both meta.json and trace.jsonl.
This commit is contained in:
1 parent
34db6457ac
commit
7823f53453
2 files changed
+235
No files matched your search
@@ -0,0 +1,113 @@
|
||||
// Package tracecorpus loads recorded runs for measures that read stored
|
||||
// traces with no device attached.
|
||||
package tracecorpus
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/trace"
|
||||
)
|
||||
|
||||
const maxStepBytes = 64 * 1024 * 1024
|
||||
|
||||
// Run is one run directory: its meta and every step in file order, including
|
||||
// the synthetic end-of-run record a finalised liveness obligation lands on.
|
||||
type Run struct {
|
||||
Directory string
|
||||
Meta trace.Meta
|
||||
Steps []trace.Step
|
||||
}
|
||||
|
||||
// Load reads a run directory and refuses anything an offline measure cannot
|
||||
// read. A step written before the format change stores no element depths, so
|
||||
// its hierarchy decodes with a nil root: a structural hash over it is the
|
||||
// empty string for every screen, and every run would look identical to every
|
||||
// other. The refusal has to name the version rather than report that number.
|
||||
func Load(directory string) (Run, error) {
|
||||
metaBody, err := os.ReadFile(filepath.Join(directory, "meta.json"))
|
||||
if err != nil {
|
||||
return Run{}, fmt.Errorf("read meta: %w", err)
|
||||
}
|
||||
var meta trace.Meta
|
||||
if err := json.Unmarshal(metaBody, &meta); err != nil {
|
||||
return Run{}, fmt.Errorf("decode meta: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath.Join(directory, "trace.jsonl"))
|
||||
if err != nil {
|
||||
return Run{}, fmt.Errorf("open trace: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
run := Run{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 Run{}, fmt.Errorf("decode step on line %d: %w", line, err)
|
||||
}
|
||||
if step.TraceVersion != trace.TraceVersion {
|
||||
return Run{}, fmt.Errorf(
|
||||
"step %d is trace_version %d and an offline measure reads version %d only: "+
|
||||
"an older step stores no element depths, no logs and no exceptions, so "+
|
||||
"its hierarchy decodes with a nil root and nothing retrofits it",
|
||||
step.Index, step.TraceVersion, trace.TraceVersion)
|
||||
}
|
||||
if step.Hierarchy != nil && len(step.Hierarchy.Elements) > 0 &&
|
||||
step.Hierarchy.Root == nil {
|
||||
return Run{}, fmt.Errorf(
|
||||
"step %d stores %d elements and no tree shape, so its hierarchy "+
|
||||
"cannot be rebuilt",
|
||||
step.Index, len(step.Hierarchy.Elements))
|
||||
}
|
||||
run.Steps = append(run.Steps, step)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Run{}, fmt.Errorf("read trace: %w", err)
|
||||
}
|
||||
if len(run.Steps) == 0 {
|
||||
return Run{}, fmt.Errorf("trace has no steps")
|
||||
}
|
||||
return run, nil
|
||||
}
|
||||
|
||||
// Discover finds every run directory at or below root, a run directory being
|
||||
// one holding both meta.json and trace.jsonl.
|
||||
func Discover(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
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package tracecorpus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||
"github.com/priyanshujain/sanderling/internal/trace"
|
||||
)
|
||||
|
||||
const oneScreen = `{"attributes": {"text": "Hi", "bounds": "[0,0,10,10]"}, "children": [
|
||||
{"attributes": {"text": "child"}, "children": []}
|
||||
]}`
|
||||
|
||||
func TestLoadRebuildsTheTreeAStepStored(t *testing.T) {
|
||||
directory := writeRun(t, oneScreen)
|
||||
|
||||
run, err := Load(directory)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if len(run.Steps) != 1 {
|
||||
t.Fatalf("steps = %d, want 1", len(run.Steps))
|
||||
}
|
||||
root := run.Steps[0].Hierarchy.Root
|
||||
if root == nil {
|
||||
t.Fatal("stored hierarchy came back with no root, so no selector or hash reads it")
|
||||
}
|
||||
if len(root.Children) != 1 || root.Children[0].Text != "child" {
|
||||
t.Fatalf("rebuilt tree lost its child: %+v", root)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRefusesAVersionZeroStepByVersion(t *testing.T) {
|
||||
directory := writeRun(t, oneScreen)
|
||||
downgrade(t, filepath.Join(directory, "trace.jsonl"))
|
||||
|
||||
_, err := Load(directory)
|
||||
if err == nil {
|
||||
t.Fatal("a version 0 step must be refused, not measured")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "trace_version 0") {
|
||||
t.Fatalf("refusal must name the version, got %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRefusesElementsWithNoStoredShape covers the failure that would be
|
||||
// silent: a tree that decodes with elements and no root hashes to the empty
|
||||
// string, which reads as one state shared by every screen.
|
||||
func TestLoadRefusesElementsWithNoStoredShape(t *testing.T) {
|
||||
directory := writeRun(t, oneScreen)
|
||||
path := filepath.Join(directory, "trace.jsonl")
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var step map[string]any
|
||||
if err := json.Unmarshal(body, &step); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hierarchyField := step["hierarchy"].(map[string]any)
|
||||
delete(hierarchyField, "depths")
|
||||
rewrite(t, path, step)
|
||||
|
||||
if _, err := Load(directory); err == nil ||
|
||||
!strings.Contains(err.Error(), "cannot be rebuilt") {
|
||||
t.Fatalf("a shapeless tree must be refused, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeRun(t *testing.T, dumps ...string) string {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
writer, err := trace.NewWriter(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.WriteMeta(trace.Meta{Seed: 7, Platform: "web"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for index, dump := range dumps {
|
||||
tree, err := hierarchy.Parse(dump)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.WriteStep(trace.Step{Index: index + 1, Hierarchy: tree}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return directory
|
||||
}
|
||||
|
||||
func downgrade(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var step map[string]any
|
||||
if err := json.Unmarshal(body, &step); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delete(step, "trace_version")
|
||||
rewrite(t, path, step)
|
||||
}
|
||||
|
||||
func rewrite(t *testing.T, path string, step map[string]any) {
|
||||
t.Helper()
|
||||
body, err := json.Marshal(step)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, append(body, '\n'), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user