You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2014/03/10 21:30:47 UTC

[1/4] git commit: Improve dependencies tests by grouping with beforeStart() Fix for dependency cycle / throw error.

Repository: cordova-plugman
Updated Branches:
  refs/heads/master 7e26c88ab -> a4a51d7ce


Improve dependencies tests by grouping with beforeStart() Fix for dependency cycle / throw error.


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

Branch: refs/heads/master
Commit: f58db0a998022b1da39158d4c219a419fa38e532
Parents: ae2ce7a
Author: jbondc <jb...@openmv.com>
Authored: Fri Feb 28 18:44:46 2014 -0500
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Mon Mar 10 16:29:27 2014 -0400

----------------------------------------------------------------------
 spec/install.spec.js | 98 +++++++++++++++++++----------------------------
 src/install.js       |  6 +--
 2 files changed, 43 insertions(+), 61 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/f58db0a9/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index bff78c2..5a40de3 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -36,7 +36,7 @@ var install = require('../src/install'),
     dummy_id = 'com.phonegap.plugins.dummyplugin';
 
 function installPromise(f) {
-  f.then(function() { done = true; }, function(err) { done = err; });
+  f.then(function(res) { done = true; }, function(err) { done = err; });
 }
 
 var existsSync = fs.existsSync;
@@ -259,7 +259,7 @@ describe('install', function() {
                 // <engine name="mega-fun-plugin" version=">=1.0.0" scriptSrc="megaFunVersion" platform="*" />
                 // <engine name="mega-boring-plugin" version=">=3.0.0" scriptSrc="megaBoringVersion" platform="ios|android" />
 
-				var plugmanVersion = require('../package.json').version;
+                var plugmanVersion = require('../package.json').version;
 
                 expect(spy.calls.length).toBe(4);
                 expect(spy.calls[0].args).toEqual([ '', '>=2.3.0' ]);
@@ -280,15 +280,17 @@ describe('install', function() {
         });
 
         describe('with dependencies', function() {
-            it('should install any dependent plugins if missing', function() {
+            var emit;
+            beforeEach(function() {
                 spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
                 fetchSpy.andCallFake( fake['fetch']['dependencies'] );
                 emit = spyOn(events, 'emit');
-
                 exec.andCallFake(function(cmd, cb) {
                     cb(null, '9.0.0\n');
                 });
-
+            });											   
+   
+            it('should install any dependent plugins if missing', function() {
                 runs(function() {
                     installPromise( install('android', project, plugins['A']) );
                 });
@@ -304,14 +306,8 @@ describe('install', function() {
                     ]);
                 });
             });
-            it('should install any dependent plugins from registry when url is not defined', function() {
-                spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
-                fetchSpy.andCallFake( fake['fetch']['dependencies'] );
-				emit = spyOn(events, 'emit');
-                exec.andCallFake(function(cmd, cb) {
-                    cb(null, '9.0.0\n', '');
-                });
 
+            it('should install any dependent plugins from registry when url is not defined', function() {
                 // Plugin A depends on C & D
                 runs(function() {
                     installPromise( install('android', project, plugins['A']) );
@@ -328,53 +324,39 @@ describe('install', function() {
                     ]);;
                 });
             });
+
+            it('should process all dependent plugins with alternate routes to the same plugin', function() {
+                // Plugin F depends on A, C, D and E
+                runs(function () {
+                    installPromise(install('android', project, plugins['F']));
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 500);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+    
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "F" on android.'
+                    ]);
+                });
+            });
+
+            it('should throw if there is a cyclic dependency', function() {
+                runs(function () {
+                    installPromise( install('android', project, plugins['G']) );
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 500);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+       
+                    expect(done.message).toEqual('Cyclic dependency from G to H');
+                });
+            });				
         });
-		it('should process all dependent plugins with alternate routes to the same plugin', function() {
-			spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
-			fetchSpy.andCallFake( fake['fetch']['dependencies'] );
-			emit = spyOn(events, 'emit');
-			exec.andCallFake(function(cmd, cb) {
-				cb(null, '9.0.0\n', '');
-			});			
-
-			// Plugin F depends on A, C, D and E
-			runs(function () {
-				installPromise(install('android', project, plugins['F']));
-			});
-			waitsFor(function () { return done; }, 'install promise never resolved', 500);
-			runs(function () {
-				var install = common.spy.getInstall(emit);
-
-				expect(install).toEqual([
-					'Install start for "C" on android.',
-					'Install start for "D" on android.',
-					'Install start for "A" on android.',
-					'Install start for "D" on android.',
-					'Install start for "F" on android.'
-				]);
-			});
-		});
-		/*
-		it('should throw if there is a cyclic dependency', function() {
-			// TODO: group this into BeforeStart() for all dependency tests
-			spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
-			fetchSpy.andCallFake( fake['fetch']['dependencies'] );
-			emit = spyOn(events, 'emit');
-			exec.andCallFake(function(cmd, cb) {
-				cb(null, '9.0.0\n', '');
-			});
-
-			runs(function () {
-				installPromise(install('android', project, plugins['G']));
-			});
-			waitsFor(function () { return done; }, 'install promise never resolved', 500);
-			runs(function () {
-				var install = common.spy.getInstall(emit);
-				
-				console.log(install);		   
-				expect(done.message).toEqual('Cyclic dependency from G to H');
-			});
-		});*/		
+
     });
 
     xdescribe('failure', function() {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/f58db0a9/src/install.js
----------------------------------------------------------------------
diff --git a/src/install.js b/src/install.js
index 1a82f65..3fea119 100644
--- a/src/install.js
+++ b/src/install.js
@@ -285,9 +285,9 @@ var runInstall = module.exports.runInstall = function runInstall(actions, platfo
             // Check for dependencies
             var dependencies = plugin_et.findall('dependency') || [];
             dependencies = dependencies.concat(plugin_et.findall('./platform[@name="'+platform+'"]/dependency'));
-            if(dependencies && dependencies.length)
+            if(dependencies && dependencies.length) {
                 return installDependencies(install, dependencies, options);
-
+            }
             return Q(true);
         }
     ).then(
@@ -304,7 +304,7 @@ var runInstall = module.exports.runInstall = function runInstall(actions, platfo
     ).fail(
         function (error) {
             events.emit('warn', "Failed to install '"+plugin_id+"':"+ error.stack);
-            return Q(false);
+            throw error;
         }
     );
 }


[2/4] Refactoring of install & uninstall tests

Posted by ag...@apache.org.
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/install.js
----------------------------------------------------------------------
diff --git a/src/install.js b/src/install.js
index 0949068..1a82f65 100644
--- a/src/install.js
+++ b/src/install.js
@@ -9,7 +9,11 @@ var path = require('path'),
     Q = require('q'),
     platform_modules = require('./platforms'),
     os = require('os'),
-    isWindows = (os.platform() === 'win32');
+    underscore = require('underscore'),
+    shell   = require('shelljs'),
+    events = require('./events'),
+    plugman = require('../plugman'),
+    isWindows = (os.platform().substr(0,3) === 'win');
 
 /* INSTALL FLOW
    ------------
@@ -36,11 +40,15 @@ var path = require('path'),
 // Returns a promise.
 module.exports = function installPlugin(platform, project_dir, id, plugins_dir, options) {
     options = options || {};
+    options.is_top_level = true;
+    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
+
     if (!platform_modules[platform]) {
         return Q.reject(new Error(platform + " not supported."));
     }
+
     var current_stack = new action_stack();
-    options.is_top_level = true;
+
     return possiblyFetch(id, plugins_dir, options)
     .then(function(plugin_dir) {
         return runInstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
@@ -50,26 +58,31 @@ module.exports = function installPlugin(platform, project_dir, id, plugins_dir,
 // possible options: subdir, cli_variables, www_dir, git_ref, is_top_level
 // Returns a promise.
 function possiblyFetch(id, plugins_dir, options) {
-    var plugin_dir = path.join(plugins_dir, id);
+
+    // if plugin is a relative path, check if it already exists
+    var plugin_src_dir = path.join(plugins_dir, id);
+    if( isAbsolutePath(id) )
+        plugin_src_dir = id;
 
     // Check that the plugin has already been fetched.
-    if (fs.existsSync(plugin_dir)) {
-        return Q(plugin_dir);
+    if (fs.existsSync(plugin_src_dir)) {
+        return Q(plugin_src_dir);
     }
-    var opts = {
-        link: false,
-        subdir: options.subdir,
-        git_ref: options.git_ref,
-        client: 'plugman',
-        expected_id: options.expected_id,
-        searchpath: options.searchpath
-    };
-    return require('../plugman').raw.fetch(id, plugins_dir, opts);
+
+    var opts = underscore.extend({}, options, {
+        link: false, 
+        client: 'plugman'
+    });
+
+    // if plugin doesnt exist, use fetch to get it.
+    return plugman.raw.fetch(id, plugins_dir, opts);
 }
 
 function checkEngines(engines) {
+
     for(var i = 0; i < engines.length; i++) {
         var engine = engines[i];
+
         if(semver.satisfies(engine.currentVersion, engine.minVersion) || engine.currentVersion === null){
             // engine ok!
         }else{
@@ -88,15 +101,13 @@ function cleanVersionOutput(version, name){
         out = out.substr(0, rc_index) + '-' + out.substr(rc_index);
     }
 
-    // strip out the -dev and put a warning about using the dev branch
+    // put a warning about using the dev branch
     if (dev_index > -1) {
         // some platform still lists dev branches as just dev, set to null and continue
         if(out=="dev"){
             out = null;
-        }else{
-            out = out.substr(0, dev_index-1);
         }
-        require('../plugman').emit('verbose', name+' has been detected as using a development branch. Attemping to install anyways.');
+        events.emit('verbose', name+' has been detected as using a development branch. Attemping to install anyways.');
     }
 
     // add extra period/digits to conform to semver - some version scripts will output
@@ -124,29 +135,36 @@ function callEngineScripts(engines) {
     return Q.all(
         engines.map(function(engine){
             // CB-5192; on Windows scriptSrc doesn't have file extension so we shouldn't check whether the script exists
-            if(isWindows || fs.existsSync(engine.scriptSrc)){
 
+            var scriptPath = engine.scriptSrc ? '"' + engine.scriptSrc + '"' : null;		
+
+            if(scriptPath && (isWindows || fs.existsSync(engine.scriptSrc)) ) {
+
+                var d = Q.defer();
                 if(!isWindows) { // not required on Windows
                     fs.chmodSync(engine.scriptSrc, '755');
                 }
-                var d = Q.defer();
-                child_process.exec('"' + engine.scriptSrc + '"', function(error, stdout, stderr) {
+                child_process.exec(scriptPath, function(error, stdout, stderr) {
                     if (error) {
-                        require('../plugman').emit('log', 'Cordova project '+ engine.scriptSrc +' script failed, continuing anyways.');
+                        events.emit('warn', engine.name +' version check failed ('+ scriptPath +'), continuing anyways.');
                         engine.currentVersion = null;
-                        d.resolve(engine); // Yes, resolve. We're trying to continue despite the error.
                     } else {
-                        var version = cleanVersionOutput(stdout, engine.name);
-                        engine.currentVersion = version;
-                        d.resolve(engine);
+                        engine.currentVersion = cleanVersionOutput(stdout, engine.name);
                     }
+
+                    d.resolve(engine);
                 });
                 return d.promise;
-            }else if(engine.currentVersion){
-                return cleanVersionOutput(engine.currentVersion, engine.name)
-            }else{
-                require('../plugman').emit('verbose', 'Cordova project '+ engine.scriptSrc +' not detected (lacks a '+ engine.scriptSrc +' script), continuing.');
-                return null;
+
+            } else {
+
+                if(engine.currentVersion) {
+                    engine.currentVersion = cleanVersionOutput(engine.currentVersion, engine.name)
+                } else {
+                    events.emit('warn', engine.name +' version not detected (lacks script '+ scriptPath +' ), continuing.');
+                }
+
+                return Q(engine);
             }
         })
     );
@@ -159,6 +177,7 @@ function getEngines(pluginElement, platform, project_dir, plugin_dir){
     var uncheckedEngines = [];
     var cordovaEngineIndex, cordovaPlatformEngineIndex, theName, platformIndex, defaultPlatformIndex;
     // load in known defaults and update when necessary
+
     engines.forEach(function(engine){
         theName = engine.attrib["name"];
 
@@ -210,7 +229,7 @@ function isPluginInstalled(plugins_dir, platform, plugin_id) {
 
 // possible options: cli_variables, www_dir, is_top_level
 // Returns a promise.
-var runInstall = module.exports.runInstall = function runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, options, graph) {
+var runInstall = module.exports.runInstall = function runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, options) {
     var xml_path     = path.join(plugin_dir, 'plugin.xml')
       , plugin_et    = xml_helpers.parseElementtreeSync(xml_path)
       , filtered_variables = {};
@@ -218,146 +237,228 @@ var runInstall = module.exports.runInstall = function runInstall(actions, platfo
     var plugin_id    = plugin_et.getroot().attrib['id'];
     options = options || {};
 
-    graph = graph || new dep_graph();
+    options.graph = options.graph || new dep_graph();
 
     if (isPluginInstalled(plugins_dir, platform, plugin_id)) {
         if (options.is_top_level) {
-            require('../plugman').emit('results', 'Plugin "' + plugin_id + '" already installed on ' + platform + '.');
+            events.emit('results', 'Plugin "' + plugin_id + '" already installed on ' + platform + '.');
         } else {
-            require('../plugman').emit('verbose', 'Dependent plugin "' + plugin_id + '" already installed on ' + platform + '.');
+            events.emit('verbose', 'Dependent plugin "' + plugin_id + '" already installed on ' + platform + '.');
         }
         return Q();
     }
-    require('../plugman').emit('log', 'Installing ' + plugin_id + ' (' + platform + ')');
-
-    var plugin_basename = path.basename(plugin_dir);
+    events.emit('log', 'Installing "' + plugin_id + '" for ' + platform);
 
     var theEngines = getEngines(plugin_et, platform, project_dir, plugin_dir);
+
+    var install = {
+        actions: actions,
+        platform: platform,
+        project_dir: project_dir,
+        plugins_dir: plugins_dir,
+        top_plugin_id: plugin_id,
+        top_plugin_dir: plugin_dir
+    }
+
     return callEngineScripts(theEngines)
     .then(checkEngines)
-    .then(function() {
-        // checking preferences, if certain variables are not provided, we should throw.
-        var prefs = plugin_et.findall('./preference') || [];
-        prefs = prefs.concat(plugin_et.findall('./platform[@name="'+platform+'"]/preference'));
-        var missing_vars = [];
-        prefs.forEach(function (pref) {
-            var key = pref.attrib["name"].toUpperCase();
-            options.cli_variables = options.cli_variables || {};
-            if (options.cli_variables[key] === undefined)
-                missing_vars.push(key)
-            else
-                filtered_variables[key] = options.cli_variables[key]
-        });
-        if (missing_vars.length > 0) {
-            return Q.reject(new Error('Variable(s) missing: ' + missing_vars.join(", ")));
+    .then(
+        function() {
+            // checking preferences, if certain variables are not provided, we should throw.
+            var prefs = plugin_et.findall('./preference') || [];
+            prefs = prefs.concat(plugin_et.findall('./platform[@name="'+platform+'"]/preference'));
+            var missing_vars = [];
+            prefs.forEach(function (pref) {
+                var key = pref.attrib["name"].toUpperCase();
+                options.cli_variables = options.cli_variables || {};
+                if (options.cli_variables[key] === undefined)
+                    missing_vars.push(key)
+                else
+                    filtered_variables[key] = options.cli_variables[key]
+            });
+            install.filtered_variables = filtered_variables;
+    
+            if (missing_vars.length > 0) {
+                throw new Error('Variable(s) missing: ' + missing_vars.join(", "));
+            }
+
+            // Check for dependencies
+            var dependencies = plugin_et.findall('dependency') || [];
+            dependencies = dependencies.concat(plugin_et.findall('./platform[@name="'+platform+'"]/dependency'));
+            if(dependencies && dependencies.length)
+                return installDependencies(install, dependencies, options);
+
+            return Q(true);
+        }
+    ).then(
+        function(){
+            var install_plugin_dir = path.join(plugins_dir, plugin_id);
+
+            // may need to copy to destination...
+            if ( !fs.existsSync(install_plugin_dir) ) {
+                copyPlugin(plugin_dir, plugins_dir, options.link);
+            }
+
+            return handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options.www_dir, options.is_top_level);
+        }
+    ).fail(
+        function (error) {
+            events.emit('warn', "Failed to install '"+plugin_id+"':"+ error.stack);
+            return Q(false);
         }
+    );
+}
 
-        // Check for dependencies, (co)recurse to install each one
-        var dependencies = plugin_et.findall('dependency') || [];
-        dependencies = dependencies.concat(plugin_et.findall('./platform[@name="'+platform+'"]/dependency'));
-        if (dependencies && dependencies.length) {
-            require('../plugman').emit('verbose', 'Dependencies detected, iterating through them...');
-            var dep_plugin_id, dep_subdir, dep_git_ref;
-            return dependencies.reduce(function (soFar, dep) {
+function installDependencies(install, dependencies, options) {
+    events.emit('verbose', 'Dependencies detected, iterating through them...');
+
+    var top_plugins = path.join(install.top_plugin_dir, '..');
+
+    // Add directory of top-level plugin to search path
+    options.searchpath = options.searchpath || [];
+    if( top_plugins != install.plugins_dir && options.searchpath.indexOf(top_plugins) == -1 )
+        options.searchpath.push(top_plugins);
+
+    // Search for dependency by Id is:
+    // a) Look for {$top_plugins}/{$depId} directory
+    // b) Scan the top level plugin directory {$top_plugins} for matching id (searchpath)
+    // c) Fetch from registry
+
+    return dependencies.reduce(function(soFar, depXml) {
+        return soFar.then(
+            function() {   
+                var dep = {
+                    id: depXml.attrib.id,
+                    subdir: depXml.attrib.subdir,
+                    url: depXml.attrib.url || '',
+                    git_ref: depXml.attrib.commit
+                }
+
+                if (dep.subdir) {
+                    dep.subdir = path.join(dep.subdir.split('/'));
+                }
 
                 // We build the dependency graph only to be able to detect cycles, getChain will throw an error if it detects one
-                graph.add(plugin_id, dep.attrib.id);
-                graph.getChain(plugin_id);
-                return soFar.then(function() {
-                    dep_plugin_id = dep.attrib.id;
-                    dep_subdir = dep.attrib.subdir;
-                    var dep_url = dep.attrib.url;
-                    dep_git_ref = dep.attrib.commit;
-                    if (dep_subdir) {
-                        dep_subdir = path.join.apply(null, dep_subdir.split('/'));
+                options.graph.add(install.top_plugin_id, dep.id);
+                options.graph.getChain(install.top_plugin_id);
+
+                return tryFetchDependency(dep, install)
+                .then( 
+                    function(url){						
+                        dep.url = url;
+                        return installDependency(dep, install, options);
                     }
+                );
+            }
+        );
 
-                    // Handle relative dependency paths by expanding and resolving them.
-                    // The easy case of relative paths is to have a URL of '.' and a different subdir.
-                    // TODO: Implement the hard case of different repo URLs, rather than the special case of
-                    // same-repo-different-subdir.
-                    if (dep_url == '.') {
-                        // Look up the parent plugin's fetch metadata and determine the correct URL.
-                        var fetchdata = require('./util/metadata').get_fetch_metadata(plugin_dir);
-
-                        if (!fetchdata || !(fetchdata.source && fetchdata.source.type)) {
-                            return Q.reject(new Error('No fetch metadata found for ' + plugin_id + '. Cannot install relative dependencies.'));
-                        }
-
-                        // Now there are two cases here: local directory, and git URL.
-                        if (fetchdata.source.type === 'local') {
-                            dep_url = fetchdata.source.path;
-
-                            var d = Q.defer();
-                            child_process.exec('git rev-parse --show-toplevel', { cwd:dep_url }, function(err, stdout, stderr) {
-                                if (err) {
-                                    if (err.code == 128) {
-                                        return d.reject(new Error('Plugin ' + plugin_id + ' is not in git repository. All plugins must be in a git repository.'));
-                                    } else {
-                                        return d.reject(new Error('Failed to locate git repository for ' + plugin_id + ' plugin.'));
-                                    }
-                                }
-
-                                return d.resolve(stdout.trim());
-                            });
-                            return d.promise.then(function(git_repo) {
-                                //Clear out the subdir since the url now contains it
-                                var url = path.join(git_repo, dep_subdir);
-                                dep_subdir = "";
-                                return url;
-                            });
-                        } else if (fetchdata.source.type === 'git') {
-                            return Q(fetchdata.source.url);
-                        }
-                    } else {
-                        return Q(dep_url);
-                    }
-                })
-                .then(function(dep_url) {
-                    var dep_plugin_dir = path.join(plugins_dir, dep_plugin_id);
-                    if (fs.existsSync(dep_plugin_dir)) {
-                        require('../plugman').emit('verbose', 'Dependent plugin "' + dep_plugin_id + '" already fetched, using that version.');
-                        var opts = {
-                            cli_variables: filtered_variables,
-                            www_dir: options.www_dir,
-                            is_top_level: false
-                        };
-                        return runInstall(actions, platform, project_dir, dep_plugin_dir, plugins_dir, opts, graph);
+    }, Q(true));
+}
+
+function tryFetchDependency(dep, install) {
+
+    // Handle relative dependency paths by expanding and resolving them.
+    // The easy case of relative paths is to have a URL of '.' and a different subdir.
+    // TODO: Implement the hard case of different repo URLs, rather than the special case of
+    // same-repo-different-subdir.
+    if ( dep.url == '.' ) {
+
+        // Look up the parent plugin's fetch metadata and determine the correct URL.
+        var fetchdata = require('./util/metadata').get_fetch_metadata(install.top_plugin_dir);
+        if (!fetchdata || !(fetchdata.source && fetchdata.source.type)) {
+            throw new Error('No fetch metadata found for plugin ' + install.top_plugin_id + '. Cannot install relative dependency --> ' + dep.id);
+        }
+
+        // Now there are two cases here: local directory, and git URL.
+        var d = Q.defer();
+
+        if (fetchdata.source.type === 'local') {
+
+            dep.url = fetchdata.source.path;
+
+            child_process.exec('git rev-parse --show-toplevel', { cwd:dep.url }, function(err, stdout, stderr) {
+                if (err) {
+                    if (err.code == 128) {
+                        return d.reject(new Error('Plugin ' + dep.id + ' is not in git repository. All plugins must be in a git repository.'));
                     } else {
-                        require('../plugman').emit('verbose', 'Dependent plugin "' + dep_plugin_id + '" not fetched, retrieving then installing.');
-                        var opts = {
-                            cli_variables: filtered_variables,
-                            www_dir: options.www_dir,
-                            is_top_level: false,
-                            subdir: dep_subdir,
-                            git_ref: dep_git_ref,
-                            expected_id: dep_plugin_id,
-                            searchpath: options.searchpath
-                        };
-
-                        // CB-4770: registry fetching
-                        if(dep_url === undefined) {
-                            dep_url = dep_plugin_id;
-                        }
-
-                        return possiblyFetch(dep_url, plugins_dir, opts)
-                        .then(function(plugin_dir) {
-                            return runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, opts, graph);
-                        });
+                        return d.reject(new Error('Failed to locate git repository for ' + dep.id + ' plugin.'));
                     }
-                });
-            }, Q())
-            .then(function() {
-                return handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, options.www_dir, options.is_top_level);
+                }
+                return d.resolve(stdout.trim());
             });
-        } else {
-            return handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, options.www_dir, options.is_top_level);
+
+            return d.promise.then(function(git_repo) {
+                //Clear out the subdir since the url now contains it
+                var url = path.join(git_repo, dep.subdir);
+                dep.subdir = "";
+                return Q(url);
+            }).fail(function(error){
+//console.log("Failed to resolve url='.': " + error);
+                return Q(dep.url);	
+            });
+
+        } else if (fetchdata.source.type === 'git') {
+            return Q(fetchdata.source.url);
         }
-    });
+    }
+
+    // Test relative to parent folder 
+    if( dep.url && isRelativePath(dep.url) ) {
+        var relativePath = path.resolve(install.top_plugin_dir, '../' + dep.url);
+
+        if( fs.existsSync(relativePath) ) {
+           dep.url = relativePath;
+        }
+    }
+
+    // CB-4770: registry fetching
+    if(dep.url === undefined) {
+        dep.url = dep.plugin_id;							
+    }	
+
+    return Q(dep.url);	
 }
 
-function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level) {
-    require('../plugman').emit('verbose', 'Installing plugin ' + plugin_id);
+function installDependency(dep, install, options) {
+
+    dep.install_dir = path.join(install.plugins_dir, dep.id);
+
+    if ( fs.existsSync(dep.install_dir) ) {
+        events.emit('verbose', 'Dependent plugin "' + dep.id + '" already fetched, using that version.');
+        var opts = underscore.extend({}, options, {
+            cli_variables: install.filtered_variables, 
+            is_top_level: false
+        });
+
+       return runInstall(install.actions, install.platform, install.project_dir, dep.install_dir, install.plugins_dir, opts);
+
+    } else {
+        events.emit('verbose', 'Dependent plugin "' + dep.id + '" not fetched, retrieving then installing.');
+
+        var opts = underscore.extend({}, options, {
+            cli_variables: install.filtered_variables, 
+            is_top_level: false,
+            subdir: dep.subdir,
+            git_ref: dep.git_ref,
+            expected_id: dep.id
+        });
+
+        var dep_src = dep.url.length ? dep.url : dep.id;
+
+        return possiblyFetch(dep_src, install.plugins_dir, opts)
+        .then(
+            function(plugin_dir) {
+                return runInstall(install.actions, install.platform, install.project_dir, plugin_dir, install.plugins_dir, opts);
+            }
+        );
+    };
+}
+
+function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_dir, filtered_variables, www_dir, is_top_level) {
+
+    // @tests - important this event is checked spec/install.spec.js
+    events.emit('verbose', 'Install start for "' + plugin_id + '" on ' + platform + '.');
+
     var handler = platform_modules[platform];
     www_dir = www_dir || handler.www_dir(project_dir);
 
@@ -415,20 +516,20 @@ function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plu
     return actions.process(platform, project_dir)
     .then(function(err) {
         // queue up the plugin so prepare knows what to do.
-        config_changes.add_installed_plugin_to_prepare_queue(plugins_dir, plugin_basename, platform, filtered_variables, is_top_level);
+        config_changes.add_installed_plugin_to_prepare_queue(plugins_dir, plugin_id, platform, filtered_variables, is_top_level);
         // call prepare after a successful install
-        require('./../plugman').prepare(project_dir, platform, plugins_dir, www_dir);
+        plugman.prepare(project_dir, platform, plugins_dir, www_dir);
 
-        require('../plugman').emit('verbose', 'Install complete for ' + plugin_id + ' on ' + platform + '.');
+        events.emit('verbose', 'Install complete for ' + plugin_id + ' on ' + platform + '.');
         // WIN!
         // Log out plugin INFO element contents in case additional install steps are necessary
         var info = plugin_et.findall('./info');
         if(info.length) {
-            require('../plugman').emit('results', interp_vars(filtered_variables, info[0].text));
+            events.emit('results', interp_vars(filtered_variables, info[0].text));
         }
         info = (platformTag ? platformTag.findall('./info') : []);
         if(info.length) {
-            require('../plugman').emit('results', interp_vars(filtered_variables, info[0].text));
+            events.emit('results', interp_vars(filtered_variables, info[0].text));
         }
     });
 }
@@ -440,3 +541,52 @@ function interp_vars(vars, text) {
     });
     return text;
 }
+
+function isAbsolutePath(path) {
+    return path && (path[0] === '/' || path[0] === '\\' || path.indexOf(':\\') > 0 );
+}
+
+function isRelativePath(path) {
+    return !isAbsolutePath();	
+}
+
+function readId(plugin_dir) {
+    var xml_path = path.join(plugin_dir, 'plugin.xml');
+    events.emit('verbose', 'Fetch is reading plugin.xml from location "' + xml_path + '"...');
+    var et = xml_helpers.parseElementtreeSync(xml_path);
+
+    return et.getroot().attrib.id;
+}
+
+// Copy or link a plugin from plugin_dir to plugins_dir/plugin_id.
+function copyPlugin(plugin_src_dir, plugins_dir, link) {
+    var plugin_id = readId(plugin_src_dir);
+    var dest = path.join(plugins_dir, plugin_id);
+    shell.rm('-rf', dest);
+
+    if (link) {
+        events.emit('verbose', 'Symlinking from location "' + plugin_src_dir + '" to location "' + dest + '"');
+        fs.symlinkSync(plugin_src_dir, dest, 'dir');
+    } else {
+        shell.mkdir('-p', dest);
+        events.emit('verbose', 'Copying from location "' + plugin_src_dir + '" to location "' + dest + '"');
+        shell.cp('-R', path.join(plugin_src_dir, '*') , dest);
+    }
+
+    return dest;
+}
+
+function isPluginInstalled(plugins_dir, platform, plugin_id) {
+    var platform_config = config_changes.get_platform_json(plugins_dir, platform);
+    for (var installed_plugin_id in platform_config.installed_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    for (var installed_plugin_id in platform_config.dependent_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    return false;
+}

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/android.js
----------------------------------------------------------------------
diff --git a/src/platforms/android.js b/src/platforms/android.js
index 038f92d..e396680 100644
--- a/src/platforms/android.js
+++ b/src/platforms/android.js
@@ -37,9 +37,8 @@ module.exports = {
     "source-file":{
         install:function(source_el, plugin_dir, project_dir, plugin_id) {
             var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
-            var target_path = common.resolveTargetPath(project_dir, dest);
-            if (fs.existsSync(target_path)) throw new Error('"' + target_path + '" already exists!');
-            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+
+            common.copyNewFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
         },
         uninstall:function(source_el, project_dir, plugin_id) {
             var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/blackberry10.js
----------------------------------------------------------------------
diff --git a/src/platforms/blackberry10.js b/src/platforms/blackberry10.js
index d3709c0..d9e93ad 100644
--- a/src/platforms/blackberry10.js
+++ b/src/platforms/blackberry10.js
@@ -39,9 +39,8 @@ module.exports = {
             var target = source_el.attrib['target-dir'] || plugin_id;
             TARGETS.forEach(function(arch) {
                 var dest = path.join("native", arch, "chrome", "plugin", target, path.basename(src));
-                var target_path = common.resolveTargetPath(project_dir, dest);
-                if (fs.existsSync(target_path)) throw new Error('"' + target_path + '" already exists!');
-                common.copyFile(plugin_dir, src, project_dir, dest);
+
+                common.copyNewFile(plugin_dir, src, project_dir, dest);
             });
         },
         uninstall:function(source_el, project_dir, plugin_id) {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/common.js
----------------------------------------------------------------------
diff --git a/src/platforms/common.js b/src/platforms/common.js
index 671e722..8fe1730 100644
--- a/src/platforms/common.js
+++ b/src/platforms/common.js
@@ -1,8 +1,9 @@
 var shell = require('shelljs'),
     path  = require('path'),
-    fs    = require('fs');
+    fs    = require('fs'),
+	common;
 
-module.exports = {
+module.exports = common = {
     // helper for resolving source paths from plugin.xml
     resolveSrcPath:function(plugin_dir, relative_path) {
         var full_path = path.resolve(plugin_dir, relative_path);
@@ -22,11 +23,19 @@ module.exports = {
 
         // XXX sheljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
         if(fs.statSync(src).isDirectory()) {
-            shell.cp('-R', src+'/*', dest);
+            shell.cp('-Rf', src+'/*', dest);
         } else {
             shell.cp('-f', src, dest);
         }
     },
+    // Same as copy file but throws error if target exists
+    copyNewFile:function(plugin_dir, src, project_dir, dest) {
+		var target_path = common.resolveTargetPath(project_dir, dest);
+        if (fs.existsSync(target_path)) 
+			throw new Error('"' + target_path + '" already exists!');
+
+        common.copyFile(plugin_dir, src, project_dir, dest);
+    },
     // checks if file exists and then deletes. Error if doesn't exist
     removeFile:function(project_dir, src) {
         var file = module.exports.resolveSrcPath(project_dir, src);
@@ -41,7 +50,7 @@ module.exports = {
         var file = path.resolve(project_dir, destFile);
         if (!fs.existsSync(file)) return;
 
-        module.exports.removeFileF(file);
+        common.removeFileF(file);
 
         // check if directory is empty
         var curDir = path.dirname(file);
@@ -69,7 +78,7 @@ module.exports = {
                 throw new Error('<asset> tag without required "target" attribute');
             }
 
-            module.exports.copyFile(plugin_dir, src, www_dir, target);
+            common.copyFile(plugin_dir, src, www_dir, target);
         },
         uninstall:function(asset_el, www_dir, plugin_id) {
             var target = asset_el.attrib.target || asset_el.attrib.src;
@@ -78,8 +87,8 @@ module.exports = {
                 throw new Error('<asset> tag without required "target" attribute');
             }
 
-            module.exports.removeFile(www_dir, target);
-            module.exports.removeFileF(path.resolve(www_dir, 'plugins', plugin_id));
+            common.removeFile(www_dir, target);
+            common.removeFileF(path.resolve(www_dir, 'plugins', plugin_id));
         }
     }
 };

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/windows8.js
----------------------------------------------------------------------
diff --git a/src/platforms/windows8.js b/src/platforms/windows8.js
index 287c048..2232192 100644
--- a/src/platforms/windows8.js
+++ b/src/platforms/windows8.js
@@ -47,9 +47,8 @@ module.exports = {
         install:function(source_el, plugin_dir, project_dir, plugin_id, project_file) {
             var targetDir = source_el.attrib['target-dir'] || '';
             var dest = path.join('www', 'plugins', plugin_id, targetDir, path.basename(source_el.attrib['src']));
-            var target_path = common.resolveTargetPath(project_dir, dest);
-            if (fs.existsSync(target_path)) throw new Error('"' + target_path + '" already exists!');
-            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+
+            common.copyNewFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
             // add reference to this file to jsproj.
             project_file.addSourceFile(dest);
         },

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/wp7.js
----------------------------------------------------------------------
diff --git a/src/platforms/wp7.js b/src/platforms/wp7.js
index b60697c..ebbd862 100644
--- a/src/platforms/wp7.js
+++ b/src/platforms/wp7.js
@@ -43,9 +43,8 @@ module.exports = {
     "source-file":{
         install:function(source_el, plugin_dir, project_dir, plugin_id, project_file) {
             var dest = path.join('Plugins', plugin_id, source_el.attrib['target-dir'] ? source_el.attrib['target-dir'] : '', path.basename(source_el.attrib['src']));
-            var target_path = common.resolveTargetPath(project_dir, dest);
-            if (fs.existsSync(target_path)) throw new Error('"' + target_path + '" already exists!');
-            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+
+            common.copyNewFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
             // add reference to this file to csproj.
             project_file.addSourceFile(dest);
         },

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/platforms/wp8.js
----------------------------------------------------------------------
diff --git a/src/platforms/wp8.js b/src/platforms/wp8.js
index 4a74a1f..8eb62c2 100644
--- a/src/platforms/wp8.js
+++ b/src/platforms/wp8.js
@@ -43,9 +43,8 @@ module.exports = {
     "source-file":{
         install:function(source_el, plugin_dir, project_dir, plugin_id, project_file) {
             var dest = path.join('Plugins', plugin_id, source_el.attrib['target-dir'] ? source_el.attrib['target-dir'] : '', path.basename(source_el.attrib['src']));
-            var target_path = common.resolveTargetPath(project_dir, dest);
-            if (fs.existsSync(target_path)) throw new Error('"' + target_path + '" already exists!');
-            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+
+            common.copyNewFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
             // add reference to this file to csproj.
             project_file.addSourceFile(dest);
         },

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/prepare.js
----------------------------------------------------------------------
diff --git a/src/prepare.js b/src/prepare.js
index 46e9487..3fbb7c0 100644
--- a/src/prepare.js
+++ b/src/prepare.js
@@ -30,6 +30,7 @@ var platform_modules = require('./platforms'),
     fs              = require('fs'),
     shell           = require('shelljs'),
     util            = require('util'),
+    events          = require('./events'),
     plugman         = require('../plugman'),
     et              = require('elementtree');
 
@@ -47,7 +48,7 @@ module.exports = function handlePrepare(project_dir, platform, plugins_dir, www_
     // - For each js-module (general first, then platform) build up an object storing the path and any clobbers, merges and runs for it.
     // - Write this object into www/cordova_plugins.json.
     // - Cordova.js contains code to load them at runtime from that file.
-    plugman.emit('verbose', 'Preparing ' + platform + ' project');
+    events.emit('verbose', 'Preparing ' + platform + ' project');
     var platform_json = config_changes.get_platform_json(plugins_dir, platform);
     var wwwDir = www_dir || platform_modules[platform].www_dir(project_dir);
 
@@ -60,17 +61,15 @@ module.exports = function handlePrepare(project_dir, platform, plugins_dir, www_
             plugins_to_uninstall.forEach(function(plug) {
                 var id = plug.id;
                 var plugin_modules = path.join(plugins_www, id);
-                if (!fs.existsSync(plugin_modules)) {
-                    plugman.emit('verbose', 'There is no directory "'+plugin_modules+'"');
-                    return;
+                if (fs.existsSync(plugin_modules)) {
+                    events.emit('verbose', 'Removing plugins directory from www "'+plugin_modules+'"');
+                    shell.rm('-rf', plugin_modules);
                 }
-                plugman.emit('verbose', 'Removing plugins directory from www "'+plugin_modules+'"');
-                shell.rm('-rf', plugin_modules);
             });
         }
     }
 
-    plugman.emit('verbose', 'Processing configuration changes for plugins.');
+    events.emit('verbose', 'Processing configuration changes for plugins.');
     config_changes.process(plugins_dir, project_dir, platform);
 
     // for windows phone plaform we need to add all www resources to the .csproj file
@@ -123,7 +122,7 @@ module.exports = function handlePrepare(project_dir, platform, plugins_dir, www_
     var plugins = Object.keys(platform_json.installed_plugins).concat(Object.keys(platform_json.dependent_plugins));
     var moduleObjects = [];
     var pluginMetadata = {};
-    plugman.emit('verbose', 'Iterating over installed plugins:', plugins);
+    events.emit('verbose', 'Iterating over installed plugins:', plugins);
 
     plugins && plugins.forEach(function(plugin) {
         var pluginDir = path.join(plugins_dir, plugin),
@@ -155,7 +154,7 @@ module.exports = function handlePrepare(project_dir, platform, plugins_dir, www_
 
         // Copy www assets described in <asset> tags.
         assets = assets || [];
-        assets.forEach(function(asset) {
+        assets.forEach(function(asset) {					
             common.asset.install(asset, pluginDir, wwwDir);
         });
 
@@ -223,7 +222,7 @@ module.exports = function handlePrepare(project_dir, platform, plugins_dir, www_
     final_contents += '// BOTTOM OF METADATA\n';
     final_contents += '});'; // Close cordova.define.
 
-    plugman.emit('verbose', 'Writing out cordova_plugins.js...');
+    events.emit('verbose', 'Writing out cordova_plugins.js...');
     fs.writeFileSync(path.join(wwwDir, 'cordova_plugins.js'), final_contents, 'utf-8');
 
     if(platform == 'wp7' || platform == 'wp8' || platform == "windows8") {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/uninstall.js
----------------------------------------------------------------------
diff --git a/src/uninstall.js b/src/uninstall.js
index 0d55670..18d7a18 100644
--- a/src/uninstall.js
+++ b/src/uninstall.js
@@ -1,3 +1,4 @@
+
 var path = require('path'),
     fs   = require('fs'),
     et   = require('elementtree'),
@@ -9,85 +10,124 @@ var path = require('path'),
     underscore = require('underscore'),
     Q = require('q'),
     plugins = require('./util/plugins'),
-    platform_modules = require('./platforms');
+    underscore = require('underscore'),
+    events = require('./events'),
+    platform_modules = require('./platforms'),
+    plugman = require('../plugman');
 
 // possible options: cli_variables, www_dir
 // Returns a promise.
 module.exports = function(platform, project_dir, id, plugins_dir, options) {
+    options = options || {};
+    options.is_top_level = true;
+    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
+
+    // Allow path to file to grab an ID
+    var xml_path = path.join(id, 'plugin.xml');
+    if ( fs.existsSync(xml_path) ) {
+        var plugin_et  = xml_helpers.parseElementtreeSync(xml_path),
+        id = plugin_et._root.attrib['id'];
+    }
+
     return module.exports.uninstallPlatform(platform, project_dir, id, plugins_dir, options)
-    .then(function(uninstalled) {
-        return module.exports.uninstallPlugin(id, plugins_dir);
+    .then(function() {
+        return module.exports.uninstallPlugin(id, plugins_dir, options);
     });
 }
 
 // Returns a promise.
 module.exports.uninstallPlatform = function(platform, project_dir, id, plugins_dir, options) {
     options = options || {};
+    options.is_top_level = true;
+    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
+
     if (!platform_modules[platform]) {
         return Q.reject(new Error(platform + " not supported."));
     }
 
     var plugin_dir = path.join(plugins_dir, id);
-
     if (!fs.existsSync(plugin_dir)) {
         return Q.reject(new Error('Plugin "' + id + '" not found. Already uninstalled?'));
     }
 
     var current_stack = new action_stack();
 
-    options.is_top_level = true;
-    return runUninstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
+    return runUninstallPlatform(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
 };
 
 // Returns a promise.
-module.exports.uninstallPlugin = function(id, plugins_dir) {
+module.exports.uninstallPlugin = function(id, plugins_dir, options) {
+    options = options || {};
+
     var plugin_dir = path.join(plugins_dir, id);
+
     // If already removed, skip.
     if (!fs.existsSync(plugin_dir)) {
         return Q();
     }
-    var xml_path     = path.join(plugin_dir, 'plugin.xml')
-      , plugin_et    = xml_helpers.parseElementtreeSync(xml_path);
 
-    require('../plugman').emit('log', 'Deleting plugin ' + id);
+    var xml_path  = path.join(plugin_dir, 'plugin.xml')
+      , plugin_et = xml_helpers.parseElementtreeSync(xml_path);
+
+    events.emit('log', 'Deleting "'+ id +'"');
 
     var doDelete = function(id) {
         var plugin_dir = path.join(plugins_dir, id);
-        if (!fs.existsSync(plugin_dir)) return;
+        if ( !fs.existsSync(plugin_dir) ) 
+            return;
+
         shell.rm('-rf', plugin_dir);
-        require('../plugman').emit('verbose', id + ' deleted.');
+        events.emit('verbose', '"'+ id +'" deleted.');
     };
 
     // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
     // Instead, we list all dependencies of the target plugin, and check the remaining metadata to see if
     // anything depends on them, or if they're listed as top-level.
     // If neither, they can be deleted.
+    var top_plugin_id = id;
     var toDelete = plugin_et.findall('dependency');
     toDelete = toDelete && toDelete.length ? toDelete.map(function(p) { return p.attrib.id; }) : [];
-    toDelete.push(id);
+    toDelete.push(top_plugin_id);
 
     // Okay, now we check if any of these are depended on, or top-level.
     // Find the installed platforms by whether they have a metadata file.
-    var platforms = Object.keys(platform_modules).filter(function(plat) {
-        return fs.existsSync(path.join(plugins_dir, plat + '.json'));
+    var platforms = Object.keys(platform_modules).filter(function(platform) {
+        return fs.existsSync(path.join(plugins_dir, platform + '.json'));
     });
 
-    var found = [];
-    platforms.forEach(function(plat) {
-        var tlps = dependencies.generate_dependency_info(plugins_dir, plat).top_level_plugins;
+    var dependList = {};
+    platforms.forEach(function(platform) {				   
+        var depsInfo = dependencies.generate_dependency_info(plugins_dir, platform);
+        var tlps = depsInfo.top_level_plugins,
+            deps, i;
+
         toDelete.forEach(function(plugin) {
-            if (tlps.indexOf(plugin) >= 0 || dependencies.dependents(plugin, plugins_dir, plat).length) {
-                found.push(plugin);
+            deps = dependencies.dependents(plugin, depsInfo);
+            var i = deps.indexOf(top_plugin_id);
+            if(i >= 0)
+                 deps.splice(i, 1); // remove current/top-level plugin as blocking uninstall
+
+            if(deps.length) {
+                dependList[plugin] = deps.join(', ');
             }
         });
     });
 
-    var danglers = underscore.difference(toDelete, found);
-    if (danglers && danglers.length) {
-        require('../plugman').emit('log', 'Found ' + danglers.length + ' removable plugins. Deleting them.');
-        danglers.forEach(doDelete);
-    } else {
-        require('../plugman').emit('log', 'No dangling plugins to remove.');
+    var i, plugin_id, msg;
+    for(i in toDelete) {
+        plugin_id = toDelete[i];
+
+        if( dependList[plugin_id] ) {
+            msg = '"' + plugin_id + '" is required by ('+ dependList[plugin_id] + ')';
+            if(options.force) {
+                events.emit('log', msg +' but forcing removal.');
+            } else {
+                events.emit('warn', msg +' and cannot be removed (hint: use -f or --force)');
+                continue;
+            }
+        }
+
+        doDelete(plugin_id);
     }
 
     return Q();
@@ -95,36 +135,47 @@ module.exports.uninstallPlugin = function(id, plugins_dir) {
 
 // possible options: cli_variables, www_dir, is_top_level
 // Returns a promise
-function runUninstall(actions, platform, project_dir, plugin_dir, plugins_dir, options) {
-    var xml_path     = path.join(plugin_dir, 'plugin.xml')
-      , plugin_et    = xml_helpers.parseElementtreeSync(xml_path);
-    var plugin_id    = plugin_et._root.attrib['id'];
+function runUninstallPlatform(actions, platform, project_dir, plugin_dir, plugins_dir, options) {
     options = options || {};
 
+    var xml_path     = path.join(plugin_dir, 'plugin.xml');
+    var plugin_et    = xml_helpers.parseElementtreeSync(xml_path);
+    var plugin_id    = plugin_et._root.attrib['id'];
+
+    // deps info can be passed recusively
+    var depsInfo = options.depsInfo || dependencies.generate_dependency_info(plugins_dir, platform, 'remove');
+
     // Check that this plugin has no dependents.
-    var dependents = dependencies.dependents(plugin_id, plugins_dir, platform);
+    var dependents = dependencies.dependents(plugin_id, depsInfo, platform);
+
     if(options.is_top_level && dependents && dependents.length > 0) {
-        require('../plugman').emit('verbose', 'Other top-level plugins (' + dependents.join(', ') + ') depend on ' + plugin_id + ', skipping uninstallation.');
-        return Q();
+        var msg = "The plugin '"+ plugin_id +"' is required by (" + dependents.join(', ') + ")";
+        if(options.force) {
+            events.emit("info", msg + " but forcing removal");	
+        } else {
+            return Q.reject( new Error(msg + ", skipping uninstallation.") );
+        }
     }
 
     // Check how many dangling dependencies this plugin has.
-    var dependency_info = dependencies.generate_dependency_info(plugins_dir, platform);
-    var deps = dependency_info.graph.getChain(plugin_id);
-    var danglers = dependencies.danglers(plugin_id, plugins_dir, platform);
+    var deps = depsInfo.graph.getChain(plugin_id);
+    var danglers = dependencies.danglers(plugin_id, depsInfo, platform);
 
     var promise;
     if (deps && deps.length && danglers && danglers.length) {
-        require('../plugman').emit('log', 'Uninstalling ' + danglers.length + ' dangling dependent plugins.');
+        
+        // @tests - important this event is checked spec/uninstall.spec.js
+        events.emit('log', 'Uninstalling ' + danglers.length + ' dependent plugins.');
         promise = Q.all(
             danglers.map(function(dangler) {
-                var dependent_path = path.join(plugins_dir, dangler);
-                var opts = {
-                    www_dir: options.www_dir,
-                    cli_variables: options.cli_variables,
-                    is_top_level: dependency_info.top_level_plugins.indexOf(dangler) > -1
-                };
-                return runUninstall(actions, platform, project_dir, dependent_path, plugins_dir, opts);
+                var dependent_path = dependencies.resolvePath(dangler, plugins_dir);
+
+                var opts = underscore.extend({}, options, { 
+                    is_top_level: depsInfo.top_level_plugins.indexOf(dangler) > -1,
+                    depsInfo: depsInfo
+                });
+
+                return runUninstallPlatform(actions, platform, project_dir, dependent_path, plugins_dir, opts);
             })
         );
     } else {
@@ -142,7 +193,7 @@ function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, w
     var handler = platform_modules[platform];
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
     www_dir = www_dir || handler.www_dir(project_dir);
-    require('../plugman').emit('log', 'Uninstalling ' + plugin_id + ' from ' + platform);
+    events.emit('log', 'Uninstalling ' + plugin_id + ' from ' + platform);
     
     var assets = plugin_et.findall('./asset');
     if (platformTag) {
@@ -201,11 +252,10 @@ function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, w
     return actions.process(platform, project_dir)
     .then(function() {
         // WIN!
-        require('../plugman').emit('verbose', plugin_id + ' uninstalled from ' + platform + '.');
+        events.emit('verbose', plugin_id + ' uninstalled from ' + platform + '.');
         // queue up the plugin so prepare can remove the config changes
-        config_changes.add_uninstalled_plugin_to_prepare_queue(plugins_dir, path.basename(plugin_dir), platform, is_top_level);
+        config_changes.add_uninstalled_plugin_to_prepare_queue(plugins_dir, plugin_id, platform, is_top_level);
         // call prepare after a successful uninstall
-        require('./../plugman').prepare(project_dir, platform, plugins_dir, www_dir);
+        plugman.prepare(project_dir, platform, plugins_dir, www_dir);
     });
 }
-

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/util/default-engines.js
----------------------------------------------------------------------
diff --git a/src/util/default-engines.js b/src/util/default-engines.js
index 2ca8b13..926c206 100644
--- a/src/util/default-engines.js
+++ b/src/util/default-engines.js
@@ -3,10 +3,9 @@ var path = require('path');
 module.exports = function(project_dir){
     return {
         'cordova': 
-            { 'platform':'*', 'scriptSrc': path.join(project_dir,'cordova','version') },   
-        // no location needed for plugman as this should be the calling process
+            { 'platform':'*', 'scriptSrc': path.join(project_dir,'cordova','version') },
         'cordova-plugman': 
-            { 'platform':'*', 'currentVersion': process.version },
+            { 'platform':'*', 'currentVersion': require('../../package.json').version },
         'cordova-android': 
             { 'platform':'android', 'scriptSrc': path.join(project_dir,'cordova','version') },
         'cordova-ios': 

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/util/dependencies.js
----------------------------------------------------------------------
diff --git a/src/util/dependencies.js b/src/util/dependencies.js
index acd7816..8abbc5b 100644
--- a/src/util/dependencies.js
+++ b/src/util/dependencies.js
@@ -4,33 +4,43 @@ var dep_graph = require('dep-graph'),
     plugman = require('../../plugman'),
     config_changes = require('./config-changes'),
     underscore = require('underscore'),
-    xml_helpers = require('./xml-helpers');
+    xml_helpers = require('./xml-helpers'),
+    package;
+
+module.exports = package = {
+
+    resolvePath: function(plugin_id, plugins_dir)
+    {
+        return path.join(plugins_dir, plugin_id);
+    },
+
+    resolveConfig: function(plugin_id, plugins_dir)
+    {
+        return path.join(plugins_dir, plugin_id, 'plugin.xml');
+    },
 
-module.exports = {
     generate_dependency_info:function(plugins_dir, platform) {
         var json = config_changes.get_platform_json(plugins_dir, platform);
+
+        // TODO: store whole dependency tree in plugins/[platform].json
+        // in case plugins are forcefully removed...
         var tlps = [];
         var graph = new dep_graph();
-        Object.keys(json.installed_plugins).forEach(function(tlp) {
-            tlps.push(tlp);
-            var xml = xml_helpers.parseElementtreeSync(path.join(plugins_dir, tlp, 'plugin.xml'));
+        Object.keys(json.installed_plugins).forEach(function(plugin_id) {
+            tlps.push(plugin_id);
+
+            var xml = xml_helpers.parseElementtreeSync( package.resolveConfig(plugin_id, plugins_dir) );
             var deps = xml.findall('dependency');
+
             deps && deps.forEach(function(dep) {
-                var id = dep.attrib.id;
-                graph.add(tlp, id);
+                graph.add(plugin_id, dep.attrib.id);
             });
         });
-        Object.keys(json.dependent_plugins).forEach(function(plug) {
-            var pluginXML = path.join(plugins_dir, plug, 'plugin.xml');
-            if (!fs.existsSync(pluginXML)) {
-                plugman.emit('verbose', 'Could not find "' + pluginXML + '"');
-                return;
-            }
-            var xml = xml_helpers.parseElementtreeSync(pluginXML);
+        Object.keys(json.dependent_plugins).forEach(function(plugin_id) {
+            var xml = xml_helpers.parseElementtreeSync( package.resolveConfig(plugin_id, plugins_dir) );
             var deps = xml.findall('dependency');
             deps && deps.forEach(function(dep) {
-                var id = dep.attrib.id;
-                graph.add(plug, id);
+                graph.add(plugin_id, dep.attrib.id);
             });
         });
 
@@ -42,10 +52,13 @@ module.exports = {
 
     // Returns a list of top-level plugins which are (transitively) dependent on the given plugin.
     dependents: function(plugin_id, plugins_dir, platform) {
-        var dependency_info = module.exports.generate_dependency_info(plugins_dir, platform);
-        var graph = dependency_info.graph;
+        if(typeof plugins_dir == 'object')
+            var depsInfo = plugins_dir;
+        else
+            var depsInfo = package.generate_dependency_info(plugins_dir, platform);
 
-        var tlps = dependency_info.top_level_plugins;
+        var graph = depsInfo.graph;
+        var tlps = depsInfo.top_level_plugins;
         var dependents = tlps.filter(function(tlp) {
             return tlp != plugin_id && graph.getChain(tlp).indexOf(plugin_id) >= 0;
         });
@@ -56,11 +69,15 @@ module.exports = {
     // Returns a list of plugins which the given plugin depends on, for which it is the only dependent.
     // In other words, if the given plugin were deleted, these dangling dependencies should be deleted too.
     danglers: function(plugin_id, plugins_dir, platform) {
-        var dependency_info = module.exports.generate_dependency_info(plugins_dir, platform);
-        var graph = dependency_info.graph;
+        if(typeof plugins_dir == 'object')
+            var depsInfo = plugins_dir;
+        else
+            var depsInfo = package.generate_dependency_info(plugins_dir, platform);
+
+        var graph = depsInfo.graph;
         var dependencies = graph.getChain(plugin_id);
 
-        var tlps = dependency_info.top_level_plugins;
+        var tlps = depsInfo.top_level_plugins;
         var diff_arr = [];
         tlps.forEach(function(tlp) {
             if (tlp != plugin_id) {
@@ -76,4 +93,4 @@ module.exports = {
         danglers = danglers && danglers.filter(function(x) { return tlps.indexOf(x) < 0; });
         return danglers;
     }
-};
+};
\ No newline at end of file


[4/4] git commit: Fix to never remove top-level plugins that are dependencies + tests.

Posted by ag...@apache.org.
Fix to never remove top-level plugins that are dependencies + tests.

github: close #58


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

Branch: refs/heads/master
Commit: a4a51d7ce8200594bf90ecc524afbfc899c4e361
Parents: f58db0a
Author: jbondc <jb...@openmv.com>
Authored: Sun Mar 9 18:31:01 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Mon Mar 10 16:30:30 2014 -0400

----------------------------------------------------------------------
 spec/common.js         | 41 ++++++++++++++++-------
 spec/install.spec.js   |  9 ++---
 spec/uninstall.spec.js | 80 ++++++++++++++++++++++++++++++++++++---------
 src/uninstall.js       | 49 ++++++++++++++++++++-------
 4 files changed, 135 insertions(+), 44 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/a4a51d7c/spec/common.js
----------------------------------------------------------------------
diff --git a/spec/common.js b/spec/common.js
index 2991806..6a87e23 100644
--- a/spec/common.js
+++ b/spec/common.js
@@ -29,15 +29,34 @@ if(opt.debug) {
         plugman.on('log', console.log);
 }
 
-module.exports = {
-	spy: { 
-		getInstall: function(emitSpy){
-			var install = [], i;
-			for(i in emitSpy.argsForCall) {
-				if(emitSpy.argsForCall[i][1].substr(0, 13) === 'Install start')
-					install.push(emitSpy.argsForCall[i][1]);
-			}
-			return install;
-		}
-	}
+module.exports = common = {
+    spy: { 
+        getInstall: function(emitSpy){
+            return common.spy.startsWith(emitSpy, 'Install start');
+        },
+
+        getDeleted: function(emitSpy){
+            return common.spy.startsWith(emitSpy, 'Deleted');
+        },
+
+        startsWith: function(emitSpy, string)
+        {
+            var match = [], i;
+            for(i in emitSpy.argsForCall) {
+                if(emitSpy.argsForCall[i][1].substr(0, string.length) === string)
+                    match.push(emitSpy.argsForCall[i][1]);
+            }
+            return match;
+        },
+
+        contains: function(emitSpy, string)
+        {
+            var match = [], i;
+            for(i in emitSpy.argsForCall) {
+                if(emitSpy.argsForCall[i][1].indexOf(string) >= 0)
+                    match.push(emitSpy.argsForCall[i][1]);
+            }
+            return match;
+        }
+    }
 };

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/a4a51d7c/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index 5a40de3..f552c49 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -411,17 +411,14 @@ describe('end', function() {
                          
     it('end', function() {
         done = false;
-        var finish = function(err){
+
+        promise.fin(function(err){
             if(err)
                 events.emit('error', err);
 
             shell.rm('-rf', project);
             done = true;	
-        }
-
-        promise.then( 
-
-        ).fin(finish);
+        });
 
         waitsFor(function() { return done; }, 'promise never resolved', 500);
     });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/a4a51d7c/spec/uninstall.spec.js
----------------------------------------------------------------------
diff --git a/spec/uninstall.spec.js b/spec/uninstall.spec.js
index 7ae8a14..bf9aa7f 100644
--- a/spec/uninstall.spec.js
+++ b/spec/uninstall.spec.js
@@ -13,9 +13,12 @@ var uninstall = require('../src/uninstall'),
     done    = false,
     srcProject = path.join(spec, 'projects', 'android_uninstall'),
     project = path.join(spec, 'projects', 'android_uninstall.test'),
+    project2 = path.join(spec, 'projects', 'android_uninstall.test2'),
 
     plugins_dir = path.join(spec, 'plugins'),
     plugins_install_dir = path.join(project, 'cordova', 'plugins'),	
+    plugins_install_dir2 = path.join(project2, 'cordova', 'plugins'),	
+
     plugins = {
         'DummyPlugin' : path.join(plugins_dir, 'DummyPlugin'),
         'A' : path.join(plugins_dir, 'dependencies', 'A'),
@@ -32,7 +35,9 @@ describe('start', function() {
 
     it('start', function() {
         shell.rm('-rf', project);
+        shell.rm('-rf', project2);
         shell.cp('-R', path.join(srcProject, '*'), project);
+        shell.cp('-R', path.join(srcProject, '*'), project2);
 
         done = false;
         promise = Q()
@@ -41,6 +46,10 @@ describe('start', function() {
         ).then(
             function(){ return install('android', project, plugins['A']) }
         ).then(
+            function(){ return install('android', project2, plugins['C']) }
+        ).then(
+            function(){ return install('android', project2, plugins['A']) }
+        ).then(
             function(){ done = true; }
         );
         waitsFor(function() { return done; }, 'promise never resolved', 500);
@@ -134,28 +143,67 @@ describe('uninstallPlatform', function() {
 });
 
 describe('uninstallPlugin', function() {							 
-    var rm, fsWrite, rmstack = [];
+    var rm, fsWrite, rmstack = [], emit;
 
     beforeEach(function() {
         fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
         rm = spyOn(shell, 'rm').andCallFake(function(f,p) { rmstack.push(p); return true});
         rmstack = [];
+        emit = spyOn(events, 'emit');
         done = false;
     });
     describe('with dependencies', function() {
 
-        it('should delete all dangling plugins', function() {
-                                              
+        it('should delete all dependent plugins', function() {                              
             runs(function() {
                 uninstallPromise( uninstall.uninstallPlugin('A', plugins_install_dir) );
             });
             waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
-                expect(done).toEqual(true);
-                expect(rmstack[0]).toBe( path.join(plugins_install_dir, 'C') );
-                expect(rmstack[1]).toBe( path.join(plugins_install_dir, 'D') );
-                expect(rmstack[2]).toBe( path.join(plugins_install_dir, 'A') );
-                expect(rm.calls.length).toBe(3);
+                var del = common.spy.getDeleted(emit);
+
+                expect(del).toEqual([
+                    'Deleted "C"',
+                    'Deleted "D"',
+                    'Deleted "A"'
+                ]);
+            });
+        });
+
+        it("should fail if plugin is a required dependency", function() {  
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('C', plugins_install_dir) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(done.message).toBe('"C" is required by (A) and cannot be removed (hint: use -f or --force)');
+            });
+        });
+
+        it("allow forcefully removing a plugin", function() {  
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('C', plugins_install_dir, {force: true}) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(done).toBe(true);
+                var del = common.spy.getDeleted(emit);
+                expect(del).toEqual(['Deleted "C"']);
+            });
+        });		
+
+        it("never remove top level plugins if they are a dependency", function() {  
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('A', plugins_install_dir2) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                var del = common.spy.getDeleted(emit);
+
+                expect(del).toEqual([
+                    'Deleted "D"',
+                    'Deleted "A"'
+                ]);
             });
         });
     });
@@ -208,13 +256,6 @@ describe('end', function() {
                          
     it('end', function() {
         done = false;
-        var finish = function(err){
-            if(err)
-                plugman.emit('error', err);
-
-            shell.rm('-rf', project);
-            done = true;	
-        }
 
         promise.then( 
             function(){ 
@@ -234,7 +275,14 @@ describe('end', function() {
                 // dependencies on C,D ... should this only work with --recursive? prompt user..?
                 return uninstall('android', project, plugins['A']) 
             }
-        ).fin(finish);
+        ).fin(function(err){
+            if(err)
+                plugman.emit('error', err);
+
+            shell.rm('-rf', project);
+            shell.rm('-rf', project2);
+            done = true;	
+        });
 
         waitsFor(function() { return done; }, 'promise never resolved', 500);
     });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/a4a51d7c/src/uninstall.js
----------------------------------------------------------------------
diff --git a/src/uninstall.js b/src/uninstall.js
index 18d7a18..98c57ad 100644
--- a/src/uninstall.js
+++ b/src/uninstall.js
@@ -61,23 +61,27 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
 
     var plugin_dir = path.join(plugins_dir, id);
 
+    // @tests - important this event is checked spec/uninstall.spec.js
+    events.emit('log', 'Removing "'+ id +'"');
+
     // If already removed, skip.
-    if (!fs.existsSync(plugin_dir)) {
+    if ( !fs.existsSync(plugin_dir) ) {
+        events.emit('verbose', 'Plugin "'+ id +'" already removed ('+ plugin_dir +')');
         return Q();
     }
 
     var xml_path  = path.join(plugin_dir, 'plugin.xml')
       , plugin_et = xml_helpers.parseElementtreeSync(xml_path);
 
-    events.emit('log', 'Deleting "'+ id +'"');
-
     var doDelete = function(id) {
         var plugin_dir = path.join(plugins_dir, id);
-        if ( !fs.existsSync(plugin_dir) ) 
-            return;
+        if ( !fs.existsSync(plugin_dir) ) {
+            events.emit('verbose', 'Plugin "'+ id +'" already removed ('+ plugin_dir +')');
+            return Q();
+        }
 
         shell.rm('-rf', plugin_dir);
-        events.emit('verbose', '"'+ id +'" deleted.');
+        events.emit('verbose', 'Deleted "'+ id +'"');
     };
 
     // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
@@ -85,7 +89,11 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
     // anything depends on them, or if they're listed as top-level.
     // If neither, they can be deleted.
     var top_plugin_id = id;
-    var toDelete = plugin_et.findall('dependency');
+
+    // Recursively remove plugins which were installed as dependents (that are not top-level)
+    // optional?
+    var recursive = true;
+    var toDelete = recursive ? plugin_et.findall('dependency') : [];
     toDelete = toDelete && toDelete.length ? toDelete.map(function(p) { return p.attrib.id; }) : [];
     toDelete.push(top_plugin_id);
 
@@ -95,14 +103,26 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
         return fs.existsSync(path.join(plugins_dir, platform + '.json'));
     });
 
+    // Can have missing plugins on some platforms when not supported..
     var dependList = {};
-    platforms.forEach(function(platform) {				   
+    platforms.forEach(function(platform) {
         var depsInfo = dependencies.generate_dependency_info(plugins_dir, platform);
         var tlps = depsInfo.top_level_plugins,
             deps, i;
 
+        // Top-level deps must always be explicitely asked to remove by user
+        tlps.forEach(function(plugin_id){
+            if(top_plugin_id == plugin_id)
+                return;
+
+            var i = toDelete.indexOf(plugin_id);
+            if(i >= 0)
+                toDelete.splice(i, 1);
+        });
+
         toDelete.forEach(function(plugin) {
             deps = dependencies.dependents(plugin, depsInfo);
+
             var i = deps.indexOf(top_plugin_id);
             if(i >= 0)
                  deps.splice(i, 1); // remove current/top-level plugin as blocking uninstall
@@ -122,8 +142,15 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
             if(options.force) {
                 events.emit('log', msg +' but forcing removal.');
             } else {
-                events.emit('warn', msg +' and cannot be removed (hint: use -f or --force)');
-                continue;
+                // @tests - error and event message is checked spec/uninstall.spec.js
+                msg += ' and cannot be removed (hint: use -f or --force)';
+
+                if(plugin_id == top_plugin_id) {
+                    return Q.reject( new Error(msg) );
+                } else {
+                    events.emit('warn', msg +' and cannot be removed (hint: use -f or --force)');
+                    continue;	
+                }
             }
         }
 
@@ -163,7 +190,7 @@ function runUninstallPlatform(actions, platform, project_dir, plugin_dir, plugin
 
     var promise;
     if (deps && deps.length && danglers && danglers.length) {
-        
+
         // @tests - important this event is checked spec/uninstall.spec.js
         events.emit('log', 'Uninstalling ' + danglers.length + ' dependent plugins.');
         promise = Q.all(


[3/4] git commit: Refactoring of install & uninstall tests

Posted by ag...@apache.org.
Refactoring of install & uninstall tests


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

Branch: refs/heads/master
Commit: ae2ce7ac6325d1c6558365b4be82e5a8ff0983b9
Parents: 7e26c88
Author: jbondc <jb...@openmv.com>
Authored: Fri Feb 28 12:44:00 2014 -0500
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Mon Mar 10 16:29:27 2014 -0400

----------------------------------------------------------------------
 plugman.js                                      |  11 +-
 spec/common.js                                  |  43 ++
 spec/install.spec.js                            | 513 +++++++++++--------
 .../android_install/AndroidManifest.xml         |  20 +
 .../android_install/cordova/android_sdk_version |   1 +
 spec/projects/android_install/cordova/version   |   1 +
 .../android_install/cordova/version.bat         |   2 +
 .../android_uninstall/AndroidManifest.xml       |  20 +
 spec/projects/android_uninstall/cordova/version |   1 +
 .../android_uninstall/cordova/version.bat       |   2 +
 spec/uninstall.spec.js                          | 338 +++++-------
 src/events.js                                   |   3 +-
 src/install.js                                  | 458 +++++++++++------
 src/platforms/android.js                        |   5 +-
 src/platforms/blackberry10.js                   |   5 +-
 src/platforms/common.js                         |  23 +-
 src/platforms/windows8.js                       |   5 +-
 src/platforms/wp7.js                            |   5 +-
 src/platforms/wp8.js                            |   5 +-
 src/prepare.js                                  |  19 +-
 src/uninstall.js                                | 148 ++++--
 src/util/default-engines.js                     |   5 +-
 src/util/dependencies.js                        |  63 ++-
 23 files changed, 1017 insertions(+), 679 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/plugman.js
----------------------------------------------------------------------
diff --git a/plugman.js b/plugman.js
index 5ceac60..4aa1843 100644
--- a/plugman.js
+++ b/plugman.js
@@ -19,7 +19,7 @@
 
 // copyright (c) 2013 Andrew Lunny, Adobe Systems
 
-var emitter = require('./src/events');
+var events = require('./src/events');
 var Q = require('q');
 
 function addProperty(o, symbol, modulePath, doWrap) {
@@ -53,12 +53,13 @@ function addProperty(o, symbol, modulePath, doWrap) {
 }
 
 plugman = {
-    on:                 emitter.addListener,
-    off:                emitter.removeListener,
-    removeAllListeners: emitter.removeAllListeners,
-    emit:               emitter.emit,
+    on:                 events.on.bind(events),
+    off:                events.removeListener.bind(events),
+    removeAllListeners: events.removeAllListeners.bind(events),
+    emit:               events.emit.bind(events),
     raw:                {}
 };
+
 addProperty(plugman, 'help', './src/help');
 addProperty(plugman, 'install', './src/install', true);
 addProperty(plugman, 'uninstall', './src/uninstall', true);

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/common.js
----------------------------------------------------------------------
diff --git a/spec/common.js b/spec/common.js
new file mode 100644
index 0000000..2991806
--- /dev/null
+++ b/spec/common.js
@@ -0,0 +1,43 @@
+
+var plugman = require('../plugman'),
+    nopt = require('nopt');
+
+var known_opts = {
+    'verbose' : Boolean,
+    'debug' : Number
+}, shortHands = { 'd' : ['--debug'] };
+
+var opt = nopt(known_opts, shortHands);
+var mapNames = {
+    'verbose' : 7,
+    'info'    : 6,
+    'notice'  : 5,
+    'warn'    : 4,
+    'error'   : 3
+}
+
+if(opt.verbose)
+    opt.debug = 7;
+
+if(opt.debug) {
+    for(var i in mapNames) {
+        if(mapNames[i] <= opt.debug)
+            plugman.on(i, console.log);
+    }
+
+    if(opt.debug >= 6)
+        plugman.on('log', console.log);
+}
+
+module.exports = {
+	spy: { 
+		getInstall: function(emitSpy){
+			var install = [], i;
+			for(i in emitSpy.argsForCall) {
+				if(emitSpy.argsForCall[i][1].substr(0, 13) === 'Install start')
+					install.push(emitSpy.argsForCall[i][1]);
+			}
+			return install;
+		}
+	}
+};

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index 8401526..bff78c2 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -2,324 +2,407 @@ var install = require('../src/install'),
     actions = require('../src/util/action-stack'),
     config_changes = require('../src/util/config-changes'),
     xml_helpers = require('../src/util/xml-helpers'),
+    events  = require('../src/events'),
     plugman = require('../plugman'),
+    platforms = require('../src/platforms/common'),
+    common  = require('./common'),
     fs      = require('fs'),
-    os      = require('osenv'),
+    os      = require('os'),
     path    = require('path'),
     shell   = require('shelljs'),
     child_process = require('child_process'),
     semver  = require('semver'),
-    temp    = __dirname,
-    dummyplugin = 'DummyPlugin',
-    dummy_id = 'com.phonegap.plugins.dummyplugin',
-    variableplugin = 'VariablePlugin',
-    engineplugin = 'EnginePlugin',
-    childplugin = 'ChildBrowser',
     Q = require('q'),
-    plugins_dir = path.join(temp, 'plugins');
+    spec    = __dirname,	
+    done    = false,
+    srcProject = path.join(spec, 'projects', 'android_install'),
+    project = path.join(os.tmpdir(), 'plugman-test', 'android_install'),
 
-describe('install', function() {
-    var exists, get_json, chmod, exec, proc, add_to_queue, prepare, actions_push, c_a, mkdir, done;
+    plugins_dir = path.join(spec, 'plugins'),
+    plugins_install_dir = path.join(project, 'cordova', 'plugins'),	
+    plugins = {
+        'DummyPlugin' : path.join(plugins_dir, 'DummyPlugin'),
+        'EnginePlugin' : path.join(plugins_dir, 'EnginePlugin'),
+        'EnginePluginAndroid' : path.join(plugins_dir, 'EnginePluginAndroid'),
+        'ChildBrowser' : path.join(plugins_dir, 'ChildBrowser'),
+        'VariablePlugin' : path.join(plugins_dir, 'VariablePlugin'),
+        'A' : path.join(plugins_dir, 'dependencies', 'A'),
+        'C' : path.join(plugins_dir, 'dependencies', 'C'),
+        'F' : path.join(plugins_dir, 'dependencies', 'F'),
+        'G' : path.join(plugins_dir, 'dependencies', 'G')
+    },
+    promise,
+    results = {}, 
+    dummy_id = 'com.phonegap.plugins.dummyplugin';
+
+function installPromise(f) {
+  f.then(function() { done = true; }, function(err) { done = err; });
+}
+
+var existsSync = fs.existsSync;
+
+// Mocked functions for tests
+var fake = {
+    'existsSync' : {
+        'noPlugins' : function(path){
+            // fake installed plugin directories as 'not found'
+            if( path.slice(-5) !== '.json' && path.indexOf(plugins_install_dir) >= 0) {
+                return false;
+            }
 
-    function installPromise(f) {
-        f.then(function() { done = true; }, function(err) { done = err; });
+            return existsSync(path);
+        }
+    },
+    'fetch' : {
+        'dependencies' : function(id, dir) {
+            if(id == plugins['A'])
+                return Q(id); // full path to plugin
+
+            return Q( path.join(plugins_dir, 'dependencies', id) ); 
+        }
     }
+}
+
+describe('start', function() {	
+    var prepare, config_queue_add, proc, actions_push, ca, emit;
 
     beforeEach(function() {
-        proc = spyOn(actions.prototype, 'process').andCallFake(function(platform, proj) {
-            return Q();
-        });
-        mkdir = spyOn(shell, 'mkdir');
-        actions_push = spyOn(actions.prototype, 'push');
-        c_a = spyOn(actions.prototype, 'createAction');
         prepare = spyOn(plugman, 'prepare');
-        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, options, cb) {
-            if (!cb) cb = options;
+        config_queue_add = spyOn(config_changes, 'add_installed_plugin_to_prepare_queue');
+        proc = spyOn(actions.prototype, 'process').andReturn( Q(true) );
+        actions_push = spyOn(actions.prototype, 'push');
+        ca = spyOn(actions.prototype, 'createAction');
+    });
+    it('start', function() {
+        shell.rm('-rf', project);
+        shell.cp('-R', path.join(srcProject, '*'), project);
+
+        done = false;
+        promise = Q()
+         .then(
+            function(){ return install('android', project, plugins['DummyPlugin']) }
+        ).then(
+            function(){ 
+                results['actions_callCount'] = actions_push.callCount;
+                results['actions_create'] = ca.argsForCall[0];
+                results['config_add'] = config_queue_add.argsForCall[0];
+
+                return Q();
+            }
+        ).then(
+            function(){ return install('android', project, plugins['EnginePlugin']) }
+        ).then(
+            function(){
+                emit = spyOn(events, 'emit');
+                return install('android', project, plugins['ChildBrowser']) 
+            }
+        ).then(
+            function(){ 
+                return install('android', project, plugins['VariablePlugin'], plugins_install_dir, { cli_variables:{API_KEY:'batman'} }) 
+            }
+        ).then(
+            function(){ 
+                done = true;
+                results['prepareCount'] = prepare.callCount;
+                results['emit_results'] = [];
+
+                for(var i in emit.calls) {
+                    if(emit.calls[i].args[0] === 'results')
+                        results['emit_results'].push(emit.calls[i].args[1]);
+                }
+
+                events.emit("verbose", "***** DONE START *****");
+            }
+        );
+        waitsFor(function() { return done; }, 'promise never resolved', 500);		
+    });
+});
+
+describe('install', function() {
+    var chmod, exec, proc, add_to_queue, prepare, actions_push, c_a, mkdir, cp, rm, fetchSpy, emit;
+
+    beforeEach(function() {
+        prepare = spyOn(plugman, 'prepare').andReturn( Q(true) );
+    
+        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, cb) {
             cb(false, '', '');
         });
-        chmod = spyOn(fs, 'chmodSync');
-        exists = spyOn(fs, 'existsSync').andReturn(true);
-        get_json = spyOn(config_changes, 'get_platform_json').andReturn({
-            installed_plugins:{},
-            dependent_plugins:{}
-        });
+        spyOn(fs, 'mkdirSync').andReturn(true);
+        spyOn(shell, 'mkdir').andReturn(true);
+        spyOn(platforms, 'copyFile').andReturn(true);
+
+        fetchSpy = spyOn(plugman.raw, 'fetch').andReturn( Q( plugins['EnginePlugin'] ) );
+        chmod = spyOn(fs, 'chmodSync').andReturn(true);
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        cp = spyOn(shell, 'cp').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
         add_to_queue = spyOn(config_changes, 'add_installed_plugin_to_prepare_queue');
         done = false;
     });
+
     describe('success', function() {
         it('should call prepare after a successful install', function() {
-            runs(function() {
-                installPromise(install('android', temp, dummyplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(done).toBe(true);
-                expect(prepare).toHaveBeenCalled();
-            });
+           expect(results['prepareCount']).toBe(4);
         });
+
+        it('should emit a results event with platform-agnostic <info>', function() {
+            // ChildBrowser
+            expect(results['emit_results'][0]).toBe('No matter what platform you are installing to, this notice is very important.');
+        });
+        it('should emit a results event with platform-specific <info>', function() {
+            // ChildBrowser
+            expect(results['emit_results'][1]).toBe('Please make sure you read this because it is very important to complete the installation of your plugin.');
+        });
+        it('should interpolate variables into <info> tags', function() {
+            // VariableBrowser
+            expect(results['emit_results'][2]).toBe('Remember that your api key is batman!');
+        });
+
         it('should call fetch if provided plugin cannot be resolved locally', function() {
-            fetchSpy = spyOn(plugman.raw, 'fetch').andReturn(Q(path.join(plugins_dir, dummyplugin)));
-            exists.andReturn(false);
+            fetchSpy.andReturn( Q( plugins['DummyPlugin'] ) );
+            spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+
             runs(function() {
-                installPromise(install('android', temp, 'CLEANYOURSHORTS', plugins_dir, {}));
+                installPromise(install('android', project, 'CLEANYOURSHORTS' ));
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(done).toBe(true);
                 expect(fetchSpy).toHaveBeenCalled();
             });
         });
-        it('should call the config-changes module\'s add_installed_plugin_to_prepare_queue method after processing an install', function() {
-            runs(function() {
-                installPromise(install('android', temp, dummyplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(add_to_queue).toHaveBeenCalledWith(plugins_dir, 'DummyPlugin', 'android', {}, true);
-            });
+
+        it('should call the config-changes module\'s add_installed_plugin_to_prepare_queue method after processing an install', function() {																												
+           expect(results['config_add']).toEqual([plugins_install_dir, dummy_id, 'android', {}, true]);
         });
-        it('should notify if plugin is already installed into project', function() {
-            var spy = spyOn(plugman, 'emit');
-            get_json.andReturn({
-                installed_plugins:{
-                    'com.phonegap.plugins.dummyplugin':{}
-                },
-                dependent_plugins:{}
-            });
-            installPromise(install('android', temp, dummyplugin, plugins_dir, {}));
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(spy).toHaveBeenCalledWith('results', 'Plugin "'+dummy_id+'" already installed on android.');
-            });
+        it('should queue up actions as appropriate for that plugin and call process on the action stack', 			
+           function() {																											 				expect(results['actions_callCount']).toEqual(3);
+                expect(results['actions_create']).toEqual([jasmine.any(Function), [jasmine.any(Object), path.join(plugins_install_dir, dummy_id), project, dummy_id], jasmine.any(Function), [jasmine.any(Object), project, dummy_id]]);
         });
+
         it('should check version if plugin has engine tag', function(){
-            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            var satisfies = spyOn(semver, 'satisfies').andReturn(true);
             exec.andCallFake(function(cmd, cb) {
-                cb(null, '2.5.0\n', '');
+                cb(null, '2.5.0\n');
             });
+
             runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('android', project, plugins['EnginePlugin']) );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
-                expect(spy).toHaveBeenCalledWith('2.5.0','>=2.3.0');
+                expect(satisfies).toHaveBeenCalledWith('2.5.0','>=2.3.0');
             });
         });
         it('should check version and munge it a little if it has "rc" in it so it plays nice with semver (introduce a dash in it)', function() {
-            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            var satisfies = spyOn(semver, 'satisfies').andReturn(true);
             exec.andCallFake(function(cmd, cb) {
                 cb(null, '3.0.0rc1\n');
             });
+
             runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('android', project, plugins['EnginePlugin']) );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
-                expect(spy).toHaveBeenCalledWith('3.0.0-rc1','>=2.3.0');
+                expect(satisfies).toHaveBeenCalledWith('3.0.0-rc1','>=2.3.0');
             });
         });
         it('should check specific platform version over cordova version if specified', function() {
             var spy = spyOn(semver, 'satisfies').andReturn(true);
             exec.andCallFake(function(cmd, cb) {
-                cb(null, '3.1.0\n', '');
+                cb(null, '3.1.0\n');
             });
+            fetchSpy.andReturn( Q( plugins['EnginePluginAndroid'] ) );
+
             runs(function() {
-                installPromise(install('android', temp, 'EnginePluginAndroid', plugins_dir, {}));
+                installPromise( install('android', project, plugins['EnginePluginAndroid']) );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(spy).toHaveBeenCalledWith('3.1.0','>=3.1.0');
             });
         });
         it('should check platform sdk version if specified', function() {
             var spy = spyOn(semver, 'satisfies').andReturn(true);
+            fetchSpy.andReturn( Q( plugins['EnginePluginAndroid'] ) );
             exec.andCallFake(function(cmd, cb) {
-                cb(null, '18\n', '');
-            });
-            runs(function() {
-                installPromise(install('android', temp, 'EnginePluginAndroid', plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(spy).toHaveBeenCalledWith('18.0.0','>=18');
-            });
-        });
-        it('should check plugmans version', function() {
-            var spy = spyOn(semver, 'satisfies').andReturn(true);
-            runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(spy).toHaveBeenCalledWith('','>=0.10.0');
+                cb(null, '18\n');
             });
-        });
-        it('should check custom engine version', function() {
-            var spy = spyOn(semver, 'satisfies').andReturn(true);
+
             runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('android', project, 'EnginePluginAndroid') );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
-                expect(spy).toHaveBeenCalledWith('','>=1.0.0');
+                // <engine name="cordova" VERSION=">=3.0.0"/>
+                // <engine name="cordova-android" VERSION=">=3.1.0"/>
+                // <engine name="android-sdk" VERSION=">=18"/>
+
+                expect(spy.calls.length).toBe(3);
+                expect(spy.calls[0].args).toEqual([ '18.0.0', '>=3.0.0' ]);
+                expect(spy.calls[1].args).toEqual([ '18.0.0', '>=3.1.0' ]);
+                expect(spy.calls[2].args).toEqual([ '18.0.0','>=18' ]);
             });
         });
-        it('should check custom engine version that supports multiple platforms', function() {
+        it('should check engine versions', function() {
             var spy = spyOn(semver, 'satisfies').andReturn(true);
+            fetchSpy.andReturn( Q( plugins['EnginePlugin'] ) );
+
             runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('android', project, plugins['EnginePlugin']) );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
-                expect(spy).toHaveBeenCalledWith('','>=3.0.0');
+                // <engine name="cordova" version=">=2.3.0"/>
+                // <engine name="cordova-plugman" version=">=0.10.0" />
+                // <engine name="mega-fun-plugin" version=">=1.0.0" scriptSrc="megaFunVersion" platform="*" />
+                // <engine name="mega-boring-plugin" version=">=3.0.0" scriptSrc="megaBoringVersion" platform="ios|android" />
+
+				var plugmanVersion = require('../package.json').version;
+
+                expect(spy.calls.length).toBe(4);
+                expect(spy.calls[0].args).toEqual([ '', '>=2.3.0' ]);
+                expect(spy.calls[1].args).toEqual([ plugmanVersion, '>=0.10.0' ]);
+                expect(spy.calls[2].args).toEqual([ '', '>=1.0.0' ]);
+                expect(spy.calls[3].args).toEqual([ '', '>=3.0.0' ]);
             });
         });
         it('should not check custom engine version that is not supported for platform', function() {
             var spy = spyOn(semver, 'satisfies').andReturn(true);
             runs(function() {
-                installPromise(install('blackberry10', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('blackberry10', project, plugins['EnginePlugin']) );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(spy).not.toHaveBeenCalledWith('','>=3.0.0');
             });
         });
-        it('should queue up actions as appropriate for that plugin and call process on the action stack', function() {
-            runs(function() {
-                installPromise(install('android', temp, dummyplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(actions_push.calls.length).toEqual(3);
-                expect(c_a).toHaveBeenCalledWith(jasmine.any(Function), [jasmine.any(Object), path.join(plugins_dir, dummyplugin), temp, dummy_id], jasmine.any(Function), [jasmine.any(Object), temp, dummy_id]);
-                expect(proc).toHaveBeenCalled();
-            });
-        });
-        it('should emit a results event with platform-agnostic <info>', function() {
-            var emit = spyOn(plugman, 'emit');
-            runs(function() {
-                installPromise(install('android', temp, childplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(emit).toHaveBeenCalledWith('results', 'No matter what platform you are installing to, this notice is very important.');
-            });
-        });
-        it('should emit a results event with platform-specific <info>', function() {
-            var emit = spyOn(plugman, 'emit');
-            runs(function() {
-                installPromise(install('android', temp, childplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(emit).toHaveBeenCalledWith('results', 'Please make sure you read this because it is very important to complete the installation of your plugin.');
-            });
-        });
-        it('should interpolate variables into <info> tags', function() {
-            var emit = spyOn(plugman, 'emit');
-            runs(function() {
-                installPromise(install('android', temp, variableplugin, plugins_dir, {cli_variables:{API_KEY:'batman'}}));
-            });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
-            runs(function() {
-                expect(emit).toHaveBeenCalledWith('results', 'Remember that your api key is batman!');
-            });
-        });
 
         describe('with dependencies', function() {
-            it('should process all dependent plugins', function() {
-                // Plugin A depends on C & D
-                runs(function() {
-                    installPromise(install('android', temp, 'A', path.join(plugins_dir, 'dependencies'), {}));
-                });
-                waitsFor(function() { return done; }, 'install promise never resolved', 500);
-                runs(function() {
-                    // So process should be called 3 times
-                    expect(proc.calls.length).toEqual(3);
+            it('should install any dependent plugins if missing', function() {
+                spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+                fetchSpy.andCallFake( fake['fetch']['dependencies'] );
+                emit = spyOn(events, 'emit');
+
+                exec.andCallFake(function(cmd, cb) {
+                    cb(null, '9.0.0\n');
                 });
-            });
-            it('should fetch any dependent plugins if missing', function() {
-                var deps_dir = path.join(plugins_dir, 'dependencies'),
-                    s = spyOn(plugman.raw, 'fetch').andCallFake(function(id, dir, opts) {
-                        return Q(path.join(dir, id));
-                    });
+
                 runs(function() {
-                    exists.andReturn(false);
-                    // Plugin A depends on C & D
-                    install('android', temp, 'A', deps_dir, {});
+                    installPromise( install('android', project, plugins['A']) );
                 });
-                waits(100);
-                runs(function() {
-                    expect(s).toHaveBeenCalledWith('C', deps_dir, { link: false, subdir: undefined, git_ref: undefined, client: 'plugman', expected_id: 'C' });
-                    expect(s.calls.length).toEqual(3);
+                waitsFor(function() { return done; }, 'install promise never resolved', 200);
+                runs(function() {							  
+                    // Look for 'Installing plugin ...' in events
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.'
+                    ]);
                 });
             });
-            it('should try to fetch any dependent plugins from registry when url is not defined', function() {
-                var deps_dir = path.join(plugins_dir, 'dependencies'),
-                    s = spyOn(plugman.raw, 'fetch').andCallFake(function(id, dir) {
-                        return Q(path.join(dir,id));
-                    });
-                exists.andReturn(false);
+            it('should install any dependent plugins from registry when url is not defined', function() {
+                spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+                fetchSpy.andCallFake( fake['fetch']['dependencies'] );
+				emit = spyOn(events, 'emit');
+                exec.andCallFake(function(cmd, cb) {
+                    cb(null, '9.0.0\n', '');
+                });
+
                 // Plugin A depends on C & D
                 runs(function() {
-                    installPromise(install('android', temp, 'E', deps_dir, {}));
+                    installPromise( install('android', project, plugins['A']) );
                 });
-                waitsFor(function() { return done; }, 'promise never resolved', 500);
+                waitsFor(function() { return done; }, 'promise never resolved', 200);
                 runs(function() {
-                    expect(s).toHaveBeenCalledWith('D', deps_dir, { link: false, subdir: undefined, git_ref: undefined, client: 'plugman', expected_id: 'D' });
-                    expect(s.calls.length).toEqual(2);
-                });
-            });
-            it('should process all dependent plugins with alternate routes to the same plugin', function() {
-                // Plugin F depends on A, C, D and E
-                runs(function () {
-                    installPromise(install('android', temp, 'F', path.join(plugins_dir, 'dependencies'), {}));
-                });
-                waitsFor(function () { return done; }, 'install promise never resolved', 500);
-                runs(function () {
+                    // TODO: this is same test as above? Need test other dependency with url=?
+                    var install = common.spy.getInstall(emit);
 
-                    // So process should be called 5 times
-                    expect(proc.calls.length).toEqual(5);
-                });
-            });
-            it('should throw if there is a cyclic dependency', function() {
-                runs(function () {
-                    installPromise(install('android', temp, 'G', path.join(plugins_dir, 'dependencies'), {}));
-                });
-                waitsFor(function () { return done; }, 'install promise never resolved', 500);
-                runs(function () {
-                    expect(done.message).toEqual('Cyclic dependency from G to H');
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.'
+                    ]);;
                 });
             });
         });
+		it('should process all dependent plugins with alternate routes to the same plugin', function() {
+			spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+			fetchSpy.andCallFake( fake['fetch']['dependencies'] );
+			emit = spyOn(events, 'emit');
+			exec.andCallFake(function(cmd, cb) {
+				cb(null, '9.0.0\n', '');
+			});			
+
+			// Plugin F depends on A, C, D and E
+			runs(function () {
+				installPromise(install('android', project, plugins['F']));
+			});
+			waitsFor(function () { return done; }, 'install promise never resolved', 500);
+			runs(function () {
+				var install = common.spy.getInstall(emit);
+
+				expect(install).toEqual([
+					'Install start for "C" on android.',
+					'Install start for "D" on android.',
+					'Install start for "A" on android.',
+					'Install start for "D" on android.',
+					'Install start for "F" on android.'
+				]);
+			});
+		});
+		/*
+		it('should throw if there is a cyclic dependency', function() {
+			// TODO: group this into BeforeStart() for all dependency tests
+			spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+			fetchSpy.andCallFake( fake['fetch']['dependencies'] );
+			emit = spyOn(events, 'emit');
+			exec.andCallFake(function(cmd, cb) {
+				cb(null, '9.0.0\n', '');
+			});
+
+			runs(function () {
+				installPromise(install('android', project, plugins['G']));
+			});
+			waitsFor(function () { return done; }, 'install promise never resolved', 500);
+			runs(function () {
+				var install = common.spy.getInstall(emit);
+				
+				console.log(install);		   
+				expect(done.message).toEqual('Cyclic dependency from G to H');
+			});
+		});*/		
     });
 
     xdescribe('failure', function() {
         it('should throw if platform is unrecognized', function() {
             runs(function() {
-                installPromise(install('atari', temp, 'SomePlugin', plugins_dir, {}));
+                installPromise( install('atari', project, 'SomePlugin') );
             });
-            waitsFor(function() { return done; }, 'install promise never resolved', 500);
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('atari not supported.'));
             });
         });
         it('should throw if variables are missing', function() {
             runs(function() {
-                installPromise(install('android', temp, variableplugin, plugins_dir, {}));
+                installPromise( install('android', project, plugins['VariablePlugin']) );
             });
-            waitsFor(function(){ return done; }, 'install promise never resolved', 500);
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('Variable(s) missing: API_KEY'));
             });
         });
         it('should throw if git is not found on the path and a remote url is requested', function() {
-            exists.andReturn(false);
+            spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
             var which_spy = spyOn(shell, 'which').andReturn(null);
             runs(function() {
-                installPromise(install('android', temp, 'https://git-wip-us.apache.org/repos/asf/cordova-plugin-camera.git', plugins_dir, {}));
+                installPromise( install('android', project, 'https://git-wip-us.apache.org/repos/asf/cordova-plugin-camera.git') );
             });
-            waitsFor(function(){ return done; }, 'install promise never resolved', 500);
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('"git" command line tool is not installed: make sure it is accessible on your PATH.'));
             });
@@ -327,15 +410,37 @@ describe('install', function() {
         it('should throw if plugin version is less than the minimum requirement', function(){
             var spy = spyOn(semver, 'satisfies').andReturn(false);
             exec.andCallFake(function(cmd, cb) {
-                cb(null, '0.0.1\n', '');
+                cb(null, '0.0.1\n');
             });
             runs(function() {
-                installPromise(install('android', temp, engineplugin, plugins_dir, {}));
+                installPromise( install('android', project, plugins['EnginePlugin']) );
             });
-            waitsFor(function(){ return done; }, 'install promise never resolved', 500);
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('Plugin doesn\'t support this project\'s cordova version. cordova: 0.0.1, failed version requirement: >=2.3.0'));
             });
         });
     });
+
+});
+
+
+describe('end', function() {
+                         
+    it('end', function() {
+        done = false;
+        var finish = function(err){
+            if(err)
+                events.emit('error', err);
+
+            shell.rm('-rf', project);
+            done = true;	
+        }
+
+        promise.then( 
+
+        ).fin(finish);
+
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
 });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_install/AndroidManifest.xml
----------------------------------------------------------------------
diff --git a/spec/projects/android_install/AndroidManifest.xml b/spec/projects/android_install/AndroidManifest.xml
new file mode 100644
index 0000000..b5fea9d
--- /dev/null
+++ b/spec/projects/android_install/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='utf-8'?>
+<manifest android:versionCode="5" android:versionName="1.1" android:windowSoftInputMode="adjustPan" package="cordova.test.uninstall" xmlns:android="http://schemas.android.com/apk/res/android">
+    <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
+    <application android:debuggable="true" android:icon="@drawable/icon" android:label="@string/app_name">
+        <activity android:configChanges="orientation|keyboardHidden" android:label="@string/app_name" android:name="ChildApp">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:configChanges="orientation|keyboardHidden" android:label="@string/app_name" android:name="com.phonegap.DroidGap">
+            <intent-filter>
+            </intent-filter>
+        </activity>
+        <activity android:label="@string/app_name" android:name="com.phonegap.plugins.dummyplugin.DummyPlugin">
+            <intent-filter />
+        </activity>
+    </application>
+    <uses-sdk android:minSdkVersion="5" />
+</manifest>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_install/cordova/android_sdk_version
----------------------------------------------------------------------
diff --git a/spec/projects/android_install/cordova/android_sdk_version b/spec/projects/android_install/cordova/android_sdk_version
new file mode 100644
index 0000000..0ab155c
--- /dev/null
+++ b/spec/projects/android_install/cordova/android_sdk_version
@@ -0,0 +1 @@
+echo 18.0.9
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_install/cordova/version
----------------------------------------------------------------------
diff --git a/spec/projects/android_install/cordova/version b/spec/projects/android_install/cordova/version
new file mode 100644
index 0000000..01f68fd
--- /dev/null
+++ b/spec/projects/android_install/cordova/version
@@ -0,0 +1 @@
+echo 9.0.0
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_install/cordova/version.bat
----------------------------------------------------------------------
diff --git a/spec/projects/android_install/cordova/version.bat b/spec/projects/android_install/cordova/version.bat
new file mode 100644
index 0000000..c637d7c
--- /dev/null
+++ b/spec/projects/android_install/cordova/version.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+echo 9.0.0
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_uninstall/AndroidManifest.xml
----------------------------------------------------------------------
diff --git a/spec/projects/android_uninstall/AndroidManifest.xml b/spec/projects/android_uninstall/AndroidManifest.xml
new file mode 100644
index 0000000..b5fea9d
--- /dev/null
+++ b/spec/projects/android_uninstall/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='utf-8'?>
+<manifest android:versionCode="5" android:versionName="1.1" android:windowSoftInputMode="adjustPan" package="cordova.test.uninstall" xmlns:android="http://schemas.android.com/apk/res/android">
+    <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
+    <application android:debuggable="true" android:icon="@drawable/icon" android:label="@string/app_name">
+        <activity android:configChanges="orientation|keyboardHidden" android:label="@string/app_name" android:name="ChildApp">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:configChanges="orientation|keyboardHidden" android:label="@string/app_name" android:name="com.phonegap.DroidGap">
+            <intent-filter>
+            </intent-filter>
+        </activity>
+        <activity android:label="@string/app_name" android:name="com.phonegap.plugins.dummyplugin.DummyPlugin">
+            <intent-filter />
+        </activity>
+    </application>
+    <uses-sdk android:minSdkVersion="5" />
+</manifest>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_uninstall/cordova/version
----------------------------------------------------------------------
diff --git a/spec/projects/android_uninstall/cordova/version b/spec/projects/android_uninstall/cordova/version
new file mode 100644
index 0000000..01f68fd
--- /dev/null
+++ b/spec/projects/android_uninstall/cordova/version
@@ -0,0 +1 @@
+echo 9.0.0
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/projects/android_uninstall/cordova/version.bat
----------------------------------------------------------------------
diff --git a/spec/projects/android_uninstall/cordova/version.bat b/spec/projects/android_uninstall/cordova/version.bat
new file mode 100644
index 0000000..c637d7c
--- /dev/null
+++ b/spec/projects/android_uninstall/cordova/version.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+echo 9.0.0
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/spec/uninstall.spec.js
----------------------------------------------------------------------
diff --git a/spec/uninstall.spec.js b/spec/uninstall.spec.js
index 432e9f4..7ae8a14 100644
--- a/spec/uninstall.spec.js
+++ b/spec/uninstall.spec.js
@@ -1,131 +1,112 @@
 var uninstall = require('../src/uninstall'),
+    install = require('../src/install'),
     actions = require('../src/util/action-stack'),
     config_changes = require('../src/util/config-changes'),
-    dependencies = require('../src/util/dependencies'),
-    xml_helpers = require('../src/util/xml-helpers'),
-    plugins = require('../src/util/plugins.js'),
+    events  = require('../src/events'),   
     plugman = require('../plugman'),
+    common  = require('./common'),
     fs      = require('fs'),
-    os      = require('osenv'),
     path    = require('path'),
     shell   = require('shelljs'),
-    semver  = require('semver'),
-    temp    = __dirname,
-    dummyplugin = 'DummyPlugin',
-    dummy_id = 'com.phonegap.plugins.dummyplugin',
-    variableplugin = 'VariablePlugin',
-    engineplugin = 'EnginePlugin',
-    Q = require('q'),
-    plugins_dir = path.join(temp, 'plugins');
+    Q       = require('q'),
+    spec    = __dirname,
+    done    = false,
+    srcProject = path.join(spec, 'projects', 'android_uninstall'),
+    project = path.join(spec, 'projects', 'android_uninstall.test'),
+
+    plugins_dir = path.join(spec, 'plugins'),
+    plugins_install_dir = path.join(project, 'cordova', 'plugins'),	
+    plugins = {
+        'DummyPlugin' : path.join(plugins_dir, 'DummyPlugin'),
+        'A' : path.join(plugins_dir, 'dependencies', 'A'),
+        'C' : path.join(plugins_dir, 'dependencies', 'C')
+    },
+    promise,
+    dummy_id = 'com.phonegap.plugins.dummyplugin';	
+
+function uninstallPromise(f) {
+    return f.then(function() { done = true; }, function(err) { done = err; });
+}
+
+describe('start', function() {	
+
+    it('start', function() {
+        shell.rm('-rf', project);
+        shell.cp('-R', path.join(srcProject, '*'), project);
 
-describe('uninstallPlatform', function() {
-    var exists, get_json, chmod, proc, add_to_queue, prepare, actions_push, c_a, rm, done;
-    var gen_deps, get_chain;
+        done = false;
+        promise = Q()
+        .then(
+            function(){ return install('android', project, plugins['DummyPlugin']) }
+        ).then(
+            function(){ return install('android', project, plugins['A']) }
+        ).then(
+            function(){ done = true; }
+        );
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
+});
 
-    function uninstallPromise(f) {
-        return f.then(function() { done = true; }, function(err) { done = err; });
-    }
+describe('uninstallPlatform', function() {								   
+    var proc, prepare, actions_push, add_to_queue, c_a, rm;
+    var fsWrite;
 
-    beforeEach(function() {
+    var plat_common = require('../src/platforms/common');
+
+    beforeEach(function() {	
         proc = spyOn(actions.prototype, 'process').andReturn(Q());
         actions_push = spyOn(actions.prototype, 'push');
         c_a = spyOn(actions.prototype, 'createAction');
         prepare = spyOn(plugman, 'prepare');
-        chmod = spyOn(fs, 'chmodSync');
-        exists = spyOn(fs, 'existsSync').andReturn(true);
-        get_json = spyOn(config_changes, 'get_platform_json').andReturn({
-            installed_plugins:{},
-            dependent_plugins:{}
-        });
-        rm = spyOn(shell, 'rm');
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
+        spyOn(shell, 'cp').andReturn(true);
         add_to_queue = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
-        dependents = jasmine.createSpy('getChain').andReturn([]),
-        gen_deps = spyOn(dependencies, 'generate_dependency_info').andReturn({
-            graph:{
-                getChain:dependents
-            },
-            top_level_plugins:[]
-        });
         done = false;
     });
     describe('success', function() {
         it('should call prepare after a successful uninstall', function() {
             runs(function() {
-                uninstallPromise(uninstall.uninstallPlatform('android', temp, dummyplugin, plugins_dir, {}));
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id));
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(prepare).toHaveBeenCalled();
             });
         });
         it('should call the config-changes module\'s add_uninstalled_plugin_to_prepare_queue method after processing an install', function() {
             runs(function() {
-                uninstallPromise(uninstall.uninstallPlatform('android', temp, dummyplugin, plugins_dir, {}));
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id));
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
-            runs(function() {
-                expect(add_to_queue).toHaveBeenCalledWith(plugins_dir, 'DummyPlugin', 'android', true);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {	  
+                expect(add_to_queue).toHaveBeenCalledWith(plugins_install_dir, dummy_id, 'android', true);
             });
         });
         it('should queue up actions as appropriate for that plugin and call process on the action stack', function() {
             runs(function() {
-                uninstallPromise(uninstall.uninstallPlatform('android', temp, dummyplugin, plugins_dir, {}));
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id));
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(actions_push.calls.length).toEqual(5);
-                expect(c_a).toHaveBeenCalledWith(jasmine.any(Function), [jasmine.any(Object), temp, dummy_id], jasmine.any(Function), [jasmine.any(Object), path.join(plugins_dir, dummyplugin), temp, dummy_id]);
                 expect(proc).toHaveBeenCalled();
             });
         });
 
         describe('with dependencies', function() {
-            var parseET, emit, danglers, dependents;
+            var emit;
             beforeEach(function() {
-                emit = spyOn(plugman, 'emit');
-                parseET = spyOn(xml_helpers, 'parseElementtreeSync').andReturn({
-                    _root:{
-                        attrib:{}
-                    },
-                    find:function() { return null },
-                    findall:function() { return [] }
-                });
-                danglers = spyOn(dependencies, 'danglers');
-                dependents = spyOn(dependencies, 'dependents').andReturn([]);
+                emit = spyOn(events, 'emit');
             });
-            it('should uninstall "dangling" dependencies', function(done) {
-                // TODO: this is a terrible way to do conditional mocking, i am sorry.
-                // if you have a better idea on how to mock out this recursive function, plz patch it
-                var counter = 0;
-                gen_deps.andCallFake(function() {
-                    var obj;
-                    if (counter === 0) {
-                        counter++;
-                        obj = {
-                            graph:{
-                                getChain:function() { return ['one','two'] }
-                            },
-                            top_level_plugins:[]
-                        };
-                    } else {
-                        obj = {
-                            graph:{
-                                getChain:dependents
-                            },
-                            top_level_plugins:[]
-                        };
-                    }
-                    return obj;
+            it('should uninstall "dangling" dependencies', function() {
+                runs(function() {
+                    uninstallPromise(uninstall.uninstallPlatform('android', project, 'A'));
+                });
+                waitsFor(function() { return done; }, 'promise never resolved', 200);	
+                runs(function() {
+                    expect(emit).toHaveBeenCalledWith('log', 'Uninstalling 2 dependent plugins.');
                 });
-
-                danglers.andReturn(['one', 'two']);
-
-                uninstall.uninstallPlatform('android', temp, dummyplugin, plugins_dir, {})
-                .then(function() {
-                    expect(emit).toHaveBeenCalledWith('log', 'Uninstalling 2 dangling dependent plugins.');
-                }, function(err) {
-                    expect(err).toBeUndefined();
-                }).done(done);
             });
         });
     });
@@ -133,19 +114,18 @@ describe('uninstallPlatform', function() {
     describe('failure', function() {
         it('should throw if platform is unrecognized', function() {
             runs(function() {
-                uninstallPromise(uninstall.uninstallPlatform('atari', temp, 'SomePlugin', plugins_dir, {}));
+                uninstallPromise( uninstall.uninstallPlatform('atari', project, 'SomePlugin') );
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('atari not supported.'));
             });
         });
         it('should throw if plugin is missing', function() {
-            exists.andReturn(false);
             runs(function() {
-                uninstallPromise(uninstall.uninstallPlatform('android', temp, 'SomePluginThatDoesntExist', plugins_dir, {}));
+                uninstallPromise( uninstall.uninstallPlatform('android', project, 'SomePluginThatDoesntExist') );
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?'));
             });
@@ -153,165 +133,109 @@ describe('uninstallPlatform', function() {
     });
 });
 
-describe('uninstallPlugin', function() {
-    var exists, get_json, chmod, proc, add_to_queue, prepare, actions_push, c_a, rm, uninstall_plugin, done;
-
-    function uninstallPromise(f) {
-        return f.then(function() { done = true; }, function(err) { done = err; });
-    }
+describe('uninstallPlugin', function() {							 
+    var rm, fsWrite, rmstack = [];
 
     beforeEach(function() {
-        uninstall_plugin = spyOn(uninstall, 'uninstallPlugin').andCallThrough();
-        proc = spyOn(actions.prototype, 'process').andReturn(Q());
-        actions_push = spyOn(actions.prototype, 'push');
-        c_a = spyOn(actions.prototype, 'createAction');
-        prepare = spyOn(plugman, 'prepare');
-        chmod = spyOn(fs, 'chmodSync');
-        exists = spyOn(fs, 'existsSync').andReturn(true);
-        get_json = spyOn(config_changes, 'get_platform_json').andReturn({
-            installed_plugins:{},
-            dependent_plugins:{}
-        });
-        rm = spyOn(shell, 'rm');
-        add_to_queue = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andCallFake(function(f,p) { rmstack.push(p); return true});
+        rmstack = [];
         done = false;
     });
-    describe('success', function() {
-        it('should remove the plugin directory', function() {
-            runs(function() {
-                uninstallPromise(uninstall.uninstallPlugin(dummyplugin, plugins_dir));
-            });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
-            runs(function() {
-                expect(rm).toHaveBeenCalled();
-            });
-        });
-        describe('with dependencies', function() {
-            var parseET, emit;
-            beforeEach(function() {
-                emit = spyOn(plugman, 'emit');
-                var counter = 0;
-                parseET = spyOn(xml_helpers, 'parseElementtreeSync').andCallFake(function(p) {
-                    return {
-                        _root:{
-                            attrib:{}
-                        },
-                        find:function() { return null },
-                        findall:function() { 
-                            if (counter === 0) {
-                                counter++;
-                                return [{attrib:{id:'somedependent'}}] 
-                            }
-                            else return [];
-                        }
-                    }
-                });
-            });
-            it('should delete all dangling plugins', function(done) {
-                uninstall.uninstallPlugin(dummyplugin, plugins_dir)
-                .then(function() {
-                    expect(rm).toHaveBeenCalledWith('-rf', path.join(plugins_dir, dummyplugin));
-                    expect(rm).toHaveBeenCalledWith('-rf', path.join(plugins_dir, 'somedependent'));
-                    expect(rm.calls.length).toBe(2);
-                }, function(err) {
-                    expect(err).toBeUndefined();
-                }).fin(done);
-            });
-        });
-    });
+    describe('with dependencies', function() {
 
-    describe('failure', function() {
-        it('should throw if plugin is missing', function() {
-            exists.andReturn(false);
+        it('should delete all dangling plugins', function() {
+                                              
             runs(function() {
-                uninstallPromise(uninstall('android', temp, 'SomePluginThatDoesntExist', plugins_dir, {}));
+                uninstallPromise( uninstall.uninstallPlugin('A', plugins_install_dir) );
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
-                expect(done).toEqual(new Error('Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?'));
+                expect(done).toEqual(true);
+                expect(rmstack[0]).toBe( path.join(plugins_install_dir, 'C') );
+                expect(rmstack[1]).toBe( path.join(plugins_install_dir, 'D') );
+                expect(rmstack[2]).toBe( path.join(plugins_install_dir, 'A') );
+                expect(rm.calls.length).toBe(3);
             });
         });
     });
 });
 
 describe('uninstall', function() {
-    var exists, get_json, chmod, proc, add_to_queue, prepare, actions_push, c_a, rm, done;
-
-    function uninstallPromise(f) {
-        return f.then(function() { done = true; }, function(err) { done = err; });
-    }
+    var fsWrite, rm, add_to_queue;
 
     beforeEach(function() {
-        proc = spyOn(actions.prototype, 'process').andReturn(Q());
-        actions_push = spyOn(actions.prototype, 'push');
-        c_a = spyOn(actions.prototype, 'createAction');
-        prepare = spyOn(plugman, 'prepare');
-        chmod = spyOn(fs, 'chmodSync');
-        exists = spyOn(fs, 'existsSync').andReturn(true);
-        get_json = spyOn(config_changes, 'get_platform_json').andReturn({
-            installed_plugins:{},
-            dependent_plugins:{}
-        });
-        rm = spyOn(shell, 'rm');
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
         add_to_queue = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
         done = false;
     });
     describe('success', function() {
-        it('should call prepare after a successful uninstall', function() {
-            runs(function() {
-                uninstallPromise(uninstall('android', temp, dummyplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
-            runs(function() {
-                expect(prepare).toHaveBeenCalled();
-            });
-        });
         it('should call the config-changes module\'s add_uninstalled_plugin_to_prepare_queue method after processing an install', function() {
             runs(function() {
-                uninstallPromise(uninstall('android', temp, dummyplugin, plugins_dir, {}));
+                uninstallPromise( uninstall('android', project, plugins['DummyPlugin']) );
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
-                expect(add_to_queue).toHaveBeenCalledWith(plugins_dir, 'DummyPlugin', 'android', true);
+                expect(add_to_queue).toHaveBeenCalledWith(plugins_install_dir, dummy_id, 'android', true);
             });
         });
-        it('should queue up actions as appropriate for that plugin and call process on the action stack', function() {
-            runs(function() {
-                uninstallPromise(uninstall('android', temp, dummyplugin, plugins_dir, {}));
-            });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
-            runs(function() {
-                expect(actions_push.calls.length).toEqual(5);
-                expect(c_a).toHaveBeenCalledWith(jasmine.any(Function), [jasmine.any(Object), temp, dummy_id], jasmine.any(Function), [jasmine.any(Object), path.join(plugins_dir, dummyplugin), temp, dummy_id]);
-                expect(proc).toHaveBeenCalled();
-            });
-        });
-
-        describe('with dependencies', function() {
-            it('should uninstall "dangling" dependencies');
-            it('should not uninstall any dependencies that are relied on by other plugins');
-        });
     });
 
     describe('failure', function() {
         it('should throw if platform is unrecognized', function() {
             runs(function() {
-                uninstallPromise(uninstall('atari', temp, 'SomePlugin', plugins_dir, {}));
+                uninstallPromise(uninstall('atari', project, 'SomePlugin'));
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('atari not supported.'));
             });
         });
         it('should throw if plugin is missing', function() {
-            exists.andReturn(false);
             runs(function() {
-                uninstallPromise(uninstall('android', temp, 'SomePluginThatDoesntExist', plugins_dir, {}));
+                uninstallPromise(uninstall('android', project, 'SomePluginThatDoesntExist'));
             });
-            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
             runs(function() {
                 expect(done).toEqual(new Error('Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?'));
             });
         });
     });
 });
+
+describe('end', function() {
+                         
+    it('end', function() {
+        done = false;
+        var finish = function(err){
+            if(err)
+                plugman.emit('error', err);
+
+            shell.rm('-rf', project);
+            done = true;	
+        }
+
+        promise.then( 
+            function(){ 
+                return uninstall('android', project, plugins['DummyPlugin']) 
+            }
+        ).then( 
+            function(){ 
+                // Fails... A depends on 
+                return uninstall('android', project, plugins['C']) 
+            }
+        ).fail( 
+            function(err) {
+                expect(err.message).toBe("The plugin 'C' is required by (A), skipping uninstallation.");
+            }
+        ).then( 
+            function(){ 
+                // dependencies on C,D ... should this only work with --recursive? prompt user..?
+                return uninstall('android', project, plugins['A']) 
+            }
+        ).fin(finish);
+
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ae2ce7ac/src/events.js
----------------------------------------------------------------------
diff --git a/src/events.js b/src/events.js
index 1cec85a..ed78839 100644
--- a/src/events.js
+++ b/src/events.js
@@ -1,3 +1,2 @@
-var emitter = new (require('events').EventEmitter)();
 
-module.exports = emitter;
+module.exports = new (require('events').EventEmitter)();