mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(runner): relaunch app when foreground escapes during exploration
This commit is contained in:
1 parent
dea39d3bac
commit
dff91095b8
3 files changed
+111
No files matched your search
@@ -55,6 +55,12 @@ type Driver struct {
|
||||
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
|
||||
}
|
||||
|
||||
func New() *Driver {
|
||||
@@ -96,6 +102,20 @@ func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool, _ m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) ForegroundApp(_ context.Context) (string, error) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
if len(d.ForegroundResults) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
index := d.foregroundIndex
|
||||
if index >= len(d.ForegroundResults) {
|
||||
index = len(d.ForegroundResults) - 1
|
||||
}
|
||||
d.foregroundIndex++
|
||||
return d.ForegroundResults[index], nil
|
||||
}
|
||||
|
||||
func (d *Driver) Terminate(ctx context.Context) error {
|
||||
if err := d.failure(ActionTerminate); err != nil {
|
||||
return err
|
||||
|
||||
@@ -67,6 +67,14 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
stepIndex++
|
||||
stepStart := time.Now()
|
||||
|
||||
// Keep exploration scoped to the app under test. If a prior action
|
||||
// backed out of (or otherwise left) the app, relaunch it before we
|
||||
// observe or act, so properties never evaluate against a foreign app
|
||||
// and actions never land outside the app.
|
||||
if ensureForeground(ctx, options, logger, stepIndex) {
|
||||
lastAction = nil
|
||||
}
|
||||
|
||||
// Hierarchy, metrics, and logs are independent device reads — run
|
||||
// them concurrently so metrics+logs hide behind the hierarchy fetch.
|
||||
var tree *hierarchy.Tree
|
||||
@@ -263,6 +271,36 @@ func violationNames(verdicts map[string]ltl.Verdict) []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// ensureForeground keeps the app under test in the foreground. When the driver
|
||||
// can report the foreground app and it no longer matches the bundle under test,
|
||||
// the app is relaunched. Returns true when a relaunch happened so the caller
|
||||
// can drop the now-stale lastAction. Drivers without ForegroundChecker (web,
|
||||
// iOS) are a no-op.
|
||||
func ensureForeground(ctx context.Context, options Options, logger *slog.Logger, stepIndex int) bool {
|
||||
checker, ok := options.Driver.(driver.ForegroundChecker)
|
||||
if !ok || options.BundleID == "" {
|
||||
return false
|
||||
}
|
||||
foreground, err := checker.ForegroundApp(ctx)
|
||||
if err != nil {
|
||||
logger.Warn("foreground check failed", "step", stepIndex, "err", err)
|
||||
return false
|
||||
}
|
||||
if foreground == "" || foreground == options.BundleID {
|
||||
return false
|
||||
}
|
||||
logger.Warn("app left foreground; relaunching",
|
||||
"step", stepIndex, "foreground", foreground, "want", options.BundleID)
|
||||
if err := options.Driver.Launch(ctx, options.BundleID, false, nil); err != nil {
|
||||
logger.Warn("relaunch failed", "step", stepIndex, "err", err)
|
||||
return false
|
||||
}
|
||||
idleCtx, cancel := context.WithTimeout(ctx, options.IdleTimeout)
|
||||
_ = options.Driver.WaitForIdle(idleCtx, options.IdleTimeout)
|
||||
cancel()
|
||||
return true
|
||||
}
|
||||
|
||||
func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.Action, tree *hierarchy.Tree) error {
|
||||
switch action.Kind {
|
||||
case verifier.ActionKindTap:
|
||||
|
||||
@@ -426,3 +426,56 @@ func containsProperty(records []ViolationRecord, property string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestRunner_RelaunchesWhenAppLeavesForeground(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
// Always report a foreign app, so every step's guard must relaunch.
|
||||
state.mock.ForegroundResults = []string{"com.android.chrome"}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, err := Run(ctx, Options{
|
||||
Duration: 100 * time.Millisecond,
|
||||
IdleTimeout: 20 * time.Millisecond,
|
||||
BundleID: "app.folio",
|
||||
Driver: state.mock,
|
||||
Verifier: state.verifier,
|
||||
TraceWriter: state.writer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
relaunches := 0
|
||||
for _, a := range state.mock.Actions() {
|
||||
if a.Kind == mockdriver.ActionLaunch && a.BundleID == "app.folio" && !a.ClearState {
|
||||
relaunches++
|
||||
}
|
||||
}
|
||||
if relaunches == 0 {
|
||||
t.Fatal("expected runner to relaunch app.folio when foreground escaped, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunner_NoRelaunchWhenAppInForeground(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
state.mock.ForegroundResults = []string{"app.folio"}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, err := Run(ctx, Options{
|
||||
Duration: 100 * time.Millisecond,
|
||||
IdleTimeout: 20 * time.Millisecond,
|
||||
BundleID: "app.folio",
|
||||
Driver: state.mock,
|
||||
Verifier: state.verifier,
|
||||
TraceWriter: state.writer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
for _, a := range state.mock.Actions() {
|
||||
if a.Kind == mockdriver.ActionLaunch {
|
||||
t.Fatalf("expected no relaunch while app in foreground, got %v", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user