// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package app_test import ( "fmt" "image" "image/color" _ "image/png" "io/ioutil" "net" "os" "os/exec" "strings" "testing" "time" "golang.org/x/mobile/app/internal/apptest" "golang.org/x/mobile/event/size" ) // TestAndroidApp tests the lifecycle, event, and window semantics of a // simple android app. // // Beyond testing the app package, the goal is to eventually have // helper libraries that make tests like these easy to write. Hopefully // having a user of such a fictional package will help illuminate the way. func TestAndroidApp(t *testing.T) { t.Skip("see issue #23835") if _, err := exec.Command("which", "adb").CombinedOutput(); err != nil { t.Skip("command adb not found, skipping") } devicesTxt, err := exec.Command("adb", "devices").CombinedOutput() if err != nil { t.Errorf("adb devices failed: %v: %v", err, devicesTxt) } deviceCount := 0 for _, d := range strings.Split(strings.TrimSpace(string(devicesTxt)), "\n") { if strings.Contains(d, "List of devices") { continue } // TODO(crawshaw): I believe some unusable devices can appear in the // list with note on them, but I cannot reproduce this right now. deviceCount++ } if deviceCount == 0 { t.Skip("no android devices attached") } run(t, "gomobile", "version") origWD, err := os.Getwd() if err != nil { t.Fatal(err) } tmpdir, err := ioutil.TempDir("", "app-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpdir) if err := os.Chdir(tmpdir); err != nil { t.Fatal(err) } defer os.Chdir(origWD) run(t, "gomobile", "install", "golang.org/x/mobile/app/internal/testapp") ln, err := net.Listen("tcp4", "localhost:0") if err != nil { t.Fatal(err) } defer ln.Close() localaddr := fmt.Sprintf("tcp:%d", ln.Addr().(*net.TCPAddr).Port) t.Logf("local address: %s", localaddr) exec.Command("adb", "reverse", "--remove", "tcp:"+apptest.Port).Run() // ignore failure run(t, "adb", "reverse", "tcp:"+apptest.Port, localaddr) const ( KeycodePower = "26" KeycodeUnlock = "82" ) run(t, "adb", "shell", "input", "keyevent", KeycodePower) run(t, "adb", "shell", "input", "keyevent", KeycodeUnlock) const ( rotationPortrait = "0" rotationLandscape = "1" ) rotate := func(rotation string) { run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", "value:i:"+rotation) } // turn off automatic rotation and start in portrait run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:accelerometer_rotation", "--bind", "value:i:0") rotate(rotationPortrait) // start testapp run(t, "adb", "shell", "am", "start", "-n", "org.golang.testapp/org.golang.app.GoNativeActivity", ) var conn net.Conn connDone := make(chan struct{}) go func() { conn, err = ln.Accept() connDone <- struct{}{} }() select { case <-time.After(5 * time.Second): t.Fatal("timeout waiting for testapp to dial host") case <-connDone: if err != nil { t.Fatalf("ln.Accept: %v", err) } } defer conn.Close() comm := &apptest.Comm{ Conn: conn, Fatalf: t.Fatalf, Printf: t.Logf, } var pixelsPerPt float32 var orientation size.Orientation comm.Recv("hello_from_testapp") comm.Send("hello_from_host") comm.Recv("lifecycle_visible") comm.Recv("size", &pixelsPerPt, &orientation) if pixelsPerPt < 0.1 { t.Fatalf("bad pixelsPerPt: %f", pixelsPerPt) } // A single paint event is sent when the lifecycle enters // StageVisible, and after the end of a touch event. var color string comm.Recv("paint", &color) // Ignore the first paint color, it may be slow making it to the screen. rotate(rotationLandscape) comm.Recv("size", &pixelsPerPt, &orientation) if want := size.OrientationLandscape; orientation != want { t.Errorf("want orientation %d, got %d", want, orientation) } var x, y int var ty string tap(t, 50, 260) comm.Recv("touch", &ty, &x, &y) if ty != "begin" || x != 50 || y != 260 { t.Errorf("want touch begin(50, 260), got %s(%d,%d)", ty, x, y) } comm.Recv("touch", &ty, &x, &y) if ty != "end" || x != 50 || y != 260 { t.Errorf("want touch end(50, 260), got %s(%d,%d)", ty, x, y) } comm.Recv("paint", &color) if gotColor := currentColor(t); color != gotColor { t.Errorf("app reports color %q, but saw %q", color, gotColor) } rotate(rotationPortrait) comm.Recv("size", &pixelsPerPt, &orientation) if want := size.OrientationPortrait; orientation != want { t.Errorf("want orientation %d, got %d", want, orientation) } tap(t, 50, 260) comm.Recv("touch", &ty, &x, &y) // touch begin comm.Recv("touch", &ty, &x, &y) // touch end comm.Recv("paint", &color) if gotColor := currentColor(t); color != gotColor { t.Errorf("app reports color %q, but saw %q", color, gotColor) } // TODO: lifecycle testing (NOTE: adb shell input keyevent 4 is the back button) } func currentColor(t *testing.T) string { file := fmt.Sprintf("app-screen-%d.png", time.Now().Unix()) run(t, "adb", "shell", "screencap", "-p", "/data/local/tmp/"+file) run(t, "adb", "pull", "/data/local/tmp/"+file) run(t, "adb", "shell", "rm", "/data/local/tmp/"+file) defer os.Remove(file) f, err := os.Open(file) if err != nil { t.Errorf("currentColor: cannot open screencap: %v", err) return "" } m, _, err := image.Decode(f) if err != nil { t.Errorf("currentColor: cannot decode screencap: %v", err) return "" } var center color.Color { b := m.Bounds() x, y := b.Min.X+(b.Max.X-b.Min.X)/2, b.Min.Y+(b.Max.Y-b.Min.Y)/2 center = m.At(x, y) } r, g, b, _ := center.RGBA() switch { case r == 0xffff && g == 0x0000 && b == 0x0000: return "red" case r == 0x0000 && g == 0xffff && b == 0x0000: return "green" case r == 0x0000 && g == 0x0000 && b == 0xffff: return "blue" default: return fmt.Sprintf("indeterminate: %v", center) } } func tap(t *testing.T, x, y int) { run(t, "adb", "shell", "input", "tap", fmt.Sprintf("%d", x), fmt.Sprintf("%d", y)) } func run(t *testing.T, cmdName string, arg ...string) { cmd := exec.Command(cmdName, arg...) t.Log(strings.Join(cmd.Args, " ")) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%s %v: %s", strings.Join(cmd.Args, " "), err, out) } }