mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
test(trace): set Hierarchy in round-trip and lock lossy Tree contract
Also add a -race concurrent WriteStep test that asserts N well-formed JSONL lines, catching torn lines if the writer mutex is dropped.
This commit is contained in:
1 parent
042f5c7e7c
commit
3e9276bdc9
1 file changed
+69
@@ -6,8 +6,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteMeta_RoundTrip(t *testing.T) {
|
func TestWriteMeta_RoundTrip(t *testing.T) {
|
||||||
@@ -99,6 +102,12 @@ func TestWriteStep_HierarchyAndResidualsRoundTrip(t *testing.T) {
|
|||||||
writer, _ := NewWriter(directory)
|
writer, _ := NewWriter(directory)
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
|
tree, err := hierarchy.Parse(`{"attributes":{"resource-id":"root"},"children":[
|
||||||
|
{"attributes":{"resource-id":"child","text":"hi"},"children":[]}]}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
step := Step{
|
step := Step{
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Timestamp: time.Now().UTC(),
|
Timestamp: time.Now().UTC(),
|
||||||
@@ -108,6 +117,7 @@ func TestWriteStep_HierarchyAndResidualsRoundTrip(t *testing.T) {
|
|||||||
ResolvedBounds: &BoundsRecord{X: 10, Y: 20, Width: 100, Height: 50},
|
ResolvedBounds: &BoundsRecord{X: 10, Y: 20, Width: 100, Height: 50},
|
||||||
TapPoint: &PointRecord{X: 60, Y: 45},
|
TapPoint: &PointRecord{X: 60, Y: 45},
|
||||||
},
|
},
|
||||||
|
Hierarchy: tree,
|
||||||
Residuals: map[string]json.RawMessage{
|
Residuals: map[string]json.RawMessage{
|
||||||
"prop1": json.RawMessage(`{"op":"true"}`),
|
"prop1": json.RawMessage(`{"op":"true"}`),
|
||||||
},
|
},
|
||||||
@@ -132,6 +142,24 @@ func TestWriteStep_HierarchyAndResidualsRoundTrip(t *testing.T) {
|
|||||||
if string(got.Residuals["prop1"]) != `{"op":"true"}` {
|
if string(got.Residuals["prop1"]) != `{"op":"true"}` {
|
||||||
t.Errorf("residuals round-trip wrong: %s", got.Residuals["prop1"])
|
t.Errorf("residuals round-trip wrong: %s", got.Residuals["prop1"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intentionally-lossy contract: Tree marshals only Elements (Root and
|
||||||
|
// Node.Children are json:"-"). The flat element list survives; tree
|
||||||
|
// structure does not. Lock both halves so a regression that drops the
|
||||||
|
// element list, or one that silently starts persisting structure the
|
||||||
|
// replay UI would then depend on, is caught.
|
||||||
|
if got.Hierarchy == nil {
|
||||||
|
t.Fatal("hierarchy dropped from trace")
|
||||||
|
}
|
||||||
|
if len(got.Hierarchy.Elements) != 2 {
|
||||||
|
t.Fatalf("hierarchy elements not preserved: got %d", len(got.Hierarchy.Elements))
|
||||||
|
}
|
||||||
|
if got.Hierarchy.Elements[1].Text != "hi" {
|
||||||
|
t.Errorf("element field lost: %+v", got.Hierarchy.Elements[1])
|
||||||
|
}
|
||||||
|
if got.Hierarchy.Root != nil {
|
||||||
|
t.Errorf("Root is json:\"-\" and must decode nil, got %+v", got.Hierarchy.Root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteStep_OmitsEmptyHierarchyAndResiduals(t *testing.T) {
|
func TestWriteStep_OmitsEmptyHierarchyAndResiduals(t *testing.T) {
|
||||||
@@ -201,6 +229,47 @@ func TestWriteStep_MultipleStepsAppend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug class: dropping the writer mutex would let concurrent encoder.Encode
|
||||||
|
// calls interleave, producing torn JSONL lines. Run under -race: every line
|
||||||
|
// must be a complete, parseable Step and all N must arrive.
|
||||||
|
func TestWriteStep_ConcurrentWritesAreWellFormed(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
writer, err := NewWriter(directory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
const n = 50
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for index := 0; index < n; index++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := writer.WriteStep(Step{Index: index, Screen: "s"}); err != nil {
|
||||||
|
t.Errorf("WriteStep: %v", err)
|
||||||
|
}
|
||||||
|
}(index)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
lines := readLines(t, filepath.Join(directory, "trace.jsonl"))
|
||||||
|
if len(lines) != n {
|
||||||
|
t.Fatalf("expected %d lines, got %d", n, len(lines))
|
||||||
|
}
|
||||||
|
seen := make(map[int]bool, n)
|
||||||
|
for _, line := range lines {
|
||||||
|
var got Step
|
||||||
|
if err := json.Unmarshal([]byte(line), &got); err != nil {
|
||||||
|
t.Fatalf("torn JSONL line: %v\n%s", err, line)
|
||||||
|
}
|
||||||
|
seen[got.Index] = true
|
||||||
|
}
|
||||||
|
if len(seen) != n {
|
||||||
|
t.Errorf("expected %d distinct steps, got %d", n, len(seen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteStep_ViolationsAreGreppable(t *testing.T) {
|
func TestWriteStep_ViolationsAreGreppable(t *testing.T) {
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
writer, _ := NewWriter(directory)
|
writer, _ := NewWriter(directory)
|
||||||
|
|||||||
Reference in new issue
Block a user