From 649599a96ce270004fad8176bf44df1c377a2f9e Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Wed, 10 Apr 2013 17:36:45 -0700 Subject: [PATCH 01/38] add selendroid submodule --- .gitmodules | 3 +++ submodules/selendroid | 1 + 2 files changed, 4 insertions(+) create mode 160000 submodules/selendroid diff --git a/.gitmodules b/.gitmodules index de30a3944..1aacce35e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "submodules/appium.io"] path = submodules/appium.io url = https://github.com/appium/appium.io.git +[submodule "submodules/selendroid"] + path = submodules/selendroid + url = https://github.com/DominikDary/selendroid.git diff --git a/submodules/selendroid b/submodules/selendroid new file mode 160000 index 000000000..a8c74e419 --- /dev/null +++ b/submodules/selendroid @@ -0,0 +1 @@ +Subproject commit a8c74e419126dd86919a3d45d750ea3d597eaf4c From 7631fb7f750b2ffa4d5f737976c2a71970d23283 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Wed, 10 Apr 2013 17:48:10 -0700 Subject: [PATCH 02/38] reset.sh for selendroid --- .gitignore | 1 + reset.sh | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 04f0265e5..bcadb9a66 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ app/android/AndroidManifest.xml *~ uiautomator/bootstrap/target/ org.eclipse.ltk.core.refactoring.prefs +selendroid diff --git a/reset.sh b/reset.sh index 68d9ca7fd..2fdf5e395 100755 --- a/reset.sh +++ b/reset.sh @@ -7,20 +7,29 @@ set -e should_reset_android=0 should_reset_ios=0 +should_reset_selendroid=0 +appium_home=$(pwd) +selendroid_ver="0.3" while test $# != 0 do - echo $1 case "$1" in "--android") should_reset_android=1;; "--ios") should_reset_ios=1;; + "--selendroid") should_reset_selendroid=1;; esac shift done -if [[ $should_reset_android -eq 0 ]] && [[ $should_reset_ios -eq 0 ]]; then +if [[ $should_reset_android -eq 0 ]] && [[ $should_reset_ios -eq 0 ]] && [[ $should_reset_selendroid -eq 0 ]]; then should_reset_android=1 should_reset_ios=1 + should_reset_selendroid=1 +fi + +# if selendroid is selected, we need all the android stuff too +if [ $should_reset_selendroid -eq 1 ]; then + should_reset_android=1 fi reset_general() { @@ -68,7 +77,7 @@ reset_android() { echo "Downloading/updating AndroidApiDemos" git submodule update --init submodules/ApiDemos rm -rf sample-code/apps/ApiDemos - ln -s $(pwd)/submodules/ApiDemos $(pwd)/sample-code/apps/ApiDemos + ln -s $appium_home/submodules/ApiDemos $appium_home/sample-code/apps/ApiDemos echo "Building Android bootstrap" grunt configAndroidBootstrap grunt buildAndroidBootstrap @@ -77,6 +86,20 @@ reset_android() { grunt buildAndroidApp:ApiDemos } +reset_selendroid() { + echo "---- RESETTING SELENDROID ----" + echo "Downloading/updating selendroid" + git submodule update --init submodules/selendroid + rm -rf selendroid + ln -s $appium_home/submodules/selendroid $appium_home/selendroid + echo "Building selendroid server for ApiDemos" + pushd selendroid/selendroid-server + mvn install -Dandroid.renameInstrumentationTargetPackage=com.example.android.apis + echo "Copying ApiDemos selendroid server to a good home" + cp target/selendroid-server-$selendroid_ver.apk $appium_home/selendroid/selendroid-apidemos.apk + popd +} + cleanup() { echo "---- CLEANING UP ----" echo "Cleaning temp files" @@ -92,4 +115,7 @@ fi if [ $should_reset_android -eq 1 ]; then reset_android fi +if [ $should_reset_selendroid -eq 1 ]; then + reset_selendroid +fi cleanup From 43e3cfebcf3ed3368cc160854db5a32e184507a0 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Wed, 10 Apr 2013 18:22:23 -0700 Subject: [PATCH 03/38] stub out selendroid.js --- app/selendroid.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 app/selendroid.js diff --git a/app/selendroid.js b/app/selendroid.js new file mode 100644 index 000000000..1c0b07f0b --- /dev/null +++ b/app/selendroid.js @@ -0,0 +1,31 @@ +"use strict"; + +var errors = require('./errors') + , adb = require('../uiautomator/adb') + , _ = require('underscore') + , logger = require('../logger').get('appium') + , status = require("./uiauto/lib/status") + , exec = require('child_process').exec + , fs = require('fs') + , async = require('async') + , path = require('path') + , UnknownError = errors.UnknownError; + +var Selendroid = function(opts) { + this.apkPath = opts.apkPath; + this.udid = opts.udid; + this.appPackage = opts.appPackage; + this.appActivity = opts.appActivity; + this.verbose = opts.verbose; + this.onStop = function() {}; + this.adb = null; + this.isProxy = true; +}; + +Selendroid.prototype.getProxiedSession = function(cb) { +}; + +module.exports = function(opts) { + return new Selendroid(opts); +}; + From 536243ae681b5a3bd237486a014b57ffeec6ab32 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Wed, 10 Apr 2013 18:34:09 -0700 Subject: [PATCH 04/38] getting some of the test setup going for selendroid --- app/appium.js | 18 ++++++++++++++++++ app/selendroid.js | 4 +++- test/helpers/driverblock.js | 10 ++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/appium.js b/app/appium.js index 0df035dd8..7e130dc97 100644 --- a/app/appium.js +++ b/app/appium.js @@ -15,6 +15,7 @@ var routing = require('./routing') , _ = require('underscore') , ios = require('./ios') , android = require('./android') + , selendroid = require('./selendroid') , status = require("./uiauto/lib/status"); var Appium = function(args) { @@ -108,6 +109,8 @@ Appium.prototype.getDeviceType = function(desiredCaps) { return "ios"; } else if (desiredCaps.device.toLowerCase().indexOf('ipad') !== -1) { return "ios"; + } else if (desiredCaps.device.toLowerCase().indexOf('selendroid') !== -1) { + return "selendroid"; } else { return "android"; } @@ -116,6 +119,8 @@ Appium.prototype.getDeviceType = function(desiredCaps) { return "ios"; } else if (desiredCaps.browserName.toLowerCase() === "safari") { return "ios"; + } else if (desiredCaps.browserName.toLowerCase().indexOf('selendroid') !== -1) { + return "selendroid"; } else { return "android"; } @@ -147,6 +152,10 @@ Appium.prototype.isAndroid = function() { return this.deviceType === "android"; }; +Appium.prototype.isSelendroid = function() { + return this.deviceType === "selendroid"; +}; + Appium.prototype.getAppExt = function() { return this.isIos() ? ".app" : ".apk"; }; @@ -352,6 +361,15 @@ Appium.prototype.invoke = function() { , fastReset: this.args.fastReset }; this.devices[this.deviceType] = android(androidOpts); + } else if (this.isSelendroid()) { + var selendroidOpts = { + apkPath: this.args.app + , verbose: this.args.verbose + , udid: this.args.udid + , appPackage: this.args.androidPackage + , appActivity: this.args.androidActivity + }; + this.devices[this.deviceType] = selendroid(selendroidOpts); } else { throw new Error("Tried to start a device that doesn't exist: " + this.deviceType); diff --git a/app/selendroid.js b/app/selendroid.js index 1c0b07f0b..6c45be795 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -22,7 +22,9 @@ var Selendroid = function(opts) { this.isProxy = true; }; -Selendroid.prototype.getProxiedSession = function(cb) { +Selendroid.prototype.start = function(cb) { + console.log("starting selendroid"); + cb(null); }; module.exports = function(opts) { diff --git a/test/helpers/driverblock.js b/test/helpers/driverblock.js index 1851e8e06..751d46b85 100644 --- a/test/helpers/driverblock.js +++ b/test/helpers/driverblock.js @@ -108,22 +108,24 @@ var describeForApp = function(app, device, appPackage, appActivity) { if (typeof device === "undefined") { device = "ios"; } - var browserName, appPath; + var browserName, appPath, realDevice; if (device === "ios") { + realDevice = "iPhone Simulator"; browserName = "iOS"; } else if (device === "android") { - browserName = "Android"; + browserName = realDevice = "Android"; + } else if (device === "selendroid") { + browserName = realDevice = "Selendroid"; } if (/\//.exec(app) || /\./.exec(app)) { appPath = app; } else { if (device === "ios") { appPath = path.resolve(__dirname, "../../sample-code/apps/" + app + "/build/Release-iphonesimulator/" + app + ".app"); - } else if (device === "android") { + } else if (device === "android" || device === "selendroid") { appPath = path.resolve(__dirname, "../../sample-code/apps/" + app + "/bin/" + app + "-debug.apk"); } } - var realDevice = device == "ios" ? "iPhone Simulator" : "Android"; return function(desc, tests, host, port, caps, extraCaps) { if (typeof extraCaps === "undefined") { From e59e703c0ab596a86e9efacf8885b094061f42e7 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:09:03 -0700 Subject: [PATCH 05/38] get selendroid server built and ready to install --- app/appium.js | 1 + app/device.js | 21 +++++++++++++++ app/helpers.js | 15 +++++++++++ app/selendroid.js | 69 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/app/appium.js b/app/appium.js index 7e130dc97..8f8890a8e 100644 --- a/app/appium.js +++ b/app/appium.js @@ -364,6 +364,7 @@ Appium.prototype.invoke = function() { } else if (this.isSelendroid()) { var selendroidOpts = { apkPath: this.args.app + , desiredCaps: this.desiredCapabilities , verbose: this.args.verbose , udid: this.args.udid , appPackage: this.args.androidPackage diff --git a/app/device.js b/app/device.js index 47f8d7534..0fafc950d 100644 --- a/app/device.js +++ b/app/device.js @@ -1,6 +1,7 @@ "use strict"; var errors = require('./errors') + , request = require('request') , logger = require('../logger').get('appium'); var UnknownError = errors.UnknownError @@ -57,3 +58,23 @@ exports.waitForCondition = function(waitMs, condFn, cb, intervalMs) { }; spin(); }; + +exports.bypass = function(url, method, body, contentType, cb) { + if (typeof cb === "undefined" && typeof contentType === "function") { + cb = contentType; + contentType = null; + } + if (typeof contentType === "undefined" || contentType === null) { + contentType = "application/json"; + } + if (!(/^https?:\/\//.exec(url))) { + url = 'http://' + url; + } + var opts = { + url: url + , method: method + , headers: {'Content-Type': contentType} + , body: body + }; + request(opts, cb); +}; diff --git a/app/helpers.js b/app/helpers.js index 6d6a4167a..fb8254d41 100644 --- a/app/helpers.js +++ b/app/helpers.js @@ -8,6 +8,7 @@ var logger = require('../logger').get('appium') , path = require('path') , rimraf = require('rimraf') , exec = require('child_process').exec + , util = require('util') , temp = require('temp'); exports.downloadFile = function(fileUrl, cb) { @@ -278,3 +279,17 @@ exports.rotateImage = function(imgPath, deg, cb) { cb(null); }); }; + +exports.copyFile = function (src, dst, cb) { + var is + , os; + + fs.stat(src, function (err) { + if (err) { + return cb(err); + } + is = fs.createReadStream(src); + os = fs.createWriteStream(dst); + util.pump(is, os, cb); + }); +}; diff --git a/app/selendroid.js b/app/selendroid.js index 6c45be795..804e141c8 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -3,10 +3,12 @@ var errors = require('./errors') , adb = require('../uiautomator/adb') , _ = require('underscore') + , bypass = require('./device').bypass , logger = require('../logger').get('appium') , status = require("./uiauto/lib/status") , exec = require('child_process').exec , fs = require('fs') + , copyFile = require('./helpers').copyFile , async = require('async') , path = require('path') , UnknownError = errors.UnknownError; @@ -16,15 +18,78 @@ var Selendroid = function(opts) { this.udid = opts.udid; this.appPackage = opts.appPackage; this.appActivity = opts.appActivity; + this.desiredCaps = opts.desiredCaps; this.verbose = opts.verbose; this.onStop = function() {}; this.adb = null; this.isProxy = true; + this.proxyHost = 'localhost'; + this.proxyPort = 8080; }; Selendroid.prototype.start = function(cb) { - console.log("starting selendroid"); - cb(null); + logger.info("Starting selendroid server"); + this.ensureServerExists(_.bind(function(err) { + if (err) return cb(err); + // modify desired caps + var desiredCaps = _.clone(this.desiredCaps); + //this.bypass('/wd/hub/session', 'POST', desiredCaps, function(err, res, body) { + //console.log(err); + //console.log(res); + //console.log(body); + //cb(null); + //}); + cb(null); + }, this)); +}; + +Selendroid.prototype.ensureServerExists = function(cb) { + logger.info("Checking whether selendroid is built for package yet"); + var fileName = 'selendroid-' + this.appPackage + '.apk'; + var filePath = path.resolve(__dirname, "../selendroid/selendroid-server", + "target", fileName); + fs.stat(filePath, _.bind(function(err) { + if (err) { + logger.info("Selendroid needs to be built"); + return this.buildServer(cb); + } + logger.info("Selendroid server already exists, not rebuilding"); + cb(null); + }, this)); +}; + +Selendroid.prototype.buildServer = function(cb) { + logger.info("Building selendroid server for package " + this.appPackage); + var buildDir = path.resolve(__dirname, "../selendroid/selendroid-server"); + var src = buildDir + "/target/selendroid-server-0.3.apk"; + var dest = buildDir + "/target/selendroid- " + this.appPackage + '.apk'; + var cmd = "mvn install -Dandroid.renameInstrumentationTargetPackage=" + + this.appPackage; + exec(cmd, {cwd: buildDir}, function(err, stdout, stderr) { + if (err) { + logger.error("Unable to build selendroid server"); + console.log(stdout); + console.log(stderr); + return cb(err); + } + logger.info("Copying selendroid server to correct destination"); + copyFile(src, dest, function(err) { + if (err) { + logger.error("Error copying selendroid to destination"); + return cb(err); + } + logger.info("Selendroid server copied successfully"); + cb(null); + }); + }); +}; + +Selendroid.prototype.bypass = function(endpoint, method, data, cb) { + if (endpoint[0] !== '/') { + endpoint = '/' + endpoint; + } + var url = 'http://' + this.proxyHost + ':' + this.proxyPort + endpoint; + bypass(url, method, data ? data : null, cb); }; module.exports = function(opts) { From eec4122525051aa120f62fdd6b89b9781e2513b4 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:11:00 -0700 Subject: [PATCH 06/38] use camel case --- uiautomator/adb.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 8b3fb19c4..d14537d33 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -153,11 +153,11 @@ ADB.prototype.compileManifest = function(cb, manifest) { } // Compile manifest into manifest.xml.apk - var compile_manifest = ['aapt package -M "', manifest, + var compileManifest = ['aapt package -M "', manifest, '" -I "', platforms + platform + '/android.jar" -F "', manifest, '.apk" -f'].join(''); - logger.debug(compile_manifest); - exec(compile_manifest, {}, function(err, stdout, stderr) { + logger.debug(compileManifest); + exec(compileManifest, {}, function(err, stdout, stderr) { if (err) { logger.debug(stderr); return cb("error compiling manifest"); From fcb310b81c3ce153ba7c1d02aca131c5a6a0dde4 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:13:15 -0700 Subject: [PATCH 07/38] rename adb.start to adb.startAppium because there will be other kinds --- app/android.js | 2 +- uiautomator/adb.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/android.js b/app/android.js index e7f319eee..828e39e28 100644 --- a/app/android.js +++ b/app/android.js @@ -127,7 +127,7 @@ Android.prototype.start = function(cb, onDie) { if (this.adb === null) { // Pass Android opts and Android ref to adb. this.adb = adb(this.opts, this); - this.adb.start(onLaunch, onExit); + this.adb.startAppium(onLaunch, onExit); } else { logger.error("Tried to start ADB when we already have one running!"); } diff --git a/uiautomator/adb.js b/uiautomator/adb.js index d14537d33..9888b003c 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -297,7 +297,7 @@ ADB.prototype.checkFastReset = function(cb) { }); }; -ADB.prototype.start = function(onReady, onExit) { +ADB.prototype.startAppium = function(onReady, onExit) { this.onExit = onExit; var doRun = _.bind(function() { this.runBootstrap(onReady, onExit); From 67ee6a79b8e2956773d4e8c19be620af8c55ce90 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:22:41 -0700 Subject: [PATCH 08/38] factor out device preparation logic so it can be reused --- uiautomator/adb.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 9888b003c..d67fdafed 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -297,14 +297,7 @@ ADB.prototype.checkFastReset = function(cb) { }); }; -ADB.prototype.startAppium = function(onReady, onExit) { - this.onExit = onExit; - var doRun = _.bind(function() { - this.runBootstrap(onReady, onExit); - }, this); - - logger.debug("Using fast reset? " + this.fastReset); - +ADB.prototype.prepareDevice = function(onReady) { var me = this; async.series( [ @@ -324,7 +317,10 @@ ADB.prototype.startAppium = function(onReady, onExit) { if (err) { logger.info("restarting..."); me.restartAdb(function() { - getDevices(cb); + getDevices(function(err) { + if (err) return onReady(err); + cb(null); + }); }); } else { cb(null); @@ -335,10 +331,28 @@ ADB.prototype.startAppium = function(onReady, onExit) { me.waitForDevice(function(err) { if (err) return onReady(err); cb(null); }); }, function(cb) { - me.pushAppium(function(err) { if (err) return onReady(err); cb(null); }); + me.forwardPort(function(err) { if (err) return onReady(err); cb(null); }); + } + ], onReady + ); +}; + +ADB.prototype.startAppium = function(onReady, onExit) { + this.onExit = onExit; + var doRun = _.bind(function() { + this.runBootstrap(onReady, onExit); + }, this); + + logger.debug("Using fast reset? " + this.fastReset); + + var me = this; + async.series( + [ + function(cb) { + me.prepareDevice(function(err) { if (err) return onReady(err); cb(null); }); }, function(cb) { - me.forwardPort(function(err) { if (err) return onReady(err); cb(null); }); + me.pushAppium(function(err) { if (err) return onReady(err); cb(null); }); }, function(cb) { if (!me.appPackage) return onReady("appPackage must be set."); From a0e20265bab91a6a6f0ade78808d223563afaeb8 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:42:42 -0700 Subject: [PATCH 09/38] fix callback return error and prettify async series cb --- app/android.js | 1 + uiautomator/adb.js | 25 ++++++++----------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/app/android.js b/app/android.js index 828e39e28..485bb3fda 100644 --- a/app/android.js +++ b/app/android.js @@ -72,6 +72,7 @@ Android.prototype.start = function(cb, onDie) { var skipRelaunchOn = [ 'App never showed up' , 'Could not sign one or more apks' + , 'Could not find a connected Android device' ]; var checkShouldSkipRelaunch = function(msg) { var skip = false; diff --git a/uiautomator/adb.js b/uiautomator/adb.js index d67fdafed..26258099c 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -301,38 +301,29 @@ ADB.prototype.prepareDevice = function(onReady) { var me = this; async.series( [ - function(cb) { - me.checkAdbPresent(function(err) { if (err) return onReady(err); cb(null); }); - }, + function(cb) { me.checkAdbPresent(cb); }, function(cb) { var getDevices = function(innerCb) { me.getConnectedDevices(function(err, devices) { if (devices.length === 0 || err) { - innerCb("Could not find a connected Android device."); + return innerCb(new Error("Could not find a connected Android device.")); } innerCb(null); }); }; getDevices(function(err) { if (err) { - logger.info("restarting..."); + logger.info("Restarting adb..."); me.restartAdb(function() { - getDevices(function(err) { - if (err) return onReady(err); - cb(null); - }); + getDevices(cb); }); } else { cb(null); } }); }, - function(cb) { - me.waitForDevice(function(err) { if (err) return onReady(err); cb(null); }); - }, - function(cb) { - me.forwardPort(function(err) { if (err) return onReady(err); cb(null); }); - } + function(cb) { me.waitForDevice(cb); }, + function(cb) { me.forwardPort(cb); } ], onReady ); }; @@ -671,13 +662,13 @@ ADB.prototype.waitForDevice = function(cb) { ADB.prototype.restartAdb = function(cb) { logger.info("Killing ADB server so it will come back online"); var cmd = this.adb + " kill-server"; - exec(cmd, _.bind(function(err) { + exec(cmd, function(err) { if (err) { logger.error("Error killing ADB server, going to see if it's online " + "anyway"); } cb(); - }, this)); + }); }; ADB.prototype.pushAppium = function(cb) { From 204580f221cf35d8fa50dc9e363f5c8b2ef087fe Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:44:08 -0700 Subject: [PATCH 10/38] more async compactification --- uiautomator/adb.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 26258099c..f31c26195 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -339,28 +339,20 @@ ADB.prototype.startAppium = function(onReady, onExit) { var me = this; async.series( [ + function(cb) { me.prepareDevice(cb); }, + function(cb) { me.pushAppium(cb); }, function(cb) { - me.prepareDevice(function(err) { if (err) return onReady(err); cb(null); }); - }, - function(cb) { - me.pushAppium(function(err) { if (err) return onReady(err); cb(null); }); - }, - function(cb) { - if (!me.appPackage) return onReady("appPackage must be set."); - me.checkFastReset(function(err) { - if (err) return onReady(err); cb(null); - }); - }, - function(cb) { - me.installApp(function(err) { if (err) return onReady(err); cb(null); }); + if (!me.appPackage) return cb(new Error("appPackage must be set.")); + me.checkFastReset(cb); }, + function(cb) { me.installApp(cb); }, function(cb) { me.startApp(function(err) { - if (err) return onReady(err); + if (err) return cb(err); doRun(function(){ cb(null); }); }); } - ] + ], onReady ); }; From 9c8eeb3488ddde5670debf9fa61beae554246b6c Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:44:44 -0700 Subject: [PATCH 11/38] make it even more compact --- uiautomator/adb.js | 80 ++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index f31c26195..811faf21d 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -299,33 +299,31 @@ ADB.prototype.checkFastReset = function(cb) { ADB.prototype.prepareDevice = function(onReady) { var me = this; - async.series( - [ - function(cb) { me.checkAdbPresent(cb); }, - function(cb) { - var getDevices = function(innerCb) { - me.getConnectedDevices(function(err, devices) { - if (devices.length === 0 || err) { - return innerCb(new Error("Could not find a connected Android device.")); - } - innerCb(null); - }); - }; - getDevices(function(err) { - if (err) { - logger.info("Restarting adb..."); - me.restartAdb(function() { - getDevices(cb); - }); - } else { - cb(null); + async.series([ + function(cb) { me.checkAdbPresent(cb); }, + function(cb) { + var getDevices = function(innerCb) { + me.getConnectedDevices(function(err, devices) { + if (devices.length === 0 || err) { + return innerCb(new Error("Could not find a connected Android device.")); } + innerCb(null); }); - }, - function(cb) { me.waitForDevice(cb); }, - function(cb) { me.forwardPort(cb); } - ], onReady - ); + }; + getDevices(function(err) { + if (err) { + logger.info("Restarting adb..."); + me.restartAdb(function() { + getDevices(cb); + }); + } else { + cb(null); + } + }); + }, + function(cb) { me.waitForDevice(cb); }, + function(cb) { me.forwardPort(cb); } + ], onReady); }; ADB.prototype.startAppium = function(onReady, onExit) { @@ -337,23 +335,21 @@ ADB.prototype.startAppium = function(onReady, onExit) { logger.debug("Using fast reset? " + this.fastReset); var me = this; - async.series( - [ - function(cb) { me.prepareDevice(cb); }, - function(cb) { me.pushAppium(cb); }, - function(cb) { - if (!me.appPackage) return cb(new Error("appPackage must be set.")); - me.checkFastReset(cb); - }, - function(cb) { me.installApp(cb); }, - function(cb) { - me.startApp(function(err) { - if (err) return cb(err); - doRun(function(){ cb(null); }); - }); - } - ], onReady - ); + async.series( [ + function(cb) { me.prepareDevice(cb); }, + function(cb) { me.pushAppium(cb); }, + function(cb) { + if (!me.appPackage) return cb(new Error("appPackage must be set.")); + me.checkFastReset(cb); + }, + function(cb) { me.installApp(cb); }, + function(cb) { + me.startApp(function(err) { + if (err) return cb(err); + doRun(function(){ cb(null); }); + }); + } + ], onReady); }; ADB.prototype.getConnectedDevices = function(cb) { From aad0c35e973b6a11062b60e6a1bede239f4b74c7 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:50:50 -0700 Subject: [PATCH 12/38] wow this is the way to do async series! --- uiautomator/adb.js | 70 ++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 811faf21d..7483de09b 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -278,6 +278,8 @@ ADB.prototype.checkFastReset = function(cb) { return cb(null); } + if (!this.appPackage) return cb(new Error("appPackage must be set.")); + var me = this; me.checkApkCert(me.cleanAPK, function(cleanSigned){ me.checkApkCert(me.apkPath, function(appSigned){ @@ -297,59 +299,55 @@ ADB.prototype.checkFastReset = function(cb) { }); }; +ADB.prototype.getDeviceWithRetry = function(cb) { + var me = this; + var getDevices = function(innerCb) { + me.getConnectedDevices(function(err, devices) { + if (devices.length === 0 || err) { + return innerCb(new Error("Could not find a connected Android device.")); + } + innerCb(null); + }); + }; + getDevices(function(err) { + if (err) { + logger.info("Could not find devices, restarting adb server..."); + me.restartAdb(function() { + getDevices(cb); + }); + } else { + cb(null); + } + }); +}; + ADB.prototype.prepareDevice = function(onReady) { var me = this; async.series([ function(cb) { me.checkAdbPresent(cb); }, - function(cb) { - var getDevices = function(innerCb) { - me.getConnectedDevices(function(err, devices) { - if (devices.length === 0 || err) { - return innerCb(new Error("Could not find a connected Android device.")); - } - innerCb(null); - }); - }; - getDevices(function(err) { - if (err) { - logger.info("Restarting adb..."); - me.restartAdb(function() { - getDevices(cb); - }); - } else { - cb(null); - } - }); - }, + function(cb) { me.getDeviceWithRetry(cb);}, function(cb) { me.waitForDevice(cb); }, function(cb) { me.forwardPort(cb); } ], onReady); }; ADB.prototype.startAppium = function(onReady, onExit) { + var me = this + , doRun = function(err) { + if (err) return onReady(err); + me.runBootstrap(onReady, onExit); + }; this.onExit = onExit; - var doRun = _.bind(function() { - this.runBootstrap(onReady, onExit); - }, this); logger.debug("Using fast reset? " + this.fastReset); - var me = this; - async.series( [ + async.series([ function(cb) { me.prepareDevice(cb); }, function(cb) { me.pushAppium(cb); }, - function(cb) { - if (!me.appPackage) return cb(new Error("appPackage must be set.")); - me.checkFastReset(cb); - }, + function(cb) { me.checkFastReset(cb); }, function(cb) { me.installApp(cb); }, - function(cb) { - me.startApp(function(err) { - if (err) return cb(err); - doRun(function(){ cb(null); }); - }); - } - ], onReady); + function(cb) { me.startApp(cb); } + ], doRun); }; ADB.prototype.getConnectedDevices = function(cb) { From c7dbf2b09ab00e1ec3ec87c61994b8a118b5e0fe Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 10:55:35 -0700 Subject: [PATCH 13/38] add concept of different system vs device ports --- uiautomator/adb.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 7483de09b..f04900cf4 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -27,7 +27,8 @@ var ADB = function(opts, android) { // Don't uninstall if using fast reset. // Uninstall if reset is set and fast reset isn't. this.skipUninstall = opts.fastReset || !(opts.reset || false); - this.port = opts.port || 4724; + this.systemPort = opts.port || 4724; + this.devicePort = opts.devicePort || 4724; this.avdName = opts.avdName; this.appPackage = opts.appPackage; this.appActivity = opts.appActivity; @@ -374,9 +375,9 @@ ADB.prototype.getConnectedDevices = function(cb) { ADB.prototype.forwardPort = function(cb) { this.requireDeviceId(); - var devicePort = 4724; - this.debug("Forwarding system:" + this.port + " to device:" + devicePort); - var arg = "tcp:" + this.port + " tcp:" + devicePort; + this.debug("Forwarding system:" + this.systemPort + " to device:" + + this.devicePort); + var arg = "tcp:" + this.systemPort + " tcp:" + this.devicePort; exec(this.adbCmd + " forward " + arg, _.bind(function(err) { if (err) { logger.error(err); @@ -436,7 +437,7 @@ ADB.prototype.checkForSocketReady = function(output) { if (/Appium Socket Server Ready/.exec(output)) { this.requirePortForwarded(); this.debug("Connecting to server on device..."); - this.socketClient = net.connect(this.port, _.bind(function() { + this.socketClient = net.connect(this.systemPort, _.bind(function() { this.debug("Connected!"); this.onSocketReady(null); }, this)); From 0a40716f3428f66d936565b0cde6bb26e219e4e7 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:02:54 -0700 Subject: [PATCH 14/38] man were we doing it wrong with async, this feels so good --- uiautomator/adb.js | 111 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index f04900cf4..02f34cf4d 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -325,10 +325,10 @@ ADB.prototype.getDeviceWithRetry = function(cb) { ADB.prototype.prepareDevice = function(onReady) { var me = this; async.series([ - function(cb) { me.checkAdbPresent(cb); }, - function(cb) { me.getDeviceWithRetry(cb);}, - function(cb) { me.waitForDevice(cb); }, - function(cb) { me.forwardPort(cb); } + me.checkAdbPresent + , me.getDeviceWithRetry + , me.waitForDevice + , me.forwardPort ], onReady); }; @@ -343,11 +343,11 @@ ADB.prototype.startAppium = function(onReady, onExit) { logger.debug("Using fast reset? " + this.fastReset); async.series([ - function(cb) { me.prepareDevice(cb); }, - function(cb) { me.pushAppium(cb); }, - function(cb) { me.checkFastReset(cb); }, - function(cb) { me.installApp(cb); }, - function(cb) { me.startApp(cb); } + me.prepareDevice + , me.pushAppium + , me.checkFastReset + , me.installApp + , me.startApp ], doRun); }; @@ -843,57 +843,52 @@ ADB.prototype.runFastReset = function(cb) { }; ADB.prototype.installApp = function(cb) { - var me = this; - var next = function() { - me.requireDeviceId(); - me.requireApk(); - var installApp = false; - var installClean = false; - async.series( - [ - function(cb) { - var listPkgCmd = me.adbCmd + " shell pm list packages -3 " + me.appPackage; - exec(listPkgCmd, function(err, stdout) { - var apkInstalledRgx = new RegExp('^package:' + me.appPackage.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); - installApp = ! apkInstalledRgx.test(stdout); - var cleanInstalledRgx = new RegExp('^package:' + (me.appPackage + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); - installClean = ! cleanInstalledRgx.test(stdout); - me.debug("Packages found:"); - me.debug(stdout); - cb(null); - }); - }, - function(cb) { - if (installApp) { - me.debug("Installing app apk"); - me.installApk(me.apkPath, function(err) { - if (err) return cb(err); - return cb(null); - }); - } else { cb(null); } - }, - function(cb) { - if (installClean) { - me.debug("Installing clean apk"); - me.installApk(me.cleanAPK, function(err) { if (err) return cb(err); return cb(null); }); - } else { cb(null); } - }, - function(cb) { - // App is already installed so reset it. - if (!installApp) { - me.runFastReset(function(err) { - if (err) return cb(err); - return cb(null); - }); - } else { cb(null); } - } - ], - // Top level callback. - function(err) { cb(err); } - ); + var me = this + , installApp = false + , installClean = false; + me.requireDeviceId(); + me.requireApk(); + + var determineInstallAndCleanStatus = function(cb) { + var listPkgCmd = me.adbCmd + " shell pm list packages -3 " + me.appPackage; + exec(listPkgCmd, function(err, stdout) { + var apkInstalledRgx = new RegExp('^package:' + me.appPackage.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); + installApp = ! apkInstalledRgx.test(stdout); + var cleanInstalledRgx = new RegExp('^package:' + (me.appPackage + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); + installClean = ! cleanInstalledRgx.test(stdout); + me.debug("Packages found:"); + me.debug(stdout); + cb(null); + }); }; - next(); + var doInstall = function(cb) { + if (installApp) { + me.debug("Installing app apk"); + me.installApk(me.apkPath, cb); + } else { cb(null); } + }; + + var doClean = function(cb) { + if (installClean) { + me.debug("Installing clean apk"); + me.installApk(me.cleanAPK, cb); + } else { cb(null); } + }; + + var doFastReset = function(cb) { + // App is already installed so reset it. + if (!installApp) { + me.runFastReset(cb); + } else { cb(null); } + }; + + async.series([ + determineInstallAndCleanStatus, + doInstall, + doClean, + doFastReset + ], cb); }; ADB.prototype.back = function(cb) { From 09b268063a6108d7a4459310ddbd10c69ba0242a Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:06:41 -0700 Subject: [PATCH 15/38] well looks like i can't get _too_ elegant --- uiautomator/adb.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 02f34cf4d..200313652 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -325,10 +325,10 @@ ADB.prototype.getDeviceWithRetry = function(cb) { ADB.prototype.prepareDevice = function(onReady) { var me = this; async.series([ - me.checkAdbPresent - , me.getDeviceWithRetry - , me.waitForDevice - , me.forwardPort + function(cb) { me.checkAdbPresent(cb); }, + function(cb) { me.getDeviceWithRetry(cb);}, + function(cb) { me.waitForDevice(cb); }, + function(cb) { me.forwardPort(cb); } ], onReady); }; @@ -343,11 +343,11 @@ ADB.prototype.startAppium = function(onReady, onExit) { logger.debug("Using fast reset? " + this.fastReset); async.series([ - me.prepareDevice - , me.pushAppium - , me.checkFastReset - , me.installApp - , me.startApp + function(cb) { me.prepareDevice(cb); }, + function(cb) { me.pushAppium(cb); }, + function(cb) { me.checkFastReset(cb); }, + function(cb) { me.installApp(cb); }, + function(cb) { me.startApp(cb); } ], doRun); }; @@ -884,10 +884,10 @@ ADB.prototype.installApp = function(cb) { }; async.series([ - determineInstallAndCleanStatus, - doInstall, - doClean, - doFastReset + function(cb) { determineInstallAndCleanStatus(cb); }, + function(cb) { doInstall(cb); }, + function(cb) { doClean(cb); }, + function(cb) { doFastReset(cb); } ], cb); }; From c735dd42896e98519297136ec116b604cedae013 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:08:06 -0700 Subject: [PATCH 16/38] moar series refactorz --- uiautomator/adb.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 200313652..a09b30137 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -110,21 +110,10 @@ ADB.prototype.buildFastReset = function(skipAppSign, cb) { fs.writeFileSync(manifest, newContent, utf8); logger.debug("Created manifest"); - async.series( - [ - function(cb) { - me.compileManifest(function(err) { if (err) return cb(err); cb(null); }, manifest); - }, - function(cb) { - me.insertManifest(manifest, skipAppSign, function(err) { - if (err) return cb(err); - cb(null); - }); - }, - ], - // Invoke top level function cb - function(err) { cb(err); } - ); + async.series([ + function(cb) { me.compileManifest(cb, manifest); }, + function(cb) { me.insertManifest(manifest, skipAppSign, cb); }, + ], cb); }; ADB.prototype.compileManifest = function(cb, manifest) { From c3e5745d0c549e39142a6360b13117a17d5e4988 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:16:36 -0700 Subject: [PATCH 17/38] more refactoring of adb --- uiautomator/adb.js | 91 +++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index a09b30137..b67b9fc53 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -55,24 +55,33 @@ var ADB = function(opts, android) { this.cmdCb = null; }; -ADB.prototype.checkAdbPresent = function(cb) { +ADB.prototype.checkSdkBinaryPresent = function(binary, cb) { + var binaryLoc = null; if (this.sdkRoot) { - this.adb = path.resolve(this.sdkRoot, "platform-tools", "adb"); - this.debug("Using adb from " + this.adb); - cb(null, this); + binaryLoc = path.resolve(this.sdkRoot, "platform-tools", binary); + this.debug("Using " + binary + " from " + binaryLoc); + cb(null, binaryLoc); } else { - exec("which adb", _.bind(function(err, stdout) { + exec("which " + binary, _.bind(function(err, stdout) { if (stdout) { - this.debug("Using adb from " + stdout); - this.adb = stdout; - cb(null, this); + this.debug("Using " + binary + " from " + stdout); + cb(null, stdout); } else { - cb("Could not find adb; do you have android SDK installed?", null); + cb(new Error("Could not find " + binary + "; do you have android " + + "SDK installed?"), + null); } }, this)); } }; +ADB.prototype.checkAdbPresent = function(cb) { + this.checkSdkBinaryPresent("adb", _.bind(function(err, binaryLoc) { + if (err) return cb(err); + this.adb = binaryLoc; + }, this)); +}; + // Fast reset ADB.prototype.buildFastReset = function(skipAppSign, cb) { // Create manifest @@ -111,55 +120,39 @@ ADB.prototype.buildFastReset = function(skipAppSign, cb) { logger.debug("Created manifest"); async.series([ + function(cb) { me.checkSdkBinaryPresent("aapt", cb); }, function(cb) { me.compileManifest(cb, manifest); }, function(cb) { me.insertManifest(manifest, skipAppSign, cb); }, ], cb); }; ADB.prototype.compileManifest = function(cb, manifest) { - async.series( - [ - function(cb) { - exec('which aapt', function(err, stdout) { - if (!stdout) { - return cb(new Error("Error finding aapt binary, is it on your path?")); - } - cb(null); - }); - }, - function(cb) { - var androidHome = process.env.ANDROID_HOME; + var androidHome = process.env.ANDROID_HOME + , platforms = androidHome + '/platforms/' + , platform = 'android-17'; - var platforms = androidHome + '/platforms/'; - var platform = 'android-17'; + // android-17 may be called android-4.2 + if (!fs.existsSync(platforms + platform)) { + platform = 'android-4.2'; - // android-17 may be called android-4.2 - if (!fs.existsSync(platforms + platform)) { - platform = 'android-4.2'; + if (!fs.existsSync(platforms + platform)) { + return cb(new Error("Platform doesn't exist " + platform)); + } + } - if (!fs.existsSync(platforms + platform)) { - return cb("Platform doesn't exist " + platform); - } - } - - // Compile manifest into manifest.xml.apk - var compileManifest = ['aapt package -M "', manifest, - '" -I "', platforms + platform + '/android.jar" -F "', - manifest, '.apk" -f'].join(''); - logger.debug(compileManifest); - exec(compileManifest, {}, function(err, stdout, stderr) { - if (err) { - logger.debug(stderr); - return cb("error compiling manifest"); - } - logger.debug("Compiled manifest"); - cb(null); - }); - }, - ], - // Invoke top level function cb - function(err) { cb(err); } - ); + // Compile manifest into manifest.xml.apk + var compileManifest = ['aapt package -M "', manifest, + '" -I "', platforms + platform + '/android.jar" -F "', + manifest, '.apk" -f'].join(''); + logger.debug(compileManifest); + exec(compileManifest, {}, function(err, stdout, stderr) { + if (err) { + logger.debug(stderr); + return cb("error compiling manifest"); + } + logger.debug("Compiled manifest"); + cb(null); + }); }; ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { From d4ca0e8d46908ca98f9951651ca4a694caeab4d7 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:17:49 -0700 Subject: [PATCH 18/38] oops forgot to call back here --- uiautomator/adb.js | 1 + 1 file changed, 1 insertion(+) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index b67b9fc53..7f780c02b 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -79,6 +79,7 @@ ADB.prototype.checkAdbPresent = function(cb) { this.checkSdkBinaryPresent("adb", _.bind(function(err, binaryLoc) { if (err) return cb(err); this.adb = binaryLoc; + cb(null); }, this)); }; From 6b9bf2886d180c7f22d11888861f113487f4221f Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 11:25:13 -0700 Subject: [PATCH 19/38] more series refactoring --- uiautomator/adb.js | 122 ++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 7f780c02b..26e952073 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -157,70 +157,68 @@ ADB.prototype.compileManifest = function(cb, manifest) { }; ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { - var me = this; - var targetAPK = me.apkPath; - var cleanAPK = me.cleanAPK; - async.series( - [ - // Extract compiled manifest from manifest.xml.apk - function(cb) { - unzipFile(manifest + '.apk', function(err, stderr) { - if (err) { - logger.debug(stderr); - return cb(err); - } - cb(null); - }); - }, - // Insert compiled manfiest into /tmp/appPackage.clean.apk - function(cb) { - var cleanAPKSource = path.resolve(__dirname, '../app/android/Clean.apk'); - logger.debug("Writing tmp apk. " + cleanAPKSource + ' to ' + cleanAPK); + var me = this + , targetAPK = me.apkPath + , cleanAPK = me.cleanAPK; - // Write clean apk to /tmp - ncp(cleanAPKSource, cleanAPK, function(err) { - if (err) return cb(err); - cb(null); - }); - }, - function(cb) { - logger.debug("Testing tmp clean apk."); - testZipArchive(cleanAPK, function(err) { - if (err) return cb(err); - cb(null); - }); - }, - function(cb) { - // -j = keep only the file, not the dirs - // -m = move manifest into target apk. - var replaceCmd = 'zip -j -m "' + cleanAPK + '" "' + manifest + '"'; - logger.debug(replaceCmd); - exec(replaceCmd, {}, function(err) { - if (err) { - return cb(err); - } - logger.debug("Inserted manifest."); - cb(null); - }); - }, - // Resign clean apk and target apk - function(cb) { - var apks = [ cleanAPK ]; - if (!skipAppSign) { - logger.debug("Signing app and clean apk."); - apks.push(targetAPK); - } else { - logger.debug("Skip app sign. Sign clean apk."); - } - me.sign(function(err) { - if (err) return cb(err); - cb(null); - }, apks); + var extractManifest = function(cb) { + // Extract compiled manifest from manifest.xml.apk + unzipFile(manifest + '.apk', function(err, stderr) { + if (err) { + logger.debug(stderr); + return cb(err); } - ], - // Invoke top level function cb - function(err) { cb(err); } - ); + cb(null); + }); + }; + + var insertManifestIntoClean = function(cb) { + // Insert compiled manfiest into /tmp/appPackage.clean.apk + var cleanAPKSource = path.resolve(__dirname, '../app/android/Clean.apk'); + logger.debug("Writing tmp apk. " + cleanAPKSource + ' to ' + cleanAPK); + + // Write clean apk to /tmp + ncp(cleanAPKSource, cleanAPK, cb); + }; + + var testCleanApk = function(cb) { + logger.debug("Testing tmp clean apk."); + testZipArchive(cleanAPK, cb); + }; + + var moveManifest = function(cb) { + // -j = keep only the file, not the dirs + // -m = move manifest into target apk. + var replaceCmd = 'zip -j -m "' + cleanAPK + '" "' + manifest + '"'; + logger.debug(replaceCmd); + exec(replaceCmd, {}, function(err) { + if (err) { + return cb(err); + } + logger.debug("Inserted manifest."); + cb(null); + }); + }; + + var resignApks = function(cb) { + // Resign clean apk and target apk + var apks = [ cleanAPK ]; + if (!skipAppSign) { + logger.debug("Signing app and clean apk."); + apks.push(targetAPK); + } else { + logger.debug("Skip app sign. Sign clean apk."); + } + me.sign(cb, apks); + }; + + async.series([ + function(cb) { extractManifest(cb); }, + function(cb) { insertManifestIntoClean(cb); }, + function(cb) { testCleanApk(cb); }, + function(cb) { moveManifest(cb); }, + function(cb) { resignApks(cb); } + ], cb); }; // apks is an array of strings. From 7b8f257b1a4fa6e120c07b4601789fe607f598c0 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 12:03:45 -0700 Subject: [PATCH 20/38] don't run fast reset if not set, and selendroid error handling --- app/appium.js | 1 + app/selendroid.js | 41 +++++++++++++++++++++++++---------------- uiautomator/adb.js | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/app/appium.js b/app/appium.js index 8f8890a8e..a9091d0e0 100644 --- a/app/appium.js +++ b/app/appium.js @@ -369,6 +369,7 @@ Appium.prototype.invoke = function() { , udid: this.args.udid , appPackage: this.args.androidPackage , appActivity: this.args.androidActivity + , appDeviceReadyTimeout: this.args.androidDeviceReadyTimeout }; this.devices[this.deviceType] = selendroid(selendroidOpts); } else { diff --git a/app/selendroid.js b/app/selendroid.js index 804e141c8..39f08b1aa 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -14,12 +14,10 @@ var errors = require('./errors') , UnknownError = errors.UnknownError; var Selendroid = function(opts) { - this.apkPath = opts.apkPath; - this.udid = opts.udid; + this.opts = opts; + this.serverApk = null; this.appPackage = opts.appPackage; - this.appActivity = opts.appActivity; this.desiredCaps = opts.desiredCaps; - this.verbose = opts.verbose; this.onStop = function() {}; this.adb = null; this.isProxy = true; @@ -29,17 +27,26 @@ var Selendroid = function(opts) { Selendroid.prototype.start = function(cb) { logger.info("Starting selendroid server"); + var opts = _.clone(this.opts); + opts.port = this.proxyPort; + opts.devicePort = 8080; + opts.fastReset = false; + opts.cleanApp = false; + this.adb = new adb(opts); this.ensureServerExists(_.bind(function(err) { if (err) return cb(err); // modify desired caps var desiredCaps = _.clone(this.desiredCaps); - //this.bypass('/wd/hub/session', 'POST', desiredCaps, function(err, res, body) { - //console.log(err); - //console.log(res); - //console.log(body); - //cb(null); - //}); - cb(null); + this.adb.startSelendroid(this.serverApk, _.bind(function(err) { + if (err) return cb(err); + //this.bypass('/wd/hub/session', 'POST', desiredCaps, function(err, res, body) { + //console.log(err); + //console.log(res); + //console.log(body); + //cb(null); + //}); + cb(null); + }, this)); }, this)); }; @@ -54,6 +61,7 @@ Selendroid.prototype.ensureServerExists = function(cb) { return this.buildServer(cb); } logger.info("Selendroid server already exists, not rebuilding"); + this.serverApk = filePath; cb(null); }, this)); }; @@ -62,10 +70,10 @@ Selendroid.prototype.buildServer = function(cb) { logger.info("Building selendroid server for package " + this.appPackage); var buildDir = path.resolve(__dirname, "../selendroid/selendroid-server"); var src = buildDir + "/target/selendroid-server-0.3.apk"; - var dest = buildDir + "/target/selendroid- " + this.appPackage + '.apk'; + var dest = buildDir + "/target/selendroid-" + this.appPackage + '.apk'; var cmd = "mvn install -Dandroid.renameInstrumentationTargetPackage=" + this.appPackage; - exec(cmd, {cwd: buildDir}, function(err, stdout, stderr) { + exec(cmd, {cwd: buildDir}, _.bind(function(err, stdout, stderr) { if (err) { logger.error("Unable to build selendroid server"); console.log(stdout); @@ -73,15 +81,16 @@ Selendroid.prototype.buildServer = function(cb) { return cb(err); } logger.info("Copying selendroid server to correct destination"); - copyFile(src, dest, function(err) { + copyFile(src, dest, _.bind(function(err) { if (err) { logger.error("Error copying selendroid to destination"); return cb(err); } logger.info("Selendroid server copied successfully"); + this.serverApk = dest; cb(null); - }); - }); + }, this)); + }, this)); }; Selendroid.prototype.bypass = function(endpoint, method, data, cb) { diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 26e952073..99b4328d7 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -46,6 +46,7 @@ var ADB = function(opts, android) { this.portForwarded = false; this.debugMode = true; this.fastReset = opts.fastReset; + this.cleanApp = opts.cleanApp || this.fastReset; this.cleanAPK = '/tmp/' + this.appPackage + '.clean.apk'; // This is set to true when the bootstrap jar crashes. this.restartBootstrap = false; @@ -309,6 +310,8 @@ ADB.prototype.prepareDevice = function(onReady) { function(cb) { me.checkAdbPresent(cb); }, function(cb) { me.getDeviceWithRetry(cb);}, function(cb) { me.waitForDevice(cb); }, + function(cb) { me.checkFastReset(cb); }, + function(cb) { me.installApp(cb); }, function(cb) { me.forwardPort(cb); } ], onReady); }; @@ -326,12 +329,35 @@ ADB.prototype.startAppium = function(onReady, onExit) { async.series([ function(cb) { me.prepareDevice(cb); }, function(cb) { me.pushAppium(cb); }, - function(cb) { me.checkFastReset(cb); }, - function(cb) { me.installApp(cb); }, function(cb) { me.startApp(cb); } ], doRun); }; +ADB.prototype.startSelendroid = function(serverPath, onReady) { + var me = this; + + var runSelendroid = function(cb) { + var cmd = "adb shell am instrument -e main_activity '" + me.appPackage + + "." + me.appActivity + "' org.openqa.selendroid/" + + "org.openqa.selendroid.ServerInstrumentation"; + logger.info("Starting instrumentation process for selendroid with cmd: " + + cmd); + exec(cmd, function(err, stdout) { + if (stdout.indexOf("Exception") !== -1) { + var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid"; + return cb(new Error(msg)); + } + cb(); + }); + }; + + async.series([ + function(cb) { me.prepareDevice(cb); }, + function(cb) { me.installApk(serverPath, cb); }, + function(cb) { runSelendroid(cb); } + ], onReady); +}; + ADB.prototype.getConnectedDevices = function(cb) { this.debug("Getting connected devices..."); exec(this.adb + " devices", _.bind(function(err, stdout) { @@ -851,7 +877,7 @@ ADB.prototype.installApp = function(cb) { }; var doClean = function(cb) { - if (installClean) { + if (installClean && me.cleanApp) { me.debug("Installing clean apk"); me.installApk(me.cleanAPK, cb); } else { cb(null); } @@ -859,7 +885,7 @@ ADB.prototype.installApp = function(cb) { var doFastReset = function(cb) { // App is already installed so reset it. - if (!installApp) { + if (!installApp && me.fastReset) { me.runFastReset(cb); } else { cb(null); } }; From e81d060287af7c9dc6b7e61a4fbeb45ad42d5c40 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 13:09:48 -0700 Subject: [PATCH 21/38] get selendroid cert signing going --- app/selendroid.js | 4 ++-- uiautomator/adb.js | 48 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/selendroid.js b/app/selendroid.js index 39f08b1aa..17010beb1 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -30,8 +30,8 @@ Selendroid.prototype.start = function(cb) { var opts = _.clone(this.opts); opts.port = this.proxyPort; opts.devicePort = 8080; - opts.fastReset = false; - opts.cleanApp = false; + //opts.fastReset = false; + //opts.cleanApp = false; this.adb = new adb(opts); this.ensureServerExists(_.bind(function(err) { if (err) return cb(err); diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 99b4328d7..fae586d10 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -210,7 +210,7 @@ ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { } else { logger.debug("Skip app sign. Sign clean apk."); } - me.sign(cb, apks); + me.sign(apks, cb); }; async.series([ @@ -223,7 +223,7 @@ ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { }; // apks is an array of strings. -ADB.prototype.sign = function(cb, apks) { +ADB.prototype.sign = function(apks, cb) { var signPath = path.resolve(__dirname, '../app/android/sign.jar'); var resign = 'java -jar "' + signPath + '" "' + apks.join('" "') + '" --override'; logger.debug("Resigning: " + resign); @@ -233,10 +233,8 @@ ADB.prototype.sign = function(cb, apks) { return cb(new Error("Could not sign one or more apks. Are you sure " + "the file paths are correct: " + JSON.stringify(apks))); - } else if (err) { - return cb(err); } - cb(null); + cb(err); }); }; @@ -272,7 +270,7 @@ ADB.prototype.checkFastReset = function(cb) { } else { if (!appSigned) { // Resign app apk because it's not signed. - me.sign(function(err) { if (err) return cb(err); cb(null); }, [me.apkPath]); + me.sign([me.apkPath], cb); } else { // App and clean are already existing and signed. cb(null); @@ -310,9 +308,7 @@ ADB.prototype.prepareDevice = function(onReady) { function(cb) { me.checkAdbPresent(cb); }, function(cb) { me.getDeviceWithRetry(cb);}, function(cb) { me.waitForDevice(cb); }, - function(cb) { me.checkFastReset(cb); }, - function(cb) { me.installApp(cb); }, - function(cb) { me.forwardPort(cb); } + function(cb) { me.checkFastReset(cb); } ], onReady); }; @@ -328,6 +324,8 @@ ADB.prototype.startAppium = function(onReady, onExit) { async.series([ function(cb) { me.prepareDevice(cb); }, + function(cb) { me.installApp(cb); }, + function(cb) { me.forwardPort(cb); }, function(cb) { me.pushAppium(cb); }, function(cb) { me.startApp(cb); } ], doRun); @@ -353,11 +351,43 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { async.series([ function(cb) { me.prepareDevice(cb); }, + function(cb) { me.checkSelendroidCerts(serverPath, cb); }, function(cb) { me.installApk(serverPath, cb); }, + function(cb) { me.installApp(cb); }, + function(cb) { me.forwardPort(cb); }, function(cb) { runSelendroid(cb); } ], onReady); }; +ADB.prototype.checkSelendroidCerts = function(serverPath, cb) { + var me = this + , alreadyReturned = false + , checks = 0; + + var onDoneSigning = function() { + checks++; + if (checks === 2 && !alreadyReturned) { + cb(); + } + }; + + // these run in parallel + var apks = [serverPath, this.apkPath]; + _.each(apks, function(apk) { + logger.info("Checking signed status of " + apk); + me.checkApkCert(apk, function(isSigned) { + if (isSigned) return onDoneSigning(); + me.sign([apk], function(err) { + if (err && !alreadyReturned) { + alreadyReturned = true; + return cb(err); + } + onDoneSigning(); + }); + }); + }); +}; + ADB.prototype.getConnectedDevices = function(cb) { this.debug("Getting connected devices..."); exec(this.adb + " devices", _.bind(function(err, stdout) { From 08a606485d4f58189dc6fb54788121d0bb6552d2 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 14:16:22 -0700 Subject: [PATCH 22/38] follow callback conventions --- uiautomator/adb.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index fae586d10..efc0e227e 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -123,12 +123,12 @@ ADB.prototype.buildFastReset = function(skipAppSign, cb) { async.series([ function(cb) { me.checkSdkBinaryPresent("aapt", cb); }, - function(cb) { me.compileManifest(cb, manifest); }, + function(cb) { me.compileManifest(manifest, cb); }, function(cb) { me.insertManifest(manifest, skipAppSign, cb); }, ], cb); }; -ADB.prototype.compileManifest = function(cb, manifest) { +ADB.prototype.compileManifest = function(manifest, cb) { var androidHome = process.env.ANDROID_HOME , platforms = androidHome + '/platforms/' , platform = 'android-17'; @@ -342,6 +342,7 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { cmd); exec(cmd, function(err, stdout) { if (stdout.indexOf("Exception") !== -1) { + logger.error(stdout); var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid"; return cb(new Error(msg)); } From 014d237fc3b8523bdba042359fb95bb54a3cdd59 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 14:56:27 -0700 Subject: [PATCH 23/38] update selendroid, factor out manifest stuff for selendroid --- submodules/selendroid | 2 +- uiautomator/adb.js | 111 ++++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/submodules/selendroid b/submodules/selendroid index a8c74e419..d51092730 160000 --- a/submodules/selendroid +++ b/submodules/selendroid @@ -1 +1 @@ -Subproject commit a8c74e419126dd86919a3d45d750ea3d597eaf4c +Subproject commit d5109273004e19034b83fd2440f2b53eea6a7bb4 diff --git a/uiautomator/adb.js b/uiautomator/adb.js index efc0e227e..525ebc8a5 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -84,47 +84,66 @@ ADB.prototype.checkAdbPresent = function(cb) { }, this)); }; -// Fast reset -ADB.prototype.buildFastReset = function(skipAppSign, cb) { - // Create manifest - var me = this; - var targetAPK = me.apkPath; - var targetPackage = me.appPackage; - // gsub AndroidManifest.xml using targetPackage - var manifestPath = path.resolve(__dirname, '../app/android/AndroidManifest.xml.src'); - - var utf8 = 'utf8'; - var data = fs.readFileSync(manifestPath, utf8); - var manifest = manifestPath.substr(0, manifestPath.length - '.src'.length); - +ADB.prototype.retargetManifest = function(newPkg, newTarget, inFile, outFile, cb) { + var data = fs.readFileSync(inFile, "utf8"); var rePkg = /package="([^"]+)"/; var reTarget = /targetPackage="([^"]+)"/; var matchPkg = rePkg.exec(data); var matchTarget = reTarget.exec(data); - if (!matchPkg) { + if (newPkg && !matchPkg) { logger.debug("Could not find package= in manifest"); return cb("could not find package= in manifest"); } - if (!matchTarget) { + if (newTarget && !matchTarget) { logger.debug("Could not find targetPackage= in manifest"); return cb("could not find targetPackage= in manifest"); } - var newPkg = matchPkg[0].replace(matchPkg[1], targetPackage + '.clean'); - var newTarget = matchTarget[0].replace(matchTarget[1], targetPackage); + if (newPkg) { + var newPkgData = matchPkg[0].replace(matchPkg[1], newPkg); + data = data.replace(matchPkg[0], newPkgData); + } - var newContent = data.replace(matchPkg[0], newPkg); - newContent = newContent.replace(matchTarget[0], newTarget); + if (newTarget) { + var newTargetData = matchTarget[0].replace(matchTarget[1], newTarget); + data = data.replace(matchTarget[0], newTargetData); + } - fs.writeFileSync(manifest, newContent, utf8); + fs.writeFileSync(outFile, data, "utf8"); logger.debug("Created manifest"); + cb(null); +}; + +// Fast reset +ADB.prototype.buildFastReset = function(skipAppSign, cb) { + // Create manifest + var me = this; + var targetAPK = me.apkPath; + var cleanAPKSrc = path.resolve(__dirname, '../app/android/Clean.apk'); + var newPackage = me.appPackage + '.clean'; + var inFile = path.resolve(__dirname, '../app/android/AndroidManifest.xml.src'); + var outFile = inFile.substr(0, inFile.length - '.src'.length); + + var resignApks = function(cb) { + // Resign clean apk and target apk + var apks = [ me.cleanAPK ]; + if (!skipAppSign) { + logger.debug("Signing app and clean apk."); + apks.push(targetAPK); + } else { + logger.debug("Skip app sign. Sign clean apk."); + } + me.sign(apks, cb); + }; async.series([ + function(cb) { me.retargetManifest(newPackage, me.appPackage, inFile, outFile, cb); }, function(cb) { me.checkSdkBinaryPresent("aapt", cb); }, - function(cb) { me.compileManifest(manifest, cb); }, - function(cb) { me.insertManifest(manifest, skipAppSign, cb); }, + function(cb) { me.compileManifest(outFile, cb); }, + function(cb) { me.insertManifest(outFile, cleanAPKSrc, me.cleanAPK, cb); }, + function(cb) { resignApks(cb); } ], cb); }; @@ -157,11 +176,7 @@ ADB.prototype.compileManifest = function(manifest, cb) { }); }; -ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { - var me = this - , targetAPK = me.apkPath - , cleanAPK = me.cleanAPK; - +ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) { var extractManifest = function(cb) { // Extract compiled manifest from manifest.xml.apk unzipFile(manifest + '.apk', function(err, stderr) { @@ -173,24 +188,21 @@ ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { }); }; - var insertManifestIntoClean = function(cb) { - // Insert compiled manfiest into /tmp/appPackage.clean.apk - var cleanAPKSource = path.resolve(__dirname, '../app/android/Clean.apk'); - logger.debug("Writing tmp apk. " + cleanAPKSource + ' to ' + cleanAPK); - - // Write clean apk to /tmp - ncp(cleanAPKSource, cleanAPK, cb); + var createTmpApk = function(cb) { + logger.debug("Writing tmp apk. " + srcApk + ' to ' + dstApk); + ncp(srcApk, dstApk, cb); }; - var testCleanApk = function(cb) { - logger.debug("Testing tmp clean apk."); - testZipArchive(cleanAPK, cb); + var testDstApk = function(cb) { + logger.debug("Testing new tmp apk."); + testZipArchive(dstApk, cb); }; var moveManifest = function(cb) { + // Insert compiled manfiest into /tmp/appPackage.clean.apk // -j = keep only the file, not the dirs // -m = move manifest into target apk. - var replaceCmd = 'zip -j -m "' + cleanAPK + '" "' + manifest + '"'; + var replaceCmd = 'zip -j -m "' + dstApk + '" "' + manifest + '"'; logger.debug(replaceCmd); exec(replaceCmd, {}, function(err) { if (err) { @@ -201,24 +213,11 @@ ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) { }); }; - var resignApks = function(cb) { - // Resign clean apk and target apk - var apks = [ cleanAPK ]; - if (!skipAppSign) { - logger.debug("Signing app and clean apk."); - apks.push(targetAPK); - } else { - logger.debug("Skip app sign. Sign clean apk."); - } - me.sign(apks, cb); - }; - async.series([ function(cb) { extractManifest(cb); }, - function(cb) { insertManifestIntoClean(cb); }, - function(cb) { testCleanApk(cb); }, - function(cb) { moveManifest(cb); }, - function(cb) { resignApks(cb); } + function(cb) { createTmpApk(cb); }, + function(cb) { testDstApk(cb); }, + function(cb) { moveManifest(cb); } ], cb); }; @@ -352,6 +351,10 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { async.series([ function(cb) { me.prepareDevice(cb); }, + // uninstall for now to make sure everything's kosher + function(cb) { me.uninstallApk('org.openqa.selendroid', cb); }, + function(cb) { me.uninstallApk(me.appPackage, cb); }, + function(cb) { me.insertSelendroidManifest(cb); }, function(cb) { me.checkSelendroidCerts(serverPath, cb); }, function(cb) { me.installApk(serverPath, cb); }, function(cb) { me.installApp(cb); }, From 41587f31b168f11befe650b5e74413b0890e219c Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 16:23:37 -0700 Subject: [PATCH 24/38] add mkdirp to deps --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ba9f6c2e8..26463170f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "ncp": "~0.4.2", "swig": "~0.13.5", "async": "~0.2.6", + "mkdirp": "~0.3.5", "binary-cookies": "~0.1.1" }, "scripts": { From 28d5177d1501a8c25b0cb375a534f9d83b799fb2 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 17:20:05 -0700 Subject: [PATCH 25/38] selendroid is alive! --- app/appium.js | 6 ++++- app/device.js | 11 ++++++-- app/selendroid.js | 55 ++++++++++++++++++++++++++++++++-------- uiautomator/adb.js | 62 +++++++++++++++++++++++++++++++--------------- 4 files changed, 101 insertions(+), 33 deletions(-) diff --git a/app/appium.js b/app/appium.js index a9091d0e0..b7c02637f 100644 --- a/app/appium.js +++ b/app/appium.js @@ -379,13 +379,17 @@ Appium.prototype.invoke = function() { } this.device = this.devices[this.deviceType]; - this.device.start(function(err) { + this.device.start(function(err, sessionIdOverride) { me.progress++; // Ensure we don't use an undefined session. if (typeof me.sessions[me.progress] === 'undefined') { me.progress--; return; } + if (sessionIdOverride) { + me.sessionId = sessionIdOverride; + logger.info("Overriding session id with " + sessionIdOverride); + } me.sessions[me.progress].sessionId = me.sessionId; me.sessions[me.progress].callback(err, me.device); diff --git a/app/device.js b/app/device.js index 0fafc950d..58d943739 100644 --- a/app/device.js +++ b/app/device.js @@ -2,6 +2,7 @@ var errors = require('./errors') , request = require('request') + , _ = require('underscore') , logger = require('../logger').get('appium'); var UnknownError = errors.UnknownError @@ -59,7 +60,7 @@ exports.waitForCondition = function(waitMs, condFn, cb, intervalMs) { spin(); }; -exports.bypass = function(url, method, body, contentType, cb) { +exports.request = function(url, method, body, contentType, cb) { if (typeof cb === "undefined" && typeof contentType === "function") { cb = contentType; contentType = null; @@ -74,7 +75,13 @@ exports.bypass = function(url, method, body, contentType, cb) { url: url , method: method , headers: {'Content-Type': contentType} - , body: body }; + if (_.contains(['put', 'post', 'patch'], method.toLowerCase())) { + if (typeof body !== "string") { + opts.json = body; + } else { + opts.body = body; + } + } request(opts, cb); }; diff --git a/app/selendroid.js b/app/selendroid.js index 17010beb1..7a0ac2dbd 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -3,7 +3,7 @@ var errors = require('./errors') , adb = require('../uiautomator/adb') , _ = require('underscore') - , bypass = require('./device').bypass + , request = require('./device').request , logger = require('../logger').get('appium') , status = require("./uiauto/lib/status") , exec = require('child_process').exec @@ -39,13 +39,25 @@ Selendroid.prototype.start = function(cb) { var desiredCaps = _.clone(this.desiredCaps); this.adb.startSelendroid(this.serverApk, _.bind(function(err) { if (err) return cb(err); - //this.bypass('/wd/hub/session', 'POST', desiredCaps, function(err, res, body) { - //console.log(err); - //console.log(res); - //console.log(body); - //cb(null); - //}); - cb(null); + logger.info("Selendroid is launching, waiting for server"); + this.waitForServer(_.bind(function(err) { + if (err) return cb(err); + logger.info("Selendroid server is alive! Proxying to it"); + var data = {desiredCapabilities: desiredCaps}; + this.proxyTo('/wd/hub/session', 'POST', data, function(err, res, body) { + if (err) return cb(err); + + if (res.statusCode === 301 && body.sessionId) { + logger.info("Successfully started selendroid session"); + cb(null, body.sessionId); + } else { + cb(new Error("Did not get session redirect from selendroid")); + console.log(res.headers); + console.log(res.statusCode); + console.log(body); + } + }); + }, this)); }, this)); }, this)); }; @@ -93,12 +105,35 @@ Selendroid.prototype.buildServer = function(cb) { }, this)); }; -Selendroid.prototype.bypass = function(endpoint, method, data, cb) { +Selendroid.prototype.waitForServer = function(cb) { + var waitMs = 20000 + , intMs = 800 + , start = Date.now(); + + var pingServer = _.bind(function() { + this.proxyTo('/wd/hub/status', 'GET', null, function(err, res, body) { + if (body === null || typeof body === "undefined" || !body.trim()) { + if (Date.now() - start < waitMs) { + setTimeout(pingServer, intMs); + } else { + cb(new Error("Waited " + (waitMs / 1000) + " secs for " + + "selendroid server and it never showed up")); + } + } else { + cb(null); + } + }); + }, this); + + pingServer(); +}; + +Selendroid.prototype.proxyTo = function(endpoint, method, data, cb) { if (endpoint[0] !== '/') { endpoint = '/' + endpoint; } var url = 'http://' + this.proxyHost + ':' + this.proxyPort + endpoint; - bypass(url, method, data ? data : null, cb); + request(url, method, data ? data : null, cb); }; module.exports = function(opts) { diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 525ebc8a5..b958b1999 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -11,6 +11,7 @@ var spawn = require('child_process').spawn , testZipArchive = require('../app/helpers').testZipArchive , async = require('async') , ncp = require('ncp') + , mkdirp = require('mkdirp') , _ = require('underscore'); var noop = function() {}; @@ -147,6 +148,26 @@ ADB.prototype.buildFastReset = function(skipAppSign, cb) { ], cb); }; +ADB.prototype.insertSelendroidManifest = function(serverPath, cb) { + var me = this + , newServerPath = serverPath.replace(/\.apk$/, ".mod.apk") + , srcManifest = path.resolve(__dirname, "../selendroid/selendroid-gem", + "selendroid-prebuild/AndroidManifest.xml") + , dstDir = '/tmp/' + this.appPackage + , dstManifest = dstDir + '/AndroidManifest.xml'; + + async.series([ + function(cb) { mkdirp(dstDir, cb); }, + function(cb) { me.retargetManifest(null, me.appPackage, srcManifest, + dstManifest, cb); }, + function(cb) { me.checkSdkBinaryPresent("aapt", cb); }, + function(cb) { me.compileManifest(dstManifest, cb); }, + function(cb) { me.insertManifest(dstManifest, serverPath, newServerPath, + cb); }, + function(cb) { me.selendroidServerPath = newServerPath; cb(); } + ], cb); +}; + ADB.prototype.compileManifest = function(manifest, cb) { var androidHome = process.env.ANDROID_HOME , platforms = androidHome + '/platforms/' @@ -332,37 +353,38 @@ ADB.prototype.startAppium = function(onReady, onExit) { ADB.prototype.startSelendroid = function(serverPath, onReady) { var me = this; - - var runSelendroid = function(cb) { - var cmd = "adb shell am instrument -e main_activity '" + me.appPackage + - "." + me.appActivity + "' org.openqa.selendroid/" + - "org.openqa.selendroid.ServerInstrumentation"; - logger.info("Starting instrumentation process for selendroid with cmd: " + - cmd); - exec(cmd, function(err, stdout) { - if (stdout.indexOf("Exception") !== -1) { - logger.error(stdout); - var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid"; - return cb(new Error(msg)); - } - cb(); - }); - }; + this.selendroidServerPath = serverPath; async.series([ function(cb) { me.prepareDevice(cb); }, // uninstall for now to make sure everything's kosher function(cb) { me.uninstallApk('org.openqa.selendroid', cb); }, function(cb) { me.uninstallApk(me.appPackage, cb); }, - function(cb) { me.insertSelendroidManifest(cb); }, - function(cb) { me.checkSelendroidCerts(serverPath, cb); }, - function(cb) { me.installApk(serverPath, cb); }, + function(cb) { me.insertSelendroidManifest(me.selendroidServerPath, cb); }, + function(cb) { me.checkSelendroidCerts(me.selendroidServerPath, cb); }, + function(cb) { me.installApk(me.selendroidServerPath, cb); }, function(cb) { me.installApp(cb); }, function(cb) { me.forwardPort(cb); }, - function(cb) { runSelendroid(cb); } + function(cb) { me.pushSelendroid(cb); } ], onReady); }; +ADB.prototype.pushSelendroid = function(cb) { + var cmd = "adb shell am instrument -e main_activity '" + this.appPackage + + "." + this.appActivity + "' org.openqa.selendroid/" + + "org.openqa.selendroid.ServerInstrumentation"; + logger.info("Starting instrumentation process for selendroid with cmd: " + + cmd); + exec(cmd, function(err, stdout) { + if (stdout.indexOf("Exception") !== -1) { + logger.error(stdout); + var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid"; + return cb(new Error(msg)); + } + cb(); + }); +}; + ADB.prototype.checkSelendroidCerts = function(serverPath, cb) { var me = this , alreadyReturned = false From c479c156c55a1d24fc78245027bec0e5e9a9093f Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Thu, 11 Apr 2013 17:41:20 -0700 Subject: [PATCH 26/38] proxy commands other than start/stop --- app/routing.js | 56 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/app/routing.js b/app/routing.js index fbd92d237..755677b71 100644 --- a/app/routing.js +++ b/app/routing.js @@ -1,18 +1,54 @@ "use strict"; var controller = require('./controller') + , request = require('./device').request + , _ = require('underscore') , logger = require('../logger').get('appium'); +var shouldProxy = function(req) { + if (req.device === null) return false; + + var avoid = [ + ['POST', '/wd/hub/session'] + , ['DELETE', '/wd/hub/session/:sessionId?'] + ]; + var method = req.route.method.toUpperCase(); + var path = req.route.path; + var shouldAvoid = false; + _.each(avoid, function(pathToAvoid) { + if (method === pathToAvoid[0] && path === pathToAvoid[1]) { + shouldAvoid = true; + } + }); + return !shouldAvoid; +}; + + module.exports = function(appium) { - var rest = appium.rest - , inject = function(req, res, next) { - req.appium = appium; - req.device = appium.device; - logger.debug("Appium request initiated at " + req.url); - if (typeof req.body === "object") { - logger.debug("Request received with params: " + JSON.stringify(req.body)); - } - next(); - }; + var rest = appium.rest; + var inject = function(req, res, next) { + req.appium = appium; + req.device = appium.device; + logger.debug("Appium request initiated at " + req.url); + if (typeof req.body === "object") { + logger.debug("Request received with params: " + JSON.stringify(req.body)); + } + if (shouldProxy(req)) { + logger.debug("Proxying command to " + req.device.proxyHost + ":" + + req.device.proxyPort); + var url = 'http://' + req.device.proxyHost + ':' + req.device.proxyPort + + req.originalUrl; + request(url, req.route.method.toUpperCase(), req.body, + req.headers['content-type'], function(err, response, body) { + if (err) return next(err); + logger.debug("Proxied response received with status " + + response.statusCode + ": " + body); + res.headers = response.headers; + res.send(response.statusCode, body); + }); + } else { + next(); + } + }; // Make appium available to all REST http requests. rest.all('/wd/*', inject); From fc57b1621136b6e9c17ba44f29a0fff7ae2b9b8d Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 08:57:55 -0700 Subject: [PATCH 27/38] check if modified selendroid server exists before rebuilding --- uiautomator/adb.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index b958b1999..f17691d1c 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -150,7 +150,7 @@ ADB.prototype.buildFastReset = function(skipAppSign, cb) { ADB.prototype.insertSelendroidManifest = function(serverPath, cb) { var me = this - , newServerPath = serverPath.replace(/\.apk$/, ".mod.apk") + , newServerPath = me.selendroidServerPath , srcManifest = path.resolve(__dirname, "../selendroid/selendroid-gem", "selendroid-prebuild/AndroidManifest.xml") , dstDir = '/tmp/' + this.appPackage @@ -163,8 +163,7 @@ ADB.prototype.insertSelendroidManifest = function(serverPath, cb) { function(cb) { me.checkSdkBinaryPresent("aapt", cb); }, function(cb) { me.compileManifest(dstManifest, cb); }, function(cb) { me.insertManifest(dstManifest, serverPath, newServerPath, - cb); }, - function(cb) { me.selendroidServerPath = newServerPath; cb(); } + cb); } ], cb); }; @@ -352,15 +351,34 @@ ADB.prototype.startAppium = function(onReady, onExit) { }; ADB.prototype.startSelendroid = function(serverPath, onReady) { - var me = this; - this.selendroidServerPath = serverPath; + var me = this + , modServerExists = false; + this.selendroidServerPath = serverPath.replace(/\.apk$/, ".mod.apk"); + + var checkModServerExists = function(cb) { + fs.stat(me.selendroidServerPath, function(err) { + modServerExists = !!!err; + cb(); + }); + }; + + var conditionalInsertManifest = function(cb) { + if (!modServerExists) { + logger.info("Rebuilt selendroid server does not exist"); + me.insertSelendroidManifest(serverPath, cb); + } else { + logger.info("Rebuilt selendroid server already exists"); + cb(); + } + }; async.series([ function(cb) { me.prepareDevice(cb); }, // uninstall for now to make sure everything's kosher function(cb) { me.uninstallApk('org.openqa.selendroid', cb); }, function(cb) { me.uninstallApk(me.appPackage, cb); }, - function(cb) { me.insertSelendroidManifest(me.selendroidServerPath, cb); }, + function(cb) { checkModServerExists(cb); }, + function(cb) { conditionalInsertManifest(cb); }, function(cb) { me.checkSelendroidCerts(me.selendroidServerPath, cb); }, function(cb) { me.installApk(me.selendroidServerPath, cb); }, function(cb) { me.installApp(cb); }, From 76b194c8876b59b72904ec783e6d03570d8423d6 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 08:59:24 -0700 Subject: [PATCH 28/38] reset.sh doesn't need demo selendroid build --- reset.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/reset.sh b/reset.sh index 2fdf5e395..5d1067192 100755 --- a/reset.sh +++ b/reset.sh @@ -92,12 +92,6 @@ reset_selendroid() { git submodule update --init submodules/selendroid rm -rf selendroid ln -s $appium_home/submodules/selendroid $appium_home/selendroid - echo "Building selendroid server for ApiDemos" - pushd selendroid/selendroid-server - mvn install -Dandroid.renameInstrumentationTargetPackage=com.example.android.apis - echo "Copying ApiDemos selendroid server to a good home" - cp target/selendroid-server-$selendroid_ver.apk $appium_home/selendroid/selendroid-apidemos.apk - popd } cleanup() { From e21c32e391ff5845ba777313e79283e892e0eedd Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 09:20:11 -0700 Subject: [PATCH 29/38] better route proxy matching --- app/routing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/routing.js b/app/routing.js index 755677b71..a54063426 100644 --- a/app/routing.js +++ b/app/routing.js @@ -8,14 +8,14 @@ var shouldProxy = function(req) { if (req.device === null) return false; var avoid = [ - ['POST', '/wd/hub/session'] - , ['DELETE', '/wd/hub/session/:sessionId?'] + ['POST', new RegExp('^/wd/hub/session$')] + , ['DELETE', new RegExp('^/wd/hub/session/[^/]+$')] ]; var method = req.route.method.toUpperCase(); - var path = req.route.path; + var path = req.originalUrl; var shouldAvoid = false; _.each(avoid, function(pathToAvoid) { - if (method === pathToAvoid[0] && path === pathToAvoid[1]) { + if (method === pathToAvoid[0] && pathToAvoid[1].exec(path)) { shouldAvoid = true; } }); From 81f45735984758abfd74e2352aa4b0843351b9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 09:20:22 -0700 Subject: [PATCH 30/38] refactor selendroid for nicer async --- app/selendroid.js | 55 ++++++++++++++++++++++------------------------ uiautomator/adb.js | 9 +++++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/selendroid.js b/app/selendroid.js index 7a0ac2dbd..1fbe3ee85 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -27,39 +27,18 @@ var Selendroid = function(opts) { Selendroid.prototype.start = function(cb) { logger.info("Starting selendroid server"); - var opts = _.clone(this.opts); + var opts = _.clone(this.opts) + , me = this; opts.port = this.proxyPort; opts.devicePort = 8080; - //opts.fastReset = false; - //opts.cleanApp = false; this.adb = new adb(opts); - this.ensureServerExists(_.bind(function(err) { - if (err) return cb(err); - // modify desired caps - var desiredCaps = _.clone(this.desiredCaps); - this.adb.startSelendroid(this.serverApk, _.bind(function(err) { - if (err) return cb(err); - logger.info("Selendroid is launching, waiting for server"); - this.waitForServer(_.bind(function(err) { - if (err) return cb(err); - logger.info("Selendroid server is alive! Proxying to it"); - var data = {desiredCapabilities: desiredCaps}; - this.proxyTo('/wd/hub/session', 'POST', data, function(err, res, body) { - if (err) return cb(err); - if (res.statusCode === 301 && body.sessionId) { - logger.info("Successfully started selendroid session"); - cb(null, body.sessionId); - } else { - cb(new Error("Did not get session redirect from selendroid")); - console.log(res.headers); - console.log(res.statusCode); - console.log(body); - } - }); - }, this)); - }, this)); - }, this)); + async.waterfall([ + function(cb) { me.ensureServerExists(cb); }, + function(cb) { me.adb.startSelendroid(me.serverApk, cb); }, + function(res, cb) { me.waitForServer(cb); }, + function(cb) { me.createSession(cb); }, + ], cb); }; Selendroid.prototype.ensureServerExists = function(cb) { @@ -120,6 +99,7 @@ Selendroid.prototype.waitForServer = function(cb) { "selendroid server and it never showed up")); } } else { + logger.info("Selendroid server is alive!"); cb(null); } }); @@ -128,6 +108,23 @@ Selendroid.prototype.waitForServer = function(cb) { pingServer(); }; +Selendroid.prototype.createSession = function(cb) { + var data = {desiredCapabilities: this.desiredCaps}; + this.proxyTo('/wd/hub/session', 'POST', data, function(err, res, body) { + if (err) return cb(err); + + if (res.statusCode === 301 && body.sessionId) { + logger.info("Successfully started selendroid session"); + cb(null, body.sessionId); + } else { + cb(new Error("Did not get session redirect from selendroid")); + console.log(res.headers); + console.log(res.statusCode); + console.log(body); + } + }); +}; + Selendroid.prototype.proxyTo = function(endpoint, method, data, cb) { if (endpoint[0] !== '/') { endpoint = '/' + endpoint; diff --git a/uiautomator/adb.js b/uiautomator/adb.js index f17691d1c..2ddef8efb 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -364,10 +364,12 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { var conditionalInsertManifest = function(cb) { if (!modServerExists) { - logger.info("Rebuilt selendroid server does not exist"); + logger.info("Rebuilt selendroid server does not exist, inserting " + + "modified manifest"); me.insertSelendroidManifest(serverPath, cb); } else { - logger.info("Rebuilt selendroid server already exists"); + logger.info("Rebuilt selendroid server already exists, no need to " + + "rebuild it with a new manifest"); cb(); } }; @@ -383,7 +385,8 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { function(cb) { me.installApk(me.selendroidServerPath, cb); }, function(cb) { me.installApp(cb); }, function(cb) { me.forwardPort(cb); }, - function(cb) { me.pushSelendroid(cb); } + function(cb) { me.pushSelendroid(cb); }, + function(cb) { logger.info("Selendroid server is launching"); cb(); } ], onReady); }; From 85ae7fa419371ae2cd8ef8f120d09a8d025b72b5 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 09:27:27 -0700 Subject: [PATCH 31/38] add special proxy behavior for delete session --- app/selendroid.js | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/app/selendroid.js b/app/selendroid.js index 1fbe3ee85..5f90c82e4 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -19,6 +19,7 @@ var Selendroid = function(opts) { this.appPackage = opts.appPackage; this.desiredCaps = opts.desiredCaps; this.onStop = function() {}; + this.selendroidSessionId = null; this.adb = null; this.isProxy = true; this.proxyHost = 'localhost'; @@ -41,6 +42,13 @@ Selendroid.prototype.start = function(cb) { ], cb); }; +Selendroid.prototype.stop = function(cb) { + logger.info("Stopping selendroid server"); + this.deleteSession(function(err) { + cb(err ? 1 : 0); + }); +}; + Selendroid.prototype.ensureServerExists = function(cb) { logger.info("Checking whether selendroid is built for package yet"); var fileName = 'selendroid-' + this.appPackage + '.apk'; @@ -66,9 +74,9 @@ Selendroid.prototype.buildServer = function(cb) { this.appPackage; exec(cmd, {cwd: buildDir}, _.bind(function(err, stdout, stderr) { if (err) { - logger.error("Unable to build selendroid server"); - console.log(stdout); - console.log(stderr); + logger.error("Unable to build selendroid server. Stdout was: "); + logger.error(stdout); + logger.error(stderr); return cb(err); } logger.info("Copying selendroid server to correct destination"); @@ -110,19 +118,28 @@ Selendroid.prototype.waitForServer = function(cb) { Selendroid.prototype.createSession = function(cb) { var data = {desiredCapabilities: this.desiredCaps}; - this.proxyTo('/wd/hub/session', 'POST', data, function(err, res, body) { + this.proxyTo('/wd/hub/session', 'POST', data, _.bind(function(err, res, body) { if (err) return cb(err); if (res.statusCode === 301 && body.sessionId) { logger.info("Successfully started selendroid session"); + this.selendroidSessionId = body.sessionId; cb(null, body.sessionId); } else { + logger.error("Selendroid create session did not work. Status was " + + res.statusCode + " and body was " + body); cb(new Error("Did not get session redirect from selendroid")); - console.log(res.headers); - console.log(res.statusCode); - console.log(body); } - }); + }, this)); +}; + +Selendroid.prototype.deleteSession = function(cb) { + var url = '/wd/hub/session/' + this.selendroidSessionId; + this.proxyTo(url, 'DELETE', null, _.bind(function(err, res) { + if (err) return cb(err); + if (res.statusCode !== 200) return cb(new Error("Status was not 200")); + cb(); + }, this)); }; Selendroid.prototype.proxyTo = function(endpoint, method, data, cb) { From 3bf9dc188b778c49ba2c2bd6c701ae6fefb3e3fa Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 09:51:08 -0700 Subject: [PATCH 32/38] only rebuild/reinstall/remanifest selendroid in certain cases --- uiautomator/adb.js | 67 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/uiautomator/adb.js b/uiautomator/adb.js index 2ddef8efb..904fae682 100644 --- a/uiautomator/adb.js +++ b/uiautomator/adb.js @@ -357,11 +357,24 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { var checkModServerExists = function(cb) { fs.stat(me.selendroidServerPath, function(err) { - modServerExists = !!!err; + modServerExists = !err; cb(); }); }; + var conditionalUninstallSelendroid = function(cb) { + if (!modServerExists) { + // assume we're going to rebuild selendroid and therefore + // need to uninstall it if it's already on device + logger.info("Rebuilt selendroid apk does not exist, uninstalling " + + "any instances of it on device to make way for new one"); + me.uninstallApk('org.openqa.selendroid', cb); + } else { + logger.info("Rebuilt selendroid apk exists, doing nothing"); + cb(); + } + }; + var conditionalInsertManifest = function(cb) { if (!modServerExists) { logger.info("Rebuilt selendroid server does not exist, inserting " + @@ -374,15 +387,25 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { } }; + var conditionalInstallSelendroid = function(cb) { + me.checkAppInstallStatus('org.openqa.selendroid', function(e, installed) { + if (!installed) { + logger.info("Rebuilt selendroid is not installed, installing it"); + me.installApk(me.selendroidServerPath, cb); + } else { + logger.info("Rebuilt selendroid is already installed"); + cb(); + } + }); + }; + async.series([ function(cb) { me.prepareDevice(cb); }, - // uninstall for now to make sure everything's kosher - function(cb) { me.uninstallApk('org.openqa.selendroid', cb); }, - function(cb) { me.uninstallApk(me.appPackage, cb); }, function(cb) { checkModServerExists(cb); }, + function(cb) { conditionalUninstallSelendroid(cb); }, function(cb) { conditionalInsertManifest(cb); }, function(cb) { me.checkSelendroidCerts(me.selendroidServerPath, cb); }, - function(cb) { me.installApk(me.selendroidServerPath, cb); }, + function(cb) { conditionalInstallSelendroid(cb); }, function(cb) { me.installApp(cb); }, function(cb) { me.forwardPort(cb); }, function(cb) { me.pushSelendroid(cb); }, @@ -926,6 +949,27 @@ ADB.prototype.runFastReset = function(cb) { }); }; +ADB.prototype.checkAppInstallStatus = function(pkg, cb) { + var installed = false + , cleanInstalled = false; + this.requireDeviceId(); + + logger.debug("Getting install/clean status for " + pkg); + var listPkgCmd = this.adbCmd + " shell pm list packages -3 " + pkg; + console.log(listPkgCmd); + exec(listPkgCmd, function(err, stdout) { + //console.log("Packages found:"); + //console.log(stdout); + var apkInstalledRgx = new RegExp('^package:' + + pkg.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); + installed = !!apkInstalledRgx.test(stdout); + var cleanInstalledRgx = new RegExp('^package:' + + (pkg + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); + cleanInstalled = !!cleanInstalledRgx.test(stdout); + cb(null, installed, cleanInstalled); + }); +}; + ADB.prototype.installApp = function(cb) { var me = this , installApp = false @@ -934,15 +978,10 @@ ADB.prototype.installApp = function(cb) { me.requireApk(); var determineInstallAndCleanStatus = function(cb) { - var listPkgCmd = me.adbCmd + " shell pm list packages -3 " + me.appPackage; - exec(listPkgCmd, function(err, stdout) { - var apkInstalledRgx = new RegExp('^package:' + me.appPackage.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); - installApp = ! apkInstalledRgx.test(stdout); - var cleanInstalledRgx = new RegExp('^package:' + (me.appPackage + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm'); - installClean = ! cleanInstalledRgx.test(stdout); - me.debug("Packages found:"); - me.debug(stdout); - cb(null); + me.checkAppInstallStatus(me.appPackage, function(err, installed, cleaned) { + installApp = !installed; + installClean = !cleaned; + cb(); }); }; From 82ba0fe61f751885451e22bf6e75e0629bbdcf2b Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:11:08 -0700 Subject: [PATCH 33/38] don't log long proxy responses --- app/routing.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routing.js b/app/routing.js index a54063426..06ad0a001 100644 --- a/app/routing.js +++ b/app/routing.js @@ -41,7 +41,8 @@ module.exports = function(appium) { req.headers['content-type'], function(err, response, body) { if (err) return next(err); logger.debug("Proxied response received with status " + - response.statusCode + ": " + body); + response.statusCode + ": " + + JSON.stringify(body).slice(0, 1000)); res.headers = response.headers; res.send(response.statusCode, body); }); From e1d7102499bbd3e0d1a68dc634e3207703bb5007 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:15:36 -0700 Subject: [PATCH 34/38] update gitignore to track selendroid test --- .gitignore | 2 +- test/functional/selendroid/basic.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/functional/selendroid/basic.js diff --git a/.gitignore b/.gitignore index bcadb9a66..6ac3a22e0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ app/android/AndroidManifest.xml *~ uiautomator/bootstrap/target/ org.eclipse.ltk.core.refactoring.prefs -selendroid +/selendroid diff --git a/test/functional/selendroid/basic.js b/test/functional/selendroid/basic.js new file mode 100644 index 000000000..d31cc48f1 --- /dev/null +++ b/test/functional/selendroid/basic.js @@ -0,0 +1,28 @@ +/*global it:true */ +"use strict"; + +var path = require('path') + , appPath = path.resolve(__dirname, "../../../sample-code/apps/ApiDemos/bin/ApiDemos-debug.apk") + , appPkg = "com.example.android.apis" + , appAct = "ApiDemos" + , driverBlock = require("../../helpers/driverblock.js") + , describeWd = driverBlock.describeForApp(appPath, "selendroid", appPkg, appAct) + , should = require('should'); + + describeWd('basic', function(h) { + it('should find and click an element', function(done) { + h.driver.elementByName('Accessibility', function(err, el) { + should.not.exist(err); + should.exist(el); + el.click(function(err) { + should.not.exist(err); + h.driver.elementByLinkText("Accessibility Node Provider", function(err, el) { + should.not.exist(err); + should.exist(el); + done(); + }); + }); + }); + }); + }); + From 50a2b8e6676039041c55fdcd0f58d333de1af527 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:25:43 -0700 Subject: [PATCH 35/38] make sure to rm selendroid targets in reset --- reset.sh | 1 + test/functional/selendroid/basic.js | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/reset.sh b/reset.sh index 5d1067192..e76cccc05 100755 --- a/reset.sh +++ b/reset.sh @@ -89,6 +89,7 @@ reset_android() { reset_selendroid() { echo "---- RESETTING SELENDROID ----" echo "Downloading/updating selendroid" + rm -rf submodules/selendroid/selendroid-server/target git submodule update --init submodules/selendroid rm -rf selendroid ln -s $appium_home/submodules/selendroid $appium_home/selendroid diff --git a/test/functional/selendroid/basic.js b/test/functional/selendroid/basic.js index d31cc48f1..b5e1f7396 100644 --- a/test/functional/selendroid/basic.js +++ b/test/functional/selendroid/basic.js @@ -11,18 +11,24 @@ var path = require('path') describeWd('basic', function(h) { it('should find and click an element', function(done) { - h.driver.elementByName('Accessibility', function(err, el) { - should.not.exist(err); - should.exist(el); - el.click(function(err) { + // selendroid appears to have some issues with implicit waits + // hence the timeouts + setTimeout(function() { + h.driver.elementByName('Accessibility', function(err, el) { should.not.exist(err); - h.driver.elementByLinkText("Accessibility Node Provider", function(err, el) { + should.exist(el); + el.click(function(err) { should.not.exist(err); - should.exist(el); - done(); + setTimeout(function() { + h.driver.elementByLinkText("Accessibility Node Provider", function(err, el) { + should.not.exist(err); + should.exist(el); + done(); + }); + }, 1000); }); }); - }); + }, 1000); }); }); From d7387395542464133a5c37e2481b1a5b153feb37 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:27:29 -0700 Subject: [PATCH 36/38] update selendroid to the real 0.3 i guess --- submodules/selendroid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/selendroid b/submodules/selendroid index d51092730..f3b5e7fde 160000 --- a/submodules/selendroid +++ b/submodules/selendroid @@ -1 +1 @@ -Subproject commit d5109273004e19034b83fd2440f2b53eea6a7bb4 +Subproject commit f3b5e7fdeaf23955aef1bc145711fe5f1fe2da63 From b38cf659c6b11c25de3f72ea2b4a89d5a01cb810 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:33:26 -0700 Subject: [PATCH 37/38] better reset.sh behavior for selendroid --- reset.sh | 14 +++++++------- test/functional/selendroid/basic.js | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/reset.sh b/reset.sh index e76cccc05..c66fdb48b 100755 --- a/reset.sh +++ b/reset.sh @@ -27,11 +27,6 @@ if [[ $should_reset_android -eq 0 ]] && [[ $should_reset_ios -eq 0 ]] && [[ $sho should_reset_selendroid=1 fi -# if selendroid is selected, we need all the android stuff too -if [ $should_reset_selendroid -eq 1 ]; then - should_reset_android=1 -fi - reset_general() { echo "---- RESETTING NPM ----" echo "Clearing dev version of wd.js" @@ -72,12 +67,16 @@ reset_ios() { grunt buildApp:WebViewApp } -reset_android() { - echo "---- RESETTING ANDROID ----" +get_apidemos() { echo "Downloading/updating AndroidApiDemos" git submodule update --init submodules/ApiDemos rm -rf sample-code/apps/ApiDemos ln -s $appium_home/submodules/ApiDemos $appium_home/sample-code/apps/ApiDemos +} + +reset_android() { + echo "---- RESETTING ANDROID ----" + get_apidemos echo "Building Android bootstrap" grunt configAndroidBootstrap grunt buildAndroidBootstrap @@ -88,6 +87,7 @@ reset_android() { reset_selendroid() { echo "---- RESETTING SELENDROID ----" + get_apidemos echo "Downloading/updating selendroid" rm -rf submodules/selendroid/selendroid-server/target git submodule update --init submodules/selendroid diff --git a/test/functional/selendroid/basic.js b/test/functional/selendroid/basic.js index b5e1f7396..c2c4d6520 100644 --- a/test/functional/selendroid/basic.js +++ b/test/functional/selendroid/basic.js @@ -14,13 +14,13 @@ var path = require('path') // selendroid appears to have some issues with implicit waits // hence the timeouts setTimeout(function() { - h.driver.elementByName('Accessibility', function(err, el) { + h.driver.elementByName('App', function(err, el) { should.not.exist(err); should.exist(el); el.click(function(err) { should.not.exist(err); setTimeout(function() { - h.driver.elementByLinkText("Accessibility Node Provider", function(err, el) { + h.driver.elementByLinkText("Action Bar", function(err, el) { should.not.exist(err); should.exist(el); done(); From da346905ed7f605c2bb03c0a4e18daad2c4a2aa5 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Fri, 12 Apr 2013 10:37:58 -0700 Subject: [PATCH 38/38] we already have ncp, no need for copyFile --- app/helpers.js | 14 -------------- app/selendroid.js | 4 ++-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/app/helpers.js b/app/helpers.js index fb8254d41..738b9f8bc 100644 --- a/app/helpers.js +++ b/app/helpers.js @@ -279,17 +279,3 @@ exports.rotateImage = function(imgPath, deg, cb) { cb(null); }); }; - -exports.copyFile = function (src, dst, cb) { - var is - , os; - - fs.stat(src, function (err) { - if (err) { - return cb(err); - } - is = fs.createReadStream(src); - os = fs.createWriteStream(dst); - util.pump(is, os, cb); - }); -}; diff --git a/app/selendroid.js b/app/selendroid.js index 5f90c82e4..53c626319 100644 --- a/app/selendroid.js +++ b/app/selendroid.js @@ -8,7 +8,7 @@ var errors = require('./errors') , status = require("./uiauto/lib/status") , exec = require('child_process').exec , fs = require('fs') - , copyFile = require('./helpers').copyFile + , ncp = require('ncp') , async = require('async') , path = require('path') , UnknownError = errors.UnknownError; @@ -80,7 +80,7 @@ Selendroid.prototype.buildServer = function(cb) { return cb(err); } logger.info("Copying selendroid server to correct destination"); - copyFile(src, dest, _.bind(function(err) { + ncp(src, dest, _.bind(function(err) { if (err) { logger.error("Error copying selendroid to destination"); return cb(err);