def remove(self, argv): """ Remove a plugin. This method is smart enough to work with only the plugin name if it happens to be unique. If there is more than one plugin with the same name but in a different bundle it will exit with an error. """ path = argv.path name = argv.name bundle = argv.bundle with self.out() as out: config = get_jigconfig(path) pm = PluginManager(config) plugins = plugins_by_name(pm) # Find the bundle if it's not specified if name in plugins and not bundle: if len(plugins[name]) > 1: # There are more than one plugin by this name raise CommandError( 'More than one plugin has the name of ' '{0}. Use the list command to see installed ' 'plugins.'.format(name)) bundle = plugins[name][0].bundle pm.remove(bundle, name) set_jigconfig(path, pm.config) out.append('Removed plugin {0}'.format(name))
def _add_plugin(self, plugin_dir): """ Adds a plugin to the jig initialized Git repository. """ config = get_jigconfig(self.gitrepodir) pm = PluginManager(config) pm.add(plugin_dir) set_jigconfig(self.gitrepodir, pm.config)
def test_add_plugin_no_settings_section(self): """ Adds a plugin if it has no settings. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin05')) self.assertEqual(1, len(pm.plugins))
def test_add_plugin_from_directory_of_plugins(self): """ Adds all the plugins in a directory of plugins. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin07')) self.assertEqual(2, len(pm.plugins))
def test_missing_bundle_and_name(self): """ Will not add a plugin if it is missing a bundle or name. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.add(join(self.fixturesdir, 'plugin06')) self.assertIn('Could not find the bundle or name', str(ec.exception))
def test_remove_non_existent_section(self): """ Try to remove a plugin that does not exist. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.remove('bundle', 'name') self.assertEqual('This plugin does not exist.', str(ec.exception))
def test_contains_parsing_errors(self): """ Adding a bad plugin catches parsing errors. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.add(join(self.fixturesdir, 'plugin02')) self.assertIn('File contains parsing errors', str(ec.exception))
def test_add_plugin_no_config_file(self): """ Will handle a plugin that has no config file. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.add(join(self.fixturesdir, 'plugin03')) self.assertIn('The plugin file', str(ec.exception)) self.assertIn('is missing', str(ec.exception))
def test_add_plugin_no_plugin_section(self): """ Will not add a plugin with missing plugin section. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.add(join(self.fixturesdir, 'plugin04')) self.assertEqual( 'The plugin config does not contain a [plugin] section.', str(ec.exception))
def test_cannot_add_plugin_twice(self): """ After a plugin has been added, it can't be added again. """ pm = PluginManager(self.jigconfig) with self.assertRaises(PluginError) as ec: pm.add(join(self.fixturesdir, 'plugin01')) # And the second time pm.add(join(self.fixturesdir, 'plugin01')) self.assertEqual('The plugin is already installed.', str(ec.exception))
def test_add_plugin(self): """ Test the add method on the plugin manager. """ # Config is empty pm = PluginManager(self.jigconfig) plugin = pm.add(join(self.fixturesdir, 'plugin01'))[0] self.assertEqual(1, len(pm.plugins)) self.assertTrue(pm.config.has_section('plugin:test01:plugin01')) self.assertEqual('plugin01', plugin.name) self.assertEqual('test01', plugin.bundle)
def test_remove_plugin(self): """ Remove a plugin. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) self.assertTrue(pm.config.has_section('plugin:test01:plugin01')) pm.remove('test01', 'plugin01') self.assertFalse(pm.config.has_section('plugin:test01:plugin01')) self.assertEqual([], pm.plugins)
def test_new_plugin_compat_plugin_manager(self): """ New plugins are compatible with the :py:class:`PluginManager` """ plugin_dir = create_plugin( self.plugindir, template='python', bundle='test', name='plugin') pm = PluginManager(self.jigconfig) pm.add(plugin_dir) self.assertEqual(1, len(pm.plugins)) self.assertEqual('plugin', pm.plugins[0].name)
def process(self, argv): path = argv.path plugins_file = argv.pluginsfile with self.out() as out: try: plugin_list = read_plugin_list(plugins_file) except IOError as e: # Grab the human-readable part of the IOError and raise that raise PluginError(e[1]) for plugin in plugin_list: config = get_jigconfig(path) pm = PluginManager(config) try: added = add_plugin(pm, plugin, path) except Exception as e: out.append('From {0}:\n - {1}'.format(plugin, e)) continue set_jigconfig(path, pm.config) out.append('From {0}:'.format(plugin)) for p in added: out.append(' - Added plugin {0} in bundle {1}'.format( p.name, p.bundle)) out.extend(USE_RUNNOW)
def set(self, argv): """ Change a single setting for an installed plugin. """ path = argv.path key = argv.key key_parts = key.split('.', 3) config_value = argv.value with self.out(): if len(key_parts) != 3: # The key is not correct raise ConfigKeyInvalid( '{0} is an invalid config key.'.format(key)) bundle, plugin, config_key = key_parts config = get_jigconfig(path) pm = PluginManager(config) if not self._has_plugin(pm, bundle, plugin): raise CommandError( 'Could not locate plugin {0}.'.format(plugin)) section_name = 'plugin:{0}:{1}'.format(bundle, plugin) # Finally change the setting pm.config.set(section_name, config_key, config_value) set_jigconfig(path, pm.config)
def list(self, argv): """ List the current settings for all plugins. """ path = argv.path with self.out() as out: config = get_jigconfig(path) pm = PluginManager(config) if not pm.plugins: out.append(u'No plugins installed.') out.extend(NO_PLUGINS_INSTALLED) return for meta in self._settings(pm): out.append( u'{bundle}.{plugin}.{config_key}={config_value}'.format( bundle=meta.plugin.bundle, plugin=meta.plugin.name, config_key=meta.key, config_value=meta.value)) if not out: out.append(u'Installed plugins have no settings.') out.extend(CHANGE_PLUGIN_SETTINGS)
def list(self, argv): """ List the installed plugins. """ path = argv.path with self.out() as out: config = get_jigconfig(path) pm = PluginManager(config) bundles = plugins_by_bundle(pm) if not bundles: out.append(u'No plugins installed.') out.extend(NO_PLUGINS_INSTALLED) return out.append(u'Installed plugins\n') out.append(u'{h1:<25} {h2}'.format( h1=u'Plugin name', h2=u'Bundle name')) sort_bundles = sorted(bundles.items(), key=lambda b: b[0]) for name, plugins in sort_bundles: sort_plugins = sorted(plugins, key=lambda p: p.name) for plugin in sort_plugins: out.append(u'{plugin:.<25} {name}'.format( name=name, plugin=plugin.name)) out.extend(USE_RUNNOW)
def test_has_empty_plugin_list(self): """ A freshly initialized repo has no plugins. """ pm = PluginManager(self.jigconfig) self.assertEqual([], pm.plugins)
def test_unicode_in_stream(self): """ UTF-8 encoded streams get converted back to unicode. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) gdi = self.git_diff_index(self.testrepo, self.testdiffs[0]) with patch.object(Popen, 'communicate'): # Send it encoded unicode to see if it will convert it back Popen.communicate.return_value = (u'å∫ç'.encode('utf-8'), '') retcode, stdout, stderr = pm.plugins[0].pre_commit(gdi) self.assertEqual(u'å∫ç', stdout) self.assertEqual(u'', stderr)
def test_could_not_parse(self): """ Tests a bad plugin config from the main config. """ self._add_plugin(self.jigconfig, 'plugin02') with self.assertRaises(PluginError) as ec: PluginManager(self.jigconfig) self.assertIn('Could not parse config file', str(ec.exception))
def test_sigpipe_error(self): """ If a SIGPIPE is received, handle it without blowing up. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) gdi = self.git_diff_index(self.testrepo, self.testdiffs[0]) with patch.object(Popen, 'communicate'): ose = OSError('SIGPIPE') ose.errno = 32 Popen.communicate.side_effect = ose retcode, stdout, stderr = pm.plugins[0].pre_commit(gdi) self.assertEqual(1, retcode) self.assertEqual('', stdout) self.assertEqual('Error: received SIGPIPE from the command', stderr)
def test_new_file_pre_commit(self): """ Test a new file pre-commit. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) gdi = self.git_diff_index(self.testrepo, self.testdiffs[0]) retcode, stdout, stderr = pm.plugins[0].pre_commit(gdi) data = json.loads(stdout) # Everything should have gone splendidly self.assertEqual(0, retcode) # Should contain our file that was added self.assertIn('argument.txt', data) # And our first line should be a warning self.assertEqual([1, u'warn', u'The cast: is +'], data['argument.txt'][0])
def _set(self, gitrepodir, bundle_name, plugin_name, key, value): """ Change a setting for a plugin and save the Jig config. """ config = get_jigconfig(self.gitrepodir) pm = PluginManager(config) pm.config.set('plugin:{0}:{1}'.format(bundle_name, plugin_name), key, value) set_jigconfig(self.gitrepodir, pm.config)
def test_oserror(self): """ If a generic OSError is received, don't blow up either. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) gdi = self.git_diff_index(self.testrepo, self.testdiffs[0]) with patch.object(Popen, 'communicate'): ose = OSError('Gazoonkle was discombobulated') ose.errno = 1 Popen.communicate.side_effect = ose retcode, stdout, stderr = pm.plugins[0].pre_commit(gdi) self.assertEqual(1, retcode) self.assertEqual('', stdout) self.assertEqual('Gazoonkle was discombobulated', stderr)
def test_new_file_pre_commit(self): """ Test a new file pre-commit. """ pm = PluginManager(self.jigconfig) pm.add(join(self.fixturesdir, 'plugin01')) gdi = self.git_diff_index(self.testrepo, self.testdiffs[0]) retcode, stdout, stderr = pm.plugins[0].pre_commit(gdi) data = json.loads(stdout) # Everything should have gone splendidly self.assertEqual(0, retcode) # Should contain our file that was added self.assertIn('argument.txt', data) # And our first line should be a warning self.assertEqual( [1, u'warn', u'The cast: is +'], data['argument.txt'][0])
def test_ignores_non_plugin_sections(self): """ Other sections of the config do not get seen as a plugin. """ self._add_plugin(self.jigconfig, 'plugin01') self.jigconfig.add_section('nonplugin') self.jigconfig.set('nonplugin', 'def1', '1') pm = PluginManager(self.jigconfig) self.assertEqual(1, len(pm.plugins))
def test_has_one_plugin(self): """ We can add one plugin to the main config file. """ self._add_plugin(self.jigconfig, 'plugin01') pm = PluginManager(self.jigconfig) self.assertEqual(1, len(pm.plugins)) plugin = pm.plugins[0] self.assertEqual('test01', plugin.bundle) self.assertEqual('plugin01', plugin.name)
def _clear_settings(self, gitrepodir): """ Remove all plugin specific settings. """ config = get_jigconfig(self.gitrepodir) pm = PluginManager(config) for section in pm.config.sections(): if not section.startswith('plugin'): continue for option, value in pm.config.items(section): if option == 'path': continue pm.config.remove_option(section, option) set_jigconfig(self.gitrepodir, pm.config)
def add(self, argv): """ Add a plugin. """ path = argv.path plugin = argv.plugin with self.out() as out: config = get_jigconfig(path) pm = PluginManager(config) added = add_plugin(pm, plugin, path) set_jigconfig(path, pm.config) for p in added: out.append( 'Added plugin {0} in bundle {1} to the ' 'repository.'.format(p.name, p.bundle)) out.extend(USE_RUNNOW)
def about(self, argv): """ Provide about/help on each plugin setting. """ path = argv.path def wrap(payload): indent = ' ' tw = TextWrapper(width=70, initial_indent=indent, subsequent_indent=indent) return u'\n'.join(tw.wrap(payload)) with self.out() as out: config = get_jigconfig(path) pm = PluginManager(config) if not pm.plugins: out.append(u'No plugins installed.') out.extend(NO_PLUGINS_INSTALLED) return for meta in self._settings(pm): out.append(u'{bundle}.{plugin}.{config_key}'.format( bundle=meta.plugin.bundle, plugin=meta.plugin.name, config_key=meta.key)) out.append(u'(default: {0})'.format(meta.default)) if meta.about: out.append(wrap(meta.about.strip())) out.append(u'') if not out: out.append(u'Installed plugins have no settings.')
def run(self, test_range=None): """ Run the tests for this plugin. Returns a list of :py:class:`Result` objects which represent the results from the test run. :param list test_range: None or the parsed range from :function:`parse_range` """ # Use an empty config, we are not going to save this to disk pm = PluginManager(SafeConfigParser()) # Add the plugin we are testing pm.add(self.plugin_dir) # The instance of our plugin we will run the pre_commit test on plugin = pm.plugins[0] # Capture the default plugin config for resets while testing default_settings = plugin.config results = [] for exp in self.expectations: # Make sure that the range is off by 1 assert exp.range[1] == exp.range[0] + 1 # Is this expectation in the specified test range? if test_range and (exp.range not in test_range): # Skip this one, it's not one of the tests we should be running continue # Update the plugin config (settings) if available if exp.settings: plugin.config = exp.settings else: plugin.config = default_settings # View to help us create the output view = ConsoleView(collect_output=True, exit_on_exception=False) # Get a GitDiffIndex object from gdi = InstrumentedGitDiffIndex( self.timeline.repo.working_dir, self.timeline.diffs()[exp.range[0] - 1]) # What is the numbered test directory reprsenting our commit? wd = abspath(join( self.plugin_dir, PLUGIN_TESTS_DIRECTORY, '{0:02d}'.format(exp.range[1]))) with cwd_bounce(wd): # Patch up the filename to be within our numbered directory # instead of the Git repository gdi.replace_path = (self.timeline.repo.working_dir, wd) # Gather up the input to the plugin for logging stdin = json.dumps({ 'config': plugin.config, 'files': gdi}, indent=2, cls=PluginDataJSONEncoder) # Now run the actual pre_commit hook for this plugin res = plugin.pre_commit(gdi) # Break apart into its pieces retcode, stdout, stderr = res # pragma: no branch try: # Is it JSON data? data = json.loads(stdout) except ValueError: # Not JSON data = stdout if retcode == 0: # Format the results according to what you normally see in the # console. view.print_results({plugin: (retcode, data, stderr)}) else: results.append(FailureResult( exp, 'Exit code: {0}\n\nStd out:\n{1}\n\nStd err:\n{2}'.format( retcode, stdout or '(none)', stderr or '(none)'), plugin)) continue # Now remove the color character sequences to make things a little # easier to read, copy, and paste. actual = strip_paint( view._collect['stdout'].getvalue() or view._collect['stderr'].getvalue()) # Also remove the summary and count at the end, these are not # really all that useful to test and just end up making the # expect.rst files overly verbose actual = RESULTS_SUMMARY_SIGNATURE_RE.sub('', actual) actual = RESULTS_SUMMARY_COUNT_RE.sub('', actual) resargs = (exp, actual, plugin, stdin, stdout) if actual.strip() != exp.output.strip(): results.append(FailureResult(*resargs)) else: results.append(SuccessResult(*resargs)) return results
def run(self, test_range=None): """ Run the tests for this plugin. Returns a list of :py:class:`Result` objects which represent the results from the test run. :param list test_range: None or the parsed range from :function:`parse_range` """ # Use an empty config, we are not going to save this to disk pm = PluginManager(SafeConfigParser()) # Add the plugin we are testing pm.add(self.plugin_dir) # The instance of our plugin we will run the pre_commit test on plugin = pm.plugins[0] # Capture the default plugin config for resets while testing default_settings = plugin.config results = [] for exp in self.expectations: # Make sure that the range is off by 1 assert exp.range[1] == exp.range[0] + 1 # Is this expectation in the specified test range? if test_range and (exp.range not in test_range): # Skip this one, it's not one of the tests we should be running continue # Update the plugin config (settings) if available if exp.settings: plugin.config = exp.settings else: plugin.config = default_settings # View to help us create the output view = ConsoleView(collect_output=True, exit_on_exception=False) # Get a GitDiffIndex object from gdi = InstrumentedGitDiffIndex( self.timeline.repo.working_dir, self.timeline.diffs()[exp.range[0] - 1]) # What is the numbered test directory reprsenting our commit? wd = abspath( join(self.plugin_dir, PLUGIN_TESTS_DIRECTORY, '{0:02d}'.format(exp.range[1]))) with cwd_bounce(wd): # Patch up the filename to be within our numbered directory # instead of the Git repository gdi.replace_path = (self.timeline.repo.working_dir, wd) # Gather up the input to the plugin for logging stdin = json.dumps({ 'config': plugin.config, 'files': gdi }, indent=2, cls=PluginDataJSONEncoder) # Now run the actual pre_commit hook for this plugin res = plugin.pre_commit(gdi) # Break apart into its pieces retcode, stdout, stderr = res # pragma: no branch try: # Is it JSON data? data = json.loads(stdout) except ValueError: # Not JSON data = stdout if retcode == 0: # Format the results according to what you normally see in the # console. view.print_results({plugin: (retcode, data, stderr)}) else: results.append( FailureResult( exp, 'Exit code: {0}\n\nStd out:\n{1}\n\nStd err:\n{2}'. format(retcode, stdout or '(none)', stderr or '(none)'), plugin)) continue # Now remove the color character sequences to make things a little # easier to read, copy, and paste. actual = strip_paint(view._collect['stdout'].getvalue() or view._collect['stderr'].getvalue()) # Also remove the summary and count at the end, these are not # really all that useful to test and just end up making the # expect.rst files overly verbose actual = RESULTS_SUMMARY_SIGNATURE_RE.sub('', actual) actual = RESULTS_SUMMARY_COUNT_RE.sub('', actual) resargs = (exp, actual, plugin, stdin, stdout) if actual.strip() != exp.output.strip(): results.append(FailureResult(*resargs)) else: results.append(SuccessResult(*resargs)) return results