From 1a17734ce016931153a118c7f40a76a265277b49 Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 1 Jun 2026 17:14:35 +0530 Subject: [PATCH] refactor(runner): collapse WebDriver forks behind ActionSource/ExtractorSource --- internal/runner/runner.go | 49 +++++++++++--------------- internal/runner/source.go | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 internal/runner/source.go diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 2368b85..ae2b21a 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -41,6 +41,10 @@ type Summary struct { EndTime time.Time Steps int Violations []ViolationRecord + // UnsupportedVerbs lists verbs the picker requested that the platform + // could not dispatch, deduped, so the report can flag a spec exercising + // gestures this target does not support. + UnsupportedVerbs []string } type ViolationRecord struct { @@ -65,6 +69,11 @@ func Run(ctx context.Context, options Options) (Summary, error) { // before the deadline is set so the settle time does not eat the run. waitForForeground(ctx, options, logger) + // Pick the action and extractor sources once from the driver's + // capabilities so the step loop runs one uniform path with no per-step + // driver type assertion. + actionSource, extractorSource := pickSources(options) + summary := Summary{StartTime: time.Now()} deadline := summary.StartTime.Add(options.Duration) stepIndex := 0 @@ -116,17 +125,15 @@ func Run(ctx context.Context, options Options) (Summary, error) { return nil }) var v8Overrides map[int]json.RawMessage - if web, ok := options.Driver.(driver.WebDriver); ok { - g.Go(func() error { - overrides, err := web.EvaluateExtractors(gctx) - if err != nil { - logger.Warn("v8 extractor evaluation failed", "step", si, "err", err) - return nil - } - v8Overrides = overrides + g.Go(func() error { + overrides, err := extractorSource.ExtractorOverrides(gctx) + if err != nil { + logger.Warn("v8 extractor evaluation failed", "step", si, "err", err) return nil - }) - } + } + v8Overrides = overrides + return nil + }) // All goroutines write to local variables and return nil, so the Wait // error is always nil; ignored intentionally. _ = g.Wait() @@ -197,13 +204,7 @@ func Run(ctx context.Context, options Options) (Summary, error) { } logger.Info("step", "index", stepIndex, "screen", screen, "nodes", treeSize) - var nextAction verifier.Action - var nextErr error - if web, ok := options.Driver.(driver.WebDriver); ok { - nextAction, nextErr = nextActionFromV8(ctx, web) - } else { - nextAction, nextErr = options.Verifier.NextAction() - } + nextAction, nextErr := actionSource.NextAction(ctx) var traceAction *trace.Action if nextErr == nil { traceAction = traceActionFor(nextAction, tree) @@ -298,6 +299,7 @@ func Run(ctx context.Context, options Options) (Summary, error) { } } + summary.UnsupportedVerbs = options.Verifier.UnsupportedVerbs() summary.EndTime = time.Now() return summary, nil } @@ -770,19 +772,6 @@ func captureMetrics(ctx context.Context, options Options, logger *slog.Logger, s } } -// nextActionFromV8 invokes the V8-side action generator and decodes the -// resulting JSON into a verifier.Action. ErrNoAction is returned when the -// generator declined to act this tick. -func nextActionFromV8(ctx context.Context, web driver.WebDriver) (verifier.Action, error) { - raw, err := web.NextActionFromV8(ctx) - if err != nil { - return verifier.Action{}, fmt.Errorf("v8 next action: %w", err) - } - // Both engines emit the unified flat camelCase wire contract; one decoder - // reads it. A null payload means the generator declined to act this tick. - return verifier.DecodeAction(raw) -} - // collectWitnesses gathers the violation witness for each newly-violated // property, logs its cause, and returns them keyed by property name for the // trace. Properties without a captured witness are skipped. diff --git a/internal/runner/source.go b/internal/runner/source.go new file mode 100644 index 0000000..f2a7367 --- /dev/null +++ b/internal/runner/source.go @@ -0,0 +1,72 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/priyanshujain/sanderling/internal/driver" + "github.com/priyanshujain/sanderling/internal/verifier" +) + +// ActionSource resolves the next action for a step. Both runtimes (the goja +// picker and the web/V8 picker) implement it so the runner loop has one path +// and no per-step driver type assertion. NextAction returns verifier.ErrNoAction +// when the picker declined to act this tick. +type ActionSource interface { + NextAction(ctx context.Context) (verifier.Action, error) +} + +// ExtractorSource yields per-step extractor overrides the runner applies after +// PushSnapshot. The mobile path has none (returns nil); the web path returns the +// values its extractors computed in V8 against the real DOM. +type ExtractorSource interface { + ExtractorOverrides(ctx context.Context) (map[int]json.RawMessage, error) +} + +// gojaSource drives both action selection and (trivially) extractor overrides +// for the mobile path, where the goja-bundled picker runs in-process and no V8 +// extractor values exist. +type gojaSource struct { + verifier *verifier.Verifier +} + +func (s gojaSource) NextAction(context.Context) (verifier.Action, error) { + return s.verifier.NextAction() +} + +func (gojaSource) ExtractorOverrides(context.Context) (map[int]json.RawMessage, error) { + return nil, nil +} + +// webSource adapts the chrome driver's V8 picker: it evaluates the bundled +// __sanderlingNextAction__ / __sanderlingExtractors__ globals and decodes their +// JSON into the unified action and override shapes. +type webSource struct { + web driver.WebDriver +} + +func (s webSource) NextAction(ctx context.Context) (verifier.Action, error) { + raw, err := s.web.NextActionFromV8(ctx) + if err != nil { + return verifier.Action{}, fmt.Errorf("v8 next action: %w", err) + } + // Both engines emit the unified flat camelCase wire contract; one decoder + // reads it. A null payload means the generator declined to act this tick. + return verifier.DecodeAction(raw) +} + +func (s webSource) ExtractorOverrides(ctx context.Context) (map[int]json.RawMessage, error) { + return s.web.EvaluateExtractors(ctx) +} + +// pickSources selects the runtime's action and extractor sources ONCE at setup +// from the driver's capabilities, so the step loop never type-asserts. +func pickSources(options Options) (ActionSource, ExtractorSource) { + if web, ok := options.Driver.(driver.WebDriver); ok { + source := webSource{web: web} + return source, source + } + source := gojaSource{verifier: options.Verifier} + return source, source +}