From bc1b71f5e26c9f828c4e547b6ed4cdbc64108361 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 23:20:03 +0530 Subject: [PATCH] test(ioscompanion): add gated simulator integration smoke test --- .../transport/integration_test.go | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 internal/driver/ioscompanion/transport/integration_test.go diff --git a/internal/driver/ioscompanion/transport/integration_test.go b/internal/driver/ioscompanion/transport/integration_test.go new file mode 100644 index 0000000..25c9dcb --- /dev/null +++ b/internal/driver/ioscompanion/transport/integration_test.go @@ -0,0 +1,105 @@ +package transport + +import ( + "context" + "encoding/json" + "net" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" +) + +// TestIntegrationAccessibilityInfo spawns a local companion against a booted +// simulator and exercises a real AccessibilityInfo call. It is gated behind +// SANDERLING_IOS_INTEGRATION so it never runs by default. +func TestIntegrationAccessibilityInfo(t *testing.T) { + if os.Getenv("SANDERLING_IOS_INTEGRATION") == "" { + t.Skip("set SANDERLING_IOS_INTEGRATION=1 to run the integration smoke test") + } + + udid := bootedSimulatorUDID(t) + port := freePort(t) + + companionBinary := "/opt/homebrew/bin/idb_companion" + command := exec.Command(companionBinary, "--udid", udid, "--grpc-port", strconv.Itoa(port)) + if err := command.Start(); err != nil { + t.Fatalf("start companion: %v", err) + } + defer func() { + _ = command.Process.Kill() + _, _ = command.Process.Wait() + }() + + address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) + waitForListener(t, address) + + companion, err := Dial(address) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer companion.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + info, err := companion.AccessibilityInfo(ctx) + if err != nil { + t.Fatalf("AccessibilityInfo: %v", err) + } + if strings.TrimSpace(info) == "" { + t.Fatal("AccessibilityInfo returned empty json") + } +} + +func bootedSimulatorUDID(t *testing.T) string { + t.Helper() + output, err := exec.Command("xcrun", "simctl", "list", "devices", "booted", "--json").Output() + if err != nil { + t.Fatalf("simctl list: %v", err) + } + var parsed struct { + Devices map[string][]struct { + UDID string `json:"udid"` + State string `json:"state"` + } `json:"devices"` + } + if err := json.Unmarshal(output, &parsed); err != nil { + t.Fatalf("parse simctl json: %v", err) + } + for _, devices := range parsed.Devices { + for _, device := range devices { + if device.State == "Booted" { + return device.UDID + } + } + } + t.Skip("no booted simulator available") + return "" +} + +func freePort(t *testing.T) int { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve port: %v", err) + } + port := listener.Addr().(*net.TCPAddr).Port + _ = listener.Close() + return port +} + +func waitForListener(t *testing.T, address string) { + t.Helper() + deadline := time.Now().Add(20 * time.Second) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", address, time.Second) + if err == nil { + _ = conn.Close() + return + } + time.Sleep(200 * time.Millisecond) + } + t.Fatalf("companion did not listen on %s", address) +}