mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(ios): launch via XCTest with env vars for hierarchy/tap access (#39)
* feat(proto): add env map to LaunchRequest * feat(driver): add env param to Launch interface and all implementations * feat(sidecar): add IosDriverBackend using Maestro IOSDriver + env pass-through * feat(testrun): launch iOS app via XCTest with env vars instead of simctl * fix(ios): replace LaunchApp with BootedUDID; simctl launch moved to XCTest path * test(cli): add tests for ios platform flag and ios-device flag parsing * fix(sdk-ios): check semaphore wait result; resolve port from args and env; register extractors before start
This commit is contained in:
20 files changed
+323
-90
No files matched your search
@@ -146,6 +146,35 @@ func TestRun_Doctor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTestArgs_AcceptsIosPlatform(t *testing.T) {
|
||||||
|
options, err := parseTestArgs([]string{
|
||||||
|
"--spec", "s.ts",
|
||||||
|
"--bundle-id", "com.example.app",
|
||||||
|
"--platform", "ios",
|
||||||
|
}, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error for ios platform: %v", err)
|
||||||
|
}
|
||||||
|
if options.platform != "ios" {
|
||||||
|
t.Errorf("expected platform=ios, got %q", options.platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestArgs_IosDeviceFlag(t *testing.T) {
|
||||||
|
options, err := parseTestArgs([]string{
|
||||||
|
"--spec", "s.ts",
|
||||||
|
"--bundle-id", "com.example.app",
|
||||||
|
"--platform", "ios",
|
||||||
|
"--ios-device", "iPhone 15 Pro",
|
||||||
|
}, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if options.iosDevice != "iPhone 15 Pro" {
|
||||||
|
t.Errorf("expected iosDevice=iPhone 15 Pro, got %q", options.iosDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRun_TestSubcommand_PipelineErrors(t *testing.T) {
|
func TestRun_TestSubcommand_PipelineErrors(t *testing.T) {
|
||||||
// Without a real spec, a real device, or a bootable AVD the pipeline
|
// Without a real spec, a real device, or a bootable AVD the pipeline
|
||||||
// must surface a specific error rather than panicking — proves the flag
|
// must surface a specific error rather than panicking — proves the flag
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ private val backGestureTarget = BackGestureTarget()
|
|||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
fun MainViewController(): UIViewController {
|
fun MainViewController(): UIViewController {
|
||||||
Repository.init()
|
Repository.init()
|
||||||
app.folio.sanderling.SanderlingIos.start()
|
|
||||||
app.folio.sanderling.AuthSnapshots
|
app.folio.sanderling.AuthSnapshots
|
||||||
app.folio.sanderling.AccountSnapshots
|
app.folio.sanderling.AccountSnapshots
|
||||||
app.folio.sanderling.LedgerSnapshots
|
app.folio.sanderling.LedgerSnapshots
|
||||||
app.folio.sanderling.NavigationSnapshots
|
app.folio.sanderling.NavigationSnapshots
|
||||||
|
app.folio.sanderling.SanderlingIos.start()
|
||||||
val vc = ComposeUIViewController { App() }
|
val vc = ComposeUIViewController { App() }
|
||||||
val gesture = UIScreenEdgePanGestureRecognizer(
|
val gesture = UIScreenEdgePanGestureRecognizer(
|
||||||
target = backGestureTarget,
|
target = backGestureTarget,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ internal object IosPauser {
|
|||||||
dispatch_semaphore_signal(snapshotReady)
|
dispatch_semaphore_signal(snapshotReady)
|
||||||
dispatch_semaphore_wait(resumeGate, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
dispatch_semaphore_wait(resumeGate, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
||||||
}
|
}
|
||||||
dispatch_semaphore_wait(snapshotReady, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
val waited = dispatch_semaphore_wait(snapshotReady, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
||||||
|
check(waited == 0L) { "snapshot timed out: main thread did not signal within 5s" }
|
||||||
return capturedSnapshot
|
return capturedSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,24 @@ object SanderlingIos {
|
|||||||
internal val extractors = mutableMapOf<String, () -> Any?>()
|
internal val extractors = mutableMapOf<String, () -> Any?>()
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val env = NSProcessInfo.processInfo.environment
|
val port = resolvePort() ?: return
|
||||||
val portStr = env["SANDERLING_PORT"] as? String ?: return
|
|
||||||
val port = portStr.toIntOrNull() ?: return
|
|
||||||
IosAgent.start("127.0.0.1", port)
|
IosAgent.start("127.0.0.1", port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolvePort(): Int? {
|
||||||
|
// Env var set via SIMCTL_CHILD_SANDERLING_PORT (simctl direct launch).
|
||||||
|
(NSProcessInfo.processInfo.environment["SANDERLING_PORT"] as? String)
|
||||||
|
?.toIntOrNull()?.let { return it }
|
||||||
|
// Launch argument -SANDERLING_PORT <value> (Maestro simctl launch).
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val args = NSProcessInfo.processInfo.arguments as? List<String> ?: return null
|
||||||
|
val idx = args.indexOfFirst { it == "-SANDERLING_PORT" }
|
||||||
|
if (idx >= 0 && idx + 1 < args.size) {
|
||||||
|
return args[idx + 1].toIntOrNull()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun extract(name: String, block: () -> Any?) {
|
fun extract(name: String, block: () -> Any?) {
|
||||||
extractors[name] = block
|
extractors[name] = block
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func New() *Driver {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool) error {
|
func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, _ map[string]string) error {
|
||||||
if clearState {
|
if clearState {
|
||||||
if err := chromedp.Run(d.tabCtx, network.ClearBrowserCookies()); err != nil {
|
if err := chromedp.Run(d.tabCtx, network.ClearBrowserCookies()); err != nil {
|
||||||
return fmt.Errorf("clear cookies: %w", err)
|
return fmt.Errorf("clear cookies: %w", err)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// lives under driver/sidecar; the web implementation under driver/chrome;
|
// lives under driver/sidecar; the web implementation under driver/chrome;
|
||||||
// tests use driver/mock.
|
// tests use driver/mock.
|
||||||
type DeviceDriver interface {
|
type DeviceDriver interface {
|
||||||
Launch(ctx context.Context, bundleID string, clearState bool) error
|
Launch(ctx context.Context, bundleID string, clearState bool, env map[string]string) error
|
||||||
Terminate(ctx context.Context) error
|
Terminate(ctx context.Context) error
|
||||||
|
|
||||||
Tap(ctx context.Context, x, y int) error
|
Tap(ctx context.Context, x, y int) error
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (d *Driver) failure(kind ActionKind) error {
|
|||||||
return d.Failures[kind]
|
return d.Failures[kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool) error {
|
func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool, _ map[string]string) error {
|
||||||
if err := d.failure(ActionLaunch); err != nil {
|
if err := d.failure(ActionLaunch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestRecordsAllActionsInOrder(t *testing.T) {
|
|||||||
mock := New()
|
mock := New()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
if err := mock.Launch(ctx, "com.example", true); err != nil {
|
if err := mock.Launch(ctx, "com.example", true, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := mock.Tap(ctx, 100, 200); err != nil {
|
if err := mock.Tap(ctx, 100, 200); err != nil {
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ func (c *Client) WaitForHealth(ctx context.Context, pollInterval time.Duration)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Launch(ctx context.Context, bundleID string, clearState bool) error {
|
func (c *Client) Launch(ctx context.Context, bundleID string, clearState bool, env map[string]string) error {
|
||||||
_, err := c.stub.Launch(ctx, &driverpb.LaunchRequest{
|
_, err := c.stub.Launch(ctx, &driverpb.LaunchRequest{
|
||||||
BundleId: bundleID,
|
BundleId: bundleID,
|
||||||
ClearState: clearState,
|
ClearState: clearState,
|
||||||
|
Env: env,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func TestClient_LaunchAndTerminate(t *testing.T) {
|
|||||||
client, _ := Dial(state.address)
|
client, _ := Dial(state.address)
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
if err := client.Launch(context.Background(), "com.example", true); err != nil {
|
if err := client.Launch(context.Background(), "com.example", true, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if state.fake.launchedBundleID != "com.example" || !state.fake.clearState {
|
if state.fake.launchedBundleID != "com.example" || !state.fake.clearState {
|
||||||
|
|||||||
+6
-12
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -55,18 +54,13 @@ func EnsureSimulator(ctx context.Context, deviceName string, stdout io.Writer) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LaunchApp(ctx context.Context, bundleID string, env map[string]string) error {
|
// BootedUDID returns the UDID of the currently booted iOS simulator, or "" if none is booted.
|
||||||
args := []string{"simctl", "launch", "--terminate-running-process", "booted", bundleID}
|
func BootedUDID(ctx context.Context) string {
|
||||||
cmd := exec.CommandContext(ctx, "xcrun", args...)
|
d, _ := bootedSimulator(ctx)
|
||||||
cmd.Env = os.Environ()
|
if d == nil {
|
||||||
for k, v := range env {
|
return ""
|
||||||
cmd.Env = append(cmd.Env, "SIMCTL_CHILD_"+k+"="+v)
|
|
||||||
}
|
}
|
||||||
out, err := cmd.CombinedOutput()
|
return d.UDID
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("launch %s: %w\n%s", bundleID, err, strings.TrimSpace(string(out)))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootedSimulator(ctx context.Context) (*simDevice, error) {
|
func bootedSimulator(ctx context.Context) (*simDevice, error) {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package ios
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPickSimulator_ByName(t *testing.T) {
|
||||||
|
available := []simDevice{
|
||||||
|
{UDID: "aaa", Name: "iPad Pro", IsAvailable: true},
|
||||||
|
{UDID: "bbb", Name: "iPhone 15", IsAvailable: true},
|
||||||
|
}
|
||||||
|
got, err := pickSimulator("iPhone 15", available)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.UDID != "bbb" {
|
||||||
|
t.Errorf("got %q, want bbb", got.UDID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickSimulator_ByUDID(t *testing.T) {
|
||||||
|
available := []simDevice{
|
||||||
|
{UDID: "aaa", Name: "iPad Pro", IsAvailable: true},
|
||||||
|
{UDID: "bbb", Name: "iPhone 14", IsAvailable: true},
|
||||||
|
}
|
||||||
|
got, err := pickSimulator("aaa", available)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Name != "iPad Pro" {
|
||||||
|
t.Errorf("got %q, want iPad Pro", got.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickSimulator_UnknownName(t *testing.T) {
|
||||||
|
available := []simDevice{
|
||||||
|
{UDID: "aaa", Name: "iPad Pro", IsAvailable: true},
|
||||||
|
}
|
||||||
|
_, err := pickSimulator("Pixel 7", available)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for unknown simulator name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickSimulator_EmptyName_PrefersIPhone(t *testing.T) {
|
||||||
|
available := []simDevice{
|
||||||
|
{UDID: "aaa", Name: "iPad mini", IsAvailable: true},
|
||||||
|
{UDID: "bbb", Name: "iPhone 16", IsAvailable: true},
|
||||||
|
{UDID: "ccc", Name: "Apple Watch", IsAvailable: true},
|
||||||
|
}
|
||||||
|
got, err := pickSimulator("", available)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.UDID != "bbb" {
|
||||||
|
t.Errorf("got %q, want bbb (iPhone)", got.UDID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickSimulator_EmptyName_FallsBackToFirst(t *testing.T) {
|
||||||
|
available := []simDevice{
|
||||||
|
{UDID: "aaa", Name: "iPad Air", IsAvailable: true},
|
||||||
|
{UDID: "bbb", Name: "Apple TV", IsAvailable: true},
|
||||||
|
}
|
||||||
|
got, err := pickSimulator("", available)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.UDID != "aaa" {
|
||||||
|
t.Errorf("got %q, want aaa (first available)", got.UDID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickSimulator_EmptyList(t *testing.T) {
|
||||||
|
_, err := pickSimulator("", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty simulator list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootedUDID_CanceledContext(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
udid := BootedUDID(ctx)
|
||||||
|
if udid != "" {
|
||||||
|
t.Errorf("expected empty UDID on canceled context, got %q", udid)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
"github.com/priyanshujain/sanderling/internal/driver/chrome"
|
"github.com/priyanshujain/sanderling/internal/driver/chrome"
|
||||||
driverSidecar "github.com/priyanshujain/sanderling/internal/driver/sidecar"
|
driverSidecar "github.com/priyanshujain/sanderling/internal/driver/sidecar"
|
||||||
|
"github.com/priyanshujain/sanderling/internal/ios"
|
||||||
"github.com/priyanshujain/sanderling/internal/sidecar"
|
"github.com/priyanshujain/sanderling/internal/sidecar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,10 +36,16 @@ func buildDriver(ctx context.Context, options Options, stdout io.Writer) (driver
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
sidecarCommand := exec.CommandContext(ctx, "java", "-jar", jarPath,
|
sidecarArgs := []string{"-jar", jarPath,
|
||||||
"--port", strconv.Itoa(sidecarPort),
|
"--port", strconv.Itoa(sidecarPort),
|
||||||
"--platform", options.Platform,
|
"--platform", options.Platform,
|
||||||
)
|
}
|
||||||
|
if options.Platform == "ios" {
|
||||||
|
if udid := ios.BootedUDID(ctx); udid != "" {
|
||||||
|
sidecarArgs = append(sidecarArgs, "--udid", udid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sidecarCommand := exec.CommandContext(ctx, "java", sidecarArgs...)
|
||||||
sidecarCommand.Stdout = stdout
|
sidecarCommand.Stdout = stdout
|
||||||
sidecarCommand.Stderr = stdout
|
sidecarCommand.Stderr = stdout
|
||||||
sidecarCommand.Env = android.EnvWithAndroidPlatformTools(os.Environ())
|
sidecarCommand.Env = android.EnvWithAndroidPlatformTools(os.Environ())
|
||||||
|
|||||||
+22
-17
@@ -110,7 +110,7 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
acceptChannel <- acceptResult{conn: conn, err: acceptErr}
|
acceptChannel <- acceptResult{conn: conn, err: acceptErr}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := activeDriver.Launch(ctx, options.BundleID, false); err != nil {
|
if err := activeDriver.Launch(ctx, options.BundleID, false, nil); err != nil {
|
||||||
return fmt.Errorf("launch app: %w", err)
|
return fmt.Errorf("launch app: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "launched %s; waiting for SDK to connect (%.0fs timeout)\n", options.BundleID, sdkAcceptTimeout.Seconds())
|
fmt.Fprintf(stdout, "launched %s; waiting for SDK to connect (%.0fs timeout)\n", options.BundleID, sdkAcceptTimeout.Seconds())
|
||||||
@@ -133,35 +133,40 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
agentPort := listener.Addr().(*net.TCPAddr).Port
|
agentPort := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
|
||||||
// Launch app via simctl with SANDERLING_PORT so the SDK can connect.
|
agentServer := agent.NewServer(listener)
|
||||||
if err := ios.LaunchApp(ctx, options.BundleID, map[string]string{
|
|
||||||
|
type acceptResult struct {
|
||||||
|
conn *agent.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
acceptChannel := make(chan acceptResult, 1)
|
||||||
|
go func() {
|
||||||
|
acceptCtx, cancel := context.WithTimeout(ctx, sdkAcceptTimeout)
|
||||||
|
defer cancel()
|
||||||
|
conn, acceptErr := agentServer.Accept(acceptCtx)
|
||||||
|
acceptChannel <- acceptResult{conn: conn, err: acceptErr}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Launch app via XCTest with SANDERLING_PORT so the SDK can connect.
|
||||||
|
if err := activeDriver.Launch(ctx, options.BundleID, false, map[string]string{
|
||||||
"SANDERLING_PORT": strconv.Itoa(agentPort),
|
"SANDERLING_PORT": strconv.Itoa(agentPort),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("launch app: %w", err)
|
return fmt.Errorf("launch app: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "iOS app launched with SANDERLING_PORT=%d; waiting for SDK (%.0fs timeout)\n", agentPort, sdkAcceptTimeout.Seconds())
|
fmt.Fprintf(stdout, "iOS app launched with SANDERLING_PORT=%d; waiting for SDK (%.0fs timeout)\n", agentPort, sdkAcceptTimeout.Seconds())
|
||||||
|
|
||||||
agentServer := agent.NewServer(listener)
|
result := <-acceptChannel
|
||||||
acceptCtx, acceptCancel := context.WithTimeout(ctx, sdkAcceptTimeout)
|
if result.err != nil {
|
||||||
conn, acceptErr := agentServer.Accept(acceptCtx)
|
return fmt.Errorf("accept SDK: %w", result.err)
|
||||||
acceptCancel()
|
|
||||||
if acceptErr != nil {
|
|
||||||
return fmt.Errorf("accept SDK: %w", acceptErr)
|
|
||||||
}
|
}
|
||||||
connection = conn
|
connection = result.conn
|
||||||
defer connection.Close()
|
defer connection.Close()
|
||||||
hello := connection.Hello()
|
hello := connection.Hello()
|
||||||
fmt.Fprintf(stdout, "SDK connected: platform=%s app=%s sdk=%s\n", hello.Platform, hello.AppPackage, hello.Version)
|
fmt.Fprintf(stdout, "SDK connected: platform=%s app=%s sdk=%s\n", hello.Platform, hello.AppPackage, hello.Version)
|
||||||
|
|
||||||
// Initialize Maestro XCTest session after the app is running.
|
|
||||||
// clearState=false avoids relaunching the already-connected app.
|
|
||||||
if err := activeDriver.Launch(ctx, options.BundleID, false); err != nil {
|
|
||||||
fmt.Fprintf(stdout, "warn: maestro launch: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "web":
|
case "web":
|
||||||
fmt.Fprintln(stdout, "web mode: skipping SDK")
|
fmt.Fprintln(stdout, "web mode: skipping SDK")
|
||||||
if err := activeDriver.Launch(ctx, options.BundleID, false); err != nil {
|
if err := activeDriver.Launch(ctx, options.BundleID, false, nil); err != nil {
|
||||||
return fmt.Errorf("launch app: %w", err)
|
return fmt.Errorf("launch app: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-38
@@ -61,6 +61,7 @@ type LaunchRequest struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
BundleId string `protobuf:"bytes,1,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty"`
|
BundleId string `protobuf:"bytes,1,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty"`
|
||||||
ClearState bool `protobuf:"varint,2,opt,name=clear_state,json=clearState,proto3" json:"clear_state,omitempty"`
|
ClearState bool `protobuf:"varint,2,opt,name=clear_state,json=clearState,proto3" json:"clear_state,omitempty"`
|
||||||
|
Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -109,6 +110,13 @@ func (x *LaunchRequest) GetClearState() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LaunchRequest) GetEnv() map[string]string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Env
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Point struct {
|
type Point struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
||||||
@@ -839,11 +847,15 @@ var File_driverpb_driver_proto protoreflect.FileDescriptor
|
|||||||
const file_driverpb_driver_proto_rawDesc = "" +
|
const file_driverpb_driver_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x15driverpb/driver.proto\x12\x14sanderling.driver.v1\"\a\n" +
|
"\x15driverpb/driver.proto\x12\x14sanderling.driver.v1\"\a\n" +
|
||||||
"\x05Empty\"M\n" +
|
"\x05Empty\"\xc5\x01\n" +
|
||||||
"\rLaunchRequest\x12\x1b\n" +
|
"\rLaunchRequest\x12\x1b\n" +
|
||||||
"\tbundle_id\x18\x01 \x01(\tR\bbundleId\x12\x1f\n" +
|
"\tbundle_id\x18\x01 \x01(\tR\bbundleId\x12\x1f\n" +
|
||||||
"\vclear_state\x18\x02 \x01(\bR\n" +
|
"\vclear_state\x18\x02 \x01(\bR\n" +
|
||||||
"clearState\"#\n" +
|
"clearState\x12>\n" +
|
||||||
|
"\x03env\x18\x03 \x03(\v2,.sanderling.driver.v1.LaunchRequest.EnvEntryR\x03env\x1a6\n" +
|
||||||
|
"\bEnvEntry\x12\x10\n" +
|
||||||
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
|
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"#\n" +
|
||||||
"\x05Point\x12\f\n" +
|
"\x05Point\x12\f\n" +
|
||||||
"\x01x\x18\x01 \x01(\x05R\x01x\x12\f\n" +
|
"\x01x\x18\x01 \x01(\x05R\x01x\x12\f\n" +
|
||||||
"\x01y\x18\x02 \x01(\x05R\x01y\"\x1c\n" +
|
"\x01y\x18\x02 \x01(\x05R\x01y\"\x1c\n" +
|
||||||
@@ -919,7 +931,7 @@ func file_driverpb_driver_proto_rawDescGZIP() []byte {
|
|||||||
return file_driverpb_driver_proto_rawDescData
|
return file_driverpb_driver_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_driverpb_driver_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
var file_driverpb_driver_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||||
var file_driverpb_driver_proto_goTypes = []any{
|
var file_driverpb_driver_proto_goTypes = []any{
|
||||||
(*Empty)(nil), // 0: sanderling.driver.v1.Empty
|
(*Empty)(nil), // 0: sanderling.driver.v1.Empty
|
||||||
(*LaunchRequest)(nil), // 1: sanderling.driver.v1.LaunchRequest
|
(*LaunchRequest)(nil), // 1: sanderling.driver.v1.LaunchRequest
|
||||||
@@ -937,42 +949,44 @@ var file_driverpb_driver_proto_goTypes = []any{
|
|||||||
(*LogEntry)(nil), // 13: sanderling.driver.v1.LogEntry
|
(*LogEntry)(nil), // 13: sanderling.driver.v1.LogEntry
|
||||||
(*MetricsRequest)(nil), // 14: sanderling.driver.v1.MetricsRequest
|
(*MetricsRequest)(nil), // 14: sanderling.driver.v1.MetricsRequest
|
||||||
(*MetricsResponse)(nil), // 15: sanderling.driver.v1.MetricsResponse
|
(*MetricsResponse)(nil), // 15: sanderling.driver.v1.MetricsResponse
|
||||||
|
nil, // 16: sanderling.driver.v1.LaunchRequest.EnvEntry
|
||||||
}
|
}
|
||||||
var file_driverpb_driver_proto_depIdxs = []int32{
|
var file_driverpb_driver_proto_depIdxs = []int32{
|
||||||
2, // 0: sanderling.driver.v1.SwipeRequest.from:type_name -> sanderling.driver.v1.Point
|
16, // 0: sanderling.driver.v1.LaunchRequest.env:type_name -> sanderling.driver.v1.LaunchRequest.EnvEntry
|
||||||
2, // 1: sanderling.driver.v1.SwipeRequest.to:type_name -> sanderling.driver.v1.Point
|
2, // 1: sanderling.driver.v1.SwipeRequest.from:type_name -> sanderling.driver.v1.Point
|
||||||
13, // 2: sanderling.driver.v1.LogEntries.entries:type_name -> sanderling.driver.v1.LogEntry
|
2, // 2: sanderling.driver.v1.SwipeRequest.to:type_name -> sanderling.driver.v1.Point
|
||||||
1, // 3: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
|
13, // 3: sanderling.driver.v1.LogEntries.entries:type_name -> sanderling.driver.v1.LogEntry
|
||||||
0, // 4: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
|
1, // 4: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
|
||||||
2, // 5: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
|
0, // 5: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
|
||||||
4, // 6: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
|
2, // 6: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
|
||||||
3, // 7: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
|
4, // 7: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
|
||||||
9, // 8: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
|
3, // 8: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
|
||||||
10, // 9: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
|
9, // 9: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
|
||||||
0, // 10: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
|
10, // 10: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
|
||||||
0, // 11: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
|
0, // 11: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
|
||||||
11, // 12: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
|
0, // 12: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
|
||||||
7, // 13: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
|
11, // 13: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
|
||||||
0, // 14: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
|
7, // 14: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
|
||||||
14, // 15: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
|
0, // 15: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
|
||||||
0, // 16: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
|
14, // 16: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
|
||||||
0, // 17: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
|
0, // 17: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 18: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
|
0, // 18: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 19: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
|
0, // 19: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 20: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
|
0, // 20: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 21: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
|
0, // 21: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 22: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
|
0, // 22: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
|
||||||
5, // 23: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
|
0, // 23: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
|
||||||
6, // 24: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
|
5, // 24: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
|
||||||
12, // 25: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
|
6, // 25: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
|
||||||
0, // 26: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
|
12, // 26: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
|
||||||
8, // 27: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
|
0, // 27: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
|
||||||
15, // 28: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
|
8, // 28: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
|
||||||
16, // [16:29] is the sub-list for method output_type
|
15, // 29: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
|
||||||
3, // [3:16] is the sub-list for method input_type
|
17, // [17:30] is the sub-list for method output_type
|
||||||
3, // [3:3] is the sub-list for extension type_name
|
4, // [4:17] is the sub-list for method input_type
|
||||||
3, // [3:3] is the sub-list for extension extendee
|
4, // [4:4] is the sub-list for extension type_name
|
||||||
0, // [0:3] is the sub-list for field type_name
|
4, // [4:4] is the sub-list for extension extendee
|
||||||
|
0, // [0:4] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_driverpb_driver_proto_init() }
|
func init() { file_driverpb_driver_proto_init() }
|
||||||
@@ -986,7 +1000,7 @@ func file_driverpb_driver_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_driverpb_driver_proto_rawDesc), len(file_driverpb_driver_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_driverpb_driver_proto_rawDesc), len(file_driverpb_driver_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 16,
|
NumMessages: 17,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ message Empty {}
|
|||||||
message LaunchRequest {
|
message LaunchRequest {
|
||||||
string bundle_id = 1;
|
string bundle_id = 1;
|
||||||
bool clear_state = 2;
|
bool clear_state = 2;
|
||||||
|
map<string, string> env = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Point {
|
message Point {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ val maestroVersion = "1.40.0"
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation("dev.mobile:maestro-client:$maestroVersion")
|
implementation("dev.mobile:maestro-client:$maestroVersion")
|
||||||
implementation("dev.mobile:maestro-utils:$maestroVersion")
|
implementation("dev.mobile:maestro-utils:$maestroVersion")
|
||||||
|
implementation("dev.mobile:maestro-ios:$maestroVersion")
|
||||||
|
implementation("dev.mobile:maestro-ios-driver:$maestroVersion")
|
||||||
|
|
||||||
implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
|
implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
|
||||||
implementation("io.grpc:grpc-protobuf:$grpcVersion")
|
implementation("io.grpc:grpc-protobuf:$grpcVersion")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.sanderling.sidecar
|
package dev.sanderling.sidecar
|
||||||
|
|
||||||
interface DriverBackend {
|
interface DriverBackend {
|
||||||
fun launch(bundleId: String, clearState: Boolean)
|
fun launch(bundleId: String, clearState: Boolean, env: Map<String, String> = emptyMap())
|
||||||
fun terminate(bundleId: String)
|
fun terminate(bundleId: String)
|
||||||
fun tap(x: Int, y: Int)
|
fun tap(x: Int, y: Int)
|
||||||
fun tapSelector(selector: String)
|
fun tapSelector(selector: String)
|
||||||
@@ -131,7 +131,7 @@ class StubDriverBackend(private val platform: String) : DriverBackend {
|
|||||||
@Volatile var lastInputText: String? = null
|
@Volatile var lastInputText: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun launch(bundleId: String, clearState: Boolean) {
|
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {
|
||||||
launchCount++
|
launchCount++
|
||||||
lastBundleId = bundleId
|
lastBundleId = bundleId
|
||||||
if (clearState) {
|
if (clearState) {
|
||||||
@@ -386,9 +386,9 @@ class MaestroDriverBackend(private val serial: String?) : DriverBackend {
|
|||||||
driver.open()
|
driver.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun launch(bundleId: String, clearState: Boolean) {
|
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {
|
||||||
if (clearState) driver.clearAppState(bundleId)
|
if (clearState) driver.clearAppState(bundleId)
|
||||||
driver.launchApp(bundleId, emptyMap(), java.util.UUID.randomUUID())
|
driver.launchApp(bundleId, env, java.util.UUID.randomUUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun terminate(bundleId: String) = driver.stopApp(bundleId)
|
override fun terminate(bundleId: String) = driver.stopApp(bundleId)
|
||||||
@@ -486,6 +486,80 @@ private fun pngHeight(bytes: ByteArray): Int {
|
|||||||
(bytes[22].toInt() and 0xFF shl 8) or (bytes[23].toInt() and 0xFF)
|
(bytes[22].toInt() and 0xFF shl 8) or (bytes[23].toInt() and 0xFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IosDriverBackend(private val udid: String) : DriverBackend {
|
||||||
|
private val driver: maestro.drivers.IOSDriver
|
||||||
|
|
||||||
|
init {
|
||||||
|
val httpClient = xcuitest.api.OkHttpClientInstance.get()
|
||||||
|
val metrics = maestro.utils.NoOpMetrics()
|
||||||
|
val wdaPort = maestro.utils.SocketUtils.nextFreePort(22000, 23000)
|
||||||
|
val installer = xcuitest.installer.LocalXCTestInstaller(
|
||||||
|
udid,
|
||||||
|
"localhost",
|
||||||
|
false,
|
||||||
|
wdaPort,
|
||||||
|
metrics,
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
val xcTestDriverClient = xcuitest.XCTestDriverClient(installer, httpClient, false)
|
||||||
|
val xcTestDevice = ios.xctest.XCTestIOSDevice(udid, xcTestDriverClient) { emptySet() }
|
||||||
|
val simctlDevice = ios.simctl.SimctlIOSDevice(udid)
|
||||||
|
val device = ios.LocalIOSDevice(udid, xcTestDevice, simctlDevice, maestro.utils.NoopInsights)
|
||||||
|
driver = maestro.drivers.IOSDriver(device, maestro.utils.NoopInsights, metrics)
|
||||||
|
driver.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {
|
||||||
|
runCatching { driver.stopApp(bundleId) }
|
||||||
|
if (clearState) driver.clearAppState(bundleId)
|
||||||
|
driver.launchApp(bundleId, env, java.util.UUID.randomUUID())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun terminate(bundleId: String) = driver.stopApp(bundleId)
|
||||||
|
|
||||||
|
override fun tap(x: Int, y: Int) = driver.tap(maestro.Point(x, y))
|
||||||
|
|
||||||
|
override fun tapSelector(selector: String) {
|
||||||
|
val root = driver.contentDescriptor(false)
|
||||||
|
val bounds = findBoundsBySelector(root, selector) ?: return
|
||||||
|
driver.tap(maestro.Point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inputText(text: String) = driver.inputText(text)
|
||||||
|
|
||||||
|
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) =
|
||||||
|
driver.swipe(maestro.Point(fromX, fromY), maestro.Point(toX, toY), maxOf(durationMillis, 250L))
|
||||||
|
|
||||||
|
override fun pressKey(key: String) {
|
||||||
|
StubDriverBackend.KEY_MAP[key]?.let { keyCode ->
|
||||||
|
keyCodeToMaestro(keyCode)?.let { driver.pressKey(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun screenshot(): Triple<ByteArray, Int, Int> {
|
||||||
|
val buf = okio.Buffer()
|
||||||
|
driver.takeScreenshot(buf, false)
|
||||||
|
val bytes = buf.readByteArray()
|
||||||
|
return Triple(bytes, pngWidth(bytes), pngHeight(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hierarchy(): String =
|
||||||
|
com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||||
|
.writeValueAsString(driver.contentDescriptor(false))
|
||||||
|
|
||||||
|
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList()
|
||||||
|
|
||||||
|
override fun waitForIdle(durationMillis: Long) {
|
||||||
|
driver.waitForAppToSettle(null, null, durationMillis.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun healthy() = runCatching { driver.contentDescriptor(false); true }.getOrElse { false }
|
||||||
|
|
||||||
|
override fun metrics(bundleId: String) = MetricsSample(0.0, 0L, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? {
|
private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? {
|
||||||
return when (adbKeyCode) {
|
return when (adbKeyCode) {
|
||||||
"KEYCODE_BACK" -> maestro.KeyCode.BACK
|
"KEYCODE_BACK" -> maestro.KeyCode.BACK
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class DriverService(
|
|||||||
|
|
||||||
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
|
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.launch(request.bundleId, request.clearState)
|
backend.launch(request.bundleId, request.clearState, request.envMap)
|
||||||
launchedBundleId.set(request.bundleId)
|
launchedBundleId.set(request.bundleId)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,8 @@ class DriverService(
|
|||||||
observer.onNext(block())
|
observer.onNext(block())
|
||||||
observer.onCompleted()
|
observer.onCompleted()
|
||||||
} catch (cause: Exception) {
|
} catch (cause: Exception) {
|
||||||
observer.onError(cause)
|
observer.onError(io.grpc.Status.INTERNAL.withDescription(cause.toString())
|
||||||
|
.withCause(cause).asRuntimeException())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,14 @@ fun main(arguments: Array<String>) {
|
|||||||
val serial = arguments.indexOf("--serial").let { index ->
|
val serial = arguments.indexOf("--serial").let { index ->
|
||||||
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null
|
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null
|
||||||
}
|
}
|
||||||
|
val udid = arguments.indexOf("--udid").let { index ->
|
||||||
|
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null
|
||||||
|
}
|
||||||
|
|
||||||
val backend: DriverBackend = if (platform == "android") {
|
val backend: DriverBackend = when (platform) {
|
||||||
MaestroDriverBackend(serial)
|
"android" -> MaestroDriverBackend(serial)
|
||||||
} else {
|
"ios" -> IosDriverBackend(udid ?: error("--udid required for ios platform"))
|
||||||
StubDriverBackend(platform)
|
else -> StubDriverBackend(platform)
|
||||||
}
|
}
|
||||||
val service = DriverService(platform = platform, backend = backend)
|
val service = DriverService(platform = platform, backend = backend)
|
||||||
val server = SidecarServer(port, service)
|
val server = SidecarServer(port, service)
|
||||||
|
|||||||
Reference in new issue
Block a user