mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(runner): assert an element reaches trace.jsonl and its witness
This commit is contained in:
1 parent
abdc8b45a3
commit
b5ec7b2ce6
1 file changed
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/priyanshujain/sanderling/internal/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// elementExtractorSpec reads a live ax element, the shape every field and
|
||||||
|
// button in examples/folio/sanderling/spec.ts is extracted with. The property
|
||||||
|
// is false the moment the field is on screen, so the run records a witness
|
||||||
|
// whose only interesting content is that element.
|
||||||
|
const elementExtractorSpec = `
|
||||||
|
import { actions, always, extract } from "@sanderling/spec";
|
||||||
|
const amountField = extract("amountField", s => s.ax.find({ "resource-id": "TxnAmountField" }));
|
||||||
|
globalThis.properties = {
|
||||||
|
noAmountField: always(() => amountField.current === undefined),
|
||||||
|
};
|
||||||
|
globalThis.actions = actions(() => []);
|
||||||
|
`
|
||||||
|
|
||||||
|
const amountFieldTreeJSON = `{
|
||||||
|
"attributes": {"resource-id": "root", "bounds": "[0,0,400,800]"},
|
||||||
|
"enabled": true,
|
||||||
|
"children": [
|
||||||
|
{"attributes": {"resource-id": "TxnAmountField", "text": "199", "bounds": "[0,100,400,160]"},
|
||||||
|
"editable": true, "enabled": true, "children": []}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// TestRunner_TraceRecordsElementValuedExtractors is the guard on the artifact a
|
||||||
|
// person opens to decide whether a conviction is real. An element-valued
|
||||||
|
// extractor used to reach the trace as null on the goja hosts (ios, android):
|
||||||
|
// its exported value carries the element's find/findAll host functions, which
|
||||||
|
// json.Marshal refuses, so the encoding failed and both the per-step diff and
|
||||||
|
// the witness recorded nothing. A witness that reads null for the field the
|
||||||
|
// property fired on describes a state the property could not have fired in,
|
||||||
|
// which is worse than a blank.
|
||||||
|
func TestRunner_TraceRecordsElementValuedExtractors(t *testing.T) {
|
||||||
|
state := newHarnessWithSpec(t, elementExtractorSpec)
|
||||||
|
state.mock.HierarchyJSON = amountFieldTreeJSON
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
summary, err := Run(ctx, Options{
|
||||||
|
Duration: time.Hour,
|
||||||
|
IdleTimeout: 20 * time.Millisecond,
|
||||||
|
MaxSteps: 2,
|
||||||
|
Driver: state.mock,
|
||||||
|
Verifier: state.verifier,
|
||||||
|
TraceWriter: state.writer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if !containsProperty(summary.Violations, "noAmountField") {
|
||||||
|
t.Fatalf("noAmountField did not violate, so the element never reached a predicate: %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"`
|
||||||
|
ExtractorChanges map[string]trace.ExtractorChange `json:"extractor_changes"`
|
||||||
|
Witnesses map[string]trace.Witness `json:"witnesses"`
|
||||||
|
}
|
||||||
|
changes, witnesses := 0, 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 change, ok := line.ExtractorChanges["amountField"]; ok {
|
||||||
|
changes++
|
||||||
|
assertAmountField(t, fmt.Sprintf("step %d extractor_changes", line.Step), change.Curr)
|
||||||
|
}
|
||||||
|
for name, witness := range line.Witnesses {
|
||||||
|
witnesses++
|
||||||
|
assertAmountField(t, fmt.Sprintf("step %d %s witness", line.Step, name),
|
||||||
|
witness.Extractors["amountField"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
t.Fatalf("scan trace: %v", err)
|
||||||
|
}
|
||||||
|
if changes == 0 {
|
||||||
|
t.Error("amountField never appears in extractor_changes; the element the run read is not in the trace")
|
||||||
|
}
|
||||||
|
if witnesses == 0 {
|
||||||
|
t.Error("no witness reached the trace; nothing was compared")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertAmountField reads the recorded element the way a person opening the
|
||||||
|
// trace would: the field's text is the number the property was judged on.
|
||||||
|
func assertAmountField(t *testing.T, where string, recorded json.RawMessage) {
|
||||||
|
t.Helper()
|
||||||
|
var element struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorded, &element); err != nil {
|
||||||
|
t.Fatalf("%s: decode %s: %v", where, recorded, err)
|
||||||
|
}
|
||||||
|
if element.Text != "199" {
|
||||||
|
t.Errorf("%s: recorded element is %s, want its text to read 199", where, recorded)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user