You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by sg...@apache.org on 2016/03/25 16:39:30 UTC

[3/3] cordova-paramedic git commit: Adding real time logging and other improvements

Adding real time logging and other improvements

This closes #1


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

Branch: refs/heads/master
Commit: b1aa699dfa56d442e75294448e3253ac6c7f0995
Parents: 8615f31
Author: Vladimir Kotikov <ko...@gmail.com>
Authored: Fri Mar 25 13:54:21 2016 +0300
Committer: sgrebnov <se...@gmail.com>
Committed: Fri Mar 25 17:04:26 2016 +0300

----------------------------------------------------------------------
 .jshintrc                                       |  14 +
 .travis.yml                                     |   1 +
 README.md                                       | 164 +++++++++-
 lib/LocalServer.js                              |  99 ++++++
 lib/ParamedicConfig.js                          |  83 +++++
 lib/PluginsManager.js                           |  51 ++++
 lib/Target.js                                   |   9 +
 lib/TestsRunner.js                              | 121 ++++++++
 lib/Tunnel.js                                   |  19 ++
 lib/logger.js                                   | 150 +++++++++
 lib/paramedic.js                                | 156 ++++++++++
 lib/portScanner.js                              |  54 ++++
 lib/reporters/ConsoleReporter.js                | 153 ++++++++++
 lib/reporters/ParamedicReporter.js              |  43 +++
 lib/specReporters.js                            |  68 +++++
 lib/utils.js                                    |  22 ++
 main.js                                         |  69 +++--
 package.json                                    |  24 +-
 paramedic-plugin/JasmineParamedicProxy.js       |  56 ++++
 paramedic-plugin/paramedic.js                   |  74 +++++
 paramedic-plugin/plugin.xml                     |  37 +++
 paramedic-plugin/socket.io.js                   |   4 +
 paramedic.js                                    | 305 -------------------
 sample-config/.paramedic-core-plugins.config.js |  51 ++++
 sample-config/.paramedic.config.js              |  33 ++
 25 files changed, 1502 insertions(+), 358 deletions(-)
----------------------------------------------------------------------


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

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/.travis.yml
----------------------------------------------------------------------
diff --git a/.travis.yml b/.travis.yml
index 3c02467..aa2e054 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,7 @@ install:
   - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 
   - npm install cordova
   - npm install ios-sim
+  - npm install ios-deploy
   - npm install
   - npm link
 script:

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 305feeb..4dcc055 100644
--- a/README.md
+++ b/README.md
@@ -7,31 +7,167 @@ Runs cordova medic/buildbot tests locally.
 
 ... provides advanced levels of care at the point of illness or injury, including out of hospital treatment, and diagnostic services
 
-To install :
+# To install :
 ``` $npm install cordova-paramedic ```
 
-Usage :
+## Supported Cordova Platforms
+
+- Android
+- iOS
+- Windows Phone 8
+- Windows (Windows 8.1, Windows Phone 8.1, Windows 10 Tablet/PC)
+- Browser
+
+# Usage
+
+Paramedic parameters could be passed via command line arguments or via separate configuration file:
+
+```
+cordova-paramedic --platform PLATFORM --plugin PATH <other parameters>
+cordova-paramedic --config ./sample-config/.paramedic.config.js
+```
+
+## Command Line Interface
+
+####`--platform` (required)
+
+Specifies target cordova platform (could refer to local directory, npm or git)
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser
+cordova-paramedic --platform ios@4.0 --plugin cordova-plugin-inappbrowser
+cordova-paramedic --platform ios@../cordova-ios --plugin cordova-plugin-inappbrowser
+cordova-paramedic --platform ios@https://github.com/apache/cordova-ios.git#4.1.0 --plugin cordova-plugin-inappbrowser
+```
+
+####`--plugin` (required)
+
+Specifies test plugin, you may specify multiple --plugin flags and they will all be installed and tested together. Similat to `platform` parameter you can refer to local (or absolute) path, npm registry or git repo.
 
 ```
-cordova-paramedic --platform PLATFORM --plugin PATH [--justbuild --timeout MSECS --port PORTNUM --browserify --verbose ]`PLATFORM` : the platform id, currently only supports 'ios'
-`PATH` : the relative or absolute path to a plugin folder
-	expected to have a 'tests' folder.
-	You may specify multiple --plugin flags and they will all
-	be installed and tested together.
-`MSECS` : (optional) time in millisecs to wait for tests to pass|fail 
-	(defaults to 10 minutes) 
-`PORTNUM` : (optional) port to use for posting results from emulator back to paramedic server
---justbuild : (optional) just builds the project, without running the tests 
---browserify : (optional) plugins are browserified into cordova.js 
---verbose : (optional) verbose mode. Display more information output
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser
+cordova-paramedic --platform ios --plugin ../cordova-plugin-inappbrowser
+cordova-paramedic --platform ios --plugin https://github.com/apache/cordova-plugin-inappbrowser
+// several plugins
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --plugin cordova-plugin-contacts
+```
+####--justbuild (optional)
+
+Just builds the project, without running the tests.
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --justbuild
+```
+
+####--device (optional)
+
+Tests must be run on connected device instead of emulator.
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --device
+```
+
+####--externalServerUrl (optional)
+
+Useful when testing on real device (`--device` parameter) so that tests results from device could be posted back to paramedic server.
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --externalServerUrl http://10.0.8.254
+```
+
+####--useTunnel (optional)
+
+Use [tunneling](https://www.npmjs.com/package/localtunnel) instead of local address (default is false).
+Useful when testing on real devices and don't want to specify external ip address (see `--externalServerUrl` above) of paramedic server.
 
 ```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --useTunnel
+```
+
+####--browserify (optional)
+
+Plugins are browserified into cordova.js.
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --browserify
+```
+
+####--port (optional)
+
+Port to use for posting results from emulator back to paramedic server (default is from `8008`). You can also specify a range using `--startport` and `endport` and paramedic will select the first available.
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --port 8010
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --startport 8000 endport 8020
+```
+
+####--verbose (optional)
+
+Verbose mode. Display more information output
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --verbose
+```
+
+####--timeout (optional)
+
+Time in millisecs to wait for tests to pass|fail (defaults to 10 minutes). 
+
+```
+cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --timeout 30000
+```
+
+## Paramedic configuration file
+
+Configuration file is used when no parameters are passed to `cordova-paramedic` call or explicitly specified via `--config` parameter:
+```
+cordova-paramedic  <- paramedic will attempt to find .paramedic.config.js in working directory
+cordova-paramedic --config ./sample-config/.paramedic.config.js
+```
+Example configuration file is showed below. It supports similar arguments and has the following advantages over `Command Line Approach`:
+
+-   Supports extra arguments which could be passed to cordova so that you have full control over build and run target.
+-   Supports several test platforms (targets) to be executed as single paramedic run (results will be aggregated) so you don't need to re-install test plugins, create local server and do other steps several times.
+
+```
+module.exports = {
+    // "externalServerUrl": "http://10.0.8.254",
+    "useTunnel": true,
+    "plugins": [
+        "https://github.com/apache/cordova-plugin-inappbrowser"
+    ],
+    "targets": [
+        {
+            "platform": "ios@https://github.com/apache/cordova-ios.git",
+            "action": "run",
+             "args": "--device"
+        },
+        {
+            "platform": "android@https://github.com/apache/cordova-android.git",
+            "action": "run",
+             "args": "--device"
+        },
+        {    // Windows 8.1 Desktop(anycpu)
+            "platform": "windows@https://github.com/apache/cordova-windows.git",
+            "action": "run"
+        },
+        {   // Windows 10 Desktop(x64)
+            "platform": "windows@https://github.com/apache/cordova-windows.git",
+            "action": "run",
+            "args": "--archs=x64 -- --appx=uap"
+        }
+    ]
+}
+```
+More configuration file examples could be found in `sample-config` folder.
+
+## API Interface
 
 You can also use cordova-paramedic as a module directly :
 
 ```
   var paramedic = require('cordova-paramedic');
-  paramedic.run('ios', '../cordova-plugin-device', onCompleteCallback,justBuild,portNum,msTimeout, useBrowserify, beSilent, beVerbose);
+  paramedic.run(config);
 ```
 
 

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/LocalServer.js
----------------------------------------------------------------------
diff --git a/lib/LocalServer.js b/lib/LocalServer.js
new file mode 100644
index 0000000..a7bf42c
--- /dev/null
+++ b/lib/LocalServer.js
@@ -0,0 +1,99 @@
+var Q = require('q');
+var io = require('socket.io');
+
+var logger = require('./logger').get();
+
+var specReporters = require('./specReporters');
+
+
+function LocalServer(port, externalServerUrl, tunneledUrl) {
+    this.port = port;
+    this.tunneledUrl = tunneledUrl;
+    this.externalServerUrl = externalServerUrl;
+    this.onTestsResults = null;
+}
+
+LocalServer.startServer = function(port, externalServerUrl, tunneledUrl) {
+    var localServer = new LocalServer(port, externalServerUrl, tunneledUrl);
+    localServer.createSocketListener();
+    return Q.resolve(localServer);
+};
+
+LocalServer.prototype.createSocketListener = function() {
+    var listener = io.listen(this.port, {
+        pingTimeout: 60000, // how many ms without a pong packet to consider the connection closed
+        pingInterval: 25000 // how many ms before sending a new ping packet
+    });
+
+    var me  = this;
+
+    var routes = {
+        'log': me.onDeviceLog.bind(me),
+        'disconnect': me.onTestsCompletedOrDisconnected.bind(me),
+        'deviceInfo': me.onDeviceInfo.bind(me),
+        'jasmineStarted': specReporters.jasmineStarted.bind(specReporters),
+        'specStarted': specReporters.specStarted.bind(specReporters),
+        'specDone': specReporters.specDone.bind(specReporters),
+        'suiteStarted': specReporters.suiteStarted.bind(specReporters),
+        'suiteDone': specReporters.suiteDone.bind(specReporters),
+        'jasmineDone': me.onJasmineDone.bind(me)
+    };
+
+    listener.on('connection', function(socket) {
+        logger.info('local-server: new socket connection');
+        me.connection = socket;
+
+        for (var routeType in routes) {
+            socket.on(routeType, routes[routeType]);
+        }
+    });
+};
+
+LocalServer.prototype.haveConnectionUrl = function() {
+    return !!(this.tunneledUrl || this.externalServerUrl);
+};
+
+LocalServer.prototype.getConnectionUrl = function() {
+    return this.tunneledUrl || this.externalServerUrl + ":" + this.port;
+};
+
+LocalServer.prototype.reset = function() {
+    this.onTestsResults = null;
+    if (this.connection) {
+        this.connection.disconnect();
+        this.connection = null;
+    }
+
+    specReporters.reset();
+};
+
+LocalServer.prototype.onDeviceLog = function(data) {
+    logger.verbose('device|console.'+data.type + ': '  + data.msg[0]);
+};
+
+LocalServer.prototype.onDeviceInfo = function(data) {
+    logger.info('cordova-paramedic: Device info: ' + JSON.stringify(data));
+};
+
+LocalServer.prototype.onTestsCompleted = function(msg) {
+    logger.normal('local-server: tests completed');
+    this.lastMobileSpecResults = specReporters.getResults();
+};
+
+LocalServer.prototype.onJasmineDone = function(data) {
+    specReporters.jasmineDone(data);
+    // save results to report them later
+    this.onTestsCompleted();
+    // disconnect because all tests have been completed
+    this.connection.disconnect();
+};
+
+LocalServer.prototype.onTestsCompletedOrDisconnected = function() {
+    logger.info('local-server: tests have been completed or test device has disconnected');
+    if (this.onTestsResults) {
+        this.onTestsResults(this.lastMobileSpecResults);
+    }
+    this.lastMobileSpecResults = null;
+};
+
+module.exports = LocalServer;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/ParamedicConfig.js
----------------------------------------------------------------------
diff --git a/lib/ParamedicConfig.js b/lib/ParamedicConfig.js
new file mode 100644
index 0000000..d9acdec
--- /dev/null
+++ b/lib/ParamedicConfig.js
@@ -0,0 +1,83 @@
+var Target = require('./Target');
+
+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
+
+function ParamedicConfig(json) {
+    this._config = json;
+}
+
+ParamedicConfig.parseFromArguments = function (argv) {
+    return new ParamedicConfig({
+        "targets": [{
+            platform: argv.platform,
+            action: !!argv.justbuild ? 'build' : 'run',
+            args: (!!argv.browserify ? '--browserify ' : '') + (!!argv.device ? '--device' : '')
+        }],
+        "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,
+        "reportSavePath":  !!argv.reportSavePath? argv.reportSavePath: undefined,
+        "cleanUpAfterRun": !!argv.cleanUpAfterRun? true: false
+    });
+};
+
+ParamedicConfig.parseFromFile = function (paramedicConfigPath) {
+    return new ParamedicConfig(require(paramedicConfigPath));
+};
+
+ParamedicConfig.prototype.useTunnel = function () {
+    return this._config.useTunnel;
+
+};
+
+ParamedicConfig.prototype.getReportSavePath = function () {
+    return this._config.reportSavePath;
+};
+
+ParamedicConfig.prototype.shouldCleanUpAfterRun = function () {
+    return this._config.cleanUpAfterRun;
+};
+
+ParamedicConfig.prototype.getTargets = function () {
+    return this._config.targets.map(function(target) {
+        return new Target(target);
+    });
+};
+
+ParamedicConfig.prototype.getPlugins = function () {
+    return this._config.plugins;
+};
+
+ParamedicConfig.prototype.getExternalServerUrl= function () {
+    return this._config.externalServerUrl;
+};
+
+ParamedicConfig.prototype.isVerbose = function() {
+    return this._config.verbose;
+};
+
+ParamedicConfig.prototype.isJustBuild = function() {
+    return this._config.justbuild;
+};
+
+ParamedicConfig.prototype.isBrowserify = function() {
+    return this._config.browserify;
+};
+
+ParamedicConfig.prototype.getPorts = function() {
+    return {
+        start: this._config.startPort || DEFAULT_START_PORT,
+        end: this._config.endPort || DEFAULT_END_PORT
+    };
+};
+
+ParamedicConfig.prototype.getTimeout = function() {
+    return DEFAULT_TIMEOUT;
+};
+
+module.exports = ParamedicConfig;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/PluginsManager.js
----------------------------------------------------------------------
diff --git a/lib/PluginsManager.js b/lib/PluginsManager.js
new file mode 100644
index 0000000..3d4c441
--- /dev/null
+++ b/lib/PluginsManager.js
@@ -0,0 +1,51 @@
+var path = require('path');
+var fs = require('fs');
+var logger = require('./logger').get();
+var exec = require('./utils').exec;
+var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
+
+function PluginsManager(appRoot, storedCWD) {
+    this.appRoot = appRoot;
+    this.storedCWD = storedCWD;
+}
+
+PluginsManager.prototype.installPlugins = function(plugins) {
+    for(var n = 0; n < plugins.length; n++) {
+        var plugin = plugins[n];
+        this.installSinglePlugin(plugin);
+    }
+};
+
+PluginsManager.prototype.installTestsForExistingPlugins = function() {
+    var installedPlugins = new PluginInfoProvider().getAllWithinSearchPath(path.join(this.appRoot, 'plugins'));
+    var me = this;
+    installedPlugins.forEach(function(plugin) {
+        // there is test plugin available
+        if (fs.existsSync(path.join(plugin.dir, 'tests', 'plugin.xml'))) {
+            me.installSinglePlugin(path.join(plugin.dir, 'tests'));
+        }
+    });
+    this.showPluginsVersions();
+};
+
+PluginsManager.prototype.installSinglePlugin = function(plugin) {
+    if (fs.existsSync(path.resolve(this.storedCWD, plugin))) {
+        plugin = path.resolve(this.storedCWD, plugin);
+    }
+
+    logger.normal("cordova-paramedic: installing " + plugin);
+    // var pluginPath = path.resolve(this.storedCWD, plugin);
+
+    var plugAddCmd = exec('cordova plugin add ' + plugin);
+    if(plugAddCmd.code !== 0) {
+        logger.error('Failed to install plugin : ' + plugin);
+        throw new Error('Failed to install plugin : ' + plugin);
+    }
+};
+
+PluginsManager.prototype.showPluginsVersions = function() {
+    logger.verbose("cordova-paramedic: versions of installed plugins: ");
+    exec('cordova plugins');
+};
+
+module.exports = PluginsManager;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/Target.js
----------------------------------------------------------------------
diff --git a/lib/Target.js b/lib/Target.js
new file mode 100644
index 0000000..0cb5c53
--- /dev/null
+++ b/lib/Target.js
@@ -0,0 +1,9 @@
+
+function Target(config) {
+    this.platform = config.platform;
+    this.action =  config.action || 'run';
+    this.args = config.args || null;
+    this.platformId = this.platform.split("@")[0];
+}
+
+module.exports = Target;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/TestsRunner.js
----------------------------------------------------------------------
diff --git a/lib/TestsRunner.js b/lib/TestsRunner.js
new file mode 100644
index 0000000..8515b58
--- /dev/null
+++ b/lib/TestsRunner.js
@@ -0,0 +1,121 @@
+var Q = require('q');
+var fs = require('fs');
+var path = require('path');
+var exec = require('./utils').exec;
+var logger = require('./logger').get();
+
+var MAX_PENDING_TIME = 60000;
+
+function TestsRunner (server) {
+    this.server = server;
+}
+
+TestsRunner.prototype.runSingleTarget = function (target, savePath) {
+    logger.info("Running target: " + target.platform);
+
+    var me = this;
+
+    this.server.reset(savePath);
+
+    return this.prepareAppToRunTarget(target).then(function() {
+        return me.installPlatform(target);
+    }).then(function() {
+        return me.checkPlatform(target);
+    }).then(function() {
+        return me.runTests(target);
+    })
+    .then(function (results) {
+        logger.normal("Removing platform: " + target.platformId);
+        exec('cordova platform rm ' + target.platformId);
+
+        return results;
+    })
+    .catch(function(error) {return error;});
+};
+
+TestsRunner.prototype.prepareAppToRunTarget = function(target) {
+    var me = this;
+    return Q.Promise(function(resolve, reject) {
+        // if we know external url we can safely use it
+        if (me.server.haveConnectionUrl()) {
+            me.writeMedicLogUrl(me.server.getConnectionUrl());
+        } else {
+            // otherwise, we assume we use local PC and platforms emulators
+            switch(target.platformId) {
+                case "android":
+                    me.writeMedicLogUrl("http://10.0.2.2:" + me.server.port);
+                    break;
+                case "ios"     :
+                case "browser" :
+                case "windows" :
+                /* falls through */
+                default:
+                    me.writeMedicLogUrl("http://127.0.0.1:" + me.server.port);
+                    break;
+            }
+        }
+        resolve();
+    });
+};
+
+TestsRunner.prototype.installPlatform = function(target) {
+    return Q.Promise(function(resolve, reject) {
+        logger.normal("cordova-paramedic: adding platform : " + target.platform);
+        exec('cordova platform add ' + target.platform);
+        resolve();
+    });
+};
+
+TestsRunner.prototype.checkPlatform = function(target) {
+    return Q.Promise(function(resolve, reject) {
+        logger.normal("cordova-paramedic: checking requirements for platform " + target.platformId);
+        var result = exec('cordova requirements ' + target.platformId);
+        if (result.code !== 0) {
+            reject(new Error('Platform requirements check has failed! Skipping...'));
+        } else resolve();
+    });
+};
+
+TestsRunner.prototype.writeMedicLogUrl = function(url) {
+    logger.normal("cordova-paramedic: writing medic log url to project " + url);
+    var obj = {logurl:url};
+    fs.writeFileSync(path.join("www","medic.json"), JSON.stringify(obj));
+};
+
+TestsRunner.prototype.runTests = function(target) {
+    logger.normal('Starting tests');
+    var self = this;
+
+    var cmd = "cordova " + target.action + " " + target.platformId;
+    if (target.args) {
+        cmd += " " + target.args;
+    }
+
+    logger.normal('cordova-paramedic: running command ' + cmd);
+
+    return Q.Promise(function (resolve, reject) {
+        logger.normal('Waiting for tests result');
+
+        self.server.onTestsResults = function (results) {
+            resolve(results);
+        };
+
+        exec(cmd, function(code, output){
+                if(code) {
+                    reject(new Error("cordova build returned error code " + code));
+                }
+
+                var waitForTestResults = target.action === 'run' || target.action === 'emulate';
+
+                if (!waitForTestResults) resolve({passed: true}); // skip tests if it was justbuild
+
+                setTimeout(function(){
+                    if (!self.server.connection)
+                        reject(new Error("Seems like device not connected to local server in " + MAX_PENDING_TIME / 1000 + " secs. Skipping this platform..."));
+                }, MAX_PENDING_TIME);
+            }
+        );
+    });
+};
+
+module.exports = TestsRunner;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/Tunnel.js
----------------------------------------------------------------------
diff --git a/lib/Tunnel.js b/lib/Tunnel.js
new file mode 100644
index 0000000..bed31e5
--- /dev/null
+++ b/lib/Tunnel.js
@@ -0,0 +1,19 @@
+ var exec = require('./utils').exec;
+ var path = require('path');
+ var Q = require('Q');
+
+function Tunnel(port) {
+    this.port = port;
+}
+
+Tunnel.prototype.createTunnel = function() {
+    var self = this;
+    //TODO: use localtunnel module instead of shell
+    return Q.Promise(function(resolve, reject) {
+        exec(path.resolve(__dirname, '../node_modules/.bin/lt') + ' --port ' + self.port, null, function(output) {
+            resolve(output.split(' ')[3]);
+        });
+    });
+};
+
+module.exports = Tunnel;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/logger.js
----------------------------------------------------------------------
diff --git a/lib/logger.js b/lib/logger.js
new file mode 100644
index 0000000..6a525c9
--- /dev/null
+++ b/lib/logger.js
@@ -0,0 +1,150 @@
+/*
+ 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 ansi = require('ansi');
+var EventEmitter = require('events').EventEmitter;
+var CordovaError = require('cordova-common').CordovaError;
+var EOL = require('os').EOL;
+
+var INSTANCE;
+
+function CordovaLogger () {
+    this.levels = {};
+    this.colors = {};
+    this.stdout = process.stdout;
+    this.stderr = process.stderr;
+
+    this.stdoutCursor = ansi(this.stdout);
+    this.stderrCursor = ansi(this.stderr);
+
+    this.addLevel('verbose', 1000, 'grey');
+    this.addLevel('normal' , 2000);
+    this.addLevel('warn'   , 2000, 'yellow');
+    this.addLevel('info'   , 3000, 'blue');
+    this.addLevel('error'  , 5000, 'red');
+    this.addLevel('results' , 10000);
+
+    this.setLevel('normal');
+}
+
+CordovaLogger.get = function () {
+    return INSTANCE || (INSTANCE = new CordovaLogger());
+};
+
+CordovaLogger.VERBOSE = 'verbose';
+CordovaLogger.NORMAL = 'normal';
+CordovaLogger.WARN = 'warn';
+CordovaLogger.INFO = 'info';
+CordovaLogger.ERROR = 'error';
+CordovaLogger.RESULTS = 'results';
+
+CordovaLogger.prototype.log = function (logLevel, message) {
+    // if there is no such logLevel defined, or provided level has
+    // less severity than active level, then just ignore this call and return
+    if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel])
+        // return instance to allow to chain calls
+        return this;
+    var isVerbose = this.logLevel === 'verbose';
+    var cursor = this.stdoutCursor;
+
+    if(message instanceof Error || logLevel === CordovaLogger.ERROR) {
+        message = formatError(message, isVerbose);
+        cursor = this.stderrCursor;
+    }
+
+    var color = this.colors[logLevel];
+    if (color) {
+        cursor.bold().fg[color]();
+    }
+
+    cursor.write(message).reset().write(EOL);
+
+    return this;
+};
+
+CordovaLogger.prototype.addLevel = function (level, severity, color) {
+
+    this.levels[level] = severity;
+
+    if (color) {
+        this.colors[level] = color;
+    }
+
+    // Define own method with corresponding name
+    if (!this[level]) {
+        this[level] = this.log.bind(this, level);
+    }
+
+    return this;
+};
+
+CordovaLogger.prototype.setLevel = function (logLevel) {
+    this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;
+
+    return this;
+};
+
+CordovaLogger.prototype.subscribe = function (eventEmitter) {
+
+    if (!(eventEmitter instanceof EventEmitter))
+        throw new Error('Must provide a valid EventEmitter instance to subscribe CordovaLogger to');
+
+    var self = this;
+
+    process.on('uncaughtException', function(err) {
+        self.error(err);
+        process.exit(1);
+    });
+
+    eventEmitter.on('verbose', self.verbose)
+        .on('log', self.normal)
+        .on('info', self.info)
+        .on('warn', self.warn)
+        .on('warning', self.warn)
+        // Set up event handlers for logging and results emitted as events.
+        .on('results', self.results);
+
+    return this;
+};
+
+function formatError(error, isVerbose) {
+    var message = '';
+
+    if(error instanceof CordovaError) {
+        message = error.toString(isVerbose);
+    } else if(error instanceof Error) {
+        if(isVerbose) {
+            message = error.stack;
+        } else {
+            message = error.message;
+        }
+    } else {
+        // Plain text error message
+        message = error;
+    }
+
+    if(message.toUpperCase().indexOf('ERROR:') !== 0) {
+        // Needed for backward compatibility with external tools
+        message = 'Error: ' + message;
+    }
+
+    return message;
+}
+
+module.exports = CordovaLogger;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/paramedic.js
----------------------------------------------------------------------
diff --git a/lib/paramedic.js b/lib/paramedic.js
new file mode 100644
index 0000000..3828cf2
--- /dev/null
+++ b/lib/paramedic.js
@@ -0,0 +1,156 @@
+#!/usr/bin/env node
+
+var exec = require('./utils').exec,
+    shell = require('shelljs'),
+    Server = require('./LocalServer'),
+    Q = require('q'),
+    tmp = require('tmp'),
+    PluginsManager = require('./PluginsManager'),
+    specReporters = require('./specReporters'),
+    TestsRunner = require('./TestsRunner'),
+    portScanner = require('./portScanner'),
+    path = require('path'),
+    Tunnel = require('./Tunnel');
+
+var Q = require('q');
+var logger = require('./logger').get();
+
+var TESTS_PASS = 0;
+var TESTS_FAILURE = 1;
+var NONTESTS_FAILURE = 2;
+
+function ParamedicRunner(config, _callback) {
+    this.tunneledUrl = "";
+    this.tempFolder = null;
+    this.pluginsManager = null;
+    this.testsPassed = true;
+
+    this.config = config;
+
+    exec.setVerboseLevel(config.isVerbose());
+    logger.setLevel(config.isVerbose() ? 'verbose' : 'normal');
+}
+
+ParamedicRunner.prototype = {
+    run: function() {
+        var cordovaVersion = exec('cordova --version');
+        var npmVersion = exec('npm -v');
+
+        if (cordovaVersion.code || npmVersion.code) {
+            logger.error(cordovaVersion.output + npmVersion.output);
+            process.exit(1);
+        }
+
+        logger.normal("cordova-paramedic: using cordova version " + cordovaVersion.output.replace('\n', ''));
+        logger.normal("cordova-paramedic: using npm version " + npmVersion.output.replace('\n', ''));
+
+        var self = this;
+
+        this.createTempProject();
+        this.installPlugins();
+
+         // Set up start page for tests
+        logger.normal("cordova-paramedic: setting app start page to test page");
+        shell.sed('-i', 'src="index.html"', 'src="cdvtests/index.html"', 'config.xml');
+
+        var startPort = this.config.getPorts().start,
+            endPort   = this.config.getPorts().end;
+        logger.info("cordova-paramedic: scanning ports from " + startPort + " to " + endPort);
+
+        // Initialize test reporters
+        specReporters.initialize(this.config);
+
+        return portScanner.getFirstAvailablePort(startPort, endPort).then(function(port) {
+            self.port = port;
+
+            logger.info("cordova-paramedic: port " + port + " is available");
+
+            if (self.config.useTunnel()) {
+                self.tunnel = new Tunnel(port);
+                logger.info('cordova-paramedic: attempt to create local tunnel');
+                return self.tunnel.createTunnel();
+            }
+        }).then(function(url) {
+            if (url) {
+                logger.info('cordova-paramedic: using tunneled url ' + url);
+                self.tunneledUrl = url;
+            }
+
+            logger.info("cordova-paramedic: starting local medic server");
+            return Server.startServer(self.port, self.config.getExternalServerUrl(), self.tunneledUrl);
+        }).then(function (server) {
+
+            var testsRunner = new TestsRunner(server);
+            return self.config.getTargets().reduce(function (promise, target) {
+                return promise.then( function() {
+                    return testsRunner.runSingleTarget(target).then(function(results) {
+                        if (results instanceof Error) {
+                            self.testsPassed = false;
+                            return logger.error(results.message);
+                        }
+
+                        logger.info("cordova-paramedic: tests done for platform " + target.platform);
+
+                        var targetTestsPassed = results && results.passed;
+                        self.testsPassed = self.testsPassed && targetTestsPassed;
+
+                        if (!results) {
+                            logger.error("Result: tests has not been completed in time, crashed or there is connectivity issue.");
+                        } else if (targetTestsPassed)  {
+                            logger.info("Result: passed");
+                        } else {
+                            logger.error("Result: passed=" + results.passed + ", failures=" + results.mobilespec.failures);
+                        }
+                    });
+                });
+            }, Q());
+        }).then(function(res) {
+            if (self.testsPassed) {
+                logger.info("All tests have been passed.");
+                return TESTS_PASS;
+            } else {
+                logger.error("There are tests failures.");
+                return TESTS_FAILURE;
+            }
+        }, function(err) {
+            logger.error("Failed: " + err);
+            return NONTESTS_FAILURE;
+        }).then(function(exitCode){
+            if(self.config.shouldCleanUpAfterRun()) {
+                logger.info("cordova-paramedic: Deleting the application: " + self.tempFolder.name);
+                shell.popd();
+                shell.rm('-rf', self.tempFolder.name);
+            }
+            process.exit(exitCode);
+        });
+    },
+    createTempProject: function() {
+        this.tempFolder = tmp.dirSync();
+        tmp.setGracefulCleanup();
+        logger.info("cordova-paramedic: creating temp project at " + this.tempFolder.name);
+        exec('cordova create ' + this.tempFolder.name);
+        shell.pushd(this.tempFolder.name);
+    },
+    installPlugins: function() {
+        logger.info("cordova-paramedic: installing plugins");
+        this.pluginsManager = new PluginsManager(this.tempFolder.name, this.storedCWD);
+        this.pluginsManager.installPlugins(this.config.getPlugins());
+        this.pluginsManager.installTestsForExistingPlugins();
+        this.pluginsManager.installSinglePlugin('cordova-plugin-test-framework');
+        this.pluginsManager.installSinglePlugin('cordova-plugin-device');
+        this.pluginsManager.installSinglePlugin(path.join(__dirname, '../paramedic-plugin'));
+    }
+};
+
+var storedCWD =  null;
+
+exports.run = function(paramedicConfig) {
+
+    storedCWD = storedCWD || process.cwd();
+
+    var runner = new ParamedicRunner(paramedicConfig, null);
+    runner.storedCWD = storedCWD;
+
+    return runner.run()
+    .timeout(paramedicConfig.getTimeout(), "This test seems to be blocked :: timeout exceeded. Exiting ...");
+};

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/portScanner.js
----------------------------------------------------------------------
diff --git a/lib/portScanner.js b/lib/portScanner.js
new file mode 100644
index 0000000..df0cf15
--- /dev/null
+++ b/lib/portScanner.js
@@ -0,0 +1,54 @@
+var net = require('net');
+var Q = require('q');
+var PORT_NOT_AVAILABLE = 'EADDRINUSE';
+
+var isPortAvailable = function (port) {
+    return new Q.Promise(function(resolve, reject) {
+        var testServer = net.createServer()
+            .once('error', function(err) {
+                if (err.code === PORT_NOT_AVAILABLE) {
+                    reject(new Error('Port is not available'));
+                } else {
+                    reject(err);
+                }
+            })
+            .once('listening', function() {
+                testServer.once('close', function() {
+                    resolve(port);
+                }).close();
+            })
+            .listen(port);
+    });
+};
+
+var checkPorts = function(startPort, endPort, onFoundPort, onError) {
+    var currentPort = startPort;
+
+    isPortAvailable(currentPort)
+        .then(function(port) {
+            onFoundPort(port);
+        }, function(error) {
+            if (error.message === 'Port is not available') {
+                    currentPort++;
+                    if (currentPort > endPort) {
+                        onError(new Error('All ports are unavailable!'));
+                    } else {
+                        checkPorts(currentPort, endPort, onFoundPort, onError);
+                    }
+                } else onError(error);
+        });
+    };
+
+var getFirstAvailablePort = function (startPort, endPort) {
+    if (startPort > endPort) {
+        var buffer = startPort;
+        startPort = endPort;
+        endPort = buffer;
+    }
+
+    return new Q.Promise(function(resolve, reject) {
+        checkPorts(startPort, endPort, resolve, reject);
+    });
+};
+
+module.exports.getFirstAvailablePort = getFirstAvailablePort;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/reporters/ConsoleReporter.js
----------------------------------------------------------------------
diff --git a/lib/reporters/ConsoleReporter.js b/lib/reporters/ConsoleReporter.js
new file mode 100644
index 0000000..84f447e
--- /dev/null
+++ b/lib/reporters/ConsoleReporter.js
@@ -0,0 +1,153 @@
+var noopTimer = {
+    start: function(){},
+    elapsed: function(){ return 0; }
+  };
+
+  function ConsoleReporter(options) {
+    var print = options.print,
+      showColors = options.showColors || false,
+      onComplete = options.onComplete || function() {},
+      timer = options.timer || noopTimer,
+      specCount,
+      failureCount,
+      failedSpecs = [],
+      pendingCount,
+      ansi = {
+        green: '\x1B[32m',
+        red: '\x1B[31m',
+        yellow: '\x1B[33m',
+        none: '\x1B[0m'
+      },
+      failedSuites = [];
+
+    this.jasmineStarted = function() {
+      specCount = 0;
+      failureCount = 0;
+      pendingCount = 0;
+      print('Started');
+      printNewline();
+      timer.start();
+    };
+
+    this.jasmineDone = function() {
+      printNewline();
+      for (var i = 0; i < failedSpecs.length; i++) {
+        specFailureDetails(failedSpecs[i]);
+      }
+
+      if(specCount > 0) {
+        printNewline();
+
+        var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' +
+          failureCount + ' ' + plural('failure', failureCount);
+
+        if (pendingCount) {
+          specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount);
+        }
+
+        print(specCounts);
+      } else {
+        print('No specs found');
+      }
+
+      printNewline();
+      var seconds = timer.elapsed() / 1000;
+      print('Finished in ' + seconds + ' ' + plural('second', seconds));
+      printNewline();
+
+      for(i = 0; i < failedSuites.length; i++) {
+        suiteFailureDetails(failedSuites[i]);
+      }
+
+      onComplete(failureCount === 0);
+    };
+
+    this.specStarted = function(){};
+    this.suiteStarted = function(){};
+
+    this.specDone = function(result) {
+      specCount++;
+
+      if (result.status == 'pending') {
+        pendingCount++;
+        print(colored('yellow', '*'));
+        return;
+      }
+
+      if (result.status == 'passed') {
+        print(colored('green', '.'));
+        return;
+      }
+
+      if (result.status == 'failed') {
+        failureCount++;
+        failedSpecs.push(result);
+        print(colored('red', 'F'));
+      }
+    };
+
+    this.suiteDone = function(result) {
+      if (result.failedExpectations && result.failedExpectations.length > 0) {
+        failureCount++;
+        failedSuites.push(result);
+      }
+    };
+
+    return this;
+
+    function printNewline() {
+      print('\n');
+    }
+
+    function colored(color, str) {
+      return showColors ? (ansi[color] + str + ansi.none) : str;
+    }
+
+    function plural(str, count) {
+      return count == 1 ? str : str + 's';
+    }
+
+    function repeat(thing, times) {
+      var arr = [];
+      for (var i = 0; i < times; i++) {
+        arr.push(thing);
+      }
+      return arr;
+    }
+
+    function indent(str, spaces) {
+      var lines = (str || '').split('\n');
+      var newArr = [];
+      for (var i = 0; i < lines.length; i++) {
+        newArr.push(repeat(' ', spaces).join('') + lines[i]);
+      }
+      return newArr.join('\n');
+    }
+
+    function specFailureDetails(result) {
+      printNewline();
+      print(result.fullName);
+
+      for (var i = 0; i < result.failedExpectations.length; i++) {
+        var failedExpectation = result.failedExpectations[i];
+        printNewline();
+        print(indent(failedExpectation.message, 2));
+        print(indent(failedExpectation.stack, 2));
+      }
+
+      printNewline();
+    }
+
+    function suiteFailureDetails(result) {
+      for (var i = 0; i < result.failedExpectations.length; i++) {
+        printNewline();
+        print(colored('red', 'An error was thrown in an afterAll'));
+        printNewline();
+        print(colored('red', 'AfterAll ' + result.failedExpectations[i].message));
+
+      }
+      printNewline();
+    }
+  }
+
+module.exports = ConsoleReporter;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/reporters/ParamedicReporter.js
----------------------------------------------------------------------
diff --git a/lib/reporters/ParamedicReporter.js b/lib/reporters/ParamedicReporter.js
new file mode 100644
index 0000000..2742e67
--- /dev/null
+++ b/lib/reporters/ParamedicReporter.js
@@ -0,0 +1,43 @@
+
+function ParamedicReporter() {
+    var results = [],
+      specsExecuted = 0,
+      failureCount = 0,
+      pendingSpecCount = 0,
+      cordovaInfo = null;
+
+    this.specDone = function(result) {
+        if (result.status != "disabled") {
+            specsExecuted++;
+        }
+        if (result.status == "failed") {
+              failureCount++;
+              results.push(result);
+        }
+        if (result.status == "pending") {
+            pendingSpecCount++;
+        }
+    };
+
+  this.jasmineDone = function(data) {
+      cordovaInfo = data.cordova;
+  };
+
+  this.getResults = function() {
+      return {
+        passed: failureCount === 0,
+            mobilespec: {
+                specs:specsExecuted,
+                failures:failureCount,
+                results: results
+            },
+            platform: cordovaInfo.platform,
+            version: cordovaInfo.version,
+            model: cordovaInfo.model
+        };
+    };
+
+    return this;
+}
+
+module.exports = ParamedicReporter;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/specReporters.js
----------------------------------------------------------------------
diff --git a/lib/specReporters.js b/lib/specReporters.js
new file mode 100644
index 0000000..21e2eab
--- /dev/null
+++ b/lib/specReporters.js
@@ -0,0 +1,68 @@
+
+// not currently used
+// var ConsoleReporter = require('./reporters/ConsoleReporter');
+
+var ParamedicReporter = require('./reporters/ParamedicReporter');
+var JasmineSpecReporter = require('jasmine-spec-reporter');
+var jasmineReporters    = require('jasmine-reporters');
+
+// default reporter which is used to determine whether tests pass or not
+var paramedicReporter = null;
+
+// all reporters including additional reporters to trace results to console, etc
+var reporters = [];
+
+// specifies path to save Junit results file
+var reportSavePath = null;
+
+function reset() {
+    paramedicReporter = new ParamedicReporter();
+    // default paramedic reporter
+    reporters = [paramedicReporter];
+    // extra reporters
+    reporters.push(new JasmineSpecReporter({displayPendingSummary: false, displaySuiteNumber: true}));
+    if(reportSavePath){
+        reporters.push(new jasmineReporters.JUnitXmlReporter({savePath: reportSavePath, consolidateAll: false}));
+    }
+}
+
+module.exports = {
+    reset: reset,
+    initialize: function(config) {
+        reportSavePath = config.getReportSavePath();
+        reset();
+    },
+    getResults: function() {
+        return paramedicReporter.getResults();
+    },
+    jasmineStarted: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.jasmineStarted && reporter.jasmineStarted(data);
+        });
+    },
+    specStarted: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.specStarted && reporter.specStarted(data);
+        });
+    },
+    specDone: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.specDone && reporter.specDone(data);
+        });
+    },
+    suiteStarted: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.suiteStarted && reporter.suiteStarted(data);
+        });
+    },
+    suiteDone: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.suiteDone && reporter.suiteDone(data);
+        });
+    },
+    jasmineDone: function(data) {
+        reporters.forEach(function (reporter) {
+            reporter.jasmineDone && reporter.jasmineDone(data);
+        });
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/lib/utils.js
----------------------------------------------------------------------
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..35ce7c7
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,22 @@
+var shelljs = require('shelljs');
+var verbose;
+
+function exec(cmd, onFinish, onData) {
+    if (onFinish instanceof Function || onFinish === null) {
+        var result = shelljs.exec(cmd, {async: true, silent: !verbose}, onFinish);
+
+        if (onData instanceof Function) {
+            result.stdout.on('data', onData);
+        }
+    } else {
+        return shelljs.exec(cmd, {silent: !verbose});
+    }
+}
+
+exec.setVerboseLevel = function(_verbose) {
+    verbose = _verbose;
+};
+
+module.exports = {
+	exec: exec
+};

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/main.js
----------------------------------------------------------------------
diff --git a/main.js b/main.js
index b772665..a538c94 100755
--- a/main.js
+++ b/main.js
@@ -1,43 +1,52 @@
 #!/usr/bin/env node
 
 var parseArgs = require('minimist'),
-    paramedic = require('./paramedic');
-
-var plugins,
-    platformId;
+    path = require('path'),
+    paramedic = require('./lib/paramedic'),
+    ParamedicConfig = require('./lib/ParamedicConfig');
 
 var USAGE = "Error missing args. \n" +
-	"cordova-paramedic --platform PLATFORM --plugin PATH [--justbuild --timeout MSECS --port PORTNUM --browserify]\n" +
-	"`PLATFORM` : the platform id, currently only supports 'ios'\n" +
-	"`PATH` : the relative or absolute path to a plugin folder\n" +
-					"\texpected to have a 'tests' folder.\n" +  
-					"\tYou may specify multiple --plugin flags and they will all\n" + 
-					"\tbe installed and tested together.\n" +
-	"`MSECS` : (optional) time in millisecs to wait for tests to pass|fail \n" +
-			  "\t(defaults to 10 minutes) \n" +
-	"`PORTNUM` : (optional) port to use for posting results from emulator back to paramedic server\n" +
-	"--justbuild : (optional) just builds the project, without running the tests \n" +
+    "cordova-paramedic --platform PLATFORM --plugin PATH [--justbuild --timeout MSECS --startport PORTNUM --endport PORTNUM --browserify]\n" +
+    "`PLATFORM` : the platform id. Currently supports 'ios', 'browser', 'windows', 'android', 'wp8'.\n" +
+                    "\tPath to platform can be specified as link to git repo like:\n" +
+                    "\twindows@https://github.com/apache/cordova-windows.git\n" +
+                    "\tor path to local copied git repo like:\n" +
+                    "\twindows@../cordova-windows/\n" +
+    "`PATH` : the relative or absolute path to a plugin folder\n" +
+                    "\texpected to have a 'tests' folder.\n" +
+                    "\tYou may specify multiple --plugin flags and they will all\n" +
+                    "\tbe installed and tested together.\n" +
+    "`MSECS` : (optional) time in millisecs to wait for tests to pass|fail \n" +
+              "\t(defaults to 10 minutes) \n" +
+    "`PORTNUM` : (optional) ports to find available and use for posting results from emulator back to paramedic server(default is from 8008 to 8009)\n" +
+    "--justbuild : (optional) just builds the project, without running the tests \n" +
     "--browserify : (optional) plugins are browserified into cordova.js \n" +
     "--verbose : (optional) verbose mode. Display more information output\n" +
-    "--platformPath : (optional) path to install platform from, git or local file uri";
+    "--useTunnel : (optional) use tunneling instead of local address. default is false\n" +
+    "--config : (optional) read configuration from paramedic configuration file\n" +
+    "--reportSavePath: (optional) path to save Junit results file\n" +
+    "--cleanUpAfterRun: (optional) cleans up the application after the run.";
 
 var argv = parseArgs(process.argv.slice(2));
+var pathToParamedicConfig = argv.config && path.resolve(argv.config);
 
-if(!argv.platform) {
-    console.log(USAGE);
-    process.exit(1);
-}
+if (pathToParamedicConfig || // --config
+    argv.platform && argv.plugin) { // or --platform and --plugin
 
-var onComplete = function(resCode,resObj,logStr) {
-	console.log("result code is : " + resCode);
-	if(resObj) {
-		console.log(JSON.stringify(resObj));
-	}
-	if(logStr) {
-		console.log(logStr);
-	}
-	process.exit(resCode);
-};
+    var paramedicConfig = pathToParamedicConfig ?
+        ParamedicConfig.parseFromFile(pathToParamedicConfig):
+        ParamedicConfig.parseFromArguments(argv);
 
-paramedic.run(argv.platform, argv.plugin, onComplete, argv.justbuild, argv.port, argv.timeout, argv.browserify, false, argv.verbose, argv.platformPath);
+    paramedic.run(paramedicConfig)
+    .catch(function (error) {
+        console.log(JSON.stringify(error));
+        process.exit(1);
+    })
+    .done(function (result) {
+        console.log(JSON.stringify(result));
+    });
 
+} else {
+    console.log(USAGE);
+    process.exit(1);
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 8eddb4f..77e6ce0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
 {
   "name": "cordova-paramedic",
   "version": "0.5.0",
+  "license": "Apache-2.0",
   "description": "Use medic to test a cordova plugin locally",
   "main": "paramedic.js",
   "bin": {
@@ -11,15 +12,14 @@
     "url": "git://github.com/apache/cordova-paramedic.git"
   },
   "scripts": {
-    "test":"npm run jshint & npm run test-ios",
-    "jshint":"node node_modules/jshint/bin/jshint *.js",
-    "test-appveyor":"npm run test-windows",
-    "test-travis":"npm run test-ios",
-    "test-android":"node main.js --platform android --plugin ./spec/testable-plugin/",
+    "test": "npm run jshint & npm run test-ios",
+    "jshint": "node node_modules/jshint/bin/jshint lib/",
+    "test-appveyor": "npm run test-windows",
+    "test-travis": "npm run test-ios",
+    "test-android": "node main.js --platform android --plugin ./spec/testable-plugin/",
     "test-ios": "node main.js --platform ios --plugin ./spec/testable-plugin/ --verbose",
-    "test-windows" : "node main.js --platform windows --plugin ./spec/testable-plugin/",
+    "test-windows": "node main.js --platform windows --plugin ./spec/testable-plugin/",
     "test-wp8": "node main.js --platform wp8 --plugin ./spec/testable-plugin/"
-
   },
   "keywords": [
     "cordova",
@@ -29,14 +29,20 @@
   "author": "Jesse MacFadyen",
   "license": "Apache 2.0",
   "dependencies": {
+    "ansi": "^0.3.1",
+    "cordova-common": "^1.0.0",
+    "jasmine-spec-reporter": "^2.4.0",
+    "jasmine-reporters": "^2.1.1",
     "localtunnel": "~1.5.0",
     "minimist": "~1.1.0",
+    "q": "^1.4.1",
     "request": "^2.53.0",
     "shelljs": "~0.3.0",
+    "socket.io": "^1.4.5",
     "tmp": "0.0.25"
   },
   "devDependencies": {
-        "jasmine-node": "~1",
-        "jshint": "^2.6.0"
+    "jasmine-node": "~1",
+    "jshint": "^2.6.0"
   }
 }

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/paramedic-plugin/JasmineParamedicProxy.js
----------------------------------------------------------------------
diff --git a/paramedic-plugin/JasmineParamedicProxy.js b/paramedic-plugin/JasmineParamedicProxy.js
new file mode 100644
index 0000000..854efc6
--- /dev/null
+++ b/paramedic-plugin/JasmineParamedicProxy.js
@@ -0,0 +1,56 @@
+function JasmineParamedicProxy(socket) {
+    this.socket = socket;
+    //jasmineRequire.JsApiReporter.apply(this, arguments);
+}
+
+// JasmineParamedicProxy.prototype = jasmineRequire.JsApiReporter.prototype;
+// JasmineParamedicProxy.prototype.constructor = JasmineParamedicProxy;
+
+JasmineParamedicProxy.prototype.jasmineStarted = function (o) {
+    this.socket.emit('jasmineStarted', o);
+};
+
+JasmineParamedicProxy.prototype.specStarted = function (o) {
+    this.socket.emit('specStarted', o);
+};
+
+JasmineParamedicProxy.prototype.specDone = function (o) {
+    this.socket.emit('specDone', o);
+};
+
+JasmineParamedicProxy.prototype.suiteStarted = function (o) {
+    this.socket.emit('suiteStarted', o);
+};
+
+JasmineParamedicProxy.prototype.suiteDone = function (o) {
+    this.socket.emit('suiteDone', o);
+};
+
+JasmineParamedicProxy.prototype.jasmineDone = function (o) {
+    var p = 'Desktop';
+    var devmodel='none';
+    var version = cordova.version;
+    if(typeof device != 'undefined') {
+        p = device.platform.toLowerCase();
+        devmodel=device.model || device.name;
+        version = device.version.toLowerCase();
+    }
+
+    o = o || {};
+
+    // include platform info
+    o.cordova = {
+        platform:(platformMap.hasOwnProperty(p) ? platformMap[p] : p),
+        version:version,
+        model:devmodel
+    }
+
+    this.socket.emit('jasmineDone', o);
+};
+
+var platformMap = {
+    'ipod touch':'ios',
+    'iphone':'ios'
+};
+
+module.exports = JasmineParamedicProxy;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/paramedic-plugin/paramedic.js
----------------------------------------------------------------------
diff --git a/paramedic-plugin/paramedic.js b/paramedic-plugin/paramedic.js
new file mode 100644
index 0000000..6c210e6
--- /dev/null
+++ b/paramedic-plugin/paramedic.js
@@ -0,0 +1,74 @@
+var io = cordova.require('cordova-plugin-paramedic.socket.io');
+
+var PARAMEDIC_SERVER_DEFAULT_URL = 'http://127.0.0.1:8008';
+
+function Paramedic() {
+
+}
+
+Paramedic.prototype.initialize = function() {
+    var me = this;
+    var connectionUri = loadParamedicServerUrl();
+    this.socket = io.connect(connectionUri);
+
+    this.socket.on('connect', function () {
+        console.log("Paramedic has been susccessfully connected to server");
+        if (typeof device != 'undefined') me.socket.emit('deviceInfo', device);
+    });
+
+    this.overrideConsole();
+    this.injectJasmineReporter();
+};
+
+
+Paramedic.prototype.overrideConsole = function () {
+
+    var origConsole = window.console;
+    var me = this;
+
+    function createCustomLogger(type) {
+        return function () {
+            origConsole[type].apply(origConsole, arguments);
+
+            me.socket.emit('log', { type: type, msg: Array.prototype.slice.apply(arguments) });
+        };
+    }
+    window.console = {
+        log: createCustomLogger('log'),
+        warn: createCustomLogger('warn'),
+        error: createCustomLogger('error'),
+    };
+    console.log('Paramedic console has been installed.');
+};
+
+Paramedic.prototype.injectJasmineReporter = function () {
+    var JasmineParamedicProxy = require('cordova-plugin-paramedic.JasmineParamedicProxy');
+    var jasmineProxy = new JasmineParamedicProxy(this.socket);
+    var testsModule = cordova.require("cordova-plugin-test-framework.cdvtests");
+    var defineAutoTestsOriginal = testsModule.defineAutoTests;
+
+    testsModule.defineAutoTests = function () {
+        defineAutoTestsOriginal();
+        jasmine.getEnv().addReporter(jasmineProxy);
+    };
+};
+
+new Paramedic().initialize();
+
+function loadParamedicServerUrl() {
+
+    try {
+        // attempt to synchronously load medic config
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", "../medic.json", false);
+        xhr.send(null);
+        var cfg = JSON.parse(xhr.responseText);
+
+        return cfg.logurl || PARAMEDIC_SERVER_DEFAULT_URL;
+
+    } catch (ex) {}
+
+    return PARAMEDIC_SERVER_DEFAULT_URL;
+}
+
+module.exports = Paramedic;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/b1aa699d/paramedic-plugin/plugin.xml
----------------------------------------------------------------------
diff --git a/paramedic-plugin/plugin.xml b/paramedic-plugin/plugin.xml
new file mode 100644
index 0000000..af4d54a
--- /dev/null
+++ b/paramedic-plugin/plugin.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
+           id="cordova-plugin-paramedic"
+      version="1.2.1-dev">
+
+    <name>Paramedic</name>
+    <description>Cordova Paramedic Plugin</description>
+    <license>Apache 2.0</license>
+
+    <js-module src="socket.io.js" name="socket.io" />
+
+    <js-module src="JasmineParamedicProxy.js" name="JasmineParamedicProxy" />
+
+    <js-module src="paramedic.js" name="paramedic">
+        <runs/>
+    </js-module>
+
+</plugin>


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