tests: Add full test coverage for progress text output

Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
Cole Robinson 2021-06-09 14:29:08 -04:00
parent 078178f476
commit 58837a7641
9 changed files with 174 additions and 134 deletions

View File

@ -3,7 +3,7 @@ source=virtinst/
[report] [report]
skip_covered = yes skip_covered = yes
omit=virtinst/_progresspriv.py #omit=virtinst/_progresspriv.py
exclude_lines = exclude_lines =
# Have to re-enable the standard pragma # Have to re-enable the standard pragma

View File

@ -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 ...

View File

@ -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 ...

View File

@ -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

View File

@ -0,0 +1,7 @@
12345678 | 0 B
12345678 | 100 B
12345678 | 200 B
12345678 | 2.0 kB
12345678 | 3.9 kB
1234567890

View File

@ -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 !!!

View File

@ -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

View File

@ -3,6 +3,10 @@
# This work is licensed under the GNU GPLv2 or later. # This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory. # See the COPYING file in the top-level directory.
import io
import os
import unittest
import virtinst import virtinst
from tests import utils from tests import utils
@ -124,3 +128,74 @@ def test_misc_cpu_cornercases():
guest.cpu.model = "idontexist" guest.cpu.model = "idontexist"
guest.cpu._validate_default_host_model_only(guest) guest.cpu._validate_default_host_model_only(guest)
assert guest.cpu.model is None 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)

View File

@ -10,12 +10,11 @@
# we are just copying this for now. # we are just copying this for now.
import sys
import time
import math
import fcntl import fcntl
import struct import struct
import sys
import termios import termios
import time
# Code from https://mail.python.org/pipermail/python-list/2000-May/033365.html # Code from https://mail.python.org/pipermail/python-list/2000-May/033365.html
@ -24,11 +23,8 @@ def terminal_width(fd=1):
try: try:
buf = 'abcdefgh' buf = 'abcdefgh'
buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf) buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
ret = struct.unpack('hhhh', buf)[1] ret = struct.unpack('hhhh', buf)[1] # pragma: no cover
if ret == 0: return ret or 80 # pragma: no cover
return 80
# Add minimum too?
return ret
except IOError: except IOError:
return 80 return 80
@ -66,9 +62,7 @@ class TerminalLine:
def rest_split(self, fixed, elements=2): def rest_split(self, fixed, elements=2):
""" After a fixed length, split the rest of the line length among """ After a fixed length, split the rest of the line length among
a number of different elements (default=2). """ a number of different elements (default=2). """
if self.llen < fixed: return max(self.llen - fixed, 0) // elements
return 0
return (self.llen - fixed) // elements
def add(self, element, full_len=None): def add(self, element, full_len=None):
""" If there is room left in the line, above min_len, add element. """ If there is room left in the line, above min_len, add element.
@ -93,71 +87,48 @@ class BaseMeter:
def __init__(self): def __init__(self):
self.update_period = 0.3 # seconds self.update_period = 0.3 # seconds
self.url = None
self.basename = None
self.text = None self.text = None
self.size = None self.size = None
self.start_time = None self.start_time = None
self.fsize = None
self.last_amount_read = 0 self.last_amount_read = 0
self.last_update_time = None self.last_update_time = None
self.re = RateEstimator() self.re = RateEstimator()
def set_text(self, text):
self.text = text
def start(self, text, size): def start(self, text, size):
self.text = text self.text = text
self.size = size self.size = size
if size is not None: assert type(size) in [int, type(None)]
self.fsize = format_number(size) + 'B' assert self.text is not None
now = time.time() now = time.time()
self.start_time = now self.start_time = now
self.re.start(size, now) self.re.start(size, now)
self.last_amount_read = 0 self.last_amount_read = 0
self.last_update_time = now self.last_update_time = now
self._do_start(now)
def _do_start(self, now=None): def update(self, amount_read):
pass
def update(self, amount_read, now=None):
# for a real gui, you probably want to override and put a call # for a real gui, you probably want to override and put a call
# to your mainloop iteration function here # to your mainloop iteration function here
if now is None: assert type(amount_read) is int
now = time.time()
now = time.time()
if (not self.last_update_time or if (not self.last_update_time or
(now >= self.last_update_time + self.update_period)): (now >= self.last_update_time + self.update_period)):
self.re.update(amount_read, now) self.re.update(amount_read, now)
self.last_amount_read = amount_read self.last_amount_read = amount_read
self.last_update_time = now 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 pass
def end(self): 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 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) # update: No size (minimal: 17 chars)
# ----------------------------------- # -----------------------------------
@ -230,20 +201,11 @@ class TextMeter(BaseMeter):
BaseMeter.__init__(self) BaseMeter.__init__(self)
self.output = output self.output = output
def _do_update(self, amount_read, now=None): def _do_update(self, amount_read):
etime = self.re.elapsed_time() etime = self.re.elapsed_time()
fread = format_number(amount_read) 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()) 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 # Include text + ui_rate in minimal
tl = TerminalLine(8, 8 + 1 + 8) tl = TerminalLine(8, 8 + 1 + 8)
@ -254,7 +216,7 @@ class TextMeter(BaseMeter):
ui_time = tl.add(' %s' % format_time(etime, use_hours)) ui_time = tl.add(' %s' % format_time(etime, use_hours))
ui_end = tl.add(' ' * 5) ui_end = tl.add(' ' * 5)
ui_rate = tl.add(' %5sB/s' % ave_dl) 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) ui_rate, ui_size, ui_time, ui_end)
else: else:
rtime = self.re.remaining_time() rtime = self.re.remaining_time()
@ -264,35 +226,23 @@ class TextMeter(BaseMeter):
ui_time = tl.add(' %s' % frtime) ui_time = tl.add(' %s' % frtime)
ui_end = tl.add(' ETA ') 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_pc = tl.add(' %2i%%' % (frac * 100))
ui_rate = tl.add(' %5sB/s' % ave_dl) ui_rate = tl.add(' %5sB/s' % ave_dl)
# Make text grow a bit before we start growing the bar too # Make text grow a bit before we start growing the bar too
blen = 4 + tl.rest_split(8 + 8 + 4) blen = 4 + tl.rest_split(8 + 8 + 4)
ui_bar = _term_add_bar(tl, blen, frac) ui_bar = _term_add_bar(tl, blen, frac)
out = '\r%-*.*s%s%s%s%s%s%s%s\r' % ( out = '\r%-*.*s%s%s%s%s%s%s\r' % (
tl.rest(), tl.rest(), text, tl.rest(), tl.rest(), self.text,
ui_sofar_pc, ui_pc, ui_bar, ui_pc, ui_bar,
ui_rate, ui_size, ui_time, ui_end ui_rate, ui_size, ui_time, ui_end
) )
self.output.write(out) self.output.write(out)
self.output.flush() self.output.flush()
def _do_end(self, amount_read, now=None): def _do_end(self):
global _text_meter_total_size amount_read = self.last_amount_read
global _text_meter_sofar_size
total_size = format_number(amount_read) total_size = format_number(amount_read)
if self.text is not None:
text = self.text
else:
text = self.basename
tl = TerminalLine(8) tl = TerminalLine(8)
# For big screens, make it more readable. # 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(), ui_time = tl.add(' %s' % format_time(self.re.elapsed_time(),
use_hours)) use_hours))
ui_end, not_done = _term_add_end(tl, self.size, amount_read) 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) ui_size, ui_time, ui_end)
self.output.write(out) self.output.write(out)
self.output.flush() 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 # support classes and functions
class RateEstimator: class RateEstimator:
def __init__(self, timescale=5.0): def __init__(self, timescale=5.0):
self.timescale = timescale self.timescale = timescale
@ -333,22 +270,14 @@ class RateEstimator:
self.last_amount_read = 0 self.last_amount_read = 0
self.ave_rate = None self.ave_rate = None
def start(self, total=None, now=None): def start(self, total, now):
if now is None:
now = time.time()
self.total = total self.total = total
self.start_time = now self.start_time = now
self.last_update_time = now self.last_update_time = now
self.last_amount_read = 0 self.last_amount_read = 0
self.ave_rate = None self.ave_rate = None
def update(self, amount_read, now=None): def update(self, amount_read, now):
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.
if amount_read == 0 or amount_read < self.last_amount_read: if amount_read == 0 or amount_read < self.last_amount_read:
# if we just started this file, all bets are off # if we just started this file, all bets are off
self.last_update_time = now self.last_update_time = now
@ -386,10 +315,9 @@ class RateEstimator:
(can be None for unknown transfer size)""" (can be None for unknown transfer size)"""
if self.total is None: if self.total is None:
return None return None
elif self.total == 0: if self.total == 0:
return 1.0 return 1.0 # pragma: no cover
else: return float(self.last_amount_read) / self.total
return float(self.last_amount_read) / self.total
######################################################################### #########################################################################
# support methods # support methods
@ -413,37 +341,16 @@ class RateEstimator:
try: try:
recent_rate = read_diff / time_diff recent_rate = read_diff / time_diff
except ZeroDivisionError: except ZeroDivisionError: # pragma: no cover
recent_rate = None recent_rate = None
if last_ave is None: if last_ave is None:
return recent_rate return recent_rate
elif recent_rate is None: if recent_rate is None:
return last_ave return last_ave # pragma: no cover
# at this point, both last_ave and recent_rate are numbers # at this point, both last_ave and recent_rate are numbers
return epsilon * recent_rate + (1 - epsilon) * last_ave 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): def format_time(seconds, use_hours=0):
if seconds is None or seconds < 0: if seconds is None or seconds < 0:
@ -452,7 +359,7 @@ def format_time(seconds, use_hours=0):
else: else:
return '--:--' return '--:--'
elif seconds == float('inf'): elif seconds == float('inf'):
return 'Infinite' return 'Infinite' # pragma: no cover
else: else:
seconds = int(seconds) seconds = int(seconds)
minutes = seconds // 60 minutes = seconds // 60
@ -465,7 +372,7 @@ def format_time(seconds, use_hours=0):
return '%02i:%02i' % (minutes, seconds) return '%02i:%02i' % (minutes, seconds)
def format_number(number, SI=0, space=' '): def format_number(number):
"""Turn numbers into human-readable metric-like numbers""" """Turn numbers into human-readable metric-like numbers"""
symbols = ['', # (none) symbols = ['', # (none)
'k', # kilo 'k', # kilo
@ -477,11 +384,7 @@ def format_number(number, SI=0, space=' '):
'Z', # zetta 'Z', # zetta
'Y'] # yotta 'Y'] # yotta
if SI: step = 1024.0
step = 1000.0
else:
step = 1024.0
thresh = 999 thresh = 999
depth = 0 depth = 0
max_depth = len(symbols) - 1 max_depth = len(symbols) - 1
@ -505,4 +408,4 @@ def format_number(number, SI=0, space=' '):
else: else:
fmt = '%.0f%s%s' fmt = '%.0f%s%s'
return(fmt % (float(number or 0), space, symbols[depth])) return fmt % (float(number or 0), " ", symbols[depth])