mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(runner): retire the scaffolding the package grew by copying
Four consolidations, no assertion changed and the same 217 tests passing:
harness gains run and tryRun, retiring 68 copies of context.WithTimeout plus
Run(ctx, Options{}) plus an error check. The seven sites whose error check said
something specific keep their own wording.
mock.FailurePlan retires six wrapper drivers that were the same seven-line
method with a different error constant. The three that need more than an error
on call N stay.
traceStepLine absorbs the 15 local trace-line declarations and readTraceLines is
now the only reader. No local copy disagreed with internal/trace about a JSON
tag. Three raw readers stay on purpose: one decodes a real trace.Step, one
snapshots bytes, one asserts on raw JSON text.
One embeddable base carries the five V8-path methods the five web fakes each
redeclared.
This commit is contained in:
1 parent
d9f96602d9
commit
577993b540
14 files changed
+233
-1282
No files matched your search
@@ -1,20 +1,16 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// homeWithRows is one settled route whose list holds rows. A row arriving
|
// homeWithRows is one settled route whose list holds rows. A row arriving
|
||||||
@@ -68,19 +64,7 @@ func TestRunner_AStepWhoseTreeChangedBetweenReadsIsNotVerified(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, violationSpec)
|
state := newHarnessWithSpec(t, violationSpec)
|
||||||
device := &composesLateDriver{Driver: state.mock, composingReads: composingReads}
|
device := &composesLateDriver{Driver: state.mock, composingReads: composingReads}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
summary := state.run(t, Options{MaxSteps: 3, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 3 {
|
if summary.Steps != 3 {
|
||||||
t.Fatalf("steps = %d, want 3", summary.Steps)
|
t.Fatalf("steps = %d, want 3", summary.Steps)
|
||||||
}
|
}
|
||||||
@@ -107,7 +91,7 @@ func TestRunner_AStepWhoseTreeChangedBetweenReadsIsNotVerified(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Skipped is not lost: the step is still recorded, screenshot and all,
|
// Skipped is not lost: the step is still recorded, screenshot and all,
|
||||||
// so the run can be replayed over the frame nothing judged.
|
// so the run can be replayed over the frame nothing judged.
|
||||||
steps := traceSteps(t, directory)
|
steps := readTraceLines(t, directory)
|
||||||
if len(steps) != 3 {
|
if len(steps) != 3 {
|
||||||
t.Fatalf("trace holds %d step(s), want 3", len(steps))
|
t.Fatalf("trace holds %d step(s), want 3", len(steps))
|
||||||
}
|
}
|
||||||
@@ -201,19 +185,7 @@ func TestRunner_ASkippedStepDoesNotSwallowTheActionBeforeIt(t *testing.T) {
|
|||||||
composingRead: composingRead,
|
composingRead: composingRead,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{MaxSteps: 3, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 3 {
|
if summary.Steps != 3 {
|
||||||
t.Fatalf("steps = %d, want 3", summary.Steps)
|
t.Fatalf("steps = %d, want 3", summary.Steps)
|
||||||
}
|
}
|
||||||
@@ -277,19 +249,7 @@ func TestRunner_ARunOfSkippedStepsReportsEveryActionItApplied(t *testing.T) {
|
|||||||
composingThrough: 5,
|
composingThrough: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{MaxSteps: 6, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 6,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 6 {
|
if summary.Steps != 6 {
|
||||||
t.Fatalf("steps = %d, want 6", summary.Steps)
|
t.Fatalf("steps = %d, want 6", summary.Steps)
|
||||||
}
|
}
|
||||||
@@ -341,19 +301,7 @@ func TestRunner_AScreenThatNeverSettlesActsOnNothingAndSaysSo(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, specWithFolioPredicates(t))
|
state := newHarnessWithSpec(t, specWithFolioPredicates(t))
|
||||||
device := &submitsOnTapDriver{Driver: state.mock, commitsPerTap: 1, everyRead: true}
|
device := &submitsOnTapDriver{Driver: state.mock, commitsPerTap: 1, everyRead: true}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{MaxSteps: 5, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 5,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 5 {
|
if summary.Steps != 5 {
|
||||||
t.Fatalf("steps = %d, want 5; the run stalled instead of finishing its budget",
|
t.Fatalf("steps = %d, want 5; the run stalled instead of finishing its budget",
|
||||||
summary.Steps)
|
summary.Steps)
|
||||||
@@ -442,19 +390,7 @@ func TestRunner_OnlyAChangeOfShapeCostsAStepItsVerdict(t *testing.T) {
|
|||||||
rereadTree: testCase.reread,
|
rereadTree: testCase.reread,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
summary := state.run(t, Options{MaxSteps: 3, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 3 {
|
if summary.Steps != 3 {
|
||||||
t.Fatalf("steps = %d, want 3", summary.Steps)
|
t.Fatalf("steps = %d, want 3", summary.Steps)
|
||||||
}
|
}
|
||||||
@@ -484,26 +420,3 @@ func TestRunner_OnlyAChangeOfShapeCostsAStepItsVerdict(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type traceLine struct {
|
|
||||||
Step int `json:"step"`
|
|
||||||
Violations []string `json:"violations"`
|
|
||||||
ExtractorChanges map[string]trace.ExtractorChange `json:"extractor_changes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func traceSteps(t *testing.T, directory string) []traceLine {
|
|
||||||
t.Helper()
|
|
||||||
body, err := os.ReadFile(filepath.Join(directory, "trace.jsonl"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var steps []traceLine
|
|
||||||
for _, raw := range bytes.Split(bytes.TrimSpace(body), []byte("\n")) {
|
|
||||||
var line traceLine
|
|
||||||
if err := json.Unmarshal(raw, &line); err != nil {
|
|
||||||
t.Fatalf("decode trace line: %v", err)
|
|
||||||
}
|
|
||||||
steps = append(steps, line)
|
|
||||||
}
|
|
||||||
return steps
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// elementExtractorSpec reads a live ax element, the shape every field and
|
// elementExtractorSpec reads a live ax element, the shape every field and
|
||||||
@@ -47,43 +40,14 @@ func TestRunner_TraceRecordsElementValuedExtractors(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, elementExtractorSpec)
|
state := newHarnessWithSpec(t, elementExtractorSpec)
|
||||||
state.mock.HierarchyJSON = amountFieldTreeJSON
|
state.mock.HierarchyJSON = amountFieldTreeJSON
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
summary := state.run(t, Options{MaxSteps: 2})
|
||||||
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") {
|
if !containsProperty(summary.Violations, "noAmountField") {
|
||||||
t.Fatalf("noAmountField did not violate, so the element never reached a predicate: %v",
|
t.Fatalf("noAmountField did not violate, so the element never reached a predicate: %v",
|
||||||
summary.Violations)
|
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
|
changes, witnesses := 0, 0
|
||||||
scanner := bufio.NewScanner(file)
|
for _, line := range readTraceLines(t, state.writer.Directory()) {
|
||||||
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 {
|
if change, ok := line.ExtractorChanges["amountField"]; ok {
|
||||||
changes++
|
changes++
|
||||||
assertAmountField(t, fmt.Sprintf("step %d extractor_changes", line.Step), change.Curr)
|
assertAmountField(t, fmt.Sprintf("step %d extractor_changes", line.Step), change.Curr)
|
||||||
@@ -94,9 +58,6 @@ func TestRunner_TraceRecordsElementValuedExtractors(t *testing.T) {
|
|||||||
witness.Extractors["amountField"])
|
witness.Extractors["amountField"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
t.Fatalf("scan trace: %v", err)
|
|
||||||
}
|
|
||||||
if changes == 0 {
|
if changes == 0 {
|
||||||
t.Error("amountField never appears in extractor_changes; the element the run read is not in the trace")
|
t.Error("amountField never appears in extractor_changes; the element the run read is not in the trace")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,34 +39,13 @@ func TestRunner_LogsAndExceptionsLandInTheTrace(t *testing.T) {
|
|||||||
Message: "FATAL EXCEPTION: main",
|
Message: "FATAL EXCEPTION: main",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
state.run(t, Options{
|
||||||
defer cancel()
|
Duration: 100 * time.Millisecond,
|
||||||
if _, err := Run(ctx, Options{
|
MaxSteps: 1,
|
||||||
Duration: 100 * time.Millisecond,
|
Driver: &throwingDriver{Driver: state.mock},
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
})
|
||||||
MaxSteps: 1,
|
|
||||||
Driver: &throwingDriver{Driver: state.mock},
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := os.ReadFile(
|
stored := readTraceLines(t, state.writer.Directory())[0]
|
||||||
filepath.Join(state.writer.Directory(), "trace.jsonl"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
line := strings.SplitN(strings.TrimSpace(string(body)), "\n", 2)[0]
|
|
||||||
var stored struct {
|
|
||||||
TraceVersion int `json:"trace_version"`
|
|
||||||
Logs []trace.LogEntry `json:"logs"`
|
|
||||||
Exceptions []trace.Exception `json:"exceptions"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(line), &stored); err != nil {
|
|
||||||
t.Fatalf("decode trace line: %v\n%s", err, line)
|
|
||||||
}
|
|
||||||
if stored.TraceVersion != trace.TraceVersion {
|
if stored.TraceVersion != trace.TraceVersion {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"trace_version = %d, want %d; an old trace could not be told apart",
|
"trace_version = %d, want %d; an old trace could not be told apart",
|
||||||
|
|||||||
@@ -96,16 +96,11 @@ func TestRun_AppNeverReachesForegroundEndsTheRun(t *testing.T) {
|
|||||||
state := newHarness(t)
|
state := newHarness(t)
|
||||||
device := &neverDrawsDriver{Driver: state.mock}
|
device := &neverDrawsDriver{Driver: state.mock}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary, err := state.tryRun(t, Options{
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: time.Millisecond,
|
IdleTimeout: time.Millisecond,
|
||||||
MaxSteps: 3,
|
MaxSteps: 3,
|
||||||
BundleID: guardedBundleID,
|
BundleID: guardedBundleID,
|
||||||
Driver: device,
|
Driver: device,
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var notReached ForegroundNotReachedError
|
var notReached ForegroundNotReachedError
|
||||||
@@ -165,19 +160,12 @@ func TestRun_StepsOutsideTheAppAreRecordedInTheTrace(t *testing.T) {
|
|||||||
state := newHarness(t)
|
state := newHarness(t)
|
||||||
device := &leavesForegroundForeverDriver{Driver: state.mock}
|
device := &leavesForegroundForeverDriver{Driver: state.mock}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
state.run(t, Options{
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: time.Millisecond,
|
IdleTimeout: time.Millisecond,
|
||||||
MaxSteps: 2,
|
MaxSteps: 2,
|
||||||
BundleID: guardedBundleID,
|
BundleID: guardedBundleID,
|
||||||
Driver: device,
|
Driver: device,
|
||||||
Verifier: state.verifier,
|
})
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
records := preconditionRecords(t, state.writer.Directory())
|
records := preconditionRecords(t, state.writer.Directory())
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
@@ -146,20 +145,7 @@ func runTwoSubmitSteps(
|
|||||||
commitsPerTap int64,
|
commitsPerTap int64,
|
||||||
) []ViolationRecord {
|
) []ViolationRecord {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{MaxSteps: 2, BundleID: guardedBundleID, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
BundleID: guardedBundleID,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 2 {
|
if summary.Steps != 2 {
|
||||||
t.Fatalf("steps = %d, want 2; the run never reached the step that judges the pair",
|
t.Fatalf("steps = %d, want 2; the run never reached the step that judges the pair",
|
||||||
summary.Steps)
|
summary.Steps)
|
||||||
@@ -320,7 +306,7 @@ func runReportingTheGuard(t *testing.T, device committingDevice, state *harness)
|
|||||||
if violations := runTwoSubmitSteps(t, state, device, 1); len(violations) != 0 {
|
if violations := runTwoSubmitSteps(t, state, device, 1); len(violations) != 0 {
|
||||||
t.Errorf("the spec was told the action ran untouched by any guard: %v", violations)
|
t.Errorf("the spec was told the action ran untouched by any guard: %v", violations)
|
||||||
}
|
}
|
||||||
steps := traceSteps(t, state.writer.Directory())
|
steps := readTraceLines(t, state.writer.Directory())
|
||||||
if len(steps) != 2 {
|
if len(steps) != 2 {
|
||||||
t.Fatalf("trace holds %d step(s), want 2", len(steps))
|
t.Fatalf("trace holds %d step(s), want 2", len(steps))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -91,19 +90,7 @@ func TestGolden_TraceStreamIsReproducible(t *testing.T) {
|
|||||||
state.verifier = mustSeededVerifier(t, fixtureSpec, goldenSeed)
|
state.verifier = mustSeededVerifier(t, fixtureSpec, goldenSeed)
|
||||||
state.mock.HierarchyJSON = `{"attributes":{"resource-id":"HomeScreen"},"children":[{"attributes":{"resource-id":"next","bounds":"[40,80,240,160]"},"children":[],"clickable":true,"enabled":true}]}`
|
state.mock.HierarchyJSON = `{"attributes":{"resource-id":"HomeScreen"},"children":[{"attributes":{"resource-id":"next","bounds":"[40,80,240,160]"},"children":[],"clickable":true,"enabled":true}]}`
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
state.run(t, Options{IdleTimeout: 10 * time.Millisecond, MaxSteps: goldenSteps})
|
||||||
defer cancel()
|
|
||||||
_, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 10 * time.Millisecond,
|
|
||||||
MaxSteps: goldenSteps,
|
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
raw, err := os.ReadFile(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -119,19 +106,7 @@ func TestGolden_ViolationSummaryIsReproducible(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, violationSpec)
|
state := newHarnessWithSpec(t, violationSpec)
|
||||||
state.verifier = mustSeededVerifier(t, violationSpec, goldenSeed)
|
state.verifier = mustSeededVerifier(t, violationSpec, goldenSeed)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{IdleTimeout: 10 * time.Millisecond, MaxSteps: goldenSteps})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 10 * time.Millisecond,
|
|
||||||
MaxSteps: goldenSteps,
|
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rendered bytes.Buffer
|
var rendered bytes.Buffer
|
||||||
RenderSummary(&rendered, summary, "android")
|
RenderSummary(&rendered, summary, "android")
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ func newSeededVerifier(t *testing.T) *verifier.Verifier {
|
|||||||
func TestPickSourcesRejectsLLMWithoutSpecConfig(t *testing.T) {
|
func TestPickSourcesRejectsLLMWithoutSpecConfig(t *testing.T) {
|
||||||
for name, activeDriver := range map[string]driver.DeviceDriver{
|
for name, activeDriver := range map[string]driver.DeviceDriver{
|
||||||
"native": nil,
|
"native": nil,
|
||||||
"web": &webMockDriver{Driver: mockdriver.New()},
|
"web": &webMockDriver{webDriverBase: webDriverBase{Driver: mockdriver.New()}},
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
action, extractor, err := pickSources(Options{
|
action, extractor, err := pickSources(Options{
|
||||||
@@ -523,7 +523,7 @@ func TestPickSourcesOnWebComposesLLMWithWebExtractors(t *testing.T) {
|
|||||||
fake := newFakeOpenRouter(t)
|
fake := newFakeOpenRouter(t)
|
||||||
_, verifierInstance := newLLMSource(t, fake)
|
_, verifierInstance := newLLMSource(t, fake)
|
||||||
action, extractor, err := pickSources(Options{
|
action, extractor, err := pickSources(Options{
|
||||||
Driver: &webMockDriver{Driver: mockdriver.New()},
|
Driver: &webMockDriver{webDriverBase: webDriverBase{Driver: mockdriver.New()}},
|
||||||
Verifier: verifierInstance,
|
Verifier: verifierInstance,
|
||||||
Generator: "llm",
|
Generator: "llm",
|
||||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
@@ -541,7 +541,7 @@ func TestPickSourcesOnWebComposesLLMWithWebExtractors(t *testing.T) {
|
|||||||
|
|
||||||
func TestPickSourcesOnWebSeededKeepsBothOnV8(t *testing.T) {
|
func TestPickSourcesOnWebSeededKeepsBothOnV8(t *testing.T) {
|
||||||
action, extractor, err := pickSources(Options{
|
action, extractor, err := pickSources(Options{
|
||||||
Driver: &webMockDriver{Driver: mockdriver.New()},
|
Driver: &webMockDriver{webDriverBase: webDriverBase{Driver: mockdriver.New()}},
|
||||||
Verifier: newSeededVerifier(t),
|
Verifier: newSeededVerifier(t),
|
||||||
Generator: "seeded",
|
Generator: "seeded",
|
||||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
@@ -768,22 +768,13 @@ func TestRunner_EveryModelCallFailingIsNotACleanRun(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, llmFixtureSpec)
|
state := newHarnessWithSpec(t, llmFixtureSpec)
|
||||||
state.mock.HierarchyJSON = llmTreeJSON
|
state.mock.HierarchyJSON = llmTreeJSON
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: 30 * time.Second,
|
Duration: 30 * time.Second,
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
MaxSteps: 3,
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
Generator: "llm",
|
Generator: "llm",
|
||||||
LabelSource: verifier.LabelSourceVisibleText,
|
LabelSource: verifier.LabelSourceVisibleText,
|
||||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
calls := readLLMCalls(t, state.writer.Directory())
|
calls := readLLMCalls(t, state.writer.Directory())
|
||||||
if len(calls) != 3 {
|
if len(calls) != 3 {
|
||||||
@@ -845,22 +836,13 @@ func TestRunner_SetupActionsAreNotTheGeneratorDrivingTheApp(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, llmLoginSetupSpec)
|
state := newHarnessWithSpec(t, llmLoginSetupSpec)
|
||||||
state.mock.HierarchyJSON = llmTreeJSON
|
state.mock.HierarchyJSON = llmTreeJSON
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: 30 * time.Second,
|
Duration: 30 * time.Second,
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 4,
|
MaxSteps: 4,
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
Generator: "llm",
|
Generator: "llm",
|
||||||
LabelSource: verifier.LabelSourceVisibleText,
|
LabelSource: verifier.LabelSourceVisibleText,
|
||||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantOutcomes := []string{
|
wantOutcomes := []string{
|
||||||
trace.LLMOutcomeSetupAction, trace.LLMOutcomeSetupAction,
|
trace.LLMOutcomeSetupAction, trace.LLMOutcomeSetupAction,
|
||||||
@@ -933,22 +915,13 @@ func TestRunner_SetupAndGeneratorBothDrivingIsAHealthyRun(t *testing.T) {
|
|||||||
fake.choice = tap.Index
|
fake.choice = tap.Index
|
||||||
fake.chosenAction = tap.Description
|
fake.chosenAction = tap.Description
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: 30 * time.Second,
|
Duration: 30 * time.Second,
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 4,
|
MaxSteps: 4,
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
Generator: "llm",
|
Generator: "llm",
|
||||||
LabelSource: verifier.LabelSourceVisibleText,
|
LabelSource: verifier.LabelSourceVisibleText,
|
||||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if summary.DispatchedActions != 4 {
|
if summary.DispatchedActions != 4 {
|
||||||
t.Errorf("DispatchedActions = %d, want 4: every step drove the app",
|
t.Errorf("DispatchedActions = %d, want 4: every step drove the app",
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// navigatingDriver reports one navigation per step, the way a page that submits
|
// navigatingDriver reports one navigation per step, the way a page that submits
|
||||||
@@ -33,46 +28,20 @@ func TestRunner_TheTraceRecordsThatThePageNavigated(t *testing.T) {
|
|||||||
const url = "http://127.0.0.1/index.html?"
|
const url = "http://127.0.0.1/index.html?"
|
||||||
navigating := &navigatingDriver{Driver: state.mock, url: url}
|
navigating := &navigatingDriver{Driver: state.mock, url: url}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
state.run(t, Options{Duration: 100 * time.Millisecond, MaxSteps: 3, Driver: navigating})
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: 100 * time.Millisecond,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: navigating,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if err := state.writer.Close(); err != nil {
|
if err := state.writer.Close(); err != nil {
|
||||||
t.Fatalf("close trace: %v", err)
|
t.Fatalf("close trace: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
recorded := 0
|
recorded := 0
|
||||||
scanner := bufio.NewScanner(file)
|
for _, step := range readTraceLines(t, state.writer.Directory()) {
|
||||||
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
|
||||||
for scanner.Scan() {
|
|
||||||
var step struct {
|
|
||||||
Index int `json:"step"`
|
|
||||||
Navigations []trace.Navigation `json:"navigations"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(scanner.Bytes(), &step); err != nil {
|
|
||||||
t.Fatalf("trace line decode: %v", err)
|
|
||||||
}
|
|
||||||
for _, navigation := range step.Navigations {
|
for _, navigation := range step.Navigations {
|
||||||
recorded++
|
recorded++
|
||||||
if navigation.URL != url {
|
if navigation.URL != url {
|
||||||
t.Errorf("step %d records navigation to %q, want %q", step.Index, navigation.URL, url)
|
t.Errorf("step %d records navigation to %q, want %q", step.Step, navigation.URL, url)
|
||||||
}
|
}
|
||||||
if navigation.UnixMillis == 0 {
|
if navigation.UnixMillis == 0 {
|
||||||
t.Errorf("step %d records a navigation with no timestamp", step.Index)
|
t.Errorf("step %d records a navigation with no timestamp", step.Step)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
|
|
||||||
@@ -160,20 +159,9 @@ func TestTheTraceNeverCarriesATypedSecretThroughALastActionExtractor(t *testing.
|
|||||||
state := newHarnessWithSpec(t, fmt.Sprintf(lastActionExtractorSpec, testCase.selector))
|
state := newHarnessWithSpec(t, fmt.Sprintf(lastActionExtractorSpec, testCase.selector))
|
||||||
state.mock.HierarchyJSON = testCase.treeJSON
|
state.mock.HierarchyJSON = testCase.treeJSON
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
state.run(t, Options{MaxSteps: 2})
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
steps := traceSteps(t, state.writer.Directory())
|
steps := readTraceLines(t, state.writer.Directory())
|
||||||
if len(steps) < 2 {
|
if len(steps) < 2 {
|
||||||
t.Fatalf("trace holds %d step(s), want the step that reports the action back", len(steps))
|
t.Fatalf("trace holds %d step(s), want the step that reports the action back", len(steps))
|
||||||
}
|
}
|
||||||
|
|||||||
+172
-773
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
@@ -106,19 +105,7 @@ func TestRunner_ApplyErrorAfterDispatchDoesNotConvictTheSubmitCountingProperty(t
|
|||||||
state := newHarnessWithSpec(t, spec)
|
state := newHarnessWithSpec(t, spec)
|
||||||
device := &dispatchThenFailDriver{Driver: state.mock, commitsPerTap: commitsPerTap}
|
device := &dispatchThenFailDriver{Driver: state.mock, commitsPerTap: commitsPerTap}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
summary := state.run(t, Options{MaxSteps: 2, Driver: device})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: device,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 2 {
|
if summary.Steps != 2 {
|
||||||
t.Fatalf("steps = %d, want 2; the run never reached the step that judges the pair", summary.Steps)
|
t.Fatalf("steps = %d, want 2; the run never reached the step that judges the pair", summary.Steps)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// carrierSpec registers one extractor whose value the page supplies. It stands
|
// carrierSpec registers one extractor whose value the page supplies. It stands
|
||||||
@@ -33,7 +29,7 @@ globalThis.actions = actions(() => []);
|
|||||||
// whose page-side extractor advances a counter on every evaluation - exactly
|
// whose page-side extractor advances a counter on every evaluation - exactly
|
||||||
// what a spec-authored carrier does in V8.
|
// what a spec-authored carrier does in V8.
|
||||||
type carrierWebDriver struct {
|
type carrierWebDriver struct {
|
||||||
*mockdriver.Driver
|
webDriverBase
|
||||||
transitional bool
|
transitional bool
|
||||||
snapshots int
|
snapshots int
|
||||||
reads int
|
reads int
|
||||||
@@ -53,8 +49,6 @@ func (d *carrierWebDriver) Snapshot(ctx context.Context) (string, driver.Image,
|
|||||||
]}`, d.snapshots), image, err
|
]}`, d.snapshots), image, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *carrierWebDriver) InstallBundle(context.Context, []byte) error { return nil }
|
|
||||||
|
|
||||||
// A web target says so. The runner's per-step hierarchy reread is android-only,
|
// A web target says so. The runner's per-step hierarchy reread is android-only,
|
||||||
// and a fake claiming android would take a path no chrome run takes.
|
// and a fake claiming android would take a path no chrome run takes.
|
||||||
func (d *carrierWebDriver) Health(context.Context) (driver.Health, error) {
|
func (d *carrierWebDriver) Health(context.Context) (driver.Health, error) {
|
||||||
@@ -73,10 +67,6 @@ func (d *carrierWebDriver) NextActionFromV8(context.Context) (json.RawMessage, e
|
|||||||
return json.RawMessage(`{"kind":"Tap","x":5,"y":5}`), nil
|
return json.RawMessage(`{"kind":"Tap","x":5,"y":5}`), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *carrierWebDriver) SetLastAction(context.Context, json.RawMessage) error { return nil }
|
|
||||||
|
|
||||||
func (d *carrierWebDriver) SetLogs(context.Context, json.RawMessage) error { return nil }
|
|
||||||
|
|
||||||
// TestRunner_TransitionalStepNeverAdvancesThePageCarrier pins the ordering the
|
// TestRunner_TransitionalStepNeverAdvancesThePageCarrier pins the ordering the
|
||||||
// web path depends on. The page-side extractors must run only on steps the
|
// web path depends on. The page-side extractors must run only on steps the
|
||||||
// verifier accepts: their getters advance spec state every time they evaluate,
|
// verifier accepts: their getters advance spec state every time they evaluate,
|
||||||
@@ -86,41 +76,16 @@ func (d *carrierWebDriver) SetLogs(context.Context, json.RawMessage) error { ret
|
|||||||
// convicts an app that did nothing wrong.
|
// convicts an app that did nothing wrong.
|
||||||
func TestRunner_TransitionalStepNeverAdvancesThePageCarrier(t *testing.T) {
|
func TestRunner_TransitionalStepNeverAdvancesThePageCarrier(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, carrierSpec)
|
state := newHarnessWithSpec(t, carrierSpec)
|
||||||
web := &carrierWebDriver{Driver: state.mock}
|
web := &carrierWebDriver{webDriverBase: webDriverBase{Driver: state.mock}}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
summary := state.run(t, Options{Duration: 30 * time.Second, MaxSteps: 5, Driver: web})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: 30 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 5,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if summary.Steps != 5 {
|
if summary.Steps != 5 {
|
||||||
t.Fatalf("steps = %d, want 5", summary.Steps)
|
t.Fatalf("steps = %d, want 5", summary.Steps)
|
||||||
}
|
}
|
||||||
|
|
||||||
type traceLine struct {
|
|
||||||
Step int `json:"step"`
|
|
||||||
Transitional bool `json:"transitional"`
|
|
||||||
ExtractorChanges map[string]trace.ExtractorChange `json:"extractor_changes"`
|
|
||||||
}
|
|
||||||
body, err := os.ReadFile(filepath.Join(state.writer.Directory(), "trace.jsonl"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
verified, transitional := 0, 0
|
verified, transitional := 0, 0
|
||||||
previous := 0
|
previous := 0
|
||||||
for _, raw := range bytes.Split(bytes.TrimSpace(body), []byte("\n")) {
|
for _, line := range readTraceLines(t, state.writer.Directory()) {
|
||||||
var line traceLine
|
|
||||||
if err := json.Unmarshal(raw, &line); err != nil {
|
|
||||||
t.Fatalf("decode trace line: %v", err)
|
|
||||||
}
|
|
||||||
if line.Transitional {
|
if line.Transitional {
|
||||||
transitional++
|
transitional++
|
||||||
continue
|
continue
|
||||||
@@ -157,11 +122,9 @@ func TestRunner_TransitionalStepNeverAdvancesThePageCarrier(t *testing.T) {
|
|||||||
// lastAction: an older published @sanderling/spec runtime, a bundle that never
|
// lastAction: an older published @sanderling/spec runtime, a bundle that never
|
||||||
// installed, a tab that navigated away from it.
|
// installed, a tab that navigated away from it.
|
||||||
type installFailsWebDriver struct {
|
type installFailsWebDriver struct {
|
||||||
*mockdriver.Driver
|
webDriverBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *installFailsWebDriver) InstallBundle(context.Context, []byte) error { return nil }
|
|
||||||
|
|
||||||
func (d *installFailsWebDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
func (d *installFailsWebDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
||||||
return map[int]json.RawMessage{0: json.RawMessage(`1`)}, nil
|
return map[int]json.RawMessage{0: json.RawMessage(`1`)}, nil
|
||||||
}
|
}
|
||||||
@@ -174,8 +137,6 @@ func (d *installFailsWebDriver) SetLastAction(context.Context, json.RawMessage)
|
|||||||
return errors.New("__sanderlingSetLastAction__ is not a function")
|
return errors.New("__sanderlingSetLastAction__ is not a function")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *installFailsWebDriver) SetLogs(context.Context, json.RawMessage) error { return nil }
|
|
||||||
|
|
||||||
// TestRunner_LastActionInstallFailureFailsTheRun covers the other half of the
|
// TestRunner_LastActionInstallFailureFailsTheRun covers the other half of the
|
||||||
// same trust boundary. A run that cannot install lastAction in the page cannot
|
// same trust boundary. A run that cannot install lastAction in the page cannot
|
||||||
// apply the page's extractor values either, so the step keeps goja's
|
// apply the page's extractor values either, so the step keeps goja's
|
||||||
@@ -184,18 +145,9 @@ func (d *installFailsWebDriver) SetLogs(context.Context, json.RawMessage) error
|
|||||||
// green run reporting a violation nobody can reproduce.
|
// green run reporting a violation nobody can reproduce.
|
||||||
func TestRunner_LastActionInstallFailureFailsTheRun(t *testing.T) {
|
func TestRunner_LastActionInstallFailureFailsTheRun(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, carrierSpec)
|
state := newHarnessWithSpec(t, carrierSpec)
|
||||||
web := &installFailsWebDriver{Driver: state.mock}
|
web := &installFailsWebDriver{webDriverBase: webDriverBase{Driver: state.mock}}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
_, err := state.tryRun(t, Options{Duration: 2 * time.Second, MaxSteps: 3, Driver: web})
|
||||||
defer cancel()
|
|
||||||
_, err := Run(ctx, Options{
|
|
||||||
Duration: 2 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Run succeeded with a page that cannot take lastAction; the run " +
|
t.Fatal("Run succeeded with a page that cannot take lastAction; the run " +
|
||||||
"reported green while its extractor values came from two engines")
|
"reported green while its extractor values came from two engines")
|
||||||
@@ -229,19 +181,10 @@ func (d *logInstallFailsWebDriver) SetLogs(context.Context, json.RawMessage) err
|
|||||||
func TestRunner_LogInstallFailureFailsTheRun(t *testing.T) {
|
func TestRunner_LogInstallFailureFailsTheRun(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, carrierSpec)
|
state := newHarnessWithSpec(t, carrierSpec)
|
||||||
web := &logInstallFailsWebDriver{
|
web := &logInstallFailsWebDriver{
|
||||||
installFailsWebDriver: &installFailsWebDriver{Driver: state.mock},
|
installFailsWebDriver: &installFailsWebDriver{webDriverBase: webDriverBase{Driver: state.mock}},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
_, err := state.tryRun(t, Options{Duration: 2 * time.Second, MaxSteps: 3, Driver: web})
|
||||||
defer cancel()
|
|
||||||
_, err := Run(ctx, Options{
|
|
||||||
Duration: 2 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Run succeeded with a page that cannot take the step's logs; " +
|
t.Fatal("Run succeeded with a page that cannot take the step's logs; " +
|
||||||
"every property reading the log stream ran against an empty array")
|
"every property reading the log stream ran against an empty array")
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// engineDisagreementSpec makes the two runtimes disagree on purpose: the
|
// engineDisagreementSpec makes the two runtimes disagree on purpose: the
|
||||||
@@ -31,24 +25,14 @@ globalThis.actions = actions(() => []);
|
|||||||
// takes the V8 path, where extractor values come from the page rather than
|
// takes the V8 path, where extractor values come from the page rather than
|
||||||
// from goja.
|
// from goja.
|
||||||
type webMockDriver struct {
|
type webMockDriver struct {
|
||||||
*mockdriver.Driver
|
webDriverBase
|
||||||
overrides map[int]json.RawMessage
|
overrides map[int]json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *webMockDriver) InstallBundle(context.Context, []byte) error { return nil }
|
|
||||||
|
|
||||||
func (d *webMockDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
func (d *webMockDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
||||||
return d.overrides, nil
|
return d.overrides, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *webMockDriver) NextActionFromV8(context.Context) (json.RawMessage, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *webMockDriver) SetLastAction(context.Context, json.RawMessage) error { return nil }
|
|
||||||
|
|
||||||
func (d *webMockDriver) SetLogs(context.Context, json.RawMessage) error { return nil }
|
|
||||||
|
|
||||||
// TestRunner_TraceRecordsTheValueTheVerdictUsed fails if the trace and the
|
// TestRunner_TraceRecordsTheValueTheVerdictUsed fails if the trace and the
|
||||||
// verdict disagree about an extractor. A witness is only an explanation of a
|
// verdict disagree about an extractor. A witness is only an explanation of a
|
||||||
// violation if it holds the state the violated property was evaluated against.
|
// violation if it holds the state the violated property was evaluated against.
|
||||||
@@ -56,46 +40,18 @@ func TestRunner_TraceRecordsTheValueTheVerdictUsed(t *testing.T) {
|
|||||||
state := newHarnessWithSpec(t, engineDisagreementSpec)
|
state := newHarnessWithSpec(t, engineDisagreementSpec)
|
||||||
const pageValue = `"v8"`
|
const pageValue = `"v8"`
|
||||||
web := &webMockDriver{
|
web := &webMockDriver{
|
||||||
Driver: state.mock,
|
webDriverBase: webDriverBase{Driver: state.mock},
|
||||||
overrides: map[int]json.RawMessage{0: json.RawMessage(pageValue)},
|
overrides: map[int]json.RawMessage{0: json.RawMessage(pageValue)},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
summary := state.run(t, Options{Duration: 100 * time.Millisecond, Driver: web})
|
||||||
defer cancel()
|
|
||||||
summary, err := Run(ctx, Options{
|
|
||||||
Duration: 100 * time.Millisecond,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
if !containsProperty(summary.Violations, "ranInGoja") {
|
if !containsProperty(summary.Violations, "ranInGoja") {
|
||||||
t.Fatalf("ranInGoja did not violate, so the page value never reached the verdict: %v",
|
t.Fatalf("ranInGoja did not violate, so the page value never reached the verdict: %v",
|
||||||
summary.Violations)
|
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
|
changes, witnesses := 0, 0
|
||||||
scanner := bufio.NewScanner(file)
|
for _, line := range readTraceLines(t, state.writer.Directory()) {
|
||||||
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["engine"]; ok {
|
if change, ok := line.ExtractorChanges["engine"]; ok {
|
||||||
changes++
|
changes++
|
||||||
if got := string(change.Curr); got != pageValue {
|
if got := string(change.Curr); got != pageValue {
|
||||||
@@ -111,9 +67,6 @@ func TestRunner_TraceRecordsTheValueTheVerdictUsed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
t.Fatalf("scan trace: %v", err)
|
|
||||||
}
|
|
||||||
if changes == 0 {
|
if changes == 0 {
|
||||||
t.Error("no extractor change reached the trace; nothing was compared")
|
t.Error("no extractor change reached the trace; nothing was compared")
|
||||||
}
|
}
|
||||||
@@ -143,20 +96,11 @@ globalThis.actions = actions(() => []);
|
|||||||
func TestRunner_PartialExtractorTableIsFatal(t *testing.T) {
|
func TestRunner_PartialExtractorTableIsFatal(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, splitTableSpec)
|
state := newHarnessWithSpec(t, splitTableSpec)
|
||||||
web := &webMockDriver{
|
web := &webMockDriver{
|
||||||
Driver: state.mock,
|
webDriverBase: webDriverBase{Driver: state.mock},
|
||||||
overrides: map[int]json.RawMessage{0: json.RawMessage(`"v8"`)},
|
overrides: map[int]json.RawMessage{0: json.RawMessage(`"v8"`)},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
_, err := state.tryRun(t, Options{MaxSteps: 2, Driver: web})
|
||||||
defer cancel()
|
|
||||||
_, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("the run completed on a page that reported 1 of 2 extractors; " +
|
t.Fatal("the run completed on a page that reported 1 of 2 extractors; " +
|
||||||
"the second extractor silently kept goja's value")
|
"the second extractor silently kept goja's value")
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
@@ -29,17 +28,11 @@ globalThis.properties = {};
|
|||||||
// tappingWebDriver is a web target whose V8 picker always taps one named
|
// tappingWebDriver is a web target whose V8 picker always taps one named
|
||||||
// control, so the runner has a real applied action to report on the next step.
|
// control, so the runner has a real applied action to report on the next step.
|
||||||
type tappingWebDriver struct {
|
type tappingWebDriver struct {
|
||||||
*mockdriver.Driver
|
webDriverBase
|
||||||
installed []string
|
installed []string
|
||||||
installedLogs []string
|
installedLogs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *tappingWebDriver) InstallBundle(context.Context, []byte) error { return nil }
|
|
||||||
|
|
||||||
func (d *tappingWebDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *tappingWebDriver) NextActionFromV8(context.Context) (json.RawMessage, error) {
|
func (d *tappingWebDriver) NextActionFromV8(context.Context) (json.RawMessage, error) {
|
||||||
return json.RawMessage(`{"kind":"Tap","x":12,"y":34,"selector":"id:TxnSubmit"}`), nil
|
return json.RawMessage(`{"kind":"Tap","x":12,"y":34,"selector":"id:TxnSubmit"}`), nil
|
||||||
}
|
}
|
||||||
@@ -56,20 +49,9 @@ func (d *tappingWebDriver) SetLogs(_ context.Context, encoded json.RawMessage) e
|
|||||||
|
|
||||||
func TestRunner_WebInstallsLastActionInThePage(t *testing.T) {
|
func TestRunner_WebInstallsLastActionInThePage(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, lastActionSpec)
|
state := newHarnessWithSpec(t, lastActionSpec)
|
||||||
web := &tappingWebDriver{Driver: state.mock}
|
web := &tappingWebDriver{webDriverBase: webDriverBase{Driver: state.mock}}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
state.run(t, Options{MaxSteps: 3, Driver: web})
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 3,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(web.installed) < 2 {
|
if len(web.installed) < 2 {
|
||||||
t.Fatalf("the page was handed lastAction %d time(s); the web path never installed it",
|
t.Fatalf("the page was handed lastAction %d time(s); the web path never installed it",
|
||||||
@@ -97,20 +79,9 @@ func TestRunner_WebInstallsTheStepsLogsInThePage(t *testing.T) {
|
|||||||
state.mock.LogEntries = []driver.LogEntry{
|
state.mock.LogEntries = []driver.LogEntry{
|
||||||
{UnixMillis: 1700000000123, Level: "E", Tag: "console", Message: "boom from the page"},
|
{UnixMillis: 1700000000123, Level: "E", Tag: "console", Message: "boom from the page"},
|
||||||
}
|
}
|
||||||
web := &tappingWebDriver{Driver: state.mock}
|
web := &tappingWebDriver{webDriverBase: webDriverBase{Driver: state.mock}}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
state.run(t, Options{MaxSteps: 2, Driver: web})
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(web.installedLogs) == 0 {
|
if len(web.installedLogs) == 0 {
|
||||||
t.Fatal("the page was never handed the step's logs; every property reading " +
|
t.Fatal("the page was never handed the step's logs; every property reading " +
|
||||||
@@ -135,19 +106,7 @@ func TestRunner_ReportsALogFetchItCouldNotMake(t *testing.T) {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
logger := slog.New(slog.NewTextHandler(&buffer, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
logger := slog.New(slog.NewTextHandler(&buffer, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
state.run(t, Options{MaxSteps: 2, Logger: logger})
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: state.mock,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
Logger: logger,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(buffer.String(), "adb: device offline") {
|
if !strings.Contains(buffer.String(), "adb: device offline") {
|
||||||
t.Errorf("the run never reported the failed log fetch, so noLogcatErrors "+
|
t.Errorf("the run never reported the failed log fetch, so noLogcatErrors "+
|
||||||
@@ -170,21 +129,12 @@ func (d *failingTapWebDriver) Tap(context.Context, int, int) error {
|
|||||||
// decides for itself, which it cannot do if the page is handed a bare null.
|
// decides for itself, which it cannot do if the page is handed a bare null.
|
||||||
func TestRunner_WebInstallsAnUnconfirmedActionWithItsFateUnknown(t *testing.T) {
|
func TestRunner_WebInstallsAnUnconfirmedActionWithItsFateUnknown(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, lastActionSpec)
|
state := newHarnessWithSpec(t, lastActionSpec)
|
||||||
web := &failingTapWebDriver{tappingWebDriver: &tappingWebDriver{Driver: state.mock}}
|
web := &failingTapWebDriver{
|
||||||
|
tappingWebDriver: &tappingWebDriver{webDriverBase: webDriverBase{Driver: state.mock}},
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := Run(ctx, Options{
|
|
||||||
Duration: time.Hour,
|
|
||||||
IdleTimeout: 20 * time.Millisecond,
|
|
||||||
MaxSteps: 2,
|
|
||||||
Driver: web,
|
|
||||||
Verifier: state.verifier,
|
|
||||||
TraceWriter: state.writer,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.run(t, Options{MaxSteps: 2, Driver: web})
|
||||||
|
|
||||||
if len(web.installed) < 2 {
|
if len(web.installed) < 2 {
|
||||||
t.Fatalf("the page was handed lastAction %d time(s); the web path never installed it",
|
t.Fatalf("the page was handed lastAction %d time(s); the web path never installed it",
|
||||||
len(web.installed))
|
len(web.installed))
|
||||||
|
|||||||
Reference in new issue
Block a user