From 68d5633d1d5362f2a22365040a367a902c615108 Mon Sep 17 00:00:00 2001 From: PJ Date: Thu, 13 Aug 2026 21:45:30 +0530 Subject: [PATCH] fix: bound the pre-run app launch launch happens before the runner starts, so --duration never covered it and a wedged driver hung with no trace and no error. --- internal/testrun/testrun.go | 24 ++++++++++++++++-- internal/testrun/testrun_test.go | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/internal/testrun/testrun.go b/internal/testrun/testrun.go index 86b753b..af57b28 100644 --- a/internal/testrun/testrun.go +++ b/internal/testrun/testrun.go @@ -20,6 +20,26 @@ import ( const sidecarStartupTimeout = 30 * time.Second +// launchTimeout bounds the pre-run app launch. It happens before the runner +// starts, so --duration does not cover it, and Execute's context is the bare +// signal-aware root with no deadline of its own: a driver wedged here would +// hang the run forever having printed nothing and written no trace. Generous +// enough to sit above every driver's own launch bound (the iOS clear-state path +// reinstalls the app first) so a driver-level error is what a user usually +// sees, and this stays the backstop. A variable so the timeout test can shrink +// it. +var launchTimeout = 3 * time.Minute + +// launchApp starts the app under test under a bounded context. +func launchApp(ctx context.Context, activeDriver driver.DeviceDriver, options Options) error { + launchCtx, cancel := context.WithTimeout(ctx, launchTimeout) + defer cancel() + if err := activeDriver.Launch(launchCtx, options.BundleID, options.ClearData, nil); err != nil { + return fmt.Errorf("launch app: %w", err) + } + return nil +} + // Options are the parameters for a single test pipeline run. type Options struct { Spec string @@ -145,8 +165,8 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error { } defer cleanup() - if err := activeDriver.Launch(ctx, options.BundleID, options.ClearData, nil); err != nil { - return fmt.Errorf("launch app: %w", err) + if err := launchApp(ctx, activeDriver, options); err != nil { + return err } if web, ok := activeDriver.(driver.WebDriver); ok && len(webBundle.JavaScript) > 0 { diff --git a/internal/testrun/testrun_test.go b/internal/testrun/testrun_test.go index d5658ad..bc3f376 100644 --- a/internal/testrun/testrun_test.go +++ b/internal/testrun/testrun_test.go @@ -1,6 +1,7 @@ package testrun import ( + "context" "errors" "os" "path/filepath" @@ -8,6 +9,7 @@ import ( "testing" "time" + "github.com/priyanshujain/sanderling/internal/driver" "github.com/priyanshujain/sanderling/internal/runner" "github.com/priyanshujain/sanderling/internal/verifier" ) @@ -261,3 +263,44 @@ func TestRunOutcome_ReportsViolationsOnlyUnderTheFlag(t *testing.T) { t.Errorf("count: got %d, want 1", violations.Count) } } + +// wedgedLaunchDriver never returns from Launch, standing in for a driver whose +// device-side session is stuck. +type wedgedLaunchDriver struct { + driver.DeviceDriver + release chan struct{} +} + +func (w *wedgedLaunchDriver) Launch(ctx context.Context, _ string, _ bool, _ map[string]string) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-w.release: + return nil + } +} + +// TestLaunchAppBoundsWedgedDriver proves the pre-run launch carries a deadline. +// It runs before the runner starts, so --duration does not cover it and +// Execute's root context has no deadline: unbounded, a wedged driver hangs the +// run forever with no trace directory and no error. +func TestLaunchAppBoundsWedgedDriver(t *testing.T) { + previous := launchTimeout + launchTimeout = 100 * time.Millisecond + defer func() { launchTimeout = previous }() + + wedged := &wedgedLaunchDriver{release: make(chan struct{})} + defer close(wedged.release) + + done := make(chan error, 1) + go func() { done <- launchApp(context.Background(), wedged, Options{BundleID: "com.example.app"}) }() + + select { + case err := <-done: + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("err = %v, want a deadline-exceeded error", err) + } + case <-time.After(10 * time.Second): + t.Fatal("launchApp never returned: the pre-run launch is unbounded, so a wedged driver hangs the run forever") + } +}