mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Snapshot is not atomic on any driver, only the sidecar takes a lock. An empty minLevel is not a default: the three implementations genuinely disagree, so the comment names that and says the runner always passes one. HeapBytes is RSS only on Android. The startup gate uses FocusedWindowChecker on top of ForegroundChecker, never instead. WebAction names no type in the tree. Run relaunches the app it says the caller must launch, and the errgroup propagates no error because all three goroutines return nil.
214 lines
9.5 KiB
Go
214 lines
9.5 KiB
Go
// Package driver defines the platform-agnostic device automation interface and shared types.
|
|
package driver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ErrGestureUndelivered reports a coordinate gesture that reached no element at
|
|
// all, so the app cannot have responded to it. It is not a device fault and
|
|
// says nothing about the device's health: the runner records it on the step
|
|
// rather than counting it toward the apply-failure streak, which is what makes
|
|
// a gesture that did nothing distinguishable from one the app ignored.
|
|
var ErrGestureUndelivered = errors.New("gesture reached no element")
|
|
|
|
// ErrSelectorMatchedNothing reports an action dispatched by selector whose
|
|
// selector named nothing on the current screen. It is a resolution failure, not
|
|
// a delivery one: no point was ever computed, so it stays separate from
|
|
// ErrGestureUndelivered and the runner records it as an unresolved selector.
|
|
var ErrSelectorMatchedNothing = errors.New("selector matched no element")
|
|
|
|
// ErrNotSupported reports a contract method the driver has no way to answer on
|
|
// its platform. It is neither a device fault nor an observation: a caller that
|
|
// reads the zero value as one records a reading it never took, which is what
|
|
// lets a property over a channel the driver never opened hold vacuously with
|
|
// nothing in the run's output saying so. Callers check it with errors.Is and
|
|
// record that the read did not happen.
|
|
var ErrNotSupported = errors.New("driver: capability not supported")
|
|
|
|
// DeviceDriver abstracts the platform-specific UI automation backend. v0.1
|
|
// surface matches proto/driverpb/driver.proto. The sidecar implementation
|
|
// lives under driver/sidecar; the web implementation under driver/chrome;
|
|
// tests use driver/mock.
|
|
type DeviceDriver interface {
|
|
Launch(ctx context.Context, bundleID string, clearState bool, env map[string]string) error
|
|
Terminate(ctx context.Context) error
|
|
|
|
Tap(ctx context.Context, x, y int) error
|
|
TapSelector(ctx context.Context, selector string) error
|
|
DoubleTap(ctx context.Context, x, y int) error
|
|
DoubleTapSelector(ctx context.Context, selector string) error
|
|
InputText(ctx context.Context, text string) error
|
|
// EraseText deletes characterCount characters from the focused field.
|
|
// The runner calls it before InputText so the verb replaces existing
|
|
// content instead of appending to it.
|
|
EraseText(ctx context.Context, characterCount int) error
|
|
Swipe(ctx context.Context, fromX, fromY, toX, toY int, duration time.Duration) error
|
|
PressKey(ctx context.Context, key string) error
|
|
LongPress(ctx context.Context, x, y int) error
|
|
|
|
Hierarchy(ctx context.Context) (string, error)
|
|
Screenshot(ctx context.Context) (Image, error)
|
|
// Snapshot returns the hierarchy and screenshot as one paired capture,
|
|
// the closest a driver can put them and the reason to prefer it over
|
|
// separate Hierarchy and Screenshot reads. It is not atomic: no driver
|
|
// freezes the frame while the two reads run.
|
|
Snapshot(ctx context.Context) (string, Image, error)
|
|
// RecentLogs returns log entries at or after `since`, filtered to
|
|
// `minLevel` or above. Drivers disagree on an empty minLevel (sidecar
|
|
// defaults to "E", Chrome returns every level), so the runner always
|
|
// passes one. A driver with no log source returns ErrNotSupported, not
|
|
// an empty slice: only one of the two means the app logged nothing.
|
|
RecentLogs(ctx context.Context, since time.Time, minLevel string) ([]LogEntry, error)
|
|
|
|
WaitForIdle(ctx context.Context, duration time.Duration) error
|
|
// Health reports whether the backend is attached and serving. A driver
|
|
// that runs no readiness check returns ErrNotSupported; Ready is a
|
|
// verdict, so reporting it true without one tells the caller a check
|
|
// passed that never ran.
|
|
Health(ctx context.Context) (Health, error)
|
|
// Metrics samples the app's memory, plus CPU where the platform exposes
|
|
// it (Chrome does not). CPUPercent is percent of a single core, so
|
|
// multi-core apps can exceed 100. HeapBytes and TotalMemoryBytes are the
|
|
// platform's nearest pair: RSS and virtual size on Android, used and
|
|
// total JS heap on Chrome. A driver that cannot sample returns
|
|
// ErrNotSupported rather than a zeroed sample.
|
|
Metrics(ctx context.Context, bundleID string) (Metrics, error)
|
|
}
|
|
|
|
// ForegroundChecker is the optional capability for reporting which app is
|
|
// currently in the foreground. The runner uses it to keep exploration scoped
|
|
// to the app under test: when an action backs out of (or otherwise leaves) the
|
|
// app, the runner relaunches it before acting again. Drivers that cannot
|
|
// determine the foreground app simply do not implement this interface.
|
|
type ForegroundChecker interface {
|
|
// ForegroundApp returns the bundle id / package of the app currently in
|
|
// the foreground. An empty string means "unknown" and the runner skips
|
|
// enforcement for that step rather than relaunching blindly.
|
|
ForegroundApp(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// Scroller is the optional capability for drivers whose scroll interaction is
|
|
// not a finger drag. On a touch device the two are the same gesture, so a
|
|
// driver that does not implement this gets its Scroll actions as a Swipe. A
|
|
// browser scrolls on wheel input instead, and treats a drag as a drag.
|
|
type Scroller interface {
|
|
// Scroll moves the content under (fromX, fromY) by the vector to the
|
|
// destination point, the same endpoints Swipe takes.
|
|
Scroll(
|
|
ctx context.Context,
|
|
fromX, fromY, toX, toY int,
|
|
duration time.Duration,
|
|
) error
|
|
}
|
|
|
|
// TextReplacer is the optional capability for drivers whose InputText already
|
|
// replaces the field's content instead of appending to it. The runner must
|
|
// skip its pre-erase for such drivers: the erase would be a redundant
|
|
// round-trip on every InputText.
|
|
type TextReplacer interface {
|
|
// ReplacesTextOnInput reports whether InputText replaces existing
|
|
// content, making the runner's pre-erase unnecessary.
|
|
ReplacesTextOnInput() bool
|
|
}
|
|
|
|
// FocusedWindowChecker is the optional capability for reporting which app owns
|
|
// the focused (on-screen) window. The startup gate uses it on top of
|
|
// ForegroundChecker, never instead: the resumed-activity signal flips to a
|
|
// freshly launched app before its first frame draws, so a gate on that alone
|
|
// can let the first observe read the previous app's screen.
|
|
type FocusedWindowChecker interface {
|
|
// FocusedWindowApp returns the package owning the focused window, or ""
|
|
// when no window is focused yet (e.g. mid-launch transition).
|
|
FocusedWindowApp(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// LogEntry is one line of device log. Level is logcat's single-letter scale on
|
|
// every platform: "V", "D", "I", "W", "E", "F", ordered as written. The runner
|
|
// fetches at "E" and the default properties count entries whose level equals
|
|
// "E", so a driver that spells a level any other way empties the channel
|
|
// without failing anything: the entries never arrive and every property reading
|
|
// state.logs holds vacuously.
|
|
type LogEntry struct {
|
|
UnixMillis int64
|
|
Level string
|
|
Tag string
|
|
Message string
|
|
}
|
|
|
|
// ExceptionReporter is the optional capability for reporting the uncaught
|
|
// errors an app has captured so far. The runner feeds them to state.exceptions,
|
|
// which the default noUncaughtExceptions property reads. Drivers with no way to
|
|
// observe them simply do not implement it and the property stays vacuous there.
|
|
type ExceptionReporter interface {
|
|
Exceptions(ctx context.Context) ([]Exception, error)
|
|
}
|
|
|
|
// NavigationReporter is the optional capability for reporting the
|
|
// document-replacing navigations seen since the last call. A navigation
|
|
// restarts the app's own runtime, so a trace without them cannot separate an
|
|
// app that reloaded from a generator that repeated itself.
|
|
type NavigationReporter interface {
|
|
Navigations(ctx context.Context) ([]Navigation, error)
|
|
}
|
|
|
|
// Navigation is one document-replacing navigation: a reload, a form submit, a
|
|
// route change that swapped the document.
|
|
type Navigation struct {
|
|
URL string
|
|
UnixMillis int64
|
|
}
|
|
|
|
// Exception is one uncaught throwable the app captured.
|
|
type Exception struct {
|
|
Class string
|
|
Message string
|
|
StackTrace string
|
|
UnixMillis int64
|
|
}
|
|
|
|
type Image struct {
|
|
PNG []byte
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
type Health struct {
|
|
Ready bool
|
|
Version string
|
|
Platform string
|
|
}
|
|
|
|
type Metrics struct {
|
|
CPUPercent float64
|
|
HeapBytes int64
|
|
TotalMemoryBytes int64
|
|
}
|
|
|
|
// WebDriver is the optional capability surface exposed by the chrome driver
|
|
// for the V8-native tick path. The runner type-asserts on this interface;
|
|
// mobile drivers stay binary-compatible by simply not implementing it.
|
|
//
|
|
// Element references never cross V8/host. V8 serializes targets as {x, y}
|
|
// (or bounds) into the returned action JSON; the host dispatches via the
|
|
// normal DeviceDriver methods (Tap, InputText, etc.).
|
|
type WebDriver interface {
|
|
// InstallBundle injects the given JS source so it runs once per
|
|
// freshly-navigated document, plus immediately in the current page.
|
|
// The bundle is expected to register globals
|
|
// `__sanderlingExtractors__` and `__sanderlingNextAction__` on
|
|
// `window`.
|
|
InstallBundle(ctx context.Context, source []byte) error
|
|
// EvaluateExtractors invokes the extractor table installed by the
|
|
// bundle and returns each extractor's JSON-encoded current value
|
|
// keyed by its registration index.
|
|
EvaluateExtractors(ctx context.Context) (map[int]json.RawMessage, error)
|
|
// NextActionFromV8 invokes the action generator installed by the
|
|
// bundle and returns the resulting Action JSON for the host to
|
|
// dispatch. The shape mirrors verifier.Action's JSON form.
|
|
NextActionFromV8(ctx context.Context) (json.RawMessage, error)
|
|
}
|