mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 03:27:10 +00:00
The chrome tab is single-threaded so its Snapshot pairs the two reads without extra locking. The mock records ActionSnapshot so tests can assert the runner reaches for the paired RPC.
291 lines
7.6 KiB
Go
291 lines
7.6 KiB
Go
package mock
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/driver"
|
|
)
|
|
|
|
type ActionKind string
|
|
|
|
const (
|
|
ActionLaunch ActionKind = "launch"
|
|
ActionTerminate ActionKind = "terminate"
|
|
ActionTap ActionKind = "tap"
|
|
ActionTapSelector ActionKind = "tap_selector"
|
|
ActionInputText ActionKind = "input_text"
|
|
ActionSwipe ActionKind = "swipe"
|
|
ActionPressKey ActionKind = "press_key"
|
|
ActionHierarchy ActionKind = "hierarchy"
|
|
ActionScreenshot ActionKind = "screenshot"
|
|
ActionSnapshot ActionKind = "snapshot"
|
|
ActionRecentLogs ActionKind = "recent_logs"
|
|
ActionWaitForIdle ActionKind = "wait_for_idle"
|
|
ActionHealth ActionKind = "health"
|
|
ActionMetrics ActionKind = "metrics"
|
|
)
|
|
|
|
type Action struct {
|
|
Kind ActionKind
|
|
BundleID string
|
|
ClearState bool
|
|
X, Y int
|
|
FromX, FromY int
|
|
ToX, ToY int
|
|
Duration time.Duration
|
|
Selector string
|
|
Text string
|
|
Key string
|
|
LogLevel string
|
|
LogSince time.Time
|
|
Idle time.Duration
|
|
}
|
|
|
|
// Driver is an in-memory Driver implementation for unit tests.
|
|
// Tests can program HierarchyJSON, ImageData, HealthInfo, and per-method
|
|
// Failures, and read back Actions to assert what the runner asked for.
|
|
type Driver struct {
|
|
mutex sync.Mutex
|
|
actions []Action
|
|
|
|
HierarchyJSON string
|
|
ImageData driver.Image
|
|
HealthInfo driver.Health
|
|
LogEntries []driver.LogEntry
|
|
MetricsData driver.Metrics
|
|
Failures map[ActionKind]error
|
|
|
|
// ForegroundResults is consumed one entry per ForegroundApp call (the
|
|
// last entry repeats). Empty yields "", which disables the runner's
|
|
// app-scope guard so tests that don't care are unaffected.
|
|
ForegroundResults []string
|
|
foregroundIndex int
|
|
|
|
// FocusedWindowResults is consumed one entry per FocusedWindowApp call
|
|
// (the last entry repeats). When empty, FocusedWindowApp mirrors the
|
|
// last ForegroundApp result, so the startup gate treats the window as
|
|
// already drawn and tests that don't care are unaffected.
|
|
FocusedWindowResults []string
|
|
focusedWindowIndex int
|
|
focusedWindowCalls int
|
|
lastForeground string
|
|
}
|
|
|
|
func New() *Driver {
|
|
return &Driver{
|
|
Failures: map[ActionKind]error{},
|
|
HealthInfo: driver.Health{
|
|
Ready: true,
|
|
Version: "mock",
|
|
Platform: "android",
|
|
},
|
|
HierarchyJSON: `{"children":[]}`,
|
|
ImageData: driver.Image{PNG: []byte{}, Width: 0, Height: 0},
|
|
}
|
|
}
|
|
|
|
func (d *Driver) Actions() []Action {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return append([]Action(nil), d.actions...)
|
|
}
|
|
|
|
func (d *Driver) record(action Action) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
d.actions = append(d.actions, action)
|
|
}
|
|
|
|
func (d *Driver) failure(kind ActionKind) error {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.Failures[kind]
|
|
}
|
|
|
|
func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool, _ map[string]string) error {
|
|
if err := d.failure(ActionLaunch); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionLaunch, BundleID: bundleID, ClearState: clearState})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) ForegroundApp(_ context.Context) (string, error) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
if len(d.ForegroundResults) == 0 {
|
|
d.lastForeground = ""
|
|
return "", nil
|
|
}
|
|
index := d.foregroundIndex
|
|
if index >= len(d.ForegroundResults) {
|
|
index = len(d.ForegroundResults) - 1
|
|
}
|
|
d.foregroundIndex++
|
|
d.lastForeground = d.ForegroundResults[index]
|
|
return d.lastForeground, nil
|
|
}
|
|
|
|
func (d *Driver) FocusedWindowApp(_ context.Context) (string, error) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
d.focusedWindowCalls++
|
|
if len(d.FocusedWindowResults) == 0 {
|
|
return d.lastForeground, nil
|
|
}
|
|
index := d.focusedWindowIndex
|
|
if index >= len(d.FocusedWindowResults) {
|
|
index = len(d.FocusedWindowResults) - 1
|
|
}
|
|
d.focusedWindowIndex++
|
|
return d.FocusedWindowResults[index], nil
|
|
}
|
|
|
|
// FocusedWindowCalls reports how many times FocusedWindowApp has been called.
|
|
func (d *Driver) FocusedWindowCalls() int {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.focusedWindowCalls
|
|
}
|
|
|
|
func (d *Driver) Terminate(ctx context.Context) error {
|
|
if err := d.failure(ActionTerminate); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionTerminate})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) Tap(ctx context.Context, x, y int) error {
|
|
if err := d.failure(ActionTap); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionTap, X: x, Y: y})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) TapSelector(ctx context.Context, selector string) error {
|
|
if err := d.failure(ActionTapSelector); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionTapSelector, Selector: selector})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) InputText(ctx context.Context, text string) error {
|
|
if err := d.failure(ActionInputText); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionInputText, Text: text})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) Swipe(ctx context.Context, fromX, fromY, toX, toY int, duration time.Duration) error {
|
|
if err := d.failure(ActionSwipe); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{
|
|
Kind: ActionSwipe,
|
|
FromX: fromX,
|
|
FromY: fromY,
|
|
ToX: toX,
|
|
ToY: toY,
|
|
Duration: duration,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) PressKey(ctx context.Context, key string) error {
|
|
if err := d.failure(ActionPressKey); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionPressKey, Key: key})
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) RecentLogs(ctx context.Context, since time.Time, minLevel string) ([]driver.LogEntry, error) {
|
|
if err := d.failure(ActionRecentLogs); err != nil {
|
|
return nil, err
|
|
}
|
|
d.record(Action{Kind: ActionRecentLogs, LogSince: since, LogLevel: minLevel})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return append([]driver.LogEntry(nil), d.LogEntries...), nil
|
|
}
|
|
|
|
func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
|
|
if err := d.failure(ActionHierarchy); err != nil {
|
|
return "", err
|
|
}
|
|
d.record(Action{Kind: ActionHierarchy})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.HierarchyJSON, nil
|
|
}
|
|
|
|
func (d *Driver) Screenshot(ctx context.Context) (driver.Image, error) {
|
|
if err := d.failure(ActionScreenshot); err != nil {
|
|
return driver.Image{}, err
|
|
}
|
|
d.record(Action{Kind: ActionScreenshot})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.ImageData, nil
|
|
}
|
|
|
|
// Snapshot returns the hierarchy + screenshot pair atomically, mirroring
|
|
// the real driver's contract. It records a single ActionSnapshot so tests
|
|
// can assert the runner reached for the paired RPC instead of racing the
|
|
// two reads.
|
|
func (d *Driver) Snapshot(ctx context.Context) (string, driver.Image, error) {
|
|
if err := d.failure(ActionSnapshot); err != nil {
|
|
return "", driver.Image{}, err
|
|
}
|
|
d.record(Action{Kind: ActionSnapshot})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.HierarchyJSON, d.ImageData, nil
|
|
}
|
|
|
|
func (d *Driver) WaitForIdle(ctx context.Context, duration time.Duration) error {
|
|
if err := d.failure(ActionWaitForIdle); err != nil {
|
|
return err
|
|
}
|
|
d.record(Action{Kind: ActionWaitForIdle, Idle: duration})
|
|
if duration <= 0 {
|
|
return nil
|
|
}
|
|
timer := time.NewTimer(duration)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (d *Driver) Health(ctx context.Context) (driver.Health, error) {
|
|
if err := d.failure(ActionHealth); err != nil {
|
|
return driver.Health{}, err
|
|
}
|
|
d.record(Action{Kind: ActionHealth})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.HealthInfo, nil
|
|
}
|
|
|
|
func (d *Driver) Metrics(ctx context.Context, bundleID string) (driver.Metrics, error) {
|
|
if err := d.failure(ActionMetrics); err != nil {
|
|
return driver.Metrics{}, err
|
|
}
|
|
d.record(Action{Kind: ActionMetrics, BundleID: bundleID})
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
return d.MetricsData, nil
|
|
}
|
|
|
|
var _ driver.DeviceDriver = (*Driver)(nil)
|