From 7db52c6f67b4cec23eb3e31cce89c931e8d3d4b7 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 1/9] cli: change download-task to regular curl download also make 301/302 redirect acceptable by download-build & download-task --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 0e85b05..bb87725 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -9,7 +9,6 @@ import logging import optparse import os import pprint -import pycurl import random import re import six @@ -37,7 +36,7 @@ from koji.util import md5_constructor from koji_cli.lib import _, OptionParser, activate_session, parse_arches, \ _unique_path, _running_in_bg, _progress_callback, watch_tasks, \ arg_filter, linked_upload, list_task_output_all_volumes, \ - print_task_headers, print_task_recurse, _format_size, watch_logs, \ + print_task_headers, print_task_recurse, download_file, watch_logs, \ error, greetings @@ -6455,31 +6454,8 @@ def anon_handle_download_build(options, session, args): url = pathinfo.build(info) + '/' + fname urls.append((url, os.path.basename(fname))) - def _progress(download_t, download_d, upload_t, upload_d): - if download_t == 0: - percent_done = 0.0 - else: - percent_done = float(download_d)/float(download_t) - percent_done_str = "%02d%%" % (percent_done * 100) - data_done = _format_size(download_d) - - sys.stdout.write("[% -36s] % 4s % 10s\r" % ('='*(int(percent_done * 36)), percent_done_str, data_done)) - sys.stdout.flush() - for url, relpath in urls: - if '/' in relpath: - koji.ensuredir(os.path.dirname(relpath)) - if not suboptions.quiet: - print(relpath) - c = pycurl.Curl() - c.setopt(c.URL, url) - c.setopt(c.WRITEDATA, open(relpath, 'wb')) - if not (suboptions.quiet or suboptions.noprogress): - c.setopt(c.NOPROGRESS, False) - c.setopt(c.XFERINFOFUNCTION, _progress) - c.perform() - if not (suboptions.quiet or suboptions.noprogress): - print('') + download_file(url, relpath, suboptions.quiet) def anon_handle_download_logs(options, session, args): @@ -6604,13 +6580,19 @@ def anon_handle_download_logs(options, session, args): def anon_handle_download_task(options, session, args): - "[download] Download the output of a build task " + "[download] Download the output of a build task" usage = _("usage: %prog download-task ") usage += _("\n(Specify the --help global option for a list of other help options)") parser = OptionParser(usage=usage) parser.add_option("--arch", dest="arches", metavar="ARCH", action="append", default=[], help=_("Only download packages for this arch (may be used multiple times)")) parser.add_option("--logs", dest="logs", action="store_true", default=False, help=_("Also download build logs")) + parser.add_option("--topurl", metavar="URL", default=options.topurl, + help=_("URL under which Koji files are accessible")) + parser.add_option("--noprogress", action="store_true", + help=_("Do not display progress meter")) + parser.add_option("-q", "--quiet", action="store_true", + help=_("Suppress output"), default=options.quiet) (suboptions, args) = parser.parse_args(args) if len(args) == 0: @@ -6622,6 +6604,8 @@ def anon_handle_download_task(options, session, args): if len(suboptions.arches) > 0: suboptions.arches = ",".join(suboptions.arches).split(",") + activate_session(session, options) + # get downloadable tasks base_task = session.getTaskInfo(base_task_id) @@ -6672,17 +6656,17 @@ def anon_handle_download_task(options, session, args): error(_("Child task %d has not finished yet.") % task_id) # perform the download - number = 0 + pathinfo = koji.PathInfo(topdir=suboptions.topurl) for (task, filename, volume, new_filename) in downloads: number += 1 if volume not in (None, 'DEFAULT'): koji.ensuredir(volume) new_filename = os.path.join(volume, new_filename) - print(_("Downloading [%d/%d]: %s") % (number, len(downloads), new_filename)) - output_file = open(new_filename, "wb") - output_file.write(session.downloadTaskOutput(task["id"], filename, volume=volume)) - output_file.close() + if '..' in filename: + error(_('Invalid file name: %s') % filename) + url = '%s/%s/%s' % (pathinfo.work(volume), pathinfo.taskrelpath(task["id"]), filename) + download_file(url, new_filename, suboptions.quiet, suboptions.noprogress, len(downloads), number) def anon_handle_wait_repo(options, session, args): diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index 6b3e8a6..6c110ff 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -9,6 +9,7 @@ import socket import string import sys import time +import pycurl from six.moves import range try: @@ -470,6 +471,39 @@ def linked_upload(localfile, path, name=None): os.umask(old_umask) +def download_file(url, relpath, quiet=False, noprogress=False, size=None, num=None): + """Download files from remote""" + def _progress(download_t, download_d, upload_t, upload_d): + if download_t == 0: + percent_done = 0.0 + else: + percent_done = float(download_d) / float(download_t) + percent_done_str = "%02d%%" % (percent_done * 100) + data_done = _format_size(download_d) + + sys.stdout.write("[% -36s] % 4s % 10s\r" % ('=' * (int(percent_done * 36)), percent_done_str, data_done)) + sys.stdout.flush() + + if '/' in relpath: + koji.ensuredir(os.path.dirname(relpath)) + if not quiet: + if size and num: + print(_("Downloading [%d/%d]: %s") % (num, size, relpath)) + else: + print(_("Downloading: %s") % relpath) + c = pycurl.Curl() + c.setopt(c.URL, url) + # allow 301/302 redirect + c.setopt(pycurl.FOLLOWLOCATION, 1) + c.setopt(c.WRITEDATA, open(relpath, 'wb')) + if not (quiet or noprogress): + c.setopt(c.NOPROGRESS, False) + c.setopt(c.XFERINFOFUNCTION, _progress) + c.perform() + if not quiet: + print('') + + def error(msg=None, code=1): if msg: sys.stderr.write(msg + "\n") diff --git a/tests/test_cli/data/list-commands.txt b/tests/test_cli/data/list-commands.txt index 8e4f5fe..b1e8b40 100644 --- a/tests/test_cli/data/list-commands.txt +++ b/tests/test_cli/data/list-commands.txt @@ -86,7 +86,7 @@ build commands: download commands: download-build Download a built package download-logs Download a logs for package - download-task Download the output of a build task + download-task Download the output of a build task info commands: buildinfo Print basic information about a build From d07312ef3c30f2eea5292e7d622a47c71283be38 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 2/9] cli: remove unnecessary continue in download_task --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index bb87725..744d0f1 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -6620,25 +6620,21 @@ def anon_handle_download_task(options, session, args): downloadable_tasks.extend(list(filter(check_downloadable, subtasks))) # get files for download - downloads = [] for task in downloadable_tasks: files = list_task_output_all_volumes(session, task["id"]) for filename in files: - if filename.endswith(".log") and suboptions.logs: - for volume in files[filename]: - # rename logs, they would conflict - new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) - downloads.append((task, filename, volume, new_filename)) - continue - if filename.endswith(".rpm"): for volume in files[filename]: filearch = filename.split(".")[-2] if len(suboptions.arches) == 0 or filearch in suboptions.arches: downloads.append((task, filename, volume, filename)) - continue + elif filename.endswith(".log") and suboptions.logs: + for volume in files[filename]: + # rename logs, they would conflict + new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) + downloads.append((task, filename, volume, new_filename)) if len(downloads) == 0: error(_("No files for download found.")) From e6e2d5a346af46aaaaaf452a4eeba075a9279a90 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 3/9] unittest for cli:download-task --- diff --git a/tests/test_cli/test_download_task.py b/tests/test_cli/test_download_task.py new file mode 100644 index 0000000..53736c0 --- /dev/null +++ b/tests/test_cli/test_download_task.py @@ -0,0 +1,359 @@ +from __future__ import absolute_import +import mock +from mock import call +import os +import six +import sys +import unittest + +from koji_cli.commands import anon_handle_download_task + +progname = os.path.basename(sys.argv[0]) or 'koji' + + +class TestDownloadTask(unittest.TestCase): + # Show long diffs in error output... + maxDiff = None + + def setUp(self): + # Mock out the options parsed in main + self.options = mock.MagicMock() + self.options.quiet = None + self.options.topurl = 'https://topurl' + # Mock out the xmlrpc server + self.session = mock.MagicMock() + self.list_task_output_all_volumes = mock.patch('koji_cli.commands.list_task_output_all_volumes').start() + self.ensuredir = mock.patch('koji.ensuredir').start() + self.download_file = mock.patch('koji_cli.commands.download_file').start() + self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() + self.stderr = mock.patch('sys.stderr', new_callable=six.StringIO).start() + + def tearDown(self): + mock.patch.stopall() + + def test_handle_download_task_single(self): + task_id = 123333 + args = [str(task_id)] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'buildArch', + 'arch': 'taskarch', + 'state': 2} + self.list_task_output_all_volumes.return_value = { + 'somerpm.src.rpm': ['DEFAULT', 'vol1'], + 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], + 'somerpm.noarch.rpm': ['vol3'], + 'somelog.log': ['DEFAULT', 'vol1']} + # Run it and check immediate output + # args: task_id + # expected: success + rv = anon_handle_download_task(self.options, self.session, args) + + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_not_called() + self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) + self.assertEqual(self.download_file.mock_calls, [ + call('https://topurl/work/tasks/3333/123333/somerpm.src.rpm', + 'somerpm.src.rpm', None, None, 5, 1), + call('https://topurl/vol/vol1/work/tasks/3333/123333/somerpm.src.rpm', + 'vol1/somerpm.src.rpm', None, None, 5, 2), + call('https://topurl/work/tasks/3333/123333/somerpm.x86_64.rpm', + 'somerpm.x86_64.rpm', None, None, 5, 3), + call('https://topurl/vol/vol2/work/tasks/3333/123333/somerpm.x86_64.rpm', + 'vol2/somerpm.x86_64.rpm', None, None, 5, 4), + call('https://topurl/vol/vol3/work/tasks/3333/123333/somerpm.noarch.rpm', + 'vol3/somerpm.noarch.rpm', None, None, 5, 5)]) + self.assertIsNone(rv) + + def test_handle_download_task_parent(self): + task_id = 123333 + args = [str(task_id), '--arch=noarch,x86_64'] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'build', + 'arch': 'taskarch', + 'state': 2} + self.session.getTaskChildren.return_value = [{'id': 22222, + 'method': 'buildArch', + 'arch': 'noarch', + 'state': 2}, + {'id': 33333, + 'method': 'buildArch', + 'arch': 'x86_64', + 'state': 2}, + {'id': 44444, + 'method': 'buildArch', + 'arch': 's390', + 'state': 2}, + {'id': 55555, + 'method': 'tagBuild', + 'arch': 'noarch', + 'state': 2} + ] + self.list_task_output_all_volumes.side_effect = [ + {'somerpm.src.rpm': ['DEFAULT', 'vol1']}, + {'somerpm.x86_64.rpm': ['DEFAULT', 'vol2']}, + {'somerpm.noarch.rpm': ['vol3'], + 'somelog.log': ['DEFAULT', 'vol1']}] + # Run it and check immediate output + # args: task_id --arch=noarch,x86_64 + # expected: success + rv = anon_handle_download_task(self.options, self.session, args) + + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_called_once_with(task_id) + self.assertEqual(self.list_task_output_all_volumes.mock_calls, [ + call(self.session, 22222), + call(self.session, 33333), + call(self.session, 44444)]) + self.assertEqual(self.download_file.mock_calls, [ + call('https://topurl/work/tasks/3333/33333/somerpm.x86_64.rpm', + 'somerpm.x86_64.rpm', None, None, 3, 1), + call('https://topurl/vol/vol2/work/tasks/3333/33333/somerpm.x86_64.rpm', + 'vol2/somerpm.x86_64.rpm', None, None, 3, 2), + call('https://topurl/vol/vol3/work/tasks/4444/44444/somerpm.noarch.rpm', + 'vol3/somerpm.noarch.rpm', None, None, 3, 3)]) + self.assertIsNone(rv) + + def test_handle_download_task_log(self): + task_id = 123333 + args = [str(task_id), '--log'] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'buildArch', + 'arch': 'taskarch', + 'state': 2} + self.list_task_output_all_volumes.return_value = { + 'somerpm.src.rpm': ['DEFAULT', 'vol1'], + 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], + 'somerpm.noarch.rpm': ['vol3'], + 'somelog.log': ['DEFAULT', 'vol1']} + # Run it and check immediate output + # args: task_id --log + # expected: success + rv = anon_handle_download_task(self.options, self.session, args) + + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_not_called() + self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) + self.assertEqual(self.download_file.mock_calls, [ + call('https://topurl/work/tasks/3333/123333/somerpm.src.rpm', + 'somerpm.src.rpm', None, None, 7, 1), + call('https://topurl/vol/vol1/work/tasks/3333/123333/somerpm.src.rpm', + 'vol1/somerpm.src.rpm', None, None, 7, 2), + call('https://topurl/work/tasks/3333/123333/somerpm.x86_64.rpm', + 'somerpm.x86_64.rpm', None, None, 7, 3), + call('https://topurl/vol/vol2/work/tasks/3333/123333/somerpm.x86_64.rpm', + 'vol2/somerpm.x86_64.rpm', None, None, 7, 4), + call('https://topurl/vol/vol3/work/tasks/3333/123333/somerpm.noarch.rpm', + 'vol3/somerpm.noarch.rpm', None, None, 7, 5), + call('https://topurl/work/tasks/3333/123333/somelog.log', + 'some.taskarch.log', None, None, 7, 6), + call('https://topurl/vol/vol1/work/tasks/3333/123333/somelog.log', + 'vol1/some.taskarch.log', None, None, 7, 7)]) + self.assertIsNone(rv) + + def test_handle_download_no_download(self): + task_id = 123333 + args = [str(task_id), '--arch=s390,ppc'] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'buildArch', + 'arch': 'taskarch', + 'state': 2} + self.list_task_output_all_volumes.return_value = { + 'somerpm.src.rpm': ['DEFAULT', 'vol1'], + 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], + 'somerpm.noarch.rpm': ['vol3'], + 'somelog.log': ['DEFAULT', 'vol1'], + 'somezip.zip': ['DEFAULT'] + } + # Run it and check immediate output + # args: task_id --arch=s390,ppc + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = 'No files for download found.\n' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_not_called() + self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) + self.download_file.assert_not_called() + self.assertEqual(cm.exception.code, 1) + + def test_handle_download_parent_not_finished(self): + task_id = 123333 + args = [str(task_id)] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'buildArch', + 'arch': 'taskarch', + 'state': 3} + self.list_task_output_all_volumes.return_value = { + 'somerpm.src.rpm': ['DEFAULT', 'vol1'], + 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], + 'somerpm.noarch.rpm': ['vol3'], + 'somelog.log': ['DEFAULT', 'vol1'], + 'somezip.zip': ['DEFAULT'] + } + # Run it and check immediate output + # args: task_id + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = 'Task 123333 has not finished yet.\n' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_not_called() + self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) + self.download_file.assert_not_called() + self.assertEqual(cm.exception.code, 1) + + def test_handle_download_child_not_finished(self): + task_id = 123333 + args = [str(task_id)] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'build', + 'arch': 'taskarch', + 'state': 2} + self.session.getTaskChildren.return_value = [{'id': 22222, + 'method': 'buildArch', + 'arch': 'noarch', + 'state': 3}] + self.list_task_output_all_volumes.return_value = {'somerpm.src.rpm': ['DEFAULT', 'vol1']} + # Run it and check immediate output + # args: task_id + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = 'Child task 22222 has not finished yet.\n' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_called_once_with(task_id) + self.list_task_output_all_volumes.assert_called_once_with(self.session, 22222) + self.download_file.assert_not_called() + self.assertEqual(cm.exception.code, 1) + + def test_handle_download_invalid_file_name(self): + task_id = 123333 + args = [str(task_id)] + self.session.getTaskInfo.return_value = {'id': task_id, + 'method': 'buildArch', + 'arch': 'taskarch', + 'state': 2} + self.list_task_output_all_volumes.return_value = {'somerpm..src.rpm': ['DEFAULT', 'vol1']} + # Run it and check immediate output + # args: task_id + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = 'Invalid file name: somerpm..src.rpm\n' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + self.activate_session.assert_called_once_with(self.session, self.options) + self.session.getTaskInfo.assert_called_once_with(task_id) + self.session.getTaskChildren.assert_not_called() + self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) + self.download_file.assert_not_called() + self.assertEqual(cm.exception.code, 1) + + def test_handle_download_help(self): + args = ['--help'] + # Run it and check immediate output + # args: --help + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = """Usage: %s download-task +(Specify the --help global option for a list of other help options) + +Options: + -h, --help show this help message and exit + --arch=ARCH Only download packages for this arch (may be used multiple + times) + --logs Also download build logs + --topurl=URL URL under which Koji files are accessible + --noprogress Do not display progress meter + -q, --quiet Suppress output +""" % progname + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = '' + self.assertEqual(actual, expected) + self.assertEqual(cm.exception.code, 0) + + def test_handle_download_no_task_id(self): + args = [] + # Run it and check immediate output + # no args + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = """Usage: %s download-task +(Specify the --help global option for a list of other help options) + +%s: error: Please specify a task ID +""" % (progname, progname) + self.assertEqual(actual, expected) + self.assertEqual(cm.exception.code, 2) + + def test_handle_download_multi_task_id(self): + args = ["123", "456"] + # Run it and check immediate output + # args: 123 456 + # expected: failure + with self.assertRaises(SystemExit) as cm: + anon_handle_download_task(self.options, self.session, args) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = """Usage: %s download-task +(Specify the --help global option for a list of other help options) + +%s: error: Only one task ID may be specified +""" % (progname, progname) + self.assertEqual(actual, expected) + self.assertEqual(cm.exception.code, 2) + + +if __name__ == '__main__': + unittest.main() From b402051cbf0b89ec51b81957374ea577a7eb23e5 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 4/9] cli: add c.close() in download_file and move inner function outside --- diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index 6c110ff..26bec95 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -473,16 +473,6 @@ def linked_upload(localfile, path, name=None): def download_file(url, relpath, quiet=False, noprogress=False, size=None, num=None): """Download files from remote""" - def _progress(download_t, download_d, upload_t, upload_d): - if download_t == 0: - percent_done = 0.0 - else: - percent_done = float(download_d) / float(download_t) - percent_done_str = "%02d%%" % (percent_done * 100) - data_done = _format_size(download_d) - - sys.stdout.write("[% -36s] % 4s % 10s\r" % ('=' * (int(percent_done * 36)), percent_done_str, data_done)) - sys.stdout.flush() if '/' in relpath: koji.ensuredir(os.path.dirname(relpath)) @@ -498,12 +488,25 @@ def download_file(url, relpath, quiet=False, noprogress=False, size=None, num=No c.setopt(c.WRITEDATA, open(relpath, 'wb')) if not (quiet or noprogress): c.setopt(c.NOPROGRESS, False) - c.setopt(c.XFERINFOFUNCTION, _progress) + c.setopt(c.XFERINFOFUNCTION, _download_progress) c.perform() + c.close() if not quiet: print('') +def _download_progress(download_t, download_d, upload_t, upload_d): + if download_t == 0: + percent_done = 0.0 + else: + percent_done = float(download_d) / float(download_t) + percent_done_str = "%02d%%" % (percent_done * 100) + data_done = _format_size(download_d) + + sys.stdout.write("[% -36s] % 4s % 10s\r" % ('=' * (int(percent_done * 36)), percent_done_str, data_done)) + sys.stdout.flush() + + def error(msg=None, code=1): if msg: sys.stderr.write(msg + "\n") From 887a08d23241f3f57784e58ee13588835f62f2b4 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 5/9] cli: unittest for download_file --- diff --git a/tests/test_cli/test_download_file.py b/tests/test_cli/test_download_file.py new file mode 100644 index 0000000..02ce2ab --- /dev/null +++ b/tests/test_cli/test_download_file.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import +import mock +import six +import shutil +import tempfile +import unittest + +from koji_cli.lib import download_file, _download_progress + + +class TestDownloadFile(unittest.TestCase): + # Show long diffs in error output... + maxDiff = None + + def reset_mock(self): + self.stdout.seek(0) + self.stdout.truncate() + # self.curl.reset_mock() + self.curlClass.reset_mock() + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + self.filename = self.tempdir + "/filename" + self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() + self.curlClass = mock.patch('pycurl.Curl', create=True).start() + self.curl = self.curlClass.return_value + + def tearDown(self): + mock.patch.stopall() + shutil.rmtree(self.tempdir) + + def test_handle_download_file_dir(self): + with self.assertRaises(IOError) as cm: + download_file("http://url", self.tempdir) + actual = self.stdout.getvalue() + expected = 'Downloading: %s\n' % self.tempdir + self.assertMultiLineEqual(actual, expected) + self.assertEqual(cm.exception.args, (21, 'Is a directory')) + self.curlClass.assert_called_once() + self.assertEqual(self.curl.setopt.call_count, 2) + self.curl.perform.assert_not_called() + + def test_handle_download_file(self): + rv = download_file("http://url", self.filename) + actual = self.stdout.getvalue() + expected = 'Downloading: %s\n\n' % self.filename + self.assertMultiLineEqual(actual, expected) + self.curlClass.assert_called_once() + self.assertEqual(self.curl.setopt.call_count, 5) + self.curl.perform.assert_called_once() + self.curl.close.assert_called_once() + self.assertIsNone(rv) + + def test_handle_download_file_with_size(self): + rv = download_file("http://url", self.filename, size=10, num=8) + actual = self.stdout.getvalue() + expected = 'Downloading [8/10]: %s\n\n' % self.filename + self.assertMultiLineEqual(actual, expected) + self.curlClass.assert_called_once() + self.assertEqual(self.curl.setopt.call_count, 5) + self.curl.perform.assert_called_once() + self.curl.close.assert_called_once() + self.assertIsNone(rv) + + def test_handle_download_file_quiet_noprogress(self): + download_file("http://url", self.filename, quiet=True, noprogress=False) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(self.curl.setopt.call_count, 3) + + self.reset_mock() + download_file("http://url", self.filename, quiet=True, noprogress=True) + actual = self.stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(self.curl.setopt.call_count, 3) + + self.reset_mock() + download_file("http://url", self.filename, quiet=False, noprogress=True) + actual = self.stdout.getvalue() + expected = 'Downloading: %s\n\n' % self.filename + self.assertMultiLineEqual(actual, expected) + self.assertEqual(self.curl.setopt.call_count, 3) + + +class TestDownloadProgress(unittest.TestCase): + # Show long diffs in error output... + maxDiff = None + + def setUp(self): + self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() + + def tearDown(self): + mock.patch.stopall() + + def test_download_progress(self): + _download_progress(0, 0, None, None) + _download_progress(1024 * 92, 1024, None, None) + _download_progress(1024 * 1024 * 23, 1024 * 1024 * 11, None, None) + _download_progress(1024 * 1024 * 1024 * 35, 1024 * 1024 * 1024 * 30, None, None) + _download_progress(318921, 318921, None, None) + actual = self.stdout.getvalue() + expected = '[ ] 00% 0.00 B\r' + \ + '[ ] 01% 1.00 KiB\r' + \ + '[================= ] 47% 11.00 MiB\r' + \ + '[============================== ] 85% 30.00 GiB\r' + \ + '[====================================] 100% 311.45 KiB\r' + self.assertMultiLineEqual(actual, expected) + + +if __name__ == '__main__': + unittest.main() From c052b4248a585ac21bef8f627e2c62ca29638870 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 6/9] fix cli unittest: test_download_task.py for py3 --- diff --git a/tests/test_cli/test_download_task.py b/tests/test_cli/test_download_task.py index 53736c0..d698a4e 100644 --- a/tests/test_cli/test_download_task.py +++ b/tests/test_cli/test_download_task.py @@ -15,6 +15,27 @@ class TestDownloadTask(unittest.TestCase): # Show long diffs in error output... maxDiff = None + def gen_calls(self, task_output, pattern, blacklist=[], arch=None): + + params = [(k, v) for k, vl in + six.iteritems(task_output) + if k not in blacklist + for v in vl] + total = len(params) + calls = [] + for i, (k, v) in enumerate(params): + target = k + if v == 'DEFAULT': + subpath = '' + else: + subpath = 'vol/%s/' % v + target = '%s/%s' % (v, k) + url = pattern % (subpath, k) + if target.endswith('.log') and arch is not None: + target = "%s.%s.log" % (target.rstrip(".log"), arch) + calls.append(call(url, target, None, None, total, i + 1)) + return calls + def setUp(self): # Mock out the options parsed in main self.options = mock.MagicMock() @@ -44,6 +65,11 @@ class TestDownloadTask(unittest.TestCase): 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], 'somerpm.noarch.rpm': ['vol3'], 'somelog.log': ['DEFAULT', 'vol1']} + + calls = self.gen_calls(self.list_task_output_all_volumes.return_value, + 'https://topurl/%swork/tasks/3333/123333/%s', + ['somelog.log']) + # Run it and check immediate output # args: task_id # expected: success @@ -57,17 +83,7 @@ class TestDownloadTask(unittest.TestCase): self.session.getTaskInfo.assert_called_once_with(task_id) self.session.getTaskChildren.assert_not_called() self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) - self.assertEqual(self.download_file.mock_calls, [ - call('https://topurl/work/tasks/3333/123333/somerpm.src.rpm', - 'somerpm.src.rpm', None, None, 5, 1), - call('https://topurl/vol/vol1/work/tasks/3333/123333/somerpm.src.rpm', - 'vol1/somerpm.src.rpm', None, None, 5, 2), - call('https://topurl/work/tasks/3333/123333/somerpm.x86_64.rpm', - 'somerpm.x86_64.rpm', None, None, 5, 3), - call('https://topurl/vol/vol2/work/tasks/3333/123333/somerpm.x86_64.rpm', - 'vol2/somerpm.x86_64.rpm', None, None, 5, 4), - call('https://topurl/vol/vol3/work/tasks/3333/123333/somerpm.noarch.rpm', - 'vol3/somerpm.noarch.rpm', None, None, 5, 5)]) + self.assertListEqual(self.download_file.mock_calls, calls) self.assertIsNone(rv) def test_handle_download_task_parent(self): @@ -115,7 +131,7 @@ class TestDownloadTask(unittest.TestCase): call(self.session, 22222), call(self.session, 33333), call(self.session, 44444)]) - self.assertEqual(self.download_file.mock_calls, [ + self.assertListEqual(self.download_file.mock_calls, [ call('https://topurl/work/tasks/3333/33333/somerpm.x86_64.rpm', 'somerpm.x86_64.rpm', None, None, 3, 1), call('https://topurl/vol/vol2/work/tasks/3333/33333/somerpm.x86_64.rpm', @@ -136,6 +152,10 @@ class TestDownloadTask(unittest.TestCase): 'somerpm.x86_64.rpm': ['DEFAULT', 'vol2'], 'somerpm.noarch.rpm': ['vol3'], 'somelog.log': ['DEFAULT', 'vol1']} + + calls = self.gen_calls(self.list_task_output_all_volumes.return_value, + 'https://topurl/%swork/tasks/3333/123333/%s', arch='taskarch') + # Run it and check immediate output # args: task_id --log # expected: success @@ -149,21 +169,7 @@ class TestDownloadTask(unittest.TestCase): self.session.getTaskInfo.assert_called_once_with(task_id) self.session.getTaskChildren.assert_not_called() self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id) - self.assertEqual(self.download_file.mock_calls, [ - call('https://topurl/work/tasks/3333/123333/somerpm.src.rpm', - 'somerpm.src.rpm', None, None, 7, 1), - call('https://topurl/vol/vol1/work/tasks/3333/123333/somerpm.src.rpm', - 'vol1/somerpm.src.rpm', None, None, 7, 2), - call('https://topurl/work/tasks/3333/123333/somerpm.x86_64.rpm', - 'somerpm.x86_64.rpm', None, None, 7, 3), - call('https://topurl/vol/vol2/work/tasks/3333/123333/somerpm.x86_64.rpm', - 'vol2/somerpm.x86_64.rpm', None, None, 7, 4), - call('https://topurl/vol/vol3/work/tasks/3333/123333/somerpm.noarch.rpm', - 'vol3/somerpm.noarch.rpm', None, None, 7, 5), - call('https://topurl/work/tasks/3333/123333/somelog.log', - 'some.taskarch.log', None, None, 7, 6), - call('https://topurl/vol/vol1/work/tasks/3333/123333/somelog.log', - 'vol1/some.taskarch.log', None, None, 7, 7)]) + self.assertListEqual(self.download_file.mock_calls, calls) self.assertIsNone(rv) def test_handle_download_no_download(self): @@ -180,6 +186,7 @@ class TestDownloadTask(unittest.TestCase): 'somelog.log': ['DEFAULT', 'vol1'], 'somezip.zip': ['DEFAULT'] } + # Run it and check immediate output # args: task_id --arch=s390,ppc # expected: failure From c7cf5ae91e852ad450827c8c88b9017d8621e062 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 7/9] cli: add noprogress argument for download_file in download_build --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 744d0f1..1ed92c3 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -6455,7 +6455,7 @@ def anon_handle_download_build(options, session, args): urls.append((url, os.path.basename(fname))) for url, relpath in urls: - download_file(url, relpath, suboptions.quiet) + download_file(url, relpath, suboptions.quiet, suboptions.noprogress) def anon_handle_download_logs(options, session, args): From 4cfde28884a0b384ec7e6e4e517a338328327639 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 06:15:44 +0000 Subject: [PATCH 8/9] cli: do not print extra blank line when noprogress passed --- diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index 26bec95..58f0340 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -491,7 +491,7 @@ def download_file(url, relpath, quiet=False, noprogress=False, size=None, num=No c.setopt(c.XFERINFOFUNCTION, _download_progress) c.perform() c.close() - if not quiet: + if not (quiet or noprogress): print('') diff --git a/tests/test_cli/test_download_file.py b/tests/test_cli/test_download_file.py index 02ce2ab..8b8cfdb 100644 --- a/tests/test_cli/test_download_file.py +++ b/tests/test_cli/test_download_file.py @@ -79,7 +79,7 @@ class TestDownloadFile(unittest.TestCase): self.reset_mock() download_file("http://url", self.filename, quiet=False, noprogress=True) actual = self.stdout.getvalue() - expected = 'Downloading: %s\n\n' % self.filename + expected = 'Downloading: %s\n' % self.filename self.assertMultiLineEqual(actual, expected) self.assertEqual(self.curl.setopt.call_count, 3) From 8bf4c4338660678ca9150fb28d4ea1a864b9eec0 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: Aug 03 2017 09:29:43 +0000 Subject: [PATCH 9/9] cli: using PROGRESSFUNCTION before libcurl 7.32.0 in download_file --- diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index 58f0340..4b4a504 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -487,8 +487,15 @@ def download_file(url, relpath, quiet=False, noprogress=False, size=None, num=No c.setopt(pycurl.FOLLOWLOCATION, 1) c.setopt(c.WRITEDATA, open(relpath, 'wb')) if not (quiet or noprogress): - c.setopt(c.NOPROGRESS, False) - c.setopt(c.XFERINFOFUNCTION, _download_progress) + proc_func_param = getattr(c, 'XFERINFOFUNCTION', None) + if proc_func_param is None: + proc_func_param = getattr(c, 'PROGRESSFUNCTION', None) + if proc_func_param is not None: + c.setopt(c.NOPROGRESS, False) + c.setopt(proc_func_param, _download_progress) + else: + c.close() + error(_('Error: XFERINFOFUNCTION and PROGRESSFUNCTION are not supported by pyCurl. Quit download progress')) c.perform() c.close() if not (quiet or noprogress): diff --git a/tests/test_cli/test_download_file.py b/tests/test_cli/test_download_file.py index 8b8cfdb..e2b5783 100644 --- a/tests/test_cli/test_download_file.py +++ b/tests/test_cli/test_download_file.py @@ -1,5 +1,6 @@ from __future__ import absolute_import import mock +from mock import call import six import shutil import tempfile @@ -15,6 +16,8 @@ class TestDownloadFile(unittest.TestCase): def reset_mock(self): self.stdout.seek(0) self.stdout.truncate() + self.stderr.seek(0) + self.stderr.truncate() # self.curl.reset_mock() self.curlClass.reset_mock() @@ -22,6 +25,7 @@ class TestDownloadFile(unittest.TestCase): self.tempdir = tempfile.mkdtemp() self.filename = self.tempdir + "/filename" self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() + self.stderr = mock.patch('sys.stderr', new_callable=six.StringIO).start() self.curlClass = mock.patch('pycurl.Curl', create=True).start() self.curl = self.curlClass.return_value @@ -83,6 +87,29 @@ class TestDownloadFile(unittest.TestCase): self.assertMultiLineEqual(actual, expected) self.assertEqual(self.curl.setopt.call_count, 3) + def test_handle_download_file_curl_version(self): + self.curl.XFERINFOFUNCTION = None + download_file("http://url", self.filename, quiet=False, noprogress=False) + actual = self.stdout.getvalue() + expected = 'Downloading: %s\n\n' % self.filename + self.assertMultiLineEqual(actual, expected) + self.assertEqual(self.curl.setopt.call_count, 5) + self.curl.setopt.assert_has_calls([call(self.curl.PROGRESSFUNCTION, _download_progress)]) + + self.reset_mock() + self.curl.PROGRESSFUNCTION = None + with self.assertRaises(SystemExit) as cm: + download_file("http://url", self.filename, quiet=False, noprogress=False) + actual = self.stdout.getvalue() + expected = 'Downloading: %s\n' % self.filename + self.assertMultiLineEqual(actual, expected) + actual = self.stderr.getvalue() + expected = 'Error: XFERINFOFUNCTION and PROGRESSFUNCTION are not supported by pyCurl. Quit download progress\n' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(self.curl.setopt.call_count, 3) + self.assertEqual(cm.exception.code, 1) + + class TestDownloadProgress(unittest.TestCase): # Show long diffs in error output...