Add androidCoverage

This commit is contained in:
bootstraponline
2014-02-10 13:29:08 -05:00
parent 68135c3b53
commit bbd03f89c6
9 changed files with 222 additions and 4 deletions

View File

@@ -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`|
--

View File

@@ -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||

View File

@@ -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

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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 () {

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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