You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by al...@apache.org on 2016/07/11 11:11:32 UTC

cordova-paramedic git commit: CB-11546 Appium tests support

Repository: cordova-paramedic
Updated Branches:
  refs/heads/master 09d861aef -> afad9b3d0


CB-11546 Appium tests support


Project: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/commit/afad9b3d
Tree: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/tree/afad9b3d
Diff: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/diff/afad9b3d

Branch: refs/heads/master
Commit: afad9b3d06c77cdf94e49fe57bd8c38478a7a529
Parents: 09d861a
Author: Alexander Sorokin <al...@akvelon.com>
Authored: Thu Jul 7 19:31:39 2016 +0300
Committer: Alexander Sorokin <al...@akvelon.com>
Committed: Thu Jul 7 19:31:39 2016 +0300

----------------------------------------------------------------------
 lib/ParamedicConfig.js            |  87 ++++--
 lib/ParamedicLog.js               |  10 +-
 lib/Reporters.js                  |  22 +-
 lib/appium/.jshintrc              |  11 +
 lib/appium/AppiumRunner.js        | 473 +++++++++++++++++++++++++++++++++
 lib/appium/cordova_logo_thumb.jpg | Bin 0 -> 1932 bytes
 lib/appium/helpers/wdHelper.js    | 103 +++++--
 lib/paramedic.js                  | 299 ++++++++++++++-------
 lib/utils/utilities.js            |  23 +-
 main.js                           |  43 ++-
 package.json                      |   8 +
 11 files changed, 930 insertions(+), 149 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/ParamedicConfig.js
----------------------------------------------------------------------
diff --git a/lib/ParamedicConfig.js b/lib/ParamedicConfig.js
index 51a1114..b68345c 100644
--- a/lib/ParamedicConfig.js
+++ b/lib/ParamedicConfig.js
@@ -19,7 +19,13 @@
 
 var DEFAULT_START_PORT = 8008;
 var DEFAULT_END_PORT   = 8018;
-var DEFAULT_TIMEOUT    = 10 * 60 * 1000; // 10 minutes in msec - this will become a param
+var DEFAULT_TIMEOUT    = 60 * 60 * 1000; // 60 minutes in msec - this will become a param
+var DEFAULT_SAUCE_DEVICE_NAME_ANDROID      = 'Android Emulator';
+var DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID = '4.4';
+var DEFAULT_SAUCE_DEVICE_NAME_IOS          = 'iPhone Simulator';
+var DEFAULT_SAUCE_PLATFORM_VERSION_IOS     = '9.3';
+var DEFAULT_SAUCE_APPIUM_VERSION           = '1.5.3';
+var DEFAULT_BUILD_NAME                     = 'Paramedic sauce test';
 
 var util = require('./utils').utilities;
 
@@ -27,25 +33,36 @@ function ParamedicConfig(json) {
     this._config = json;
 }
 
+ParamedicConfig.prototype.getDefaultSauceDeviceName = function () {
+    return this._config.platform === 'android' ? DEFAULT_SAUCE_DEVICE_NAME_ANDROID : DEFAULT_SAUCE_DEVICE_NAME_IOS;
+};
+
+ParamedicConfig.prototype.getDefaultSaucePlatformVersion = function () {
+    return this._config.platform === 'android' ? DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID : DEFAULT_SAUCE_PLATFORM_VERSION_IOS;
+};
+
 ParamedicConfig.parseFromArguments = function (argv) {
     return new ParamedicConfig({
-        platform:          argv.platform,
-        action:            !!argv.justbuild ? 'build' : 'run',
-        args:              (!!argv.browserify ? '--browserify ' : ''),
-        plugins:           Array.isArray(argv.plugin) ? argv.plugin : [argv.plugin],
-        useTunnel:         !!argv.useTunnel,
-        verbose:           !!argv.verbose,
-        startPort:         argv.startport || argv.port,
-        endPort:           argv.endport || argv.port,
-        externalServerUrl: argv.externalServerUrl,
-        outputDir:         !!argv.outputDir? argv.outputDir: null,
-        logMins:           !!argv.logMins? argv.logMins: null,
-        tccDb:             !!argv.tccDbPath? argv.tccDb: null,
-        cleanUpAfterRun:   !!argv.cleanUpAfterRun? true: false,
-        shouldUseSauce:    !!argv.shouldUseSauce || false,
-        buildName:         argv.buildName || 'Paramedic sauce test',
-        sauceUser:         argv.sauceUser || process.env[util.SAUCE_USER_ENV_VAR],
-        sauceKey:          argv.sauceKey || process.env[util.SAUCE_KEY_ENV_VAR]
+        platform:             argv.platform,
+        action:               !!argv.justbuild ? 'build' : 'run',
+        args:                 (!!argv.browserify ? '--browserify ' : ''),
+        plugins:              Array.isArray(argv.plugin) ? argv.plugin : [argv.plugin],
+        useTunnel:            !!argv.useTunnel,
+        verbose:              !!argv.verbose,
+        startPort:            argv.startport || argv.port,
+        endPort:              argv.endport || argv.port,
+        externalServerUrl:    argv.externalServerUrl,
+        outputDir:            !!argv.outputDir? argv.outputDir: null,
+        logMins:              !!argv.logMins? argv.logMins: null,
+        tccDb:                !!argv.tccDbPath? argv.tccDb: null,
+        cleanUpAfterRun:      !!argv.cleanUpAfterRun? true: false,
+        shouldUseSauce:       !!argv.shouldUseSauce || false,
+        buildName:            argv.buildName,
+        sauceUser:            argv.sauceUser,
+        sauceKey:             argv.sauceKey,
+        sauceDeviceName:      argv.sauceDeviceName && argv.sauceDeviceName.toString(),
+        saucePlatformVersion: argv.saucePlatformVersion && argv.saucePlatformVersion.toString(),
+        sauceAppiumVersion:   argv.sauceAppiumVersion && argv.sauceAppiumVersion.toString()
     });
 };
 
@@ -126,15 +143,19 @@ ParamedicConfig.prototype.setShouldUseSauce = function (sus) {
 };
 
 ParamedicConfig.prototype.getBuildName = function () {
-    return this._config.buildName;
+    return this._config.buildName || DEFAULT_BUILD_NAME;
 };
 
 ParamedicConfig.prototype.setBuildName = function (buildName) {
     this._config.buildName = buildName;
 };
 
+ParamedicConfig.prototype.getDefaultBuildName = function () {
+    return DEFAULT_BUILD_NAME;
+};
+
 ParamedicConfig.prototype.getSauceUser = function () {
-    return this._config.sauceUser;
+    return this._config.sauceUser || process.env[util.SAUCE_USER_ENV_VAR];
 };
 
 ParamedicConfig.prototype.setSauceUser = function (sauceUser) {
@@ -142,13 +163,37 @@ ParamedicConfig.prototype.setSauceUser = function (sauceUser) {
 };
 
 ParamedicConfig.prototype.getSauceKey = function () {
-    return this._config.sauceKey;
+    return this._config.sauceKey || process.env[util.SAUCE_KEY_ENV_VAR];
 };
 
 ParamedicConfig.prototype.setSauceKey = function (sauceKey) {
     this._config.sauceKey = sauceKey;
 };
 
+ParamedicConfig.prototype.getSauceDeviceName = function () {
+    return this._config.sauceDeviceName || this.getDefaultSauceDeviceName();
+};
+
+ParamedicConfig.prototype.setSauceDeviceName = function (sauceDeviceName) {
+    this._config.sauceDeviceName = sauceDeviceName.toString();
+};
+
+ParamedicConfig.prototype.getSaucePlatformVersion = function () {
+    return this._config.saucePlatformVersion || this.getDefaultSaucePlatformVersion();
+};
+
+ParamedicConfig.prototype.setSaucePlatformVersion = function (saucePlatformVersion) {
+    this._config.saucePlatformVersion = saucePlatformVersion.toString();
+};
+
+ParamedicConfig.prototype.getSauceAppiumVersion = function () {
+    return this._config.sauceAppiumVersion || DEFAULT_SAUCE_APPIUM_VERSION;
+};
+
+ParamedicConfig.prototype.setSauceAppiumVersion = function (sauceAppiumVersion) {
+    this._config.sauceAppiumVersion = sauceAppiumVersion.toString();
+};
+
 ParamedicConfig.prototype.isBrowserify = function () {
     return this._config.browserify;
 };

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/ParamedicLog.js
----------------------------------------------------------------------
diff --git a/lib/ParamedicLog.js b/lib/ParamedicLog.js
index 14dea4b..3180d09 100644
--- a/lib/ParamedicLog.js
+++ b/lib/ParamedicLog.js
@@ -37,6 +37,10 @@ function ParamedicLog(platform, appPath, outputDir, targetObj) {
 }
 
 ParamedicLog.prototype.logIOS = function (appPath) {
+    if (!this.targetObj) {
+        logger.warn('It looks like there is no target to get logs from.');
+        return;
+    }
     var simId = this.targetObj.simId;
 
     if (simId) {
@@ -62,8 +66,12 @@ ParamedicLog.prototype.logWindows = function (appPath, logMins) {
 };
 
 ParamedicLog.prototype.logAndroid = function () {
-    var logCommand = 'adb -s ' + this.targetObj.target + ' logcat -d -v time';
+    if (!this.targetObj) {
+        logger.warn('It looks like there is no target to get logs from.');
+        return;
+    }
 
+    var logCommand = 'adb -s ' + this.targetObj.target + ' logcat -d -v time';
     var numDevices = util.countAndroidDevices();
     if (numDevices != 1) {
         logger.error('there must be exactly one emulator/device attached');

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/Reporters.js
----------------------------------------------------------------------
diff --git a/lib/Reporters.js b/lib/Reporters.js
index e204c45..2191e26 100644
--- a/lib/Reporters.js
+++ b/lib/Reporters.js
@@ -20,7 +20,7 @@
 var JasmineSpecReporter = require('jasmine-spec-reporter');
 var jasmineReporters    = require('jasmine-reporters');
 
-module.exports = function(outputDir) {
+module.exports.getReporters = function(outputDir) {
     var reporters = [new JasmineSpecReporter({displayPendingSummary: false, displaySuiteNumber: true})];
 
     if (outputDir) {
@@ -29,3 +29,23 @@ module.exports = function(outputDir) {
 
     return reporters;
 };
+
+module.exports.ParamedicReporter = ParamedicReporter;
+
+function ParamedicReporter(callback) {
+    this.allDoneCallback = callback;
+    this.failed = false;
+}
+
+ParamedicReporter.prototype = {
+    specDone: function (spec) {
+        if (spec.status === 'failed') {
+            this.failed = true;
+        }
+    },
+    jasmineDone: function () {
+        if (this.allDoneCallback instanceof Function) {
+            this.allDoneCallback(!this.failed);
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/.jshintrc
----------------------------------------------------------------------
diff --git a/lib/appium/.jshintrc b/lib/appium/.jshintrc
new file mode 100644
index 0000000..6997763
--- /dev/null
+++ b/lib/appium/.jshintrc
@@ -0,0 +1,11 @@
+{
+  "node": true,
+  "bitwise": true,
+  "undef": true,
+  "trailing": true,
+  "quotmark": true,
+  "indent": 4,
+  "unused": "vars",
+  "latedef": "nofunc",
+  "-W030": false
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/AppiumRunner.js
----------------------------------------------------------------------
diff --git a/lib/appium/AppiumRunner.js b/lib/appium/AppiumRunner.js
new file mode 100644
index 0000000..d126dd7
--- /dev/null
+++ b/lib/appium/AppiumRunner.js
@@ -0,0 +1,473 @@
+#!/usr/bin/env node
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* jshint node: true */
+
+'use strict';
+
+var fs               = require('fs');
+var path             = require('path');
+var util             = require('../utils').utilities;
+var logger           = require('../utils').logger;
+var wd               = require('wd');
+var wdHelper         = require('./helpers/wdHelper');
+var screenshotHelper = require('./helpers/screenshotHelper');
+var kill             = require('tree-kill');
+var child_process    = require('child_process');
+var expectTelnet     = require('expect-telnet');
+var shell            = require('shelljs');
+var Jasmine          = require('jasmine');
+var unorm            = require('unorm');
+var Q                = require('q');
+var ConfigParser     = require('cordova-common').ConfigParser;
+var Reporters        = require('../Reporters');
+var execPromise      = require('../utils').execPromise;
+var Reporters        = require('../Reporters');
+
+var KILL_SIGNAL = 'SIGINT';
+var SMALL_BUFFER_SIZE = 1024 * 1024;
+var BIG_BUFFER_SIZE = 50 * 1024 * 1024;
+var APPIUM_SERVER_PATH = getAppiumServerPath();
+
+function AppiumRunner(options) {
+    this.options = options;
+    this.prepareOptions();
+    this.createScreenshotDir();
+    this.findTests();
+    this.setGlobals();
+}
+
+function getAppiumServerPath() {
+    return path.resolve(process.cwd(), 'cordova-paramedic/node_modules/appium/build/lib/main.js');
+}
+
+function getFullAppPath(appPath) {
+    var fullPath = appPath;
+    if (!path.isAbsolute(appPath)) {
+        fullPath = path.join(__dirname, '../..', appPath);
+    }
+    return fullPath;
+}
+
+function getPackagePath(options) {
+    if (options.sauce) {
+        return options.sauceAppPath;
+    }
+
+    var fullAppPath = getFullAppPath(options.appPath);
+
+    switch (options.platform) {
+    case 'android':
+        var packagePath = path.join(fullAppPath, '/platforms/android/build/outputs/apk/android-debug.apk');
+        if (fs.existsSync(packagePath)) {
+            return packagePath;
+        }
+        throw new Error('Could not find apk');
+    case 'ios':
+        var searchDir = options.device ?
+            path.join(fullAppPath, '/platforms/ios/build/device/') :
+            path.join(fullAppPath, '/platforms/ios/build/emulator/');
+        var fileMask = options.device ? '*.ipa' : '*.app';
+        var files = shell.ls(searchDir + fileMask);
+        logger.normal('paramedic-appium: Looking for app package in ' + searchDir);
+        if (files && files.length > 0) {
+            logger.normal('paramedic-appium: Found app package: ' + files[0]);
+            return files[0];
+        }
+        throw new Error('Could not find the app package');
+    }
+}
+
+function getPluginDirs(appPath) {
+    return shell.ls(path.join(appPath, '/plugins/cordova-plugin-*'));
+}
+
+function getConfigPath(appPath) {
+    return path.join(appPath, 'config.xml');
+}
+
+function addCspSource(appPath, directive, source) {
+    var cspInclFile = path.join(appPath, 'www/csp-incl.js');
+    var indexFile = path.join(appPath, 'www/index.html');
+    var cspFile = fs.existsSync(cspInclFile) ? cspInclFile : indexFile;
+    var cspContent = fs.readFileSync(cspFile, util.DEFAULT_ENCODING);
+    var cspTagOpening = '<meta http-equiv="Content-Security-Policy" content=\'';
+    var cspRule = directive + ' ' + source;
+    var cspRuleReg = new RegExp(directive + '[^;"]+' + source.replace('*', '\\*'));
+
+    logger.normal('paramedic-appium: Adding CSP source "' + source + '" to directive "' + directive + '"');
+
+    if (cspContent.match(cspRuleReg)) {
+        logger.normal('paramedic-appium: It\'s already there.');
+    } else if (util.contains(cspContent, directive)) {
+        // if the directive is there, just add the source to it
+        cspContent = cspContent.replace(directive, cspRule);
+        fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING);
+    } else if (cspContent.match(/content=".*?default-src.+?"/)) {
+        // needed directive is not there but there is default-src directive
+        // creating needed directive and copying default-src sources to it
+        var defaultSrcReg = /(content=".*?default-src)(.+?);/;
+        cspContent = cspContent.replace(defaultSrcReg, '$1$2; ' + cspRule + '$2;');
+        fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING);
+    } else if (util.contains(cspContent, cspTagOpening)) {
+        // needed directive is not there and there is no default-src directive
+        // but the CSP tag is till present
+        // just adding needed directive to a start of CSP tag content
+        cspContent = cspContent.replace(cspTagOpening, cspTagOpening + directive + ' ' + source + '; ');
+        fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING);
+    } else {
+        // no CSP tag, skipping
+        logger.normal('paramedic-appium: WARNING: No CSP tag found.');
+    }
+}
+
+function setPreference(appPath, preference, value) {
+    var configFile = getConfigPath(appPath);
+    var config = new ConfigParser(configFile);
+
+    logger.normal('paramedic-appium: Setting "' + preference + '" preference to "' + value + '"');
+    config.setGlobalPreference(preference, value);
+    config.write();
+}
+
+function permitAccess(appPath, origin) {
+    var configFile = getConfigPath(appPath);
+    var config = new ConfigParser(configFile);
+    var accesses = config.getAccesses();
+    var accessPresent = false;
+
+    logger.normal('paramedic-appium: Adding a whitelist "access" rule for origin: ' + origin);
+    accesses.forEach(function (access) {
+        if (access.origin == origin) {
+            accessPresent = true;
+        }
+    });
+
+    if (accessPresent) {
+        logger.normal('paramedic-appium: It is already in place');
+    } else {
+        config.addElement('access', { origin: origin });
+        config.write();
+    }
+}
+
+function runCommand(command, appPath) {
+    if (appPath) {
+        shell.pushd(appPath);
+    }
+    shell.exec(command);
+    if (appPath) {
+        shell.popd();
+    }
+}
+
+function isFailFastError(error) {
+    if (error && error.message) {
+        return error.message.indexOf('Could not find a connected') > -1 ||
+            error.message.indexOf('Bad app') > -1;
+    }
+    return false;
+}
+
+function killProcess(procObj, killSignal, callback) {
+    if (procObj && procObj.alive) {
+        procObj.alive = false;
+        setTimeout(function () {
+            kill(procObj.process.pid, killSignal, callback);
+        }, 1000);
+    } else {
+        callback();
+    }
+}
+
+function installAppiumServer() {
+    logger.normal('paramedic-appium: Installing Appium server...');
+    shell.pushd(path.join(__dirname, '../..'));
+    return execPromise('npm install appium').then(function () {
+        shell.popd();
+    });
+}
+
+AppiumRunner.prototype.createScreenshotDir = function () {
+    util.mkdirSync(this.options.screenshotPath);
+};
+
+AppiumRunner.prototype.prepareOptions = function () {
+    if (!this.options.hasOwnProperty('device')) {
+        this.options.device = false;
+    }
+    if (this.options.platform === 'ios' && this.options.appiumDeviceName) {
+        this.options.appiumDeviceName = this.options.appiumDeviceName.replace('-', ' ');
+    }
+};
+
+AppiumRunner.prototype.cleanUp = function (callback) {
+    var self = this;
+
+    killProcess(self.appium, KILL_SIGNAL, function () {
+        killProcess(self.iosProxy, KILL_SIGNAL, function () {
+            callback();
+        });
+    });
+};
+
+AppiumRunner.prototype.startTests = function () {
+    var jasmine = new Jasmine();
+    var self = this;
+    var d = Q.defer();
+
+    function exitGracefully(e) {
+        if (self.exiting) {
+            return;
+        }
+        if (!!e) {
+            logger.normal('paramedic-appium: ' + e);
+        }
+        logger.normal('paramedic-appium: Uncaught exception! Killing server and exiting in 2 seconds...');
+        self.exiting = true;
+        self.cleanUp(function () {
+            setTimeout(function () {
+                d.reject(e.stack);
+            }, 2000);
+        });
+    }
+
+    process.on('uncaughtException', function(err) {
+        exitGracefully(err);
+    });
+
+    logger.normal('paramedic-appium: Running tests from:');
+    self.options.testPaths.forEach(function (testPath) {
+        logger.normal('paramedic-appium: ' + testPath);
+    });
+
+    jasmine.loadConfig({
+        spec_dir: '',
+        spec_files: self.options.testPaths
+    });
+
+    // don't use default reporter, it exits the process before
+    // we would get the chance to kill appium server
+    //jasmine.configureDefaultReporter({ showColors: false });
+
+    var outputDir = self.options.output || process.cwd();
+    var reporters = Reporters.getReporters(outputDir);
+    var paramedicReporter = new Reporters.ParamedicReporter(function (passed) {
+        self.passed = passed;
+        self.cleanUp(d.resolve);
+    });
+
+    reporters.forEach(function (reporter) {
+        jasmine.addReporter(reporter);
+    });
+    jasmine.addReporter(paramedicReporter);
+
+    try {
+        // Launch the tests!
+        jasmine.execute();
+    } catch (e) {
+        exitGracefully(e);
+    }
+
+    return d.promise;
+};
+
+AppiumRunner.prototype.startIosProxy = function () {
+    var self = this;
+    var iosProxyCommand;
+    self.iosProxy = {
+        alive: false,
+        process: null
+    };
+
+    if (this.options.platform === 'ios' && this.options.device && this.options.udid) {
+        iosProxyCommand = 'ios_webkit_debug_proxy -c ' + this.options.udid + ':27753';
+        logger.normal('paramedic-appium: Running:');
+        logger.normal('paramedic-appium: ' + iosProxyCommand);
+        self.iosProxy.alive = true;
+        self.iosProxy.process = child_process.exec(iosProxyCommand, { maxBuffer: BIG_BUFFER_SIZE }, function () {
+            self.iosProxy.alive = false;
+            logger.normal('paramedic-appium: iOS proxy process exited.');
+        });
+    }
+};
+
+AppiumRunner.prototype.startAppiumServer = function () {
+    var d = Q.defer();
+    var self = this;
+    var appiumServerCommand;
+    var additionalArgs = '';
+    self.appium = {
+        alive: false,
+        process: null
+    };
+
+    // compose a command to run the Appium server
+    switch (self.options.platform) {
+    case 'android':
+        break;
+    case 'ios':
+        if (self.options.udid) {
+            additionalArgs += ' --udid ' + self.options.udid;
+        }
+        break;
+    default:
+        throw new Error('Unsupported platform: ' + self.options.platform);
+    }
+    if (self.options.logFile) {
+        additionalArgs += ' --log ' + self.options.logFile;
+    }
+
+    appiumServerCommand = 'node ' + APPIUM_SERVER_PATH + additionalArgs;
+
+    // run the Appium server
+    logger.normal('paramedic-appium: Running:');
+    logger.normal('paramedic-appium: ' + appiumServerCommand);
+    self.appium.alive = true;
+    self.appium.process = child_process.exec(appiumServerCommand, { maxBuffer: BIG_BUFFER_SIZE }, function (error) {
+        logger.normal('paramedic-appium: Appium process exited.');
+        if (self.appium.alive && error) {
+            logger.normal('paramedic-appium: Error running appium server: ' + error);
+            if (isFailFastError(error)) {
+                self.cleanUp(d.reject);
+            } else {
+                logger.normal('paramedic-appium: Another instance already running? Will try to run tests on it.');
+                d.resolve();
+            }
+        }
+        self.appium.alive = false;
+    });
+
+    // Wait for the Appium server to start up
+    self.appium.process.stdout.on('data', function (data) {
+        if (data.indexOf('Appium REST http interface listener started') > -1) {
+            d.resolve();
+        }
+    });
+
+    return d.promise;
+};
+
+AppiumRunner.prototype.findTests = function () {
+    var self = this;
+
+    if (!self.options.pluginRepos) {
+        self.options.pluginRepos = getPluginDirs(self.options.appPath);
+    }
+
+    // looking for the tests
+    self.options.testPaths = [];
+    var searchPaths = [];
+    self.options.pluginRepos.forEach(function (pluginRepo) {
+        searchPaths.push(path.join(pluginRepo, 'appium-tests', self.options.platform));
+        searchPaths.push(path.join(pluginRepo, 'appium-tests', 'common'));
+    });
+    searchPaths.forEach(function (searchPath) {
+        if (fs.existsSync(searchPath)) {
+            logger.normal('paramedic-appium: Found tests in: ' + searchPath);
+            if (path.isAbsolute(searchPath)) {
+                searchPath = path.relative(process.cwd(), searchPath);
+            }
+            self.options.testPaths.push(path.join(searchPath, '*.spec.js'));
+        }
+    });
+};
+
+AppiumRunner.prototype.setGlobals = function () {
+    // setting up the global variables so the tests could use them
+    global.WD = wd;
+    global.WD_HELPER = wdHelper;
+    global.SCREENSHOT_HELPER = screenshotHelper;
+    global.ET = expectTelnet;
+    global.SHELL = shell;
+    global.DEVICE = this.options.device;
+    global.DEVICE_NAME = this.options.appiumDeviceName;
+    global.PLATFORM = this.options.platform;
+    global.PLATFORM_VERSION = this.options.appiumPlatformVersion;
+    global.SCREENSHOT_PATH = this.options.screenshotPath;
+    global.UNORM = unorm;
+    global.UDID = this.options.udid;
+    global.VERBOSE = this.options.verbose;
+    global.USE_SAUCE = this.options.sauce;
+    global.SAUCE_USER = this.options.sauceUser;
+    global.SAUCE_KEY = this.options.sauceKey;
+    global.SAUCE_CAPS = this.options.sauceCaps;
+    global.VERBOSE = this.options.verbose;
+    global.SAUCE_SERVER_HOST = util.SAUCE_HOST;
+    global.SAUCE_SERVER_PORT = util.SAUCE_PORT;
+};
+
+AppiumRunner.prototype.prepareApp = function () {
+    var self = this;
+    var d = Q.defer();
+    var fullAppPath = getFullAppPath(self.options.appPath);
+    var deviceString = self.options.device ? ' --device' : '';
+    var buildCommand = 'cordova build ' + self.options.platform + deviceString;
+
+    // remove medic.json and (re)build
+    shell.rm(path.join(fullAppPath, 'www', 'medic.json'));
+    fs.stat(fullAppPath, function (error, stats) {
+        // check if the app exists
+        if (error || !stats.isDirectory()) {
+            d.reject('The app directory doesn\'t exist: ' + fullAppPath);
+        }
+
+        // set properties/CSP rules
+        if (self.options.platform === 'ios') {
+            setPreference(fullAppPath, 'CameraUsesGeolocation', 'true');
+        } else if (self.options.platform === 'android') {
+            setPreference(fullAppPath, 'loadUrlTimeoutValue', 60000);
+        }
+        addCspSource(fullAppPath, 'connect-src', 'http://*');
+        permitAccess(fullAppPath, '*');
+        // add cordova-save-image-gallery plugin from npm to enable
+        // Appium tests for camera plugin to save test image to the gallery
+        runCommand('cordova plugin add cordova-save-image-gallery', fullAppPath);
+
+        // rebuild the app
+        logger.normal('paramedic-appium: Building the app...');
+        child_process.exec(buildCommand, { cwd: fullAppPath, maxBuffer: SMALL_BUFFER_SIZE }, function (error) {
+            if (error) {
+                d.reject('Couldn\'t build the app: ' + error);
+            } else {
+                global.PACKAGE_PATH = getPackagePath(self.options);
+                d.resolve();
+            }
+        });
+    });
+    return d.promise;
+};
+
+AppiumRunner.prototype.runTests = function (useSauce) {
+    var self = this;
+
+    return Q().then(function () {
+        if (!useSauce) {
+            self.startIosProxy();
+            return installAppiumServer()
+            .then(self.startAppiumServer.bind(self));
+        }
+    })
+    .then(self.startTests.bind(self))
+    .then(function () { return self.passed; });
+};
+
+module.exports = AppiumRunner;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/cordova_logo_thumb.jpg
----------------------------------------------------------------------
diff --git a/lib/appium/cordova_logo_thumb.jpg b/lib/appium/cordova_logo_thumb.jpg
new file mode 100644
index 0000000..5bc04bf
Binary files /dev/null and b/lib/appium/cordova_logo_thumb.jpg differ

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/helpers/wdHelper.js
----------------------------------------------------------------------
diff --git a/lib/appium/helpers/wdHelper.js b/lib/appium/helpers/wdHelper.js
index ce32daa..61505d5 100644
--- a/lib/appium/helpers/wdHelper.js
+++ b/lib/appium/helpers/wdHelper.js
@@ -35,6 +35,9 @@ var wd = global.WD || require('wd');
 
 module.exports.getDriver = function (platform) {
     var normalizedPlatform;
+    var driverConfig = {};
+    var serverConfig = {};
+    var driver;
     switch (platform.toLowerCase()) {
         case 'android':
             normalizedPlatform = 'Android';
@@ -46,24 +49,37 @@ module.exports.getDriver = function (platform) {
             throw 'Unknown platform: ' + platform;
     }
 
-    var serverConfig = {
-        host: APPIUM_SERVER_HOST,
-        port: APPIUM_SERVER_PORT
-    };
-
-    var driverConfig = {
-        browserName: '',
-        platformName: normalizedPlatform,
-        platformVersion: global.PLATFORM_VERSION || '',
-        deviceName: global.DEVICE_NAME || '',
-        app: global.PACKAGE_PATH,
-        autoAcceptAlerts: true,
-    };
-    if (global.UDID) {
-        driverConfig.udid = global.UDID;
+    if (global.USE_SAUCE) {
+        serverConfig = {
+            host: global.SAUCE_SERVER_HOST,
+            port: global.SAUCE_SERVER_PORT
+        };
+
+        driverConfig = global.SAUCE_CAPS;
+
+        driver = global.WD.promiseChainRemote(serverConfig.host, serverConfig.port, global.SAUCE_USER, global.SAUCE_KEY);
+    } else {
+        serverConfig = {
+            host: APPIUM_SERVER_HOST,
+            port: APPIUM_SERVER_PORT
+        };
+
+        driverConfig = {
+            browserName: '',
+            platformName: normalizedPlatform,
+            platformVersion: global.PLATFORM_VERSION || '',
+            deviceName: global.DEVICE_NAME || '',
+            app: global.PACKAGE_PATH,
+            autoAcceptAlerts: true,
+        };
+
+        if (global.UDID) {
+            driverConfig.udid = global.UDID;
+        }
+
+        driver = global.WD.promiseChainRemote(serverConfig);
     }
 
-    var driver = global.WD.promiseChainRemote(serverConfig);
     module.exports.configureLogging(driver);
 
     return driver
@@ -122,6 +138,9 @@ module.exports.injectLibraries = function (driver) {
 };
 
 module.exports.configureLogging = function (driver) {
+    if (!global.VERBOSE) {
+        return;
+    }
     driver.on('status', function (info) {
         console.log(info);
     });
@@ -200,20 +219,54 @@ module.exports.pollForEvents = function (driver, isAndroid, windowOffset) {
     });
 };
 
+module.exports.addFillerImage = function (driver) {
+    var bitmap = fs.readFileSync(path.join(__dirname, '../cordova_logo_thumb.jpg'));
+    var base64str = new Buffer(bitmap).toString('base64');
+
+    return driver.executeAsync(function (b64str, cb) {
+        if (window.imageSaver) {
+            window.imageSaver.saveBase64Image( {
+                data: b64str
+            }, function (fpath) {
+                cb(fpath);
+            }, function (err) {
+                cb('ERROR: ' + err);
+            });
+        } else {
+            cb();
+        }
+    }, [base64str]);
+};
+
+module.exports.deleteFillerImage = function (driver, testImagePath) {
+    if (!testImagePath) {
+        return driver;
+    }
+    return driver.executeAsync(function (testImagePath, cb) {
+        if (window.imageSaver) {
+            window.imageSaver.removeImage({
+                data: testImagePath
+            }, function () {
+                cb();
+            }, function (err) {
+                cb('ERROR: ' + err);
+            });
+        } else {
+            cb();
+        }
+    }, [testImagePath]);
+};
+
 wd.addPromiseChainMethod('getWebviewContext', function (retries) {
     return module.exports.getWebviewContext(this, retries);
 });
 
-wd.addPromiseChainMethod('injectLibraries', function () {
-    return module.exports.tapElementByXPath(this);
-});
-
 wd.addPromiseChainMethod('waitForDeviceReady', function () {
     return module.exports.waitForDeviceReady(this);
 });
 
 wd.addPromiseChainMethod('injectLibraries', function () {
-    return module.exports.tapElementByXPath(this);
+    return module.exports.injectLibraries(this);
 });
 
 wd.addPromiseChainMethod('tapElementByXPath', function (xpath) {
@@ -223,3 +276,11 @@ wd.addPromiseChainMethod('tapElementByXPath', function (xpath) {
 wd.addPromiseChainMethod('pollForEvents', function (isAndroid) {
     return module.exports.pollForEvents(this, isAndroid);
 });
+
+wd.addPromiseChainMethod('addFillerImage', function () {
+    return module.exports.addFillerImage(this);
+});
+
+wd.addPromiseChainMethod('deleteFillerImage', function (testImagePath) {
+    return module.exports.deleteFillerImage(this, testImagePath);
+});

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/paramedic.js
----------------------------------------------------------------------
diff --git a/lib/paramedic.js b/lib/paramedic.js
index b2c83a1..ee1a084 100644
--- a/lib/paramedic.js
+++ b/lib/paramedic.js
@@ -28,11 +28,13 @@ var fs              = require('fs');
 var logger          = require('./utils').logger;
 var util            = require('./utils').utilities;
 var PluginsManager  = require('./PluginsManager');
-var getReporters    = require('./Reporters');
+var Reporters       = require('./Reporters');
 var ParamedicKill   = require('./ParamedicKill');
 var ParamedicLog    = require('./ParamedicLog');
 var wd              = require('wd');
 var SauceLabs       = require('saucelabs');
+var randomstring    = require('randomstring');
+var AppiumRunner    = require('./appium/AppiumRunner');
 var ParamediciOSPermissions = require('./ParamediciOSPermissions');
 var ParamedicTargetChooser  = require('./ParamedicTargetChooser');
 var ParamedicAppUninstall   = require('./ParamedicAppUninstall');
@@ -44,9 +46,6 @@ require('./appium/helpers/wdHelper');
 // If device has not connected within this interval the tests are stopped.
 var INITIAL_CONNECTION_TIMEOUT = 300000; // 5mins
 
-var SAUCE_HOST = 'ondemand.saucelabs.com';
-var SAUCE_PORT = 80;
-
 var applicationsToGrantPermission = [
     'kTCCServiceAddressBook'
 ];
@@ -63,6 +62,7 @@ function ParamedicRunner(config, _callback) {
 
 ParamedicRunner.prototype.run = function () {
     var self = this;
+    var isTestPassed = false;
 
     this.checkSauceRequirements();
 
@@ -84,7 +84,9 @@ ParamedicRunner.prototype.run = function () {
         logger.normal('Start running tests at ' + (new Date()).toLocaleTimeString());
         return self.runTests();
     })
+    .timeout(self.config.getTimeout(), 'Timed out after waiting for ' + self.config.getTimeout() + ' ms.')
     .fin(function (result) {
+        isTestPassed = result;
         logger.normal('Completed tests at ' + (new Date()).toLocaleTimeString());
         // if we do --justbuild  or run on sauce,
         // we should NOT do actions below
@@ -94,7 +96,7 @@ ParamedicRunner.prototype.run = function () {
             self.killEmulatorProcess();
         }
         self.cleanUpProject();
-        return result;
+        return self.displaySauceDetails();
     });
 };
 
@@ -164,7 +166,7 @@ ParamedicRunner.prototype.setPermissions = function () {
 
 ParamedicRunner.prototype.injectReporters = function () {
     var self = this;
-    var reporters = getReporters(self.config.getOutputDir());
+    var reporters = Reporters.getReporters(self.config.getOutputDir());
 
     ['jasmineStarted', 'specStarted', 'specDone',
     'suiteStarted', 'suiteDone', 'jasmineDone'].forEach(function(route) {
@@ -190,48 +192,124 @@ ParamedicRunner.prototype.writeMedicConnectionUrl = function(url) {
     fs.writeFileSync(path.join('www','medic.json'), JSON.stringify({logurl:url}));
 };
 
-ParamedicRunner.prototype.runTests = function () {
+ParamedicRunner.prototype.buildApp = function () {
     var self = this;
-    if (this.config.shouldUseSauce()) {
-        var command = this.getCommandForBuilding();
+    var command = this.getCommandForBuilding();
+
+    logger.normal('cordova-paramedic: running command ' + command);
+
+    return execPromise(command)
+    .fail(function(output) {
+        // this trace is automatically available in verbose mode
+        // so we check for this flag to not trace twice
+        if (!self.config.verbose) {
+            logger.normal(output);
+        }
+        throw new Error('Unable to build project.');
+    });
+};
+
+ParamedicRunner.prototype.runLocalTests = function () {
+    var self = this;
+
+    return self.getCommandForStartingTests()
+    .then(function(command) {
+        self.setPermissions();
         logger.normal('cordova-paramedic: running command ' + command);
 
-        return execPromise(command)
-        .then(self.runSauceTests.bind(self), function(output) {
-            // this trace is automatically available in verbose mode
-            // so we check for this flag to not trace twice
-            if (!self.config.verbose) {
-                logger.normal(output);
-            }
-            logger.normal('cordova-paramedic: unable to build project; command log is available above');
-            throw new Error('Command "' + command + '" failed.');
+        return execPromise(command);
+    })
+    .then(function() {
+        // skip tests if it was just build
+        if (self.shouldWaitForTestResult()) {
+            return Q.promise(function(resolve, reject) {
+                // reject if timed out
+                self.waitForConnection().catch(reject);
+                // resolve if got results
+                self.waitForTests().then(resolve);
+            });
+        }
+    }, function(output) {
+        // this trace is automatically available in verbose mode
+        // so we check for this flag to not trace twice
+        if (!self.config.verbose) {
+            logger.normal(output);
+        }
+        throw new Error('Unable to run tests.');
+    });
+};
+
+ParamedicRunner.prototype.runAppiumTests = function (useSauce) {
+    var platform = this.config.getPlatformId();
+    var self = this;
+    if (platform !== 'android' && platform !== 'ios') {
+        logger.warn('Unsupported platform for Appium test run: ' + platform);
+        // just skip Appium tests
+        return Q(util.TEST_PASSED);
+    }
+    if (!useSauce && (!self.targetObj || !self.targetObj.target)) {
+        throw new Error('Cannot determine device name for Appium');
+    }
+
+    logger.normal('Running Appium tests ' + useSauce ? 'on Sauce Labs' : 'locally');
+
+    var options = {
+        platform: self.config.getPlatformId(),
+        appPath: self.tempFolder.name,
+        appiumDeviceName: self.targetObj && self.targetObj.target,
+        appiumPlatformVersion: null,
+        screenshotPath: path.join(process.cwd(), 'appium_screenshots'),
+        output: self.config.getOutputDir(),
+        verbose: self.config.isVerbose(),
+        sauce: useSauce
+    };
+    if (useSauce) {
+        options.sauceAppPath = 'sauce-storage:' + this.getAppName();
+        options.sauceUser = this.config.getSauceUser();
+        options.sauceKey = this.config.getSauceKey();
+        options.sauceCaps = this.getSauceCaps();
+        options.sauceCaps.name += '_Appium';
+    }
+
+    var appiumRunner = new AppiumRunner(options);
+    if (appiumRunner.options.testPaths && appiumRunner.options.testPaths.length === 0) {
+        logger.warn('Couldn\'t find Appium tests, skipping...');
+        return Q(util.TEST_PASSED);
+    }
+    return Q()
+    .then(function () {
+        return appiumRunner.prepareApp();
+    })
+    .then(function () {
+        if (useSauce) {
+            return self.uploadApp.bind(self);
+        }
+    })
+    .then(function () {
+        return appiumRunner.runTests(useSauce);
+    });
+};
+
+ParamedicRunner.prototype.runTests = function () {
+    var isTestPassed = false;
+    var self = this;
+    if (this.config.shouldUseSauce()) {
+        return this.runSauceTests()
+        .then(function (result) {
+            isTestPassed = result;
+            return self.runAppiumTests(true);
+        })
+        .then(function (isAppiumTestPassed) {
+            return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED;
         });
     } else {
-        return self.getCommandForStartingTests()
-        .then(function(command) {
-            self.setPermissions();
-            logger.normal('cordova-paramedic: running command ' + command);
-
-            return execPromise(command);
+        return this.runLocalTests()
+        .then(function (result) {
+            isTestPassed = result;
         })
-        .then(function() {
-            // skip tests if it was just build
-            if (self.shouldWaitForTestResult()) {
-                return Q.promise(function(resolve, reject) {
-                    // reject if timed out
-                    self.waitForConnection().catch(reject);
-                    // resolve if got results
-                    self.waitForTests().then(resolve);
-                });
-            }
-        }, function(output) {
-            // this trace is automatically available in verbose mode
-            // so we check for this flag to not trace twice
-            if (!self.config.verbose) {
-                logger.normal(output);
-            }
-            logger.normal('cordova-paramedic: unable to run tests; command log is available above');
-            throw new Error('Command "' + command + '" failed.');
+        .then(self.runAppiumTests.bind(this))
+        .then(function (isAppiumTestPassed) {
+            return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED;
         });
     }
 };
@@ -418,7 +496,6 @@ ParamedicRunner.prototype.uninstallApp = function () {
     paramedicAppUninstall.uninstallApp(this.targetObj,util.PARAMEDIC_DEFAULT_APP_NAME);
 };
 
-
 ParamedicRunner.prototype.getPackageFolder = function () {
     var packageFolder;
     switch (this.config.getPlatformId()) {
@@ -479,35 +556,49 @@ ParamedicRunner.prototype.getBinaryName = function () {
     return binaryName;
 };
 
+// Returns a name of the file at the SauceLabs storage
 ParamedicRunner.prototype.getAppName = function () {
-    var appName;
+    if (this.appName) {
+        return this.appName;
+    }
+    var appName = randomstring.generate();
     switch (this.config.getPlatformId()) {
         case 'android':
-            appName = 'mobilespec.apk';
+            appName += '.apk';
             break;
         case 'ios':
-            appName = 'HelloCordova.zip';
+            appName += '.zip';
             break;
         default:
             throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
     }
+    this.appName = appName;
     return appName;
 };
 
-ParamedicRunner.prototype.getSauceDetails = function () {
+ParamedicRunner.prototype.displaySauceDetails = function () {
+    if (!this.config.shouldUseSauce()) {
+        return Q();
+    }
+
     var self = this;
     var d = Q.defer();
 
-    logger.normal('Getting saucelabs job details...\n');
+    logger.normal('Getting saucelabs jobs details...\n');
 
     var sauce = new SauceLabs({
         username: self.config.getSauceUser(),
         password: self.config.getSauceKey()
     });
 
+    if (self.config.getBuildName() === self.config.getDefaultBuildName()) {
+        logger.warn('Build name is not specified, showing all sauce jobs with default name...');
+    }
+
     sauce.getJobs(function (err, jobs) {
+        var found = false;
         for (var job in jobs) {
-            if (jobs.hasOwnProperty(job) && jobs[job].name === self.config.getBuildName()) {
+            if (jobs.hasOwnProperty(job) && jobs[job].name && jobs[job].name.indexOf(self.config.getBuildName()) === 0) {
                 var jobUrl = 'https://saucelabs.com/beta/tests/' + jobs[job].id;
                 logger.normal('============================================================================================');
                 logger.normal('Job name: ' + jobs[job].name);
@@ -520,18 +611,66 @@ ParamedicRunner.prototype.getSauceDetails = function () {
                 }
                 logger.normal('============================================================================================');
                 logger.normal('');
-                d.resolve();
-                break;
+                found = true;
             }
         }
-        if (d.promise.inspect().state !== 'fulfilled') {
+
+        if (!found) {
             logger.warn('Can not find saucelabs job. Logs and video will be unavailable.');
-            d.resolve();
         }
+        d.resolve();
     });
     return d.promise;
 };
 
+ParamedicRunner.prototype.getSauceCaps = function () {
+    var caps = {
+        name: this.config.getBuildName(),
+        browserName: '',
+        appiumVersion: this.config.getSauceAppiumVersion(),
+        deviceOrientation: 'portrait',
+        deviceType: 'phone',
+        idleTimeout: '100', // in seconds
+        app: 'sauce-storage:' + this.getAppName(),
+        deviceName: this.config.getSauceDeviceName(),
+        platformVersion: this.config.getSaucePlatformVersion(),
+        maxDuration: util.SAUCE_MAX_DURATION
+    };
+
+    switch(this.config.getPlatformId()) {
+        case 'android':
+            caps.platformName = 'Android';
+            caps.appPackage = 'io.cordova.hellocordova';
+            caps.appActivity = 'io.cordova.hellocordova.MainActivity';
+            break;
+        case 'ios':
+            caps.platformName = 'iOS';
+            caps.autoAcceptAlerts = true;
+            caps.waitForAppScript = 'true;';
+            break;
+        default:
+            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+    }
+    return caps;
+};
+
+ParamedicRunner.prototype.connectWebdriver = function () {
+    var user = this.config.getSauceUser();
+    var key = this.config.getSauceKey();
+    var caps = this.getSauceCaps();
+
+    logger.normal('cordova-paramedic: connecting webdriver');
+
+    wd.configureHttp({
+        timeout: 3 * 60 * 1000,
+        retryDelay: 15000,
+        retries: 5
+    });
+
+    var driver = wd.promiseChainRemote(util.SAUCE_HOST, util.SAUCE_PORT, user, key);
+    return driver.init(caps);
+};
+
 ParamedicRunner.prototype.runSauceTests = function () {
     logger.info('cordova-paramedic: running sauce tests');
     var self = this;
@@ -539,52 +678,12 @@ ParamedicRunner.prototype.runSauceTests = function () {
     var pollForResults;
     var driver;
 
-    return self.packageApp()
+    return this.buildApp()
+    .then(self.packageApp.bind(self))
     .then(self.uploadApp.bind(self))
-    .then(function() {
-        logger.normal('cordova-paramedic: app uploaded; starting tests');
-
-        var user = self.config.getSauceUser();
-        var key = self.config.getSauceKey();
-
-        var caps = {
-            name: self.config.getBuildName(),
-            browserName: '',
-            appiumVersion: '1.5.2',
-            deviceOrientation: 'portrait',
-            deviceType: 'phone',
-            idleTimeout: '100', // in seconds
-            app: 'sauce-storage:' + self.getAppName()
-        };
-
-        switch(self.config.getPlatformId()) {
-            case 'android':
-                caps.deviceName = 'Android Emulator';
-                caps.platformVersion = '4.4';
-                caps.platformName = 'Android';
-                caps.appPackage = 'io.cordova.hellocordova';
-                caps.appActivity = 'io.cordova.hellocordova.MainActivity';
-                break;
-            case 'ios':
-                caps.deviceName = 'iPhone Simulator';
-                caps.platformVersion = '9.3';
-                caps.platformName = 'iOS';
-                caps.autoAcceptAlerts = true;
-                break;
-            default:
-                throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
-        }
-
-        logger.normal('cordova-paramedic: connecting webdriver');
-
-        wd.configureHttp({
-            timeout: 3 * 60 * 1000,
-            retryDelay: 15000,
-            retries: 5
-        });
-
-        driver = wd.promiseChainRemote(SAUCE_HOST, SAUCE_PORT, user, key);
-        return driver.init(caps);
+    .then(function () {
+        driver = self.connectWebdriver();
+        return driver;
     })
     .then(function () {
         if (self.config.getUseTunnel()) {
@@ -628,7 +727,6 @@ ParamedicRunner.prototype.runSauceTests = function () {
         }
         return driver.quit();
     })
-    .fin(self.getSauceDetails.bind(self))
     .then(function () {
         return isTestPassed;
     });
@@ -643,6 +741,5 @@ exports.run = function(paramedicConfig) {
     var runner = new ParamedicRunner(paramedicConfig, null);
     runner.storedCWD = storedCWD;
 
-    return runner.run()
-    .timeout(paramedicConfig.getTimeout(), 'This test seems to be blocked :: timeout exceeded. Exiting ...');
+    return runner.run();
 };

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/utils/utilities.js
----------------------------------------------------------------------
diff --git a/lib/utils/utilities.js b/lib/utils/utilities.js
index a2a8c82..87736ba 100644
--- a/lib/utils/utilities.js
+++ b/lib/utils/utilities.js
@@ -123,12 +123,24 @@ function doesFileExist(filePath) {
     return fileExists;
 }
 
+function mkdirSync(path) {
+  try {
+    fs.mkdirSync(path);
+  } catch(e) {
+    if ( e.code != 'EEXIST' ) throw e;
+  }
+}
+
 function getSqlite3InsertionCommand(destinationTCCFile, service, appName) {
     return util.format('sqlite3 %s "insert into access' +
                        '(service, client, client_type, allowed, prompt_count, csreq) values(\'%s\', \'%s\', ' +
                        '0,1,1,NULL)"', destinationTCCFile, service, appName);
 }
 
+function contains(collection, item) {
+    return collection.indexOf(item) !== (-1);
+}
+
 module.exports = {
     ANDROID:                    'android',
     IOS:                        'ios',
@@ -136,10 +148,17 @@ module.exports = {
     PARAMEDIC_DEFAULT_APP_NAME: 'io.cordova.hellocordova',
     SAUCE_USER_ENV_VAR:         'SAUCE_USER',
     SAUCE_KEY_ENV_VAR:          'SAUCE_ACCESS_KEY',
+    SAUCE_HOST:                 'ondemand.saucelabs.com',
+    SAUCE_PORT:                 80,
+    SAUCE_MAX_DURATION:         5400, // in seconds
+    DEFAULT_ENCODING:           'utf-8',
 
     DEFAULT_LOG_TIME: 15,
     DEFAULT_LOG_TIME_ADDITIONAL: 2,
 
+    TEST_PASSED: 1,
+    TEST_FAILED: 0,
+
     secToMin: secToMin,
     isWindows:  isWindows,
     countAndroidDevices: countAndroidDevices,
@@ -147,5 +166,7 @@ module.exports = {
     doesFileExist: doesFileExist,
     getSqlite3InsertionCommand: getSqlite3InsertionCommand,
     getSimulatorModelId: getSimulatorModelId,
-    getSimulatorId: getSimulatorId
+    getSimulatorId: getSimulatorId,
+    contains: contains,
+    mkdirSync: mkdirSync
 };

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/main.js
----------------------------------------------------------------------
diff --git a/main.js b/main.js
index 15b0b49..65c763d 100755
--- a/main.js
+++ b/main.js
@@ -1,5 +1,24 @@
 #!/usr/bin/env node
 
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 var parseArgs       = require('minimist');
 var path            = require('path');
 var paramedic       = require('./lib/paramedic');
@@ -27,11 +46,14 @@ var USAGE           = "Error missing args. \n" +
     "--outputDir: (optional) path to save Junit results file & Device logs\n" +
     "--cleanUpAfterRun: (optional) cleans up the application after the run\n" +
     "--logMins: (optional) Windows only - specifies number of minutes to get logs\n" +
-    "--tccDb: (optional) iOS only - specifies the path for the TCC.db file to be copied." +
+    "--tccDb: (optional) iOS only - specifies the path for the TCC.db file to be copied.\n" +
     "--shouldUseSauce: (optional) run tests on Saucelabs\n" +
     "--buildName: (optional) Build name to show in Saucelabs dashboard\n" +
     "--sauceUser: (optional) Saucelabs username\n" +
-    "--sauceKey: (optional) Saucelabs access key";
+    "--sauceKey: (optional) Saucelabs access key\n" +
+    "--sauceDeviceName: (optional) Name of the SauceLabs emulator. For example, \"iPhone Simulator\"\n" +
+    "--saucePlatformVersion: (optional) Platform version of the SauceLabs emulator. For example, \"9.3\"" +
+    "--sauceAppiumVersion: (optional) Appium version to use when running on Saucelabs. For example, \"1.5.3\"";
 
 var argv = parseArgs(process.argv.slice(2));
 var pathToParamedicConfig = argv.config && path.resolve(argv.config);
@@ -86,6 +108,18 @@ if (pathToParamedicConfig || // --config
         paramedicConfig.setSauceKey(argv.sauceKey);
     }
 
+    if (argv.sauceDeviceName) {
+        paramedicConfig.setSauceDeviceName(argv.sauceDeviceName);
+    }
+
+    if (argv.saucePlatformVersion) {
+        paramedicConfig.setSaucePlatformVersion(argv.saucePlatformVersion);
+    }
+
+    if (argv.sauceAppiumVersion) {
+        paramedicConfig.setSauceAppiumVersion(argv.sauceAppiumVersion);
+    }
+
     if (argv.useTunnel) {
         if (argv.useTunnel === 'false') {
             argv.useTunnel = false;
@@ -103,7 +137,10 @@ if (pathToParamedicConfig || // --config
         process.exit(1);
     })
     .done(function(isTestPassed) {
-        process.exit(isTestPassed ? 0 : 1);
+        var exitCode = isTestPassed ? 0 : 1;
+
+        console.log('Finished with exit code ' + exitCode);
+        process.exit(exitCode);
     });
 
 } else {

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 41ce5b3..508e80d 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,9 @@
   "bin": {
     "cordova-paramedic": "./main.js"
   },
+  "engines" : {
+      "node" : ">=0.11.2"
+  },
   "repository": {
     "type": "git",
     "url": "git://github.com/apache/cordova-paramedic.git"
@@ -29,17 +32,22 @@
   "author": "Jesse MacFadyen",
   "dependencies": {
     "cordova-common": "^1.1.0",
+    "expect-telnet": "^0.5.2",
+    "jasmine": "^2.4.1",
     "jasmine-reporters": "^2.1.1",
     "jasmine-spec-reporter": "^2.4.0",
     "localtunnel": "~1.5.0",
     "minimist": "~1.1.0",
     "path-extra": "^3.0.0",
     "q": "^1.4.1",
+    "randomstring": "^1.1.5",
     "saucelabs": "^1.2.0",
     "shelljs": "~0.3.0",
     "socket.io": "^1.4.5",
     "tcp-port-used": "^0.1.2",
     "tmp": "0.0.25",
+    "tree-kill": "^1.1.0",
+    "unorm": "^1.4.1",
     "wd": "^0.4.0"
   },
   "devDependencies": {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org