diff --git a/.coveragerc b/.coveragerc index 8e9c2a17..c404df72 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,7 +3,7 @@ source=virtinst/ [report] skip_covered = yes -omit=virtinst/_progresspriv.py +#omit=virtinst/_progresspriv.py exclude_lines = # Have to re-enable the standard pragma diff --git a/tests/data/meter/meter1.txt b/tests/data/meter/meter1.txt new file mode 100644 index 00000000..a3f7c7d2 --- /dev/null +++ b/tests/data/meter/meter1.txt @@ -0,0 +1,12 @@ + +Meter text test 0% [ ] 0 B/s | 0 B --:-- ETA + +Meter text test 1% [ ] 0 B/s | 100 B --:-- ETA + +Meter text test 2% [ ] 67 B/s | 200 B 02:27 ETA + +Meter text test 20% [=== ] 413 B/s | 2.0 kB 00:19 ETA + +Meter text test 40% [======- ] 731 B/s | 3.9 kB 00:08 ETA + +Meter text test | 3.9 kB 00:04 ... diff --git a/tests/data/meter/meter2.txt b/tests/data/meter/meter2.txt new file mode 100644 index 00000000..93e93dc3 --- /dev/null +++ b/tests/data/meter/meter2.txt @@ -0,0 +1,12 @@ + +Meter text test 0% [ ] 0 B/s | 0 B --:--:-- ETA + +Meter text test 1% [ ] 0 B/s | 100 B --:--:-- ETA + +Meter text test 2% [- ] 67 B/s | 200 B 00:02:27 ETA + +Meter text test 20% [======= ] 413 B/s | 2.0 kB 00:00:19 ETA + +Meter text test 40% [============== ] 731 B/s | 3.9 kB 00:00:08 ETA + +Meter text test | 3.9 kB 00:00:04 ... diff --git a/tests/data/meter/meter3.txt b/tests/data/meter/meter3.txt new file mode 100644 index 00000000..474e40f7 --- /dev/null +++ b/tests/data/meter/meter3.txt @@ -0,0 +1,7 @@ +Meter text test 0 B/s | 0 B 00:00 +Meter text test 0 B/s | 100 B 00:00 +Meter text test 67 B/s | 200 B 00:02 +Meter text test 413 B/s | 2.0 kB 00:03 +Meter text test 731 B/s | 3.9 kB 00:04 + +Meter text test | 3.9 kB 00:04 diff --git a/tests/data/meter/meter4.txt b/tests/data/meter/meter4.txt new file mode 100644 index 00000000..aa05acd5 --- /dev/null +++ b/tests/data/meter/meter4.txt @@ -0,0 +1,7 @@ +12345678 | 0 B +12345678 | 100 B +12345678 | 200 B +12345678 | 2.0 kB +12345678 | 3.9 kB + +1234567890 diff --git a/tests/data/meter/meter5.txt b/tests/data/meter/meter5.txt new file mode 100644 index 00000000..1d232a5d --- /dev/null +++ b/tests/data/meter/meter5.txt @@ -0,0 +1,12 @@ + +Meter text test 0% [ ] 0 B/s | 0 B --:-- ETA + +Meter text test 50% [========- ] 0 B/s | 100 B --:-- ETA + +Meter text test 100% [================] 67 B/s | 200 B 00:00 ETA + +Meter text test 1000% [================] 413 B/s | 2.0 kB --:-- ETA + +Meter text test 2000% [================] 731 B/s | 3.9 kB --:-- ETA + +Meter text test | 3.9 kB 00:04 !!! diff --git a/tests/data/meter/meter6.txt b/tests/data/meter/meter6.txt new file mode 100644 index 00000000..07d99bfd --- /dev/null +++ b/tests/data/meter/meter6.txt @@ -0,0 +1,12 @@ + +Meter text test 100% [================] 0 B/s | 0 B --:-- ETA + +Meter text test 100% [================] 0 B/s | 100 B --:-- ETA + +Meter text test 100% [================] 67 B/s | 200 B --:-- ETA + +Meter text test 100% [================] 413 B/s | 2.0 kB --:-- ETA + +Meter text test 100% [================] 731 B/s | 3.9 kB --:-- ETA + +Meter text test | 3.9 kB 00:04 diff --git a/tests/test_misc.py b/tests/test_misc.py index f908c74a..2dced1e8 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3,6 +3,10 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import io +import os +import unittest + import virtinst from tests import utils @@ -124,3 +128,74 @@ def test_misc_cpu_cornercases(): guest.cpu.model = "idontexist" guest.cpu._validate_default_host_model_only(guest) assert guest.cpu.model is None + + +def test_misc_meter(): + """ + Test coverage of our urlgrabber meter copy + """ + # pylint: disable=protected-access + from virtinst import _progresspriv + + def _test_meter_values(m, startval=10000, text="Meter text test"): + with unittest.mock.patch("time.time", return_value=1.0): + m.start(text, startval) + with unittest.mock.patch("time.time", return_value=1.1): + m.update(0) + with unittest.mock.patch("time.time", return_value=1.5): + m.update(0) + with unittest.mock.patch("time.time", return_value=2.0): + m.update(100) + with unittest.mock.patch("time.time", return_value=3.0): + m.update(200) + with unittest.mock.patch("time.time", return_value=4.0): + m.update(2000) + with unittest.mock.patch("time.time", return_value=5.0): + m.update(4000) + with unittest.mock.patch("time.time", return_value=6.0): + m.end() + + # Basic output testing + meter = _progresspriv.TextMeter(output=io.StringIO()) + _test_meter_values(meter) + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter1.txt")) + + # Fake having a longer terminal, it affects output a bit + meter = _progresspriv.TextMeter(output=io.StringIO()) + _progresspriv._term_width_val = 120 + _test_meter_values(meter) + _progresspriv._term_width_val = 80 + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter2.txt")) + + # meter with size=None + meter = _progresspriv.TextMeter(output=io.StringIO()) + _test_meter_values(meter, None) + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter3.txt")) + + # meter with size=None and small terminal size + meter = _progresspriv.TextMeter(output=io.StringIO()) + _progresspriv._term_width_val = 11 + _test_meter_values(meter, None, "1234567890") + assert meter.re.fraction_read() is None + _progresspriv._term_width_val = 80 + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter4.txt")) + + # meter with size exceeded by the update() values + meter = _progresspriv.TextMeter(output=io.StringIO()) + _test_meter_values(meter, 200) + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter5.txt")) + + # meter with size 0 + meter = _progresspriv.TextMeter(output=io.StringIO()) + _test_meter_values(meter, 0) + out = meter.output.getvalue().replace("\r", "\n") + utils.diff_compare(out, os.path.join(utils.DATADIR, "meter", "meter6.txt")) + + # BaseMeter coverage + meter = _progresspriv.BaseMeter() + _test_meter_values(meter) diff --git a/virtinst/_progresspriv.py b/virtinst/_progresspriv.py index 5e701c96..5a31a18c 100644 --- a/virtinst/_progresspriv.py +++ b/virtinst/_progresspriv.py @@ -10,12 +10,11 @@ # we are just copying this for now. -import sys -import time -import math import fcntl import struct +import sys import termios +import time # Code from https://mail.python.org/pipermail/python-list/2000-May/033365.html @@ -24,11 +23,8 @@ def terminal_width(fd=1): try: buf = 'abcdefgh' buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf) - ret = struct.unpack('hhhh', buf)[1] - if ret == 0: - return 80 - # Add minimum too? - return ret + ret = struct.unpack('hhhh', buf)[1] # pragma: no cover + return ret or 80 # pragma: no cover except IOError: return 80 @@ -66,9 +62,7 @@ class TerminalLine: def rest_split(self, fixed, elements=2): """ After a fixed length, split the rest of the line length among a number of different elements (default=2). """ - if self.llen < fixed: - return 0 - return (self.llen - fixed) // elements + return max(self.llen - fixed, 0) // elements def add(self, element, full_len=None): """ If there is room left in the line, above min_len, add element. @@ -93,71 +87,48 @@ class BaseMeter: def __init__(self): self.update_period = 0.3 # seconds - self.url = None - self.basename = None self.text = None self.size = None self.start_time = None - self.fsize = None self.last_amount_read = 0 self.last_update_time = None self.re = RateEstimator() - def set_text(self, text): - self.text = text - def start(self, text, size): self.text = text - self.size = size - if size is not None: - self.fsize = format_number(size) + 'B' + assert type(size) in [int, type(None)] + assert self.text is not None now = time.time() self.start_time = now self.re.start(size, now) self.last_amount_read = 0 self.last_update_time = now - self._do_start(now) - def _do_start(self, now=None): - pass - - def update(self, amount_read, now=None): + def update(self, amount_read): # for a real gui, you probably want to override and put a call # to your mainloop iteration function here - if now is None: - now = time.time() + assert type(amount_read) is int + + now = time.time() if (not self.last_update_time or (now >= self.last_update_time + self.update_period)): self.re.update(amount_read, now) self.last_amount_read = amount_read self.last_update_time = now - self._do_update(amount_read, now) + self._do_update(amount_read) - def _do_update(self, amount_read, now=None): + def _do_update(self, amount_read): pass def end(self): - self._do_end(self.last_amount_read, self.last_update_time) + self._do_end() - def _do_end(self, amount_read, now=None): + def _do_end(self): pass -# This is kind of a hack, but progress is gotten from grabber which doesn't -# know about the total size to download. So we do this so we can get the data -# out of band here. This will be "fixed" one way or anther soon. -_text_meter_total_size = 0 -_text_meter_sofar_size = 0 - - -def text_meter_total_size(size, downloaded=0): - global _text_meter_total_size - global _text_meter_sofar_size - _text_meter_total_size = size - _text_meter_sofar_size = downloaded - # # update: No size (minimal: 17 chars) # ----------------------------------- @@ -230,20 +201,11 @@ class TextMeter(BaseMeter): BaseMeter.__init__(self) self.output = output - def _do_update(self, amount_read, now=None): + def _do_update(self, amount_read): etime = self.re.elapsed_time() fread = format_number(amount_read) - # self.size = None - if self.text is not None: - text = self.text - else: - text = self.basename ave_dl = format_number(self.re.average_rate()) - sofar_size = None - if _text_meter_total_size: - sofar_size = _text_meter_sofar_size + amount_read - sofar_pc = (sofar_size * 100) // _text_meter_total_size # Include text + ui_rate in minimal tl = TerminalLine(8, 8 + 1 + 8) @@ -254,7 +216,7 @@ class TextMeter(BaseMeter): ui_time = tl.add(' %s' % format_time(etime, use_hours)) ui_end = tl.add(' ' * 5) ui_rate = tl.add(' %5sB/s' % ave_dl) - out = '%-*.*s%s%s%s%s\r' % (tl.rest(), tl.rest(), text, + out = '%-*.*s%s%s%s%s\r' % (tl.rest(), tl.rest(), self.text, ui_rate, ui_size, ui_time, ui_end) else: rtime = self.re.remaining_time() @@ -264,35 +226,23 @@ class TextMeter(BaseMeter): ui_time = tl.add(' %s' % frtime) ui_end = tl.add(' ETA ') - if sofar_size is None: - ui_sofar_pc = '' - else: - ui_sofar_pc = tl.add(' (%i%%)' % sofar_pc, - full_len=len(" (100%)")) - ui_pc = tl.add(' %2i%%' % (frac * 100)) ui_rate = tl.add(' %5sB/s' % ave_dl) # Make text grow a bit before we start growing the bar too blen = 4 + tl.rest_split(8 + 8 + 4) ui_bar = _term_add_bar(tl, blen, frac) - out = '\r%-*.*s%s%s%s%s%s%s%s\r' % ( - tl.rest(), tl.rest(), text, - ui_sofar_pc, ui_pc, ui_bar, + out = '\r%-*.*s%s%s%s%s%s%s\r' % ( + tl.rest(), tl.rest(), self.text, + ui_pc, ui_bar, ui_rate, ui_size, ui_time, ui_end ) self.output.write(out) self.output.flush() - def _do_end(self, amount_read, now=None): - global _text_meter_total_size - global _text_meter_sofar_size - + def _do_end(self): + amount_read = self.last_amount_read total_size = format_number(amount_read) - if self.text is not None: - text = self.text - else: - text = self.basename tl = TerminalLine(8) # For big screens, make it more readable. @@ -301,29 +251,16 @@ class TextMeter(BaseMeter): ui_time = tl.add(' %s' % format_time(self.re.elapsed_time(), use_hours)) ui_end, not_done = _term_add_end(tl, self.size, amount_read) - out = '\r%-*.*s%s%s%s\n' % (tl.rest(), tl.rest(), text, + dummy = not_done + out = '\r%-*.*s%s%s%s\n' % (tl.rest(), tl.rest(), self.text, ui_size, ui_time, ui_end) self.output.write(out) self.output.flush() - # Don't add size to the sofar size until we have all of it. - # If we don't have a size, then just pretend/hope we got all of it. - if not_done: - return - - if _text_meter_total_size: - _text_meter_sofar_size += amount_read - if _text_meter_total_size <= _text_meter_sofar_size: - _text_meter_total_size = 0 - _text_meter_sofar_size = 0 - - -text_progress_meter = TextMeter ###################################################################### # support classes and functions - class RateEstimator: def __init__(self, timescale=5.0): self.timescale = timescale @@ -333,22 +270,14 @@ class RateEstimator: self.last_amount_read = 0 self.ave_rate = None - def start(self, total=None, now=None): - if now is None: - now = time.time() + def start(self, total, now): self.total = total self.start_time = now self.last_update_time = now self.last_amount_read = 0 self.ave_rate = None - def update(self, amount_read, now=None): - if now is None: - now = time.time() - # libcurl calls the progress callback when fetching headers - # too, thus amount_read = 0 .. hdr_size .. 0 .. content_size. - # Occasionally we miss the 2nd zero and report avg speed < 0. - # Handle read_diff < 0 here. BZ 1001767. + def update(self, amount_read, now): if amount_read == 0 or amount_read < self.last_amount_read: # if we just started this file, all bets are off self.last_update_time = now @@ -386,10 +315,9 @@ class RateEstimator: (can be None for unknown transfer size)""" if self.total is None: return None - elif self.total == 0: - return 1.0 - else: - return float(self.last_amount_read) / self.total + if self.total == 0: + return 1.0 # pragma: no cover + return float(self.last_amount_read) / self.total ######################################################################### # support methods @@ -413,37 +341,16 @@ class RateEstimator: try: recent_rate = read_diff / time_diff - except ZeroDivisionError: + except ZeroDivisionError: # pragma: no cover recent_rate = None if last_ave is None: return recent_rate - elif recent_rate is None: - return last_ave + if recent_rate is None: + return last_ave # pragma: no cover # at this point, both last_ave and recent_rate are numbers return epsilon * recent_rate + (1 - epsilon) * last_ave - def _round_remaining_time(self, rt, start_time=15.0): - """round the remaining time, depending on its size - If rt is between n*start_time and (n+1)*start_time round downward - to the nearest multiple of n (for any counting number n). - If rt < start_time, round down to the nearest 1. - For example (for start_time = 15.0): - 2.7 -> 2.0 - 25.2 -> 25.0 - 26.4 -> 26.0 - 35.3 -> 34.0 - 63.6 -> 60.0 - """ - - if rt < 0: - return 0.0 - shift = int(math.log(rt / start_time) / math.log(2)) - rt = int(rt) - if shift <= 0: - return rt - return float(int(rt) >> shift << shift) - def format_time(seconds, use_hours=0): if seconds is None or seconds < 0: @@ -452,7 +359,7 @@ def format_time(seconds, use_hours=0): else: return '--:--' elif seconds == float('inf'): - return 'Infinite' + return 'Infinite' # pragma: no cover else: seconds = int(seconds) minutes = seconds // 60 @@ -465,7 +372,7 @@ def format_time(seconds, use_hours=0): return '%02i:%02i' % (minutes, seconds) -def format_number(number, SI=0, space=' '): +def format_number(number): """Turn numbers into human-readable metric-like numbers""" symbols = ['', # (none) 'k', # kilo @@ -477,11 +384,7 @@ def format_number(number, SI=0, space=' '): 'Z', # zetta 'Y'] # yotta - if SI: - step = 1000.0 - else: - step = 1024.0 - + step = 1024.0 thresh = 999 depth = 0 max_depth = len(symbols) - 1 @@ -505,4 +408,4 @@ def format_number(number, SI=0, space=' '): else: fmt = '%.0f%s%s' - return(fmt % (float(number or 0), space, symbols[depth])) + return fmt % (float(number or 0), " ", symbols[depth])