From bbd03f89c63166c1ee00b6fdcd8998d9de756139 Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Mon, 10 Feb 2014 13:29:08 -0500 Subject: [PATCH] Add androidCoverage --- docs/caps.md | 1 + docs/server-args.md | 1 + lib/appium.js | 2 + lib/devices/android/adb.js | 135 ++++++++++++++++++++++ lib/devices/android/android-controller.js | 36 +++++- lib/devices/android/android.js | 28 ++++- lib/devices/ios/ios-controller.js | 4 + lib/server/controller.js | 10 ++ lib/server/parser.js | 9 ++ 9 files changed, 222 insertions(+), 4 deletions(-) diff --git a/docs/caps.md b/docs/caps.md index 65399e535..046a5c50f 100644 --- a/docs/caps.md +++ b/docs/caps.md @@ -21,6 +21,7 @@ Appium server capabilities |`app-wait-activity`| Activity name for the Android activity you want to wait for|`SplashActivity`| |`device-ready-timeout`| Timeout in seconds while waiting for device to become ready|`5`| |`compressXml`| [setCompressedLayoutHeirarchy(true)](http://developer.android.com/tools/help/uiautomator/UiDevice.html#setCompressedLayoutHeirarchy(boolean\))| `true`| +|`androidCoverage`| Fully qualified instrumentation class. Passed to -w in adb shell am instrument -e coverage true -w | `com.my.Pkg/com.my.Pkg.instrumentation.MyInstrumentation`| -- diff --git a/docs/server-args.md b/docs/server-args.md index 1ddf0c3be..2f45dc3e3 100644 --- a/docs/server-args.md +++ b/docs/server-args.md @@ -33,6 +33,7 @@ All flags are optional, but some are required in conjunction with certain others |`--app-activity`|null|(Android-only) Activity name for the Android activity you want to launch from your package (e.g., MainActivity)|`--app-activity MainActivity`| |`--app-wait-package`|false|(Android-only) Package name for the Android activity you want to wait for (e.g., com.example.android.myApp)|`--app-wait-package com.example.android.myApp`| |`--app-wait-activity`|false|(Android-only) Activity name for the Android activity you want to wait for (e.g., SplashActivity)|`--app-wait-activity SplashActivity`| +|`--android-coverage`|false|(Android-only) Fully qualified instrumentation class. Passed to -w in adb shell am instrument -e coverage true -w |`--android-coverage com.my.Pkg/com.my.Pkg.instrumentation.MyInstrumentation`| |`--avd`|null|name of the avd to launch|`--avd @default`| |`--device-ready-timeout`|5|(Android-only) Timeout in seconds while waiting for device to become ready|`--device-ready-timeout 5`| |`--safari`|false|(IOS-Only) Use the safari app|| diff --git a/lib/appium.js b/lib/appium.js index 87ebae493..4a5e498fb 100644 --- a/lib/appium.js +++ b/lib/appium.js @@ -183,6 +183,7 @@ Appium.prototype.setAndroidArgs = function (desiredCaps) { setArgFromCaps("androidWaitPackage", "app-wait-package"); setArgFromCaps("androidWaitActivity", "app-wait-activity"); setArgFromCaps("androidDeviceReadyTimeout", "device-ready-timeout"); + setArgFromCaps("androidCoverage", "androidCoverage"); setArgFromCaps("compressXml", "compressXml"); }; @@ -598,6 +599,7 @@ Appium.prototype.initDevice = function () { , appActivity: this.args.androidActivity , appWaitPackage: this.args.androidWaitPackage , appWaitActivity: this.args.androidWaitActivity + , androidCoverage: this.args.androidCoverage , compressXml: this.args.compressXml , avdName: this.args.avd , appDeviceReadyTimeout: this.args.androidDeviceReadyTimeout diff --git a/lib/devices/android/adb.js b/lib/devices/android/adb.js index 36eb547b1..ad7709395 100644 --- a/lib/devices/android/adb.js +++ b/lib/devices/android/adb.js @@ -5,6 +5,7 @@ var spawn = require('win-spawn') , path = require('path') , fs = require('fs') , net = require('net') + , status = require('../../server/status.js') , logger = require('../../server/logger.js').get('appium') , async = require('async') , ncp = require('ncp') @@ -43,6 +44,7 @@ var ADB = function (opts) { this.debugMode = true; this.logcat = null; this.binaries = {}; + this.instrumentProc = null; }; ADB.prototype.debug = function (msg) { @@ -125,6 +127,10 @@ ADB.prototype.checkAdbPresent = function (cb) { }.bind(this)); }; +ADB.prototype.checkAaptPresent = function (cb) { + this.checkSdkBinaryPresent("aapt", cb); +}; + ADB.prototype.exec = function (cmd, cb) { if (!cmd) { return cb(new Error("You need to pass in a command to exec()")); @@ -153,6 +159,57 @@ ADB.prototype.spawn = function (args) { return spawn(adbCmd, args); }; +// android:process= may be defined in AndroidManifest.xml +// http://developer.android.com/reference/android/R.attr.html#process +ADB.prototype.processFromManifest = function (localApk, cb) { + this.checkAaptPresent(function (err) { + if (err) return cb(err); + + var extractProcess = [this.binaries.aapt, 'dump', 'xmltree', localApk, 'AndroidManifest.xml'].join(' '); + logger.debug("processFromManifest: " + extractProcess); + exec(extractProcess, { maxBuffer: 524288 }, function (err, stdout, stderr) { + if (err || stderr) { + logger.warn(stderr); + return cb(new Error("processFromManifest failed. " + err)); + } + + var process = new RegExp(/android:process\(0x01010011\)="([^"]+)"/g).exec(stdout); + if (process && process.length > 1) { + process = process[1]; + } else { + process = null; + } + cb(null, process); + }); + }.bind(this)); +}; + +ADB.prototype.processExists = function (process, cb) { + if (!this.isValidClass(process)) return cb(new Error("Invalid process name: " + process)); + + var existsCmd = "ps list -c " + process; + this.shell(existsCmd, function (err, stdout) { + if (err) { + logger.warn(err); + return cb(err); + } + + var exists = false; + + if (stdout) { + var lines = stdout.split(/\r?\n/); + if (lines.length > 1) { + var found = lines[1]; + if (found && /\S/.test(found)) { + exists = true; + } + } + } + + cb(null, exists); + }); +}; + ADB.prototype.compileManifest = function (manifest, manifestPackage, targetPackage, cb) { logger.info("Compiling manifest " + manifest); @@ -836,6 +893,84 @@ ADB.prototype.startApp = function (pkg, activity, waitPkg, waitActivity, retry, }.bind(this)); }; +ADB.prototype.isValidClass = function (classString) { + // some.package/some.package.Activity + return new RegExp(/^[a-zA-Z0-9\./_]+$/).exec(classString); +}; + +ADB.prototype.broadcastProcessEnd = function (intent, process, cb) { + // start the broadcast without waiting for it to finish. + this.broadcast(intent, function () {}); + + // wait for the process to end + var start = Date.now(); + var timeoutMs = 40000; + var intMs = 400; + + var waitForDeath = function () { + this.processExists(process, function (err, exists) { + if (!exists) { + cb(); + } else if ((Date.now() - start) < timeoutMs) { + setTimeout(waitForDeath, intMs); + } else { + cb(new Error("Process never died within " + timeoutMs + " ms.")); + } + }); + }.bind(this); + + waitForDeath(); +}; + +ADB.prototype.broadcast = function (intent, cb) { + if (!this.isValidClass(intent)) return cb(new Error("Invalid intent " + intent)); + + var cmd = "am broadcast -a " + intent; + logger.info("Broadcasting: " + cmd); + this.shell(cmd, cb); +}; + +ADB.prototype.endAndroidCoverage = function () { + if (this.instrumentProc) this.instrumentProc.kill(); +}; + +ADB.prototype.androidCoverage = function (instrumentClass, waitPkg, waitActivity, cb) { + if (!this.isValidClass(instrumentClass)) return cb(new Error("Invalid class " + instrumentClass)); + /* + [ '/path/to/android-sdk-macosx/platform-tools/adb', + '-s', + 'emulator-5554', + 'shell', + 'am', + 'instrument', + '-e', + 'coverage', + 'true', + '-w', + 'com.example.Pkg/com.example.Pkg.instrumentation.MyInstrumentation' ] + */ + var args = this.adbCmd.split(' ').concat(('shell am instrument -e coverage true -w ' + instrumentClass).split(' ')); + logger.info("Collecting coverage data with: " + args.join(' ')); + var adbPath = args.shift().replace(/\"/g, ''); // spawn fails on '"' + + var alreadyReturned = false; + this.instrumentProc = spawn(adbPath, args); // am instrument runs for the life of the app process. + this.instrumentProc.on('error', function (err) { + logger.error(err); + if (!alreadyReturned) { + alreadyReturned = true; + return cb(err); + } + }); + + this.waitForActivity(waitPkg, waitActivity, function (err) { + if (!alreadyReturned) { + alreadyReturned = true; + return cb(err); + } + }); +}; + ADB.prototype.getFocusedPackageAndActivity = function (cb) { logger.info("Getting focused package and activity"); var cmd = "dumpsys window windows" diff --git a/lib/devices/android/android-controller.js b/lib/devices/android/android-controller.js index 62a929eff..62a11682f 100644 --- a/lib/devices/android/android-controller.js +++ b/lib/devices/android/android-controller.js @@ -4,7 +4,7 @@ var errors = require('../../server/errors.js') , _ = require('underscore') , logger = require('../../server/logger.js').get('appium') , deviceCommon = require('../common.js') - , status = require("../../server/status.js") + , status = require('../../server/status.js') , NotYetImplementedError = errors.NotYetImplementedError , parseXpath = require('../../xpath.js').parseXpath , exec = require('child_process').exec @@ -361,6 +361,40 @@ androidController.setOrientation = function (orientation, cb) { this.proxy(["orientation", {orientation: orientation}], cb); }; +androidController.endCoverage = function (intentToBroadcast, ecOnDevicePath, cb) { + var localfile = temp.path({prefix: 'appium', suffix: '.ec'}); + if (fs.existsSync(localfile)) fs.unlinkSync(localfile); + var b64data = ""; + + async.series([ + function (cb) { + // ensure the ec we're pulling is newly created as a result of the intent. + this.adb.rimraf(ecOnDevicePath, function () { cb(); }); + }.bind(this), + function (cb) { + this.adb.broadcastProcessEnd(intentToBroadcast, this.appProcess, cb); + }.bind(this), + function (cb) { + this.adb.pull(ecOnDevicePath, localfile, cb); + }.bind(this), + function (cb) { + fs.readFile(localfile, function (err, data) { + if (err) return cb(err); + b64data = new Buffer(data).toString('base64'); + cb(); + }); + }.bind(this), + ], + function (err) { + if (fs.existsSync(localfile)) fs.unlinkSync(localfile); + if (err) return cb(err); + cb(null, { + status: status.codes.Success.code + , value: b64data + }); + }); +}; + androidController.localScreenshot = function (file, cb) { async.series([ function (cb) { diff --git a/lib/devices/android/android.js b/lib/devices/android/android.js index 0394abc28..678d5ebae 100644 --- a/lib/devices/android/android.js +++ b/lib/devices/android/android.js @@ -38,6 +38,8 @@ Android.prototype.initialize = function (opts) { this.appMd5Hash = null; this.appWaitPackage = opts.appWaitPackage || opts.appPackage; this.appWaitActivity = opts.appWaitActivity || opts.appActivity; + this.androidCoverage = opts.androidCoverage || false; + this.appProcess = this.appPackage; this.avdName = opts.avdName || null; this.appDeviceReadyTimeout = opts.appDeviceReadyTimeout; this.verbose = opts.verbose; @@ -86,6 +88,7 @@ Android.prototype.start = function (cb, onDie) { this.prepareDevice.bind(this), this.checkApiLevel.bind(this), this.pushStrings.bind(this), + this.processFromManifest.bind(this), this.requestXmlCompression.bind(this), this.uninstallApp.bind(this), this.installApp.bind(this), @@ -255,6 +258,20 @@ Android.prototype.checkApiLevel = function (cb) { }); }; +Android.prototype.processFromManifest = function (cb) { + if (!this.apkPath) { + return cb(); + } else { // apk must be local to process the manifest. + this.adb.processFromManifest(this.apkPath, function (err, process) { + if (process) { + this.appProcess = process; + logger.debug("Set app process to: " + process); + } + cb(); + }.bind(this)); + } +}; + Android.prototype.pushStrings = function (cb) { var remotePath = '/data/local/tmp'; var stringsJson = 'strings.json'; @@ -304,8 +321,13 @@ Android.prototype.pushAppium = function (cb) { }; Android.prototype.startApp = function (cb) { - this.adb.startApp(this.appPackage, this.appActivity, this.appWaitPackage, - this.appWaitActivity, cb); + if (!this.androidCoverage) { + this.adb.startApp(this.appPackage, this.appActivity, this.appWaitPackage, + this.appWaitActivity, cb); + } else { + this.adb.androidCoverage(this.androidCoverage, this.appWaitPackage, + this.appWaitActivity, cb); + } }; Android.prototype.requestXmlCompression = function (cb) { @@ -316,7 +338,6 @@ Android.prototype.requestXmlCompression = function (cb) { } }; - Android.prototype.stop = function (cb) { this.shuttingDown = true; @@ -356,6 +377,7 @@ Android.prototype.cleanup = function () { }; Android.prototype.shutdown = function (cb) { + this.adb.endAndroidCoverage(); var next = function () { if (this.uiautomator) { this.uiautomator.shutdown(function () { diff --git a/lib/devices/ios/ios-controller.js b/lib/devices/ios/ios-controller.js index 3d5d18bb2..6dde2eac7 100644 --- a/lib/devices/ios/ios-controller.js +++ b/lib/devices/ios/ios-controller.js @@ -1474,6 +1474,10 @@ iOSController.deleteCookies = function (cb) { }.bind(this)); }; +iOSController.endCoverage = function (intentToBroadcast, ecOnDevicePath, cb) { + cb(new NotYetImplementedError(), null); +}; + iOSController.getCurrentActivity = function (cb) { cb(new NotYetImplementedError(), null); }; diff --git a/lib/server/controller.js b/lib/server/controller.js index 82aee4599..a74993224 100644 --- a/lib/server/controller.js +++ b/lib/server/controller.js @@ -938,6 +938,15 @@ exports.localScreenshot = function (req, res) { } }; +exports.endCoverage = function (req, res) { + var intent = req.body.intent; + var path = req.body.path; + + if (checkMissingParams(res, {intent: intent, path: path})) { + req.device.endCoverage(intent, path, getResponseHandler(req, res)); + } +}; + exports.toggleData = function (req, res) { req.device.toggleData(getResponseHandler(req, res)); }; @@ -995,6 +1004,7 @@ var mobileCmdMap = { , 'toggleFlightMode': exports.toggleFlightMode , 'toggleWiFi': exports.toggleWiFi , 'toggleLocationServices': exports.toggleLocationServices +, 'endCoverage': exports.endCoverage }; exports.produceError = function (req, res) { diff --git a/lib/server/parser.js b/lib/server/parser.js index 7439a1b55..0dad0e7f3 100644 --- a/lib/server/parser.js +++ b/lib/server/parser.js @@ -220,6 +220,15 @@ var args = [ "to wait for (e.g., SplashActivity)" }], + [['--android-coverage'], { + dest: 'androidCoverage' + , defaultValue: false + , required: false + , example: 'com.my.Pkg/com.my.Pkg.instrumentation.MyInstrumentation' + , help: "(Android-only) Fully qualified instrumentation class. Passed to -w in " + + "adb shell am instrument -e coverage true -w " + }], + [['--avd'], { defaultValue: null , required: false