From c97e779383ac3360e207deb7b7be673450451626 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Jul 30 2018 05:43:47 +0000 Subject: Refactor build command This is part of the implementation of submitting builds from stream branch when run `fedpkg build`. This refactor allows to override cliClient.build without affecting the scratch-build and chain-build. In addition to the refactor, tests are also added for command build, scratch-build and chain-build. In class cliClient.build, most of the code are moved into a separate method cliClient._build, where is the right place to add general code for all of the three build commands. A global option --dry-run is added and build, scratch-build and chainbuild could perform a dry run. This new option could also be used in other commands rather than introducing new one for themselves. Signed-off-by: Chenxiong Qi --- diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py index 1168cb5..a5e716d 100644 --- a/pyrpkg/__init__.py +++ b/pyrpkg/__init__.py @@ -10,6 +10,7 @@ # the full text of the license. from __future__ import print_function + import cccolutils import errno import fnmatch @@ -17,19 +18,21 @@ import getpass import git import glob import io +import json import koji import logging import os import posixpath +import random import re import rpm import shutil import six +import subprocess import sys import tempfile -import subprocess -import json import time + from multiprocessing.dummy import Pool as ThreadPool from six.moves import configparser @@ -397,6 +400,10 @@ class Commands(object): self.load_branch_merge() return(self._branch_merge) + @branch_merge.setter + def branch_merge(self, value): + self._branch_merge = value + def load_branch_merge(self): """Find the remote tracking branch from the branch we're on. @@ -848,12 +855,26 @@ class Commands(object): self.load_target() return self._target + @target.setter + def target(self, value): + self._target = value + def load_target(self): """This creates the target attribute based on branch merge""" # If a site has a different naming scheme, this would be where # a site would override - self._target = '%s-candidate' % self.branch_merge + self._target = self.build_target(self.branch_merge) + + def build_target(self, release): + """Construct build target + + A build target is generally constructed by release and suffix + ``-candidate``. + + :param str release: the release name which is part of the target name. + """ + return '{0}-candidate'.format(release) @property def container_build_target(self): @@ -1956,11 +1977,13 @@ class Commands(object): def check_inheritance(self, build_target, dest_tag): """Check if build tag inherits from dest tag""" - ancestors = self.kojisession.getFullInheritance(build_target['build_tag']) + ancestors = self.kojisession.getFullInheritance( + build_target['build_tag']) ancestors = [ancestor['parent_id'] for ancestor in ancestors] if dest_tag['id'] not in [build_target['build_tag']] + ancestors: - raise rpkgError('Packages in destination tag %(dest_tag_name)s are not inherited by' - ' build tag %(build_tag_name)s' % build_target) + raise rpkgError( + 'Packages in destination tag %(dest_tag_name)s are not ' + 'inherited by build tag %(build_tag_name)s' % build_target) def construct_build_url(self, repo_name=None, commit_hash=None): """Construct build URL with namespaced anongiturl and commit hash @@ -1982,36 +2005,29 @@ class Commands(object): def build(self, skip_tag=False, scratch=False, background=False, url=None, chain=None, arches=None, sets=False, nvr_check=True, fail_fast=False): - """Initiate a build. Available options are: - - skip_tag: Skip the tag action after the build - - scratch: Perform a scratch build - - background: Perform the build with a low priority - - url: A url to an uploaded srpm to build from - - chain: A chain build set - - arches: A set of arches to limit the scratch build for - - sets: A boolean to let us know whether or not the chain has sets - - nvr_check: A boolean; locally construct NVR and submit a build only if - NVR doesn't exist in a build system - - fail_fast: Perform the build in fast failure mode, which will cause the - entire build to fail if any subtask/architecture build fails. - - This function submits the task to koji and returns the taskID - - It is up to the client to wait or watch the task. + """Initiate a build in build system + + :param bool skip_tag: Skip the tag action after the build. + :param bool scratch: Perform a scratch build. Default is False. + :param bool background: Perform the build with a low priority. Default + is False. + :param str url: A url to an uploaded srpm to build from. + :param list arches: A set of arches to limit the scratch build for. + :param list chain: A chain build set. Only used for chain build. + :param bool sets: whether or not the chain has sets . + :param bool nvr_check: locally construct NVR and submit a build only if + NVR doesn't exist in a build system + :param bool fail_fast: Perform the build in fast failure mode, which + will cause the entire build to fail if any subtask/architecture + build fails. + :return: task ID returned from Koji API ``build`` and ``chainBuild``. + :rtype: int """ # Ensure the repo exists as well as repo data and site data # build up the command that a user would issue cmd = [self.build_client] + # construct the url if not url: # We don't have a url, so build from the latest commit @@ -2025,6 +2041,7 @@ class Commands(object): 'Try option --srpm to make scratch build from local changes.') raise rpkgError(msg) url = self.construct_build_url() + # Check to see if the target is valid build_target = self.kojisession.getBuildTarget(self.target) if not build_target: @@ -2036,12 +2053,14 @@ class Commands(object): % build_target['dest_tag_name']) if dest_tag['locked'] and not scratch: raise rpkgError('Destination tag %s is locked' % dest_tag['name']) + if chain: cmd.append('chain-build') # We're chain building, make sure inheritance works self.check_inheritance(build_target, dest_tag) else: cmd.append('build') + # define our dictionary for options opts = {} # Set a placeholder for the build priority @@ -2100,6 +2119,7 @@ class Commands(object): 'Note: You can skip this check with' ' --skip-nvr-check. See help for more' ' info.' % self.nvr) + # Now submit the task and get the task_id to return # Handle the chain build version if chain: @@ -2114,22 +2134,46 @@ class Commands(object): chain.append([url]) # This next list comp is ugly, but it's how we properly get a : # put in between each build set - cmd.extend(' : '.join([' '.join(build_sets) for build_sets in chain]).split()) - self.log.info('Chain building %s + %s for %s', build_reference, chain[:-1], self.target) - self.log.debug('Building chain %s for %s with options %s and a priority of %s', - chain, self.target, opts, priority) + cmd.extend(' : '.join( + [' '.join(build_sets) for build_sets in chain] + ).split()) + self.log.info('Chain building %s + %s for %s', + build_reference, chain[:-1], self.target) + self.log.debug( + 'Building chain %s for %s with options %s and a priority ' + 'of %s', chain, self.target, opts, priority) self.log.debug(' '.join(cmd)) - task_id = self.kojisession.chainBuild(chain, self.target, opts, priority=priority) + + if self.dry_run: + self.log.info( + 'DRY-RUN: kojisession.chainBuild(%s, %s, %r, priority=%s)', + chain, self.target, opts, priority) + task_id = random.randint(1000, 2000) + else: + task_id = self.kojisession.chainBuild( + chain, self.target, opts, priority=priority) + # Now handle the normal build else: cmd.append(url) self.log.info('Building %s for %s', build_reference, self.target) - self.log.debug('Building %s for %s with options %s and a priority of %s', - url, self.target, opts, priority) + self.log.debug( + 'Building %s for %s with options %s and a priority of %s', + url, self.target, opts, priority) self.log.debug(' '.join(cmd)) - task_id = self.kojisession.build(url, self.target, opts, priority=priority) + + if self.dry_run: + self.log.info( + 'DRY-RUN: kojisession.build(%s, %s, %r, priority=%s)', + url, self.target, opts, priority) + task_id = random.randint(1000, 2000) + else: + task_id = self.kojisession.build( + url, self.target, opts, priority=priority) + self.log.info('Created task: %s', task_id) - self.log.info('Task info: %s/taskinfo?taskID=%s', self.kojiweburl, task_id) + self.log.info('Task info: %s/taskinfo?taskID=%s', + self.kojiweburl, task_id) return task_id def clog(self, raw=False): diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py index 782bd82..dcd0d5a 100644 --- a/pyrpkg/cli.py +++ b/pyrpkg/cli.py @@ -298,6 +298,7 @@ class cliClient(object): self._cmd.debug = self.args.debug self._cmd.verbose = self.args.v self._cmd.lookaside_request_params = items.get('lookaside_request_params') + self._cmd.dry_run = self.args.dry_run clone_config = items.get('clone_config') self._cmd.clone_config = clone_config @@ -346,6 +347,9 @@ class cliClient(object): self.parser.add_argument('--config', '-C', default=None, help='Specify a config file to use') + self.parser.add_argument( + '--dry-run', action='store_true', default=False, + help='Perform a dry run.') group = self.parser.add_mutually_exclusive_group() group.add_argument('--release', dest='release', @@ -1435,67 +1439,122 @@ see API KEY section of copr-cli(1) man page. def usage(self): self.parser.print_help() + def _upload_srpm_for_build(self): + # Figure out if we want a verbose output or not + callback = None + if not self.args.q: + callback = koji_cli.lib._progress_callback + # define a unique path for this upload. Stolen from /usr/bin/koji + uniquepath = 'cli-build/%r.%s' % ( + time.time(), + ''.join([random.choice(string.ascii_letters) for i in range(8)]) + ) + # Should have a try here, not sure what errors we'll get yet though + if self.args.dry_run: + self.log.info('DRY-RUN: self.cmd.koji_upload(%s, %s, callback=%s)', + self.args.srpm, uniquepath, callback) + else: + self.cmd.koji_upload(self.args.srpm, uniquepath, callback=callback) + if not self.args.q: + # print an extra blank line due to callback oddity + print('') + return '%s/%s' % (uniquepath, os.path.basename(self.args.srpm)) + + def _handle_srpm_option(self): + """Generate SRPM according to --srpm option value and upload it + + :return: a unique path of directory inside server into which the SRPM + will be uploaded. If --srpm is not specified, no SRPM will be + uploaded and None is returned. + :rtype: str + """ + if hasattr(self.args, 'srpm') and self.args.srpm: + # See if we need to generate the srpm first + if self.args.srpm == 'CONSTRUCT': + self.log.debug('Generating an srpm') + self.srpm() + self.args.srpm = '%s.src.rpm' % self.cmd.nvr + return self._upload_srpm_for_build() + + def _watch_build_tasks(self, task_ids): + """Watch build tasks + + If --nowait is specified, it does not start to watch. + + :param list task_ids: a list of task IDs to watch. + """ + if self.args.nowait: + return + # Pass info off to our koji task watcher + if self.args.dry_run: + self.log.info('DRY-RUN: Watch tasks: %s', task_ids) + else: + return koji_cli.lib.watch_tasks(self.cmd.kojisession, task_ids) + def build(self, sets=None): + """Implement build command""" + try: + task_id = self._build(sets=sets) + finally: + self.log.debug('Logout kojisession') + self.cmd.kojisession.logout() + + if isinstance(task_id, int): + task_ids = [task_id] + else: + task_ids = task_id + + return self._watch_build_tasks(task_ids) + + def _build(self, sets=None): + """Interface for build, scratch-build and chainbuild to start build + + This is where to add general code for all the build commands. + + :params bool sets: used for ``chainbuild`` to indicate if packages in + the chain are separated into groups in which packages will be built + in parallel. For ``build`` and ``scratch-build``, no need to pass + any value and just keep it None. + :return: task ID returned from build system. In some cases, the + overrided ``_build`` could return a list of task IDs as well. + :rtype: int or list[int] + """ # We may have gotten arches by way of scratch build, so handle them arches = None if hasattr(self.args, 'arches'): arches = self.args.arches - # Place holder for if we build with an uploaded srpm or not - url = None + # See if this is a chain or not chain = None if hasattr(self.args, 'chain'): chain = self.args.chain - # Need to do something with BUILD_FLAGS or KOJI_FLAGS here for compat - if self.args.target: - self.cmd._target = self.args.target - # handle uploading the srpm if we got one - if hasattr(self.args, 'srpm') and self.args.srpm: - # See if we need to generate the srpm first - if self.args.srpm == 'CONSTRUCT': - self.log.debug('Generating an srpm') - self.srpm() - self.args.srpm = '%s.src.rpm' % self.cmd.nvr - # Figure out if we want a verbose output or not - callback = None - if not self.args.q: - callback = koji_cli.lib._progress_callback - # define a unique path for this upload. Stolen from /usr/bin/koji - uniquepath = ('cli-build/%r.%s' - % (time.time(), - ''.join([random.choice(string.ascii_letters) - for i in range(8)]))) - # Should have a try here, not sure what errors we'll get yet though - self.cmd.koji_upload(self.args.srpm, uniquepath, callback=callback) - if not self.args.q: - # print an extra blank line due to callback oddity - print('') - url = '%s/%s' % (uniquepath, os.path.basename(self.args.srpm)) + # nvr_check option isn't set by all commands which calls this # function so handle it as an optional argument nvr_check = True if hasattr(self.args, 'nvr_check'): nvr_check = self.args.nvr_check - task_id = self.cmd.build(skip_tag=self.args.skip_tag, - scratch=self.args.scratch, - background=self.args.background, - url=url, - chain=chain, - arches=arches, - sets=sets, - nvr_check=nvr_check, - fail_fast=self.args.fail_fast) - - # Log out of the koji session - self.cmd.kojisession.logout() - if self.args.nowait: - return + # Need to do something with BUILD_FLAGS or KOJI_FLAGS here for compat + if self.args.target: + self.cmd.target = self.args.target - # Pass info off to our koji task watcher - return koji_cli.lib.watch_tasks(self.cmd.kojisession, [task_id]) + # handle uploading the srpm if we got one + url = self._handle_srpm_option() + + return self.cmd.build( + skip_tag=self.args.skip_tag, + scratch=self.args.scratch, + background=self.args.background, + url=url, + chain=chain, + arches=arches, + sets=sets, + nvr_check=nvr_check, + fail_fast=self.args.fail_fast) def chainbuild(self): + """Implement chain-build command""" if self.cmd.repo_name in self.args.package: raise Exception('%s must not be in the chain' % self.cmd.repo_name) @@ -1518,10 +1577,11 @@ see API KEY section of copr-cli(1) man page. sets = True else: # Figure out the scm url to build from package name - hash = self.cmd.get_latest_commit(component, self.cmd.branch_merge) - # Passing given package name to module_name parameter directly without - # guessing namespace as no way to guess that. rpms/ will be - # added by default if namespace is not given. + hash = self.cmd.get_latest_commit(component, + self.cmd.branch_merge) + # Passing given package name to module_name parameter directly + # without guessing namespace as no way to guess that. rpms/ + # will be added by default if namespace is not given. url = self.cmd.construct_build_url(component, hash) # If there are no ':' in the chain list, treat each object as # an individual chain @@ -1542,7 +1602,7 @@ see API KEY section of copr-cli(1) man page. self.args.chain = urls self.args.skip_tag = False self.args.scratch = False - return self.build(sets=sets) + return self.build(sets) def clean(self): dry = False diff --git a/tests/test_cli.py b/tests/test_cli.py index 377f603..c0e7f2c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2621,3 +2621,340 @@ class TestOptionNameAndNamespace(CliTestCase): cli = self.new_cli(abs_filename) self.assertEqual('somepkg', cli.cmd.repo_name) self.assertEqual('modules', cli.cmd.ns) + + +class TestBuildPackage(CliTestCase): + """Test build package, common build, scratch build and chain build""" + + UNIQUE_PATH_REGEX = r'^cli-build/\d+\.\d+\.[a-zA-Z]+$' + + @classmethod + def setUpClass(cls): + fake_koji_config = dict( + authtype='kerberos', + server='http://localhost/kojihub', + weburl='http://localhost/koji', + topurl='http://kojipkgs.localhost/', + cert='', + ) + cls.read_config_p = patch('koji.read_config', + return_value=fake_koji_config) + cls.mock_read_config = cls.read_config_p.start() + cls.load_krb_user_p = patch('pyrpkg.Commands._load_krb_user') + cls.mock_load_krb_user = cls.load_krb_user_p.start() + + cls.has_krb_creds_p = patch('pyrpkg.Commands._has_krb_creds', + return_value=True) + cls.mock_has_krb_creds = cls.has_krb_creds_p.start() + + @classmethod + def tearDownClass(cls): + cls.has_krb_creds_p.stop() + cls.load_krb_user_p.stop() + cls.read_config_p.stop() + + def setUp(self): + super(TestBuildPackage, self).setUp() + self.checkout_branch(git.Repo(self.cloned_repo_path), 'rhel-7') + + self.ClientSession_p = patch('koji.ClientSession') + self.mock_ClientSession = self.ClientSession_p.start() + + session = self.mock_ClientSession.return_value + session.getBuildTarget.return_value = { + 'id': 1, + 'name': 'rhel-7-candidate', + 'build_tag': 2, + 'build_tag_name': 'rhel-7-build', + 'dest_tag': 3, + 'dest_tag_name': 'rhel-7-updates-candidate', + } + # Get tag, which is the dest_tag_name above. + session.getTag.return_value = { + 'id': 3, + 'name': 'rhel-7-updates-candidate', + 'locked': False, + } + # The full inheritance including parent tags of build tag rhel-7-build + # above. + session.getFullInheritance.return_value = [ + {'parent_id': 3}, + {'parent_id': 6}, + {'parent_id': 7}, + {'parent_id': 8}, + ] + + session.build.return_value = 1000 + session.chainBuild.return_value = 2000 + + def tearDown(self): + self.ClientSession_p.stop() + super(TestBuildPackage, self).tearDown() + + def assert_build(self, sub_command, cli_opts=[], + expected_chain_urls=None, expected_opts={}): + session = self.mock_ClientSession.return_value + + cli_cmd = [ + 'rpkg', '--path', self.cloned_repo_path, '--name', 'docpkg', + sub_command + ] + cli_opts + + mock_build_api = None + + with patch('koji_cli.lib.watch_tasks') as watch_tasks: + with patch('sys.argv', new=cli_cmd): + cli = self.new_cli() + if sub_command == 'build': + mock_build_api = session.build + cli.build() + elif sub_command == 'scratch-build': + mock_build_api = session.build + cli.scratch_build() + elif sub_command == 'chain-build': + mock_build_api = session.chainBuild + cli.chainbuild() + + if '--nowait' in cli_cmd: + watch_tasks.assert_not_called() + else: + watch_tasks.assert_called_once_with( + session, [mock_build_api.return_value]) + + mock_build_api.assert_called_once() + + args, kwargs = mock_build_api.call_args + url, target, opts = args + + self.assertEqual('rhel-7-candidate', target) + self.assertEqual(expected_opts, opts) + + if sub_command == 'chain-build': + self.assertEqual(expected_chain_urls, url) + else: + if '--srpm' in cli_cmd: + # Magic guess if a SRPM file name is given. + i = cli_cmd.index('--srpm') + if i + 1 >= len(cli_cmd) or cli_cmd[i + 1].startswith('--'): + filename = '{0}.src.rpm'.format(cli.cmd.nvr) + else: + filename = os.path.basename(cli_cmd[i + 1]) + match_regex = '{0}/{1}$'.format( + self.UNIQUE_PATH_REGEX.rstrip('$'), filename) + six.assertRegex(self, url, match_regex) + else: + expected_url = '{0}#{1}'.format( + cli.config.get('rpkg', 'anongiturl', raw=True) % { + 'repo': 'docpkg' + }, + cli.cmd.commithash, + ) + self.assertEqual(expected_url, url) + + if '--background' in cli_cmd: + magic_priority_number = 5 + self.assertEqual(magic_priority_number, kwargs['priority']) + + session.logout.assert_called_once() + + return cli + + def test_normal_build(self): + self.assert_build('build', expected_opts={}) + + def test_scratch_build_command(self): + self.assert_build('scratch-build', expected_opts={'scratch': True}) + + def test_scratch_build_from_build_command_with_scratch_option(self): + self.assert_build('build', + cli_opts=['--scratch'], + expected_opts={'scratch': True}) + + def test_dont_wait_build_to_finish(self): + self.assert_build('build', + cli_opts=['--scratch', '--nowait'], + expected_opts={'scratch': True}) + + def assert_option_srpm_use(self, expected_srpm_file=None): + # Ensure the fake srpm file exists. + with patch('os.path.exists', return_value=True): + opts = ['--srpm'] + if expected_srpm_file: + opts.append(expected_srpm_file) + cli = self.assert_build('scratch-build', + cli_opts=opts, + expected_opts={'scratch': True}) + + session = self.mock_ClientSession.return_value + + session.uploadWrapper.assert_called_once() + args, kwargs = session.uploadWrapper.call_args + + srpm_file, unique_path = args + + if expected_srpm_file is None: + self.assertEqual('{0}.src.rpm'.format(cli.cmd.nvr), srpm_file) + else: + self.assertEqual(expected_srpm_file, srpm_file) + six.assertRegex(self, unique_path, r'^cli-build/\d+\.\d+\.[a-zA-Z]+$') + self.assertEqual({'callback': koji_cli.lib._progress_callback}, kwargs) + + def test_srpm_option_with_srpm_file(self): + self.assert_option_srpm_use('/path/to/docpkg-0.1-1.fc28.src.rpm') + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + @patch('pyrpkg.Commands._run_command') + def test_option_srpm_by_generate_srpm_from_repo(self, _run_command, nvr): + nvr.return_value = 'docpkg-0.1-1.fc28' + self.assert_option_srpm_use() + + args, kwargs = _run_command.call_args + self.assertEqual({'shell': True}, kwargs) + rpmbuild_cmd, = args + self.assertIn('-bs', rpmbuild_cmd) + + def test_option_background(self): + self.assert_build('scratch-build', + cli_opts=['--background'], + expected_opts={'scratch': True}) + + def test_option_arches(self): + self.assert_build( + 'scratch-build', + cli_opts=['--arches', 'x86_64', 'i686'], + expected_opts={ + 'scratch': True, + 'arch_override': 'x86_64 i686' + }) + + def test_exclusive_arches_with_build_command(self): + six.assertRaisesRegex( + self, rpkgError, 'Cannot override arches .+', + self.assert_build, 'build', cli_opts=['--arches', 'x86_64']) + + def test_option_skip_tag(self): + self.assert_build('build', + cli_opts=['--skip-tag'], + expected_opts={'skip_tag': True}) + + def test_option_fail_fast(self): + self.assert_build('build', + cli_opts=['--fail-fast'], + expected_opts={'fail_fast': True}) + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + def test_fail_to_get_nvr_but_has_to_check_nvr_existence(self, nvr): + nvr.side_effect = rpkgError + + six.assertRaisesRegex( + self, rpkgError, 'Cannot continue .+ constructed NVR', + self.assert_build, 'build') + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + def test_skip_failure_to_get_nvr(self, nvr): + nvr.side_effect = rpkgError + self.assert_build('build', cli_opts=['--skip-nvr-check']) + + def test_fail_if_target_does_not_exist(self): + session = self.mock_ClientSession.return_value + session.getBuildTarget.return_value = None + + six.assertRaisesRegex(self, rpkgError, 'Unknown build target: .+', + self.assert_build, 'build') + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + def test_build_fails_if_build_nvr_exists(self, nvr): + nvr.return_value = 'docpkg-0.1-1.fc28' + + session = self.mock_ClientSession.return_value + session.getBuild.return_value = {'state': 1} + + six.assertRaisesRegex( + self, rpkgError, + 'Package docpkg-0.1-1.fc28 has already been built', + self.assert_build, 'build') + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + def test_do_not_check_nvr_existence(self, nvr): + nvr.return_value = 'docpkg-0.1-1.fc28' + + self.assert_build('build', cli_opts=['--skip-nvr-check']) + + session = self.mock_ClientSession.return_value + session.getBuild.assert_not_called() + + def test_build_fail_if_repo_has_uncommitted_changed(self): + self.make_changes(filename='hello.py', content='print()') + + # This regex depends on the error message raised from GitPython. + six.assertRaisesRegex( + self, rpkgError, 'has uncommitted changes', + self.assert_build, 'build') + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + @patch('pyrpkg.Commands.commithash', new_callable=PropertyMock) + @patch('subprocess.Popen') + def test_chain_build_without_build_set(self, Popen, commithash, nvr): + commithash.return_value = '45678' + nvr.return_value = 'docpkg-0.1-1.fc28' + + Popen.return_value.communicate.side_effect = [ + ('12345', ''), + ('67890', ''), + ] + + self.assert_build( + 'chain-build', + cli_opts=['firstpkg', 'secondpkg'], + expected_chain_urls=[ + ['git://localhost/firstpkg#12345'], + ['git://localhost/secondpkg#67890'], + ['git://localhost/docpkg#45678'], + ]) + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + @patch('pyrpkg.Commands.commithash', new_callable=PropertyMock) + @patch('subprocess.Popen') + def test_chain_build_in_build_set(self, Popen, commithash, nvr): + commithash.return_value = '45678' + nvr.return_value = 'docpkg-0.1-1.fc28' + + Popen.return_value.communicate.side_effect = [ + ('12345', ''), + ('67890', ''), + ('2ae3f', ''), + ] + + self.assert_build( + 'chain-build', + cli_opts=['firstpkg', 'secondpkg', ':', 'thirdpkg', ':'], + expected_chain_urls=[ + ['git://localhost/firstpkg#12345', + 'git://localhost/secondpkg#67890'], + ['git://localhost/thirdpkg#2ae3f'], + ['git://localhost/docpkg#45678'], + ]) + + @patch('pyrpkg.Commands.nvr', new_callable=PropertyMock) + @patch('pyrpkg.Commands.commithash', new_callable=PropertyMock) + @patch('subprocess.Popen') + def test_chain_build_by_putting_last_pkg_in_its_own_build_set( + self, Popen, commithash, nvr): + commithash.return_value = '45678' + nvr.return_value = 'docpkg-0.1-1.fc28' + + Popen.return_value.communicate.side_effect = [ + ('12345', ''), + ('67890', ''), + ('2ae3f', ''), + ] + + self.assert_build( + 'chain-build', + cli_opts=['firstpkg', 'secondpkg', ':', 'thirdpkg'], + expected_chain_urls=[ + ['git://localhost/firstpkg#12345', + 'git://localhost/secondpkg#67890'], + ['git://localhost/thirdpkg#2ae3f', + 'git://localhost/docpkg#45678'], + ]) diff --git a/tests/utils.py b/tests/utils.py index dc7815d..90318c6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -179,6 +179,7 @@ class CommandTestCase(Assertions, Utils, unittest.TestCase): ['git', 'branch', '--track', 'eng-rhel-6', 'origin/eng-rhel-6'], ['git', 'branch', '--track', 'eng-rhel-6.5', 'origin/eng-rhel-6.5'], ['git', 'branch', '--track', 'eng-rhel-7', 'origin/eng-rhel-7'], + ['git', 'branch', '--track', 'rhel-7', 'origin/rhel-7'], ] for cmd in git_cmds: self.run_cmd(cmd, cwd=self.cloned_repo_path,