build/androidtest.bash: script to run all tests in a repository on an
android device.

It assumes it runs in golang.org/x/mobile repository.

This script copies the repository source tree into the connected device
and invokes go test that runs using the binary compiled from
go_android_exec.go. go_android_exec arranges to push and run the test
binary to the right place in the android device.

Change-Id: I4ac24bcfd8e26e01cccd3d7c2dddaf6b3c14951e
Reviewed-on: https://go-review.googlesource.com/1680
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/bind/java/test.bash b/bind/java/test.bash
index 53faa6d..6c1944a 100755
--- a/bind/java/test.bash
+++ b/bind/java/test.bash
@@ -18,8 +18,12 @@
   rm -rf "$ANDROID_APP"
 }
 
+if [ -z "$TMPDIR" ]; then
+	TMPDIR="/tmp"
+fi
+
 if [ -z "$ANDROID_APP" ]; then
-	ANDROID_APP=`mktemp -d /tmp/android-java.XXXXX` || die 'failed to create a temporary directory'
+	ANDROID_APP=`mktemp -d ${TMPDIR}/android-java.XXXXX` || die 'failed to create a temporary directory'
 	echo "Temporary directory for test: $ANDROID_APP"
 	trap cleanup EXIT
 fi
@@ -56,7 +60,7 @@
 
 # If there is no connected device, this will fail after creating the test apk.
 # The apk is located in $ANDROID_APP/build/outputs/apk directory.
-./gradlew connectedAndroidTest && echo "PASS"
+./gradlew connectedAndroidTest && echo "PASS" && exit 0
 
 # TODO(hyangah): copy the gradle's test output directory in case of test failure?
 
diff --git a/build/androidtest.bash b/build/androidtest.bash
new file mode 100755
index 0000000..5a80a21
--- /dev/null
+++ b/build/androidtest.bash
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+
+# Copyright 2014 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.
+
+# For testing Android. Run from a repository root.
+#   build/androidtest.bash
+# The compiler runs locally, pushes the copy of the repository
+# to a target device using adb, and runs tests in the device
+# using adb shell commands.
+
+set -e
+ulimit -c 0 # no core files
+
+function die() {
+  echo "FAIL: $1"
+  exit 1
+}
+
+if [ -z "${GOPATH}" ]; then
+  die 'GOPATH must be set.'
+fi
+
+readonly CURDIR=`pwd`
+if [ ! -f AUTHORS ]; then
+  die 'androidtest.bash must be run from the repository root.'
+fi
+
+function pkg() {
+  local paths=(${GOPATH//:/ })
+  for e in "${paths[@]}"; do
+    e="${e%/}"
+    local relpath="${CURDIR#"${e}/src/"}"
+    if [ "${relpath}" != "${CURDIR}" ]; then
+       echo "${relpath}"
+       break
+    fi
+  done
+}
+
+export PKG="$(pkg)"
+if [ -z "${PKG}" ]; then
+  die 'androidtest.bash failed to determine the repository package name.'
+fi
+
+export DEVICEDIR=/data/local/tmp/androidtest-$$
+export TMPDIR=`mktemp -d /tmp/androidtest.XXXXX`
+
+function cleanup() {
+  echo '# Cleaning up...'
+  rm -rf "$TMPDIR"
+  adb shell rm -rf "${DEVICEDIR}"
+}
+trap cleanup EXIT
+
+# 'adb sync' syncs data in ANDROID_PRODUCT_OUT directory.
+# We copy the entire golang.org/x/mobile/... (with -p option to preserve
+# file properties) to the directory assuming tests may depend only
+# on data in the same subrepository.
+echo '# Syncing test files to android device'
+export ANDROID_PRODUCT_OUT="${TMPDIR}/androidtest-$$"
+readonly LOCALDIR="${ANDROID_PRODUCT_OUT}/${DEVICEDIR}"
+
+mkdir -p "${LOCALDIR}/${PKG}"
+echo "cp -R --preserve=all ./* ${LOCALDIR}/${PKG}/"
+cp -R --preserve=all ./* "${LOCALDIR}/${PKG}/"
+
+time adb sync data &> /dev/null
+echo ''
+
+echo '# Run tests on android (arm7)'
+# Build go_android_${GOARCH}_exec that will be invoked for
+# GOOS=android GOARCH=$GOARCH go test/run.
+mkdir -p "${TMPDIR}/bin"
+export PATH=${TMPDIR}/bin:${PATH}
+GOOS="${GOHOSTOS}" GOARCH="${GOHOSTARCH}" go build \
+        -o "${TMPDIR}/bin/go_android_arm_exec" \
+        "${GOPATH}/src/golang.org/x/mobile/build/go_android_exec.go"
+CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 \
+	go test ./...
+
+# Special tests for mobile subrepository.
+if [ "$PKG" = "golang.org/x/mobile" ]; then
+	echo '# Run mobile=repository specific android tests.'
+
+	cd "${CURDIR}/bind/java"; ./test.bash
+fi
+
+exit  0
diff --git a/build/go_android_exec.go b/build/go_android_exec.go
new file mode 100644
index 0000000..fbeaf7f
--- /dev/null
+++ b/build/go_android_exec.go
@@ -0,0 +1,136 @@
+// Copyright 2014 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.
+
+// +build ignore
+
+// This program can be used as go_android_GOARCH_exec by the Go tool.
+// It executes binaries on an android device using adb.
+// This program is supposed to be called by
+// golang.org/x/mobile/build/androidtest.bash that arranges to copy
+// the tested repository source tree to the android device and invokes
+// go test. This program depends on PKG and DEVICEDIR environment variables
+// to identify the tested repository (e.g. golang.org/x/mobile) and
+// to find the source directory in the android device. The androidtest.bash
+// script is responsible for setting the environment variables.
+package main
+
+// This is adopted from golang.org/x/go/misc/android/go_android_exec.go.
+
+import (
+	"bytes"
+	"fmt"
+	"go/build"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"text/template"
+)
+
+func run(args ...string) string {
+	buf := new(bytes.Buffer)
+	cmd := exec.Command("adb", args...)
+	cmd.Stdout = io.MultiWriter(os.Stdout, buf)
+	cmd.Stderr = os.Stderr
+	log.Printf("adb %s", strings.Join(args, " "))
+	err := cmd.Run()
+	if err != nil {
+		log.Fatalf("adb %s: %v", strings.Join(args, " "), err)
+	}
+	return buf.String()
+}
+
+func rel(cwd, pkg string) (string, error) {
+	paths := build.Default.GOPATH
+	for _, p := range filepath.SplitList(paths) {
+		r, err := filepath.Rel(filepath.Join(p, "src", pkg), cwd)
+		if err == nil {
+			return r, nil
+		}
+	}
+	return "", fmt.Errorf("%q is not under GOPATH(%q)", cwd, paths)
+}
+
+func main() {
+	log.SetFlags(0)
+	log.SetPrefix("go_android_exec: ")
+
+	deviceRoot := "/data/local/tmp/"
+	if v := os.Getenv("DEVICEDIR"); v != "" {
+		deviceRoot = v
+	}
+
+	pkg := "golang.org/x/mobile"
+	if v := os.Getenv("PKG"); v != "" {
+		pkg = v
+	}
+
+	// Binary names can conflict.
+	// E.g. template.test from the {html,text}/template packages.
+	binName := filepath.Base(os.Args[1])
+	deviceBin := fmt.Sprintf("%s/%s-%d", deviceRoot, binName, os.Getpid())
+
+	// The push of the binary happens in parallel with other tests.
+	// Unfortunately, a simultaneous call to adb shell hold open
+	// file descriptors, so it is necessary to push then move to
+	// avoid a "text file busy" error on execution.
+	// https://code.google.com/p/android/issues/detail?id=65857
+	run("push", "-p", os.Args[1], deviceBin+"-tmp")
+	run("shell", "cp '"+deviceBin+"-tmp' '"+deviceBin+"'")
+	run("shell", "rm '"+deviceBin+"-tmp'")
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	subdir, err := rel(cwd, pkg)
+	if err != nil {
+		log.Fatal(err)
+	}
+	subdir = filepath.Join(deviceRoot, pkg, subdir)
+
+	// The adb shell command will return an exit code of 0 regardless
+	// of the command run. E.g.
+	//	$ adb shell false
+	//	$ echo $?
+	//	0
+	// https://code.google.com/p/android/issues/detail?id=3254
+	// So we append the exitcode to the output and parse it from there.
+	t := template.Must(template.New("cmd").Parse(
+		`export TMPDIR={{.Root}}/tmp; \
+		 mkdir -p "$TMPDIR"; \
+		 cd "{{.SubDir}}"; \
+		 {{.Bin}} {{.Args}}; \
+		 echo -n {{.ExitStr}}$?`))
+
+	var cmd bytes.Buffer
+	const exitstr = "exitcode="
+	if err := t.Execute(&cmd, struct {
+		Root, SubDir, Bin, Args, ExitStr string
+	}{
+		Root:    deviceRoot,
+		SubDir:  subdir,
+		Bin:     deviceBin,
+		Args:    strings.Join(os.Args[2:], " "),
+		ExitStr: exitstr,
+	}); err != nil {
+		log.Panicf("template error: %v", err)
+	}
+
+	output := run("shell", cmd.String())
+	output = output[strings.LastIndex(output, "\n")+1:]
+
+	if !strings.HasPrefix(output, exitstr) {
+		log.Fatalf("no exit code: %q", output)
+	}
+	code, err := strconv.Atoi(output[len(exitstr):])
+	if err != nil {
+		log.Fatalf("bad exit code: %v", err)
+	}
+	os.Exit(code)
+}