From fe6ef8507fb13949e9f5c26a17bb3cb39b58836a Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Aug 15 2018 03:03:27 +0000 Subject: Allow to create update directly with CLI options Resolves: #93 rhbz#1007157 Signed-off-by: Chenxiong Qi --- diff --git a/conf/bash-completion/fedpkg.bash b/conf/bash-completion/fedpkg.bash index baa5491..8f7ea47 100644 --- a/conf/bash-completion/fedpkg.bash +++ b/conf/bash-completion/fedpkg.bash @@ -88,10 +88,11 @@ _fedpkg() local options= local options_target= options_arches= options_branch= options_string= options_file= options_dir= options_srpm= options_mroot= options_builder= options_namespace= + local options_update_type= options_update_request= local after= after_more= case $command in - help|gimmespec|gitbuildhash|giturl|lint|new|push|unused-patches|update|verrel) + help|gimmespec|gitbuildhash|giturl|lint|new|push|unused-patches|verrel) ;; build) options="--nowait --background --skip-tag --scratch" @@ -221,10 +222,18 @@ _fedpkg() after="file" after_more=true ;; + update) + options="--not-close-bugs --suggest-reboot --disable-autokarma" + options_string="--notes --bugs --stable-karma --unstable-karma" + options_update_type="--type" + options_update_request="--request" + ;; esac local all_options="--help $options" - local all_options_value="$options_target $options_arches $options_branch $options_string $options_file $options_dir $options_srpm $options_mroot $options_builder $options_namespace" + local all_options_value="$options_target $options_arches $options_branch \ + $options_string $options_file $options_dir $options_srpm $options_mroot \ + $options_builder $options_namespace $options_update_type $options_update_request" # count non-option parameters @@ -280,6 +289,12 @@ _fedpkg() elif [[ -n $options_namespace ]] && in_array "$prev" "$options_namespace"; then COMPREPLY=( $(compgen -W "$(_fedpkg_namespaces)" -- "$cur") ) + elif [[ -n $options_update_type ]] && in_array "$prev" "$options_update_type"; then + COMPREPLY=( $(compgen -W "bugfix security enhancement newpackage" -- "$cur") ) + + elif [[ -n $options_update_request ]] && in_array "$prev" "$options_update_request"; then + COMPREPLY=( $(compgen -W "testing stable" -- "$cur") ) + else local after_options= diff --git a/conf/zsh-completion/_fedpkg b/conf/zsh-completion/_fedpkg index 054bf7e..f69b1ac 100644 --- a/conf/zsh-completion/_fedpkg +++ b/conf/zsh-completion/_fedpkg @@ -410,7 +410,16 @@ _fedpkg-retire () { (( $+functions[_fedpkg-update] )) || _fedpkg-update () { _arguments -C \ - '(-h --help)'{-h,--help}'[show help message and exit]' + '(-h --help)'{-h,--help}'[show help message and exit]' \ + '(--type)'{--type}'[update type]' \ + '(--request)'{--request}'[update request type]' \ + '(--bugs)'{--bugs}'[bug numbers to be resolved]' \ + '(--notes)'{--notes}'[update description]' \ + '(--disable-autokarma)'{--disable-autokarma}'[disable karma automatism]' \ + '(--stable-karma)'{--stable-karma}'[stable karma]' \ + '(--unstable-karma)'{--unstable-karma}'[unstable karma]' \ + '(--not-close-bugs)'{--not-close-bugs}'[do not close bugs automatically]' \ + '(--suggest-reboot)'{--suggest-reboot}'[suggest reboot]' } (( $+functions[_fedpkg_commands] )) || diff --git a/fedpkg/cli.py b/fedpkg/cli.py index 24e70be..38e737c 100644 --- a/fedpkg/cli.py +++ b/fedpkg/cli.py @@ -13,7 +13,6 @@ from __future__ import print_function from pyrpkg.cli import cliClient import argparse -import hashlib import io import os import re @@ -40,6 +39,39 @@ from fedpkg.utils import ( RELEASE_BRANCH_REGEX = r'^(f\d+|el\d+|epel\d+)$' LOCAL_PACKAGE_CONFIG = 'package.cfg' +BODHI_TEMPLATE = """\ +[ %(nvr)s ] + +# bugfix, security, enhancement, newpackage (required) +type=%(type_)s + +# testing, stable +request=%(request)s + +# Bug numbers: 1234,9876 +bugs=%(bugs)s + +%(changelog)s +# Here is where you give an explanation of your update. +# Content can span multiple lines, as long as they are indented deeper than +# the first line. For example, +# notes=first line +# second line +# and so on +notes=%(descr)s + +# Enable request automation based on the stable/unstable karma thresholds +autokarma=%(autokarma)s +stable_karma=%(stable_karma)s +unstable_karma=%(unstable_karma)s + +# Automatically close bugs when this marked as stable +close_bugs=%(close_bugs)s + +# Suggest that users restart after update +suggest_reboot=%(suggest_reboot)s +""" + def check_bodhi_version(): try: @@ -95,12 +127,110 @@ class fedpkgClient(cliClient): retire_parser.set_defaults(command=self.retire) def register_update(self): + description = ''' +This will create a bodhi update request for the current package n-v-r. + +There are two ways to specify update details. Without any argument from command +line, either update type or notes is omitted, a template editor will be shown +and let you edit the detail information interactively. + +Alternatively, you could specify argument from command line to create an update +directly, for example: + + {0} update --type bugfix --notes 'Rebuilt' --bugs 1000 1002 + +When all lines in template editor are commented out or deleted, the creation +process is aborted. If the template keeps unchanged, {0} continues on creating +update. That gives user a chance to confirm the auto-generated notes from +change log if option --notes is omitted. +'''.format(self.name) + update_parser = self.subparsers.add_parser( 'update', + formatter_class=argparse.RawDescriptionHelpFormatter, help='Submit last build as update', - description='This will create a bodhi update request for the ' - 'current package n-v-r.' + description=description, ) + + def validate_stable_karma(value): + error = argparse.ArgumentTypeError( + 'Stable karma must be an integer which is greater than zero.') + try: + karma = int(value) + except ValueError: + raise error + if karma <= 0: + raise error + + def validate_unstable_karma(value): + error = argparse.ArgumentTypeError( + 'Unstable karma must be an integer which is less than zero.') + try: + karma = int(value) + except ValueError: + raise error + if karma >= 0: + raise error + + def validate_bugs(value): + if not value.isdigit(): + raise argparse.ArgumentTypeError( + 'Invalid bug {0}. It should be an integer.'.format(value)) + + update_parser.add_argument( + '--type', + choices=['bugfix', 'security', 'enhancement', 'newpackage'], + dest='update_type', + help='Update type. Template editor will be shown if type is ' + 'omitted.') + update_parser.add_argument( + '--request', + choices=['testing', 'stable'], + default='testing', + help='Requested repository.') + update_parser.add_argument( + '--bugs', + nargs='+', + type=validate_bugs, + help='Bug numbers. If omitted, bug numbers will be extracted from' + ' change logs.') + update_parser.add_argument( + '--notes', + help='Update description. Multiple lines of notes could be ' + 'specified. If omitted, template editor will be shown.') + update_parser.add_argument( + '--disable-autokarma', + action='store_false', + default=True, + dest='autokarma', + help='Karma automatism is enabled by default. Use this option to ' + 'disable that.') + update_parser.add_argument( + '--stable-karma', + type=validate_stable_karma, + metavar='KARMA', + default=3, + help='Stable karma. Default is 3.') + update_parser.add_argument( + '--unstable-karma', + type=validate_unstable_karma, + metavar='KARMA', + default=-3, + help='Unstable karma. Default is -3.') + update_parser.add_argument( + '--not-close-bugs', + action='store_false', + default=True, + dest='close_bugs', + help='By default, update will be created by enabling to close bugs' + ' automatically. If this is what you do not want, use this ' + 'option to disable the default behavior.') + update_parser.add_argument( + '--suggest-reboot', + action='store_true', + default=False, + dest='suggest_reboot', + help='Suggest user to reboot after update. Default is False.') update_parser.set_defaults(command=self.update) def get_distgit_namespaces(self): @@ -450,95 +580,105 @@ targets to build the package for a particular stream. 'Please try to reinstall %s or consult developers to see what ' 'is wrong with it.' % self.name) - def update(self): - check_bodhi_version() - bodhi_config = self._get_bodhi_config() - template = """\ -[ %(nvr)s ] - -# bugfix, security, enhancement, newpackage (required) -type= - -# testing, stable -request=testing - -# Bug numbers: 1234,9876 -bugs=%(bugs)s - -%(changelog)s -# Here is where you give an explanation of your update. -# Content can span multiple lines, as long as they are indented deeper than -# the first line. For example, -# notes=first line -# second line -# and so on -notes=%(descr)s + @staticmethod + def is_update_aborted(template_file): + """Check if the update is aborted -# Enable request automation based on the stable/unstable karma thresholds -autokarma=True -stable_karma=3 -unstable_karma=-3 + As long as the template file cannot be loaded by configparse, abort + immediately. -# Automatically close bugs when this marked as stable -close_bugs=True - -# Suggest that users restart after update -suggest_reboot=False -""" + From user's perspective, it is similar with aborting commit when option + -m is omitted. If all lines are commented out, abort. + """ + config = configparser.ConfigParser() + try: + loaded_files = config.read(template_file) + except configparser.MissingSectionHeaderError: + return True + # Something wrong with the template, which causes it cannot be loaded. + if not loaded_files: + return True + # template can be loaded even if it's empty. + if not config.sections(): + return True + return False + def _prepare_bodhi_template(self, template_file): bodhi_args = { 'nvr': self.cmd.nvr, 'bugs': six.u(''), 'descr': six.u( - 'Here is where you give an explanation of your update.') + 'Here is where you give an explanation of your update.'), + 'request': self.args.request, + 'autokarma': str(self.args.autokarma), + 'stable_karma': self.args.stable_karma, + 'unstable_karma': self.args.unstable_karma, + 'close_bugs': str(self.args.close_bugs), + 'suggest_reboot': str(self.args.suggest_reboot), } - # Extract bug numbers from the latest changelog entry + if self.args.update_type: + bodhi_args['type_'] = self.args.update_type + else: + bodhi_args['type_'] = '' + self.cmd.clog() clog_file = os.path.join(self.cmd.path, 'clog') with io.open(clog_file, encoding='utf-8') as f: clog = f.read() - bugs = re.findall(r'#([0-9]*)', clog) - if bugs: - bodhi_args['bugs'] = ','.join(bugs) - - # Use clog as default message - bodhi_args['descr'], bodhi_args['changelog'] = \ - self._format_update_clog(clog) - template = textwrap.dedent(template) % bodhi_args + if self.args.bugs: + bodhi_args['bugs'] = self.args.bugs + else: + # Extract bug numbers from the latest changelog entry + bugs = re.findall(r'#([0-9]*)', clog) + if bugs: + bodhi_args['bugs'] = ','.join(bugs) + + if self.args.notes: + bodhi_args['descr'] = self.args.notes.replace('\n', '\n ') + bodhi_args['changelog'] = '' + else: + # Use clog as default message + bodhi_args['descr'], bodhi_args['changelog'] = \ + self._format_update_clog(clog) - # Calculate the hash of the unaltered template - orig_hash = hashlib.new('sha1') - orig_hash.update(template.encode('utf-8')) - orig_hash = orig_hash.hexdigest() + template = textwrap.dedent(BODHI_TEMPLATE) % bodhi_args - # Write out the template - with io.open('bodhi.template', 'w', encoding='utf-8') as f: + with io.open(template_file, 'w', encoding='utf-8') as f: f.write(template) - # Open the template in a text editor - editor = os.getenv('EDITOR', 'vi') - self.cmd._run_command([editor, 'bodhi.template'], shell=True) + if not self.args.update_type or not self.args.notes: + # Open the template in a text editor + editor = os.getenv('EDITOR', 'vi') + self.cmd._run_command([editor, template_file], shell=True) + + # Check to see if we got a template written out. Bail otherwise + if not os.path.isfile(template_file): + raise rpkgError('No bodhi update details saved!') + + return not self.is_update_aborted(template_file) + + return True - # Check to see if we got a template written out. Bail otherwise - if not os.path.isfile('bodhi.template'): - raise rpkgError('No bodhi update details saved!') + def update(self): + check_bodhi_version() + bodhi_config = self._get_bodhi_config() - # If the template was changed, submit it to bodhi - new_hash = self.cmd.lookasidecache.hash_file('bodhi.template', 'sha1') - if new_hash != orig_hash: + bodhi_template_file = 'bodhi.template' + ready = self._prepare_bodhi_template(bodhi_template_file) + + if ready: try: - self.cmd.update(bodhi_config, template='bodhi.template') + self.cmd.update(bodhi_config, template=bodhi_template_file) except Exception as e: raise rpkgError('Could not generate update request: %s' % e) + finally: + os.unlink(bodhi_template_file) + os.unlink('clog') else: self.log.info('Bodhi update aborted!') - # Clean up - os.unlink('bodhi.template') - os.unlink('clog') - def request_repo(self): self._request_repo( repo_name=self.args.name, diff --git a/test/test_cli.py b/test/test_cli.py index d58bd6a..20eae56 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -15,6 +15,7 @@ import io import json import os import pkg_resources +import re import six import sys @@ -40,14 +41,55 @@ from pyrpkg.errors import rpkgError from six.moves.configparser import NoOptionError from six.moves.configparser import NoSectionError from six.moves import StringIO -from tempfile import mkdtemp +from tempfile import mkdtemp, mkstemp from utils import CliTestCase +class TestIsUpdateAborted(CliTestCase): + """Test is_update_aborted""" + + require_test_repos = False + + def setUp(self): + fd, self.bodhi_template = mkstemp() + os.close(fd) + + def tearDown(self): + os.unlink(self.bodhi_template) + + def _is_update_aborted(self): + with patch('sys.argv', new=['fedpkg', 'update']): + cli = self.new_cli() + return cli.is_update_aborted(self.bodhi_template) + + def test_template_is_emtpy(self): + self.assertTrue(self._is_update_aborted()) + + def test_all_line_are_commented_out(self): + with io.open(self.bodhi_template, 'w', encoding='utf-8') as f: + f.write(six.u('# line 1\n#line 2\n#line 3\n')) + + self.assertTrue(self._is_update_aborted()) + + def test_template_is_ok(self): + with io.open(self.bodhi_template, 'w', encoding='utf-8') as f: + f.write(six.u('[fedpkg-1.34-1.fc28]\ntype=\nnotes=abc\n')) + + self.assertFalse(self._is_update_aborted()) + + def test_template_content_is_broken(self): + with io.open(self.bodhi_template, 'w', encoding='utf-8') as f: + f.write(six.u('#[fedpkg-1.34-1.fc28]\ntype=\nnotes=abc\n')) + + self.assertTrue(self._is_update_aborted()) + + @unittest.skipUnless(bodhi, 'Skip if no supported bodhi-client is available') class TestUpdate(CliTestCase): """Test update command""" + create_repo_per_test = False + def setUp(self): super(TestUpdate, self).setUp() @@ -60,17 +102,23 @@ class TestUpdate(CliTestCase): self.mock_run_command = self.run_command_patcher.start() # Let's always use the bodhi 2 command line to test here - self.check_bodhi_version_patcher = patch('fedpkg.cli.check_bodhi_version') - self.mock_check_bodhi_version = self.check_bodhi_version_patcher.start() - - # Not write clog actually. Instead, file object will be mocked and - # return fake clog content for tests. - self.clog_patcher = patch('fedpkg.Commands.clog') - self.clog_patcher.start() + self.check_bodhi_version_patcher = patch( + 'fedpkg.cli.check_bodhi_version') + self.mock_check_bodhi_version = \ + self.check_bodhi_version_patcher.start() self.os_environ_patcher = patch.dict('os.environ', {'EDITOR': 'vi'}) self.os_environ_patcher.start() + self.user_patcher = patch('pyrpkg.Commands.user', + return_value='someone') + self.mock_user = self.user_patcher.start() + + # Not write clog actually. + self.clog_patcher = patch('fedpkg.Commands.clog') + self.clog_patcher.start() + + # Logs will be read in the tests which do not specify --notes option self.fake_clog = list(six.moves.map(six.u, [ 'Add tests for command update', 'New command update - #1000', @@ -85,6 +133,7 @@ class TestUpdate(CliTestCase): if os.path.exists('bodhi.template'): os.unlink('bodhi.template') os.unlink(os.path.join(self.cloned_repo_path, 'clog')) + self.user_patcher.stop() self.os_environ_patcher.stop() self.clog_patcher.stop() self.check_bodhi_version_patcher.stop() @@ -101,10 +150,12 @@ class TestUpdate(CliTestCase): # Do not operate OpenIDC session file with lock @patch('fedora.client.OpenIdBaseClient._load_cookies') def assert_bodhi_update(self, cli, _load_cookies, send_request, csrf, - update_type=None, request_type=None): + update_type=None, request_type=None, notes=None): csrf.return_value = '123456' def run_command_side_effect(command, shell): + # The real call accepts first argument as ['vi', 'bodhi.template'], + # command[-1] returns the filename. filename = command[-1] with io.open(filename, 'r', encoding='utf-8') as f: content = f.read() @@ -112,35 +163,42 @@ class TestUpdate(CliTestCase): # Update parameters here for test if update_type: content = content.replace( - 'type=', 'type={0}'.format(update_type)) + 'type=\n', 'type={0}\n'.format(update_type)) if request_type: - content = content.replace( - 'request=', 'request={0}'.format(request_type)) + # CLI option --request has default value, so the template + # contains the default value. + content = re.sub('request=[a-z]+\n', + 'request={0}\n'.format(request_type), + content) f.write(content) self.mock_run_command.side_effect = run_command_side_effect + expected_data = { + 'autokarma': 'True', + 'bugs': '1000,2000', + 'builds': ' {0} '.format(self.mock_nvr.return_value), + 'close_bugs': True, + 'request': 'testing', + 'severity': 'unspecified', + 'suggest': 'unspecified', + 'type': update_type, + 'type_': update_type, + 'stable_karma': '3', + 'unstable_karma': '-3', + 'csrf_token': csrf.return_value, + } + if notes: + expected_data['notes'] = notes + else: + expected_data['notes'] = self.fake_clog[0] + with patch('os.unlink') as unlink: cli.update() csrf.assert_called_once_with() send_request.assert_called_once_with( - 'updates/', verb='POST', auth=True, - data={ - 'autokarma': 'True', - 'bugs': '1000,2000', - 'builds': ' {0} '.format(self.mock_nvr.return_value), - 'close_bugs': True, - 'notes': self.fake_clog[0], - 'request': 'testing', - 'severity': 'unspecified', - 'suggest': 'unspecified', - 'type': update_type, - 'type_': update_type, - 'stable_karma': '3', - 'unstable_karma': '-3', - 'csrf_token': csrf.return_value, - }) + 'updates/', verb='POST', auth=True, data=expected_data) unlink.assert_has_calls([ call('bodhi.template'), @@ -151,11 +209,14 @@ class TestUpdate(CliTestCase): bodhi_template = f.read() self.assertTrue(self.mock_nvr.return_value in bodhi_template) self.assertTrue('1000,2000' in bodhi_template) - self.assertTrue(self.fake_clog[0] in bodhi_template) - rest_clog = os.linesep.join([ - six.u('# {0}').format(line) for line in self.fake_clog[1:] - ]) - self.assertTrue(rest_clog in bodhi_template) + if notes: + self.assertTrue(notes.replace('\n', '\n ') in bodhi_template) + else: + self.assertTrue(self.fake_clog[0] in bodhi_template) + rest_clog = os.linesep.join([ + six.u('# {0}').format(line) for line in self.fake_clog[1:] + ]) + self.assertTrue(rest_clog in bodhi_template) def test_fail_if_missing_config_options(self): cli_cmd = ['fedpkg', '--path', self.cloned_repo_path, 'update'] @@ -183,14 +244,7 @@ class TestUpdate(CliTestCase): self.mock_run_command.assert_called_once_with( ['vi', 'bodhi.template'], shell=True) - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') - @patch('fedpkg.Commands.user', new_callable=PropertyMock) - def test_request_update(self, user, hash_file, hashlib_new): - user.return_value = 'cqi' - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_request_update(self): cli_cmd = ['fedpkg', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd) @@ -199,14 +253,8 @@ class TestUpdate(CliTestCase): self.mock_run_command.assert_called_once_with( ['vi', 'bodhi.template'], shell=True) - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') @patch('fedpkg.Commands.update', side_effect=OSError) - def test_handle_any_errors_raised_when_execute_bodhi( - self, update, hash_file, hashlib_new): - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_handle_any_errors_raised_when_execute_bodhi(self, update): cli_cmd = ['fedpkg', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd) @@ -214,14 +262,7 @@ class TestUpdate(CliTestCase): self, rpkgError, 'Could not generate update request', self.assert_bodhi_update, cli) - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') - @patch('fedpkg.Commands.user', new_callable=PropertyMock) - def test_create_update_in_stage_bodhi(self, user, hash_file, hashlib_new): - user.return_value = 'someone' - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_create_update_in_stage_bodhi(self): cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd, name='fedpkg-stage', @@ -231,43 +272,19 @@ class TestUpdate(CliTestCase): self.mock_run_command.assert_called_once_with( ['vi', 'bodhi.template'], shell=True) - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') - @patch('fedpkg.Commands.user', new_callable=PropertyMock) - def test_missing_update_type_in_template( - self, user, hash_file, hashlib_new): - user.return_value = 'someone' - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_missing_update_type_in_template(self): cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd) six.assertRaisesRegex(self, rpkgError, 'Missing update type', self.assert_bodhi_update, cli) - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') - @patch('fedpkg.Commands.user', new_callable=PropertyMock) - def test_incorrect_update_type_in_template( - self, user, hash_file, hashlib_new): - user.return_value = 'someone' - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_incorrect_update_type_in_template(self): cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd) six.assertRaisesRegex(self, rpkgError, 'Incorrect update type', self.assert_bodhi_update, cli, update_type='xxx') - @patch('hashlib.new') - @patch('fedpkg.lookaside.FedoraLookasideCache.hash_file') - @patch('fedpkg.Commands.user', new_callable=PropertyMock) - def test_incorrect_request_type_in_template( - self, user, hash_file, hashlib_new): - user.return_value = 'someone' - hashlib_new.return_value.hexdigest.return_value = 'origin hash' - hash_file.return_value = 'different hash' - + def test_incorrect_request_type_in_template(self): cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'update'] cli = self.get_cli(cli_cmd) six.assertRaisesRegex(self, rpkgError, 'Incorrect request type', @@ -275,6 +292,90 @@ class TestUpdate(CliTestCase): update_type='enhancement', request_type='xxx') + def test_create_with_cli_options(self): + cli_cmd = [ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--type', 'bugfix', '--notes', 'Mass rebuild' + ] + + cli = self.get_cli(cli_cmd) + + self.assert_bodhi_update( + cli, update_type='bugfix', notes='Mass rebuild') + self.mock_run_command.assert_not_called() + + def test_show_editor_if_update_type_is_omitted(self): + cli_cmd = [ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--notes', 'Mass rebuild' + ] + + cli = self.get_cli(cli_cmd) + + self.assert_bodhi_update( + cli, update_type='enhancement', notes='Mass rebuild') + self.mock_run_command.assert_called_once_with( + ['vi', 'bodhi.template'], shell=True) + + def test_show_editor_if_notes_is_omitted(self): + cli_cmd = [ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--type', 'bugfix' + ] + + cli = self.get_cli(cli_cmd) + + self.assert_bodhi_update(cli, update_type='bugfix') + self.mock_run_command.assert_called_once_with( + ['vi', 'bodhi.template'], shell=True) + + def test_create_with_notes_in_multiple_lines(self): + cli_cmd = [ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--type', 'bugfix', '--notes', 'Line 1\nLine 2\nLine 3' + ] + + cli = self.get_cli(cli_cmd) + + self.assert_bodhi_update( + cli, update_type='bugfix', notes='Line 1\nLine 2\nLine 3') + + @patch('sys.stderr', new=six.StringIO()) + def test_invalid_stable_karma_option(self): + with self.assertRaises(SystemExit): + self.get_cli([ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--stable-karma', '10s' + ]) + + with self.assertRaises(SystemExit): + self.get_cli([ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--stable-karma', '-3' + ]) + + @patch('sys.stderr', new=six.StringIO()) + def test_invalid_unstable_karma_option(self): + with self.assertRaises(SystemExit): + self.get_cli([ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--unstable-karma', '10s' + ]) + + with self.assertRaises(SystemExit): + self.get_cli([ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--unstable-karma', '3' + ]) + + @patch('sys.stderr', new=six.StringIO()) + def test_invalid_bug(self): + with self.assertRaises(SystemExit): + self.get_cli([ + 'fedpkg-stage', '--path', self.cloned_repo_path, + 'update', '--bugs', '1000', '1001', '100l' + ]) + @patch.object(BugzillaClient, 'client') class TestRequestRepo(CliTestCase): diff --git a/test/utils.py b/test/utils.py index 45d7c02..d677287 100644 --- a/test/utils.py +++ b/test/utils.py @@ -59,7 +59,8 @@ class Assertions(object): class Utils(object): - def run_cmd(self, cmd, allow_output=None, **kwargs): + @staticmethod + def run_cmd(cmd, allow_output=None, **kwargs): if not allow_output: kwargs.update({ 'stdout': subprocess.PIPE, @@ -105,7 +106,8 @@ class fedpkgConfig(object): fedpkg_test_config = fedpkgConfig() -class CommandTestCase(Assertions, Utils, unittest.TestCase): +class RepoCreationMixin(object): + """Mixin providing method to create repos for running tests""" spec_file_content = '''Summary: Dummy summary Name: docpkg @@ -134,19 +136,20 @@ rm -rf $$RPM_BUILD_ROOT - Initial version ''' - def setUp(self): + @classmethod + def create_fake_repos(cls): # create a base repo - self.repo_base = tempfile.mkdtemp(prefix='fedpkg-commands-tests-') + cls.repo_base = tempfile.mkdtemp(prefix='fedpkg-commands-tests-') # Have a namespace of rpms and a repo name of testpkg - self.repo_path = os.path.join(self.repo_base, 'rpms', 'testpkg') - os.makedirs(self.repo_path) + cls.repo_path = os.path.join(cls.repo_base, 'rpms', 'testpkg') + os.makedirs(cls.repo_path) - self.spec_filename = 'docpkg.spec' + cls.spec_filename = 'docpkg.spec' # Add spec file to this repo and commit - spec_file_path = os.path.join(self.repo_path, self.spec_filename) + spec_file_path = os.path.join(cls.repo_path, cls.spec_filename) with open(spec_file_path, 'w') as f: - f.write(self.spec_file_content) + f.write(cls.spec_file_content) git_cmds = [ ['git', 'init'], @@ -163,12 +166,12 @@ rm -rf $$RPM_BUILD_ROOT ['git', 'branch', '8'], ] for cmd in git_cmds: - self.run_cmd(cmd, cwd=self.repo_path) + cls.run_cmd(cmd, cwd=cls.repo_path) # Clone the repo - self.cloned_repo_path = tempfile.mkdtemp( + cls.cloned_repo_path = tempfile.mkdtemp( prefix='fedpkg-commands-tests-cloned-') - self.run_cmd(['git', 'clone', self.repo_path, self.cloned_repo_path]) + cls.run_cmd(['git', 'clone', cls.repo_path, cls.cloned_repo_path]) git_cmds = [ ['git', 'config', 'user.email', 'tester@example.com'], ['git', 'config', 'user.name', 'tester'], @@ -178,11 +181,36 @@ rm -rf $$RPM_BUILD_ROOT ['git', 'branch', '--track', '8', 'origin/8'], ] for cmd in git_cmds: - self.run_cmd(cmd, cwd=self.cloned_repo_path) + cls.run_cmd(cmd, cwd=cls.cloned_repo_path) + + @classmethod + def destroy_fake_repos(cls): + shutil.rmtree(cls.repo_base) + shutil.rmtree(cls.cloned_repo_path) + + +class CommandTestCase(RepoCreationMixin, Assertions, Utils, unittest.TestCase): + + create_repo_per_test = True + require_test_repos = True + + @classmethod + def setUpClass(cls): + if cls.require_test_repos and not cls.create_repo_per_test: + cls.create_fake_repos() + + @classmethod + def tearDownClass(cls): + if cls.require_test_repos and not cls.create_repo_per_test: + cls.destroy_fake_repos() + + def setUp(self): + if self.require_test_repos and self.create_repo_per_test: + self.create_fake_repos() def tearDown(self): - shutil.rmtree(self.repo_base) - shutil.rmtree(self.cloned_repo_path) + if self.require_test_repos and self.create_repo_per_test: + self.destroy_fake_repos() def make_commands(self, path=None, user=None, dist=None, target=None, quiet=None):