mirror of
https://github.com/appium/appium.git
synced 2026-02-09 03:09:02 -06:00
Add androidCoverage
This commit is contained in:
@@ -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`|
|
||||
|
||||
--
|
||||
|
||||
|
||||
@@ -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||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user