Files
sanderling/internal/testrun/driver_test.go
T

224 lines
7.3 KiB
Go

package testrun
import (
"context"
"errors"
"io"
"strings"
"testing"
"time"
"github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion"
"github.com/priyanshujain/sanderling/internal/ios"
)
// stubDeviceDriver is a no-op DeviceDriver so routing tests never build or spawn
// a real runner. Only construction wiring is under test.
type stubDeviceDriver struct{ driver.DeviceDriver }
func TestBuildDriverRoutesPhysicalIOSToDeviceDriver(t *testing.T) {
stubPreflight(t)
original := newDeviceDriver
t.Cleanup(func() { newDeviceDriver = original })
var got ioscompanion.DeviceOptions
closed := false
newDeviceDriver = func(_ context.Context, options ioscompanion.DeviceOptions) (driver.DeviceDriver, func(), error) {
got = options
return stubDeviceDriver{}, func() { closed = true }, nil
}
options := Options{Platform: "ios", BundleID: "app.folio", IosAppPath: "/tmp/iosApp.app"}
options.iosIsSimulator = false
options.iosUDID = "00008140-HW"
options.iosCoreDeviceID = "CORE-1"
d, cleanup, err := buildDriver(context.Background(), options, io.Discard)
if err != nil {
t.Fatalf("buildDriver: %v", err)
}
if d == nil {
t.Fatal("expected a device driver")
}
if got.HardwareUDID != "00008140-HW" || got.CoreDeviceID != "CORE-1" {
t.Fatalf("DeviceOptions ids = %+v, want the resolved hardware/core ids", got)
}
if got.BundleID != "app.folio" || got.AppPath != "/tmp/iosApp.app" {
t.Fatalf("DeviceOptions = %+v, want bundle and app path threaded through", got)
}
cleanup()
if !closed {
t.Fatal("cleanup must close the device driver")
}
}
func TestBuildDriverSurfacesDeviceConstructionError(t *testing.T) {
stubPreflight(t)
original := newDeviceDriver
t.Cleanup(func() { newDeviceDriver = original })
newDeviceDriver = func(context.Context, ioscompanion.DeviceOptions) (driver.DeviceDriver, func(), error) {
return nil, nil, errors.New("no signing creds")
}
options := Options{Platform: "ios"}
options.iosIsSimulator = false
if _, _, err := buildDriver(context.Background(), options, io.Discard); err == nil {
t.Fatal("expected the device construction error to surface")
}
}
// A sidecar that dies during startup leaves the health poll with nothing to
// talk to, and reporting that as a deadline sends the reader after a gRPC
// timeout instead of the exit that already happened.
func TestAwaitSidecarReportsTheExitItSaw(t *testing.T) {
exited := make(chan error, 1)
exited <- errors.New("exit status 1")
close(exited)
err := awaitSidecar(
context.Background(),
"127.0.0.1:54321",
30*time.Second,
func(ctx context.Context) error { <-ctx.Done(); return ctx.Err() },
exited,
)
if err == nil {
t.Fatal("expected an error when the sidecar exits before it is healthy")
}
for _, want := range []string{
"sidecar exited before it answered a health check on 127.0.0.1:54321: exit status 1",
"check the sidecar output above",
"sanderling doctor --platform=android",
} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error %q missing %q", err, want)
}
}
}
func TestAwaitSidecarTimeoutSaysOnlyWhatItObserved(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := awaitSidecar(
ctx,
"127.0.0.1:54321",
50*time.Millisecond,
func(ctx context.Context) error { <-ctx.Done(); return ctx.Err() },
make(chan error, 1),
)
if err == nil {
t.Fatal("expected an error when the sidecar never answers")
}
for _, want := range []string{
"sidecar did not answer a health check on 127.0.0.1:54321 within 50ms and is still running",
"check the sidecar output above",
"sanderling doctor --platform=android",
} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error %q missing %q", err, want)
}
}
if strings.Contains(err.Error(), "context deadline exceeded") {
t.Errorf("error %q must not hand the reader a bare gRPC deadline", err)
}
}
func TestAwaitSidecarHealthyReturnsNil(t *testing.T) {
err := awaitSidecar(
context.Background(),
"127.0.0.1:54321",
30*time.Second,
func(context.Context) error { return nil },
make(chan error, 1),
)
if err != nil {
t.Fatalf("expected a healthy sidecar to pass, got %v", err)
}
}
// stubPreflight bypasses the host-readiness checks so routing tests exercise
// driver construction on a Linux CI runner that lacks xcrun/java.
func stubPreflight(t *testing.T) {
t.Helper()
original := preflight
t.Cleanup(func() { preflight = original })
preflight = func(context.Context, string) error { return nil }
}
func swapIOSResolveSeams(t *testing.T) {
t.Helper()
origTarget, origDevice, origEnsure := iosResolveTarget, iosResolveDevice, iosEnsureSimulator
t.Cleanup(func() {
iosResolveTarget, iosResolveDevice, iosEnsureSimulator = origTarget, origDevice, origEnsure
})
}
func TestResolveIOSTargetPhysicalDeviceFillsIDs(t *testing.T) {
swapIOSResolveSeams(t)
iosResolveTarget = func(_ context.Context, query string) (string, bool, error) {
return query, false, nil
}
resolveDeviceCalled := ""
iosResolveDevice = func(_ context.Context, query string) (ios.Device, error) {
resolveDeviceCalled = query
return ios.Device{Name: "iPhone", HardwareUDID: "00008140-HW", CoreDeviceID: "CORE-1"}, nil
}
iosEnsureSimulator = func(context.Context, string, io.Writer) error {
t.Fatal("must not boot a simulator on the device path")
return nil
}
options := Options{Platform: "ios", IosDevice: "iPhone"}
resolved, err := resolveIOSTarget(context.Background(), options, io.Discard)
if err != nil {
t.Fatal(err)
}
if resolveDeviceCalled != "iPhone" {
t.Fatalf("ResolveDevice query = %q, want iPhone", resolveDeviceCalled)
}
if resolved.iosIsSimulator {
t.Fatal("device target must not be marked a simulator")
}
if resolved.iosUDID != "00008140-HW" || resolved.iosCoreDeviceID != "CORE-1" {
t.Fatalf("resolved ids = (%q, %q), want hardware/core ids", resolved.iosUDID, resolved.iosCoreDeviceID)
}
}
func TestResolveIOSTargetSimulatorSkipsDeviceResolution(t *testing.T) {
swapIOSResolveSeams(t)
iosResolveTarget = func(context.Context, string) (string, bool, error) {
return "sim-udid", true, nil
}
ensured := false
iosEnsureSimulator = func(context.Context, string, io.Writer) error { ensured = true; return nil }
iosResolveDevice = func(context.Context, string) (ios.Device, error) {
t.Fatal("simulator path must not resolve a physical device")
return ios.Device{}, nil
}
options := Options{Platform: "ios", IosDevice: "iPhone 17 Pro"}
resolved, err := resolveIOSTarget(context.Background(), options, io.Discard)
if err != nil {
t.Fatal(err)
}
if !ensured {
t.Fatal("simulator path must ensure the simulator is booted")
}
if !resolved.iosIsSimulator || resolved.iosUDID != "sim-udid" {
t.Fatalf("resolved = %+v, want the booted simulator", resolved)
}
}
func TestResolveIOSTargetDeviceResolutionErrorSurfaces(t *testing.T) {
swapIOSResolveSeams(t)
iosResolveTarget = func(_ context.Context, query string) (string, bool, error) { return query, false, nil }
iosResolveDevice = func(context.Context, string) (ios.Device, error) {
return ios.Device{}, errors.New("no device connected")
}
options := Options{Platform: "ios", IosDevice: "iPhone"}
if _, err := resolveIOSTarget(context.Background(), options, io.Discard); err == nil {
t.Fatal("expected the device-resolution error to surface")
}
}