diff --git a/app/app_test.go b/app/app_test.go new file mode 100644 index 0000000..921d901 --- /dev/null +++ b/app/app_test.go @@ -0,0 +1,138 @@ +// 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" + "io/ioutil" + "net" + "os" + "os/exec" + "strings" + "testing" + "time" + + "golang.org/x/mobile/app/internal/apptest" +) + +// 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) { + if _, err := exec.Command("which", "adb").CombinedOutput(); err != nil { + t.Skip("command adb not found, skipping") + } + + 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) + + // 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 + + comm.Recv("hello_from_testapp") + comm.Send("hello_from_host") + comm.Recv("lifecycle_visible") + comm.Recv("config", &PixelsPerPt) + if PixelsPerPt < 0.1 { + t.Fatalf("bad PixelsPerPt: %f", PixelsPerPt) + } + comm.Recv("paint") + + var x, y int + var ty string + + tap(t, 50, 60) + comm.Recv("touch", &ty, &x, &y) + if ty != "begin" || x != 50 || y != 60 { + t.Errorf("want touch begin(50, 60), got %s(%d,%d)", ty, x, y) + } + comm.Recv("touch", &ty, &x, &y) + if ty != "end" || x != 50 || y != 60 { + t.Errorf("want touch end(50, 60), got %s(%d,%d)", ty, x, y) + } + + // TODO: screenshot of gl.Clear to test painting + // TODO: lifecycle testing (NOTE: adb shell input keyevent 4 is the back button) + // TODO: orientation testing +} + +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) + } +} diff --git a/app/internal/apptest/apptest.go b/app/internal/apptest/apptest.go new file mode 100644 index 0000000..4806773 --- /dev/null +++ b/app/internal/apptest/apptest.go @@ -0,0 +1,67 @@ +// 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 apptest provides utilities for testing an app. +// +// It is extremely incomplete, hence it being internal. +// For starters, it should support iOS. +package apptest + +import ( + "bufio" + "bytes" + "fmt" + "net" +) + +// Port is the TCP port used to communicate with the test app. +// +// TODO(crawshaw): find a way to make this configurable. adb am extras? +const Port = "12533" + +// Comm is a simple text-based communication protocol. +// +// Assumes all sides are friendly and cooperative and that the +// communication is over at the first sign of trouble. +type Comm struct { + Conn net.Conn + Fatalf func(format string, args ...interface{}) + Printf func(format string, args ...interface{}) + + scanner *bufio.Scanner +} + +func (c *Comm) Send(cmd string, args ...interface{}) { + buf := new(bytes.Buffer) + buf.WriteString(cmd) + for _, arg := range args { + buf.WriteRune(' ') + fmt.Fprintf(buf, "%v", arg) + } + buf.WriteRune('\n') + b := buf.Bytes() + c.Printf("comm.send: %s\n", b) + if _, err := c.Conn.Write(b); err != nil { + c.Fatalf("failed to send %s: %v", b, err) + } +} + +func (c *Comm) Recv(cmd string, a ...interface{}) { + if c.scanner == nil { + c.scanner = bufio.NewScanner(c.Conn) + } + if !c.scanner.Scan() { + c.Fatalf("failed to recv %q: %v", cmd, c.scanner.Err()) + } + text := c.scanner.Text() + c.Printf("comm.recv: %s\n", text) + var recvCmd string + args := append([]interface{}{&recvCmd}, a...) + if _, err := fmt.Sscan(text, args...); err != nil { + c.Fatalf("cannot scan recv command %s: %q: %v", cmd, text, err) + } + if cmd != recvCmd { + c.Fatalf("expecting recv %q, got %v", cmd, text) + } +} diff --git a/app/internal/testapp/AndroidManifest.xml b/app/internal/testapp/AndroidManifest.xml new file mode 100644 index 0000000..3d8213e --- /dev/null +++ b/app/internal/testapp/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/app/internal/testapp/testapp.go b/app/internal/testapp/testapp.go new file mode 100644 index 0000000..dcfa637 --- /dev/null +++ b/app/internal/testapp/testapp.go @@ -0,0 +1,66 @@ +// 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. + +// Small test app used by app/app_test.go. +package main + +import ( + "log" + "net" + + "golang.org/x/mobile/app" + "golang.org/x/mobile/app/internal/apptest" + "golang.org/x/mobile/event/config" + "golang.org/x/mobile/event/lifecycle" + "golang.org/x/mobile/event/paint" + "golang.org/x/mobile/event/touch" +) + +func main() { + app.Main(func(a app.App) { + addr := "127.0.0.1:" + apptest.Port + log.Printf("addr: %s", addr) + + conn, err := net.Dial("tcp", addr) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + log.Printf("dialled") + comm := &apptest.Comm{ + Conn: conn, + Fatalf: log.Panicf, + Printf: log.Printf, + } + + comm.Send("hello_from_testapp") + comm.Recv("hello_from_host") + + sendPainting := false + var c config.Event + for e := range a.Events() { + switch e := app.Filter(e).(type) { + case lifecycle.Event: + switch e.Crosses(lifecycle.StageVisible) { + case lifecycle.CrossOn: + comm.Send("lifecycle_visible") + sendPainting = true + case lifecycle.CrossOff: + comm.Send("lifecycle_not_visible") + } + case config.Event: + c = e + comm.Send("config", c.PixelsPerPt) + case paint.Event: + if sendPainting { + comm.Send("paint") + sendPainting = false + } + a.EndPaint() + case touch.Event: + comm.Send("touch", e.Type, e.Loc.X.Px(c.PixelsPerPt), e.Loc.Y.Px(c.PixelsPerPt)) + } + } + }) +}