tests: Add full test coverage for progress text output
Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
parent
078178f476
commit
58837a7641
|
@ -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
|
||||
|
|
|
@ -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 ...
|
|
@ -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 ...
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
12345678 | 0 B
|
||||
12345678 | 100 B
|
||||
12345678 | 200 B
|
||||
12345678 | 2.0 kB
|
||||
12345678 | 3.9 kB
|
||||
|
||||
1234567890
|
|
@ -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 !!!
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
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,9 +315,8 @@ class RateEstimator:
|
|||
(can be None for unknown transfer size)"""
|
||||
if self.total is None:
|
||||
return None
|
||||
elif self.total == 0:
|
||||
return 1.0
|
||||
else:
|
||||
if self.total == 0:
|
||||
return 1.0 # pragma: no cover
|
||||
return float(self.last_amount_read) / self.total
|
||||
|
||||
#########################################################################
|
||||
|
@ -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
|
||||
|
||||
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])
|
||||
|
|
Loading…
Reference in New Issue