iotests: use python logging for iotests.log()

We can turn logging on/off globally instead of per-function.

Remove use_log from run_job, and use python logging to turn on
diffable output when we run through a script entry point.

iotest 245 changes output order due to buffering reasons.

An extended note on python logging:

A NullHandler is added to `qemu.iotests` to stop output from being
generated if this code is used as a library without configuring logging.
A NullHandler is only needed at the root, so a duplicate handler is not
needed for `qemu.iotests.diff_io`.

When logging is not configured, messages at the 'WARNING' levels or
above are printed with default settings. The NullHandler stops this from
occurring, which is considered good hygiene for code used as a library.

See https://docs.python.org/3/howto/logging.html#library-config

When logging is actually enabled (always at the behest of an explicit
call by a client script), a root logger is implicitly created at the
root, which allows messages to propagate upwards and be handled/emitted
from the root logger with default settings.

When we want iotest logging, we attach a handler to the
qemu.iotests.diff_io logger and disable propagation to avoid possible
double-printing.

For more information on python logging infrastructure, I highly
recommend downloading the pip package `logging_tree`, which provides
convenient visualizations of the hierarchical logging configuration
under different circumstances.

See https://pypi.org/project/logging_tree/ for more information.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Max Reitz <mreitz@redhat.com>
Message-Id: <20200331000014.11581-15-jsnow@redhat.com>
Reviewed-by: Kevin Wolf <kwolf@redhat.com>
Signed-off-by: Max Reitz <mreitz@redhat.com>
This commit is contained in:
John Snow 2020-03-30 20:00:14 -04:00 committed by Max Reitz
parent 59c29869e0
commit 52ea799e96
5 changed files with 39 additions and 31 deletions

View file

@ -411,8 +411,8 @@ class TestParallelOps(iotests.QMPTestCase):
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=0)
self.assert_qmp(result, 'return', {})
self.vm.run_job(job='drive0', auto_dismiss=True, use_log=False)
self.vm.run_job(job='node4', auto_dismiss=True, use_log=False)
self.vm.run_job(job='drive0', auto_dismiss=True)
self.vm.run_job(job='node4', auto_dismiss=True)
self.assert_no_active_block_jobs()
# Test a block-stream and a block-commit job in parallel

View file

@ -188,7 +188,7 @@ class MirrorBaseClass(BaseClass):
self.assert_qmp(result, 'return', {})
self.vm.run_job('mirror-job', use_log=False, auto_finalize=False,
self.vm.run_job('mirror-job', auto_finalize=False,
pre_finalize=self.openBacking, auto_dismiss=True)
def testFull(self):

View file

@ -1027,5 +1027,6 @@ class TestBlockdevReopen(iotests.QMPTestCase):
self.run_test_iothreads(None, 'iothread0')
if __name__ == '__main__':
iotests.activate_logging()
iotests.main(supported_fmts=["qcow2"],
supported_protocols=["file"])

View file

@ -1,8 +1,3 @@
.....................
----------------------------------------------------------------------
Ran 21 tests
OK
{"execute": "job-finalize", "arguments": {"id": "commit0"}}
{"return": {}}
{"data": {"id": "commit0", "type": "commit"}, "event": "BLOCK_JOB_PENDING", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
@ -15,3 +10,8 @@ OK
{"return": {}}
{"data": {"id": "stream0", "type": "stream"}, "event": "BLOCK_JOB_PENDING", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
{"data": {"device": "stream0", "len": 3145728, "offset": 3145728, "speed": 0, "type": "stream"}, "event": "BLOCK_JOB_COMPLETED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
.....................
----------------------------------------------------------------------
Ran 21 tests
OK

View file

@ -42,6 +42,14 @@
QMPResponse = Dict[str, Any]
# Use this logger for logging messages directly from the iotests module
logger = logging.getLogger('qemu.iotests')
logger.addHandler(logging.NullHandler())
# Use this logger for messages that ought to be used for diff output.
test_logger = logging.getLogger('qemu.iotests.diff_io')
faulthandler.enable()
# This will not work if arguments contain spaces but is necessary if we
@ -385,9 +393,9 @@ def log(msg: Msg,
if isinstance(msg, (dict, list)):
# Don't sort if it's already sorted
do_sort = not isinstance(msg, OrderedDict)
print(json.dumps(msg, sort_keys=do_sort, indent=indent))
test_logger.info(json.dumps(msg, sort_keys=do_sort, indent=indent))
else:
print(msg)
test_logger.info(msg)
class Timeout:
def __init__(self, seconds, errmsg="Timeout"):
@ -609,7 +617,7 @@ def qmp_log(self, cmd, filters=(), indent=None, **kwargs):
# Returns None on success, and an error string on failure
def run_job(self, job, auto_finalize=True, auto_dismiss=False,
pre_finalize=None, cancel=False, use_log=True, wait=60.0):
pre_finalize=None, cancel=False, wait=60.0):
"""
run_job moves a job from creation through to dismissal.
@ -622,7 +630,6 @@ def run_job(self, job, auto_finalize=True, auto_dismiss=False,
invoked prior to issuing job-finalize, if any.
:param cancel: Bool. When true, cancels the job after the pre_finalize
callback.
:param use_log: Bool. When false, does not log QMP messages.
:param wait: Float. Timeout value specifying how long to wait for any
event, in seconds. Defaults to 60.0.
"""
@ -640,8 +647,7 @@ def run_job(self, job, auto_finalize=True, auto_dismiss=False,
while True:
ev = filter_qmp_event(self.events_wait(events, timeout=wait))
if ev['event'] != 'JOB_STATUS_CHANGE':
if use_log:
log(ev)
log(ev)
continue
status = ev['data']['status']
if status == 'aborting':
@ -649,29 +655,18 @@ def run_job(self, job, auto_finalize=True, auto_dismiss=False,
for j in result['return']:
if j['id'] == job:
error = j['error']
if use_log:
log('Job failed: %s' % (j['error']))
log('Job failed: %s' % (j['error']))
elif status == 'ready':
if use_log:
self.qmp_log('job-complete', id=job)
else:
self.qmp('job-complete', id=job)
self.qmp_log('job-complete', id=job)
elif status == 'pending' and not auto_finalize:
if pre_finalize:
pre_finalize()
if cancel and use_log:
if cancel:
self.qmp_log('job-cancel', id=job)
elif cancel:
self.qmp('job-cancel', id=job)
elif use_log:
else:
self.qmp_log('job-finalize', id=job)
else:
self.qmp('job-finalize', id=job)
elif status == 'concluded' and not auto_dismiss:
if use_log:
self.qmp_log('job-dismiss', id=job)
else:
self.qmp('job-dismiss', id=job)
self.qmp_log('job-dismiss', id=job)
elif status == 'null':
return error
@ -991,7 +986,7 @@ def notrun(reason):
seq = os.path.basename(sys.argv[0])
open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n')
print('%s not run: %s' % (seq, reason))
logger.warning("%s not run: %s", seq, reason)
sys.exit(0)
def case_notrun(reason):
@ -1183,6 +1178,7 @@ def execute_setup_common(supported_fmts: Sequence[str] = (),
if debug:
sys.argv.remove('-d')
logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
logger.debug("iotests debugging messages active")
return debug
@ -1195,14 +1191,25 @@ def execute_test(*args, test_function=None, **kwargs):
else:
test_function()
def activate_logging():
"""Activate iotests.log() output to stdout for script-style tests."""
handler = logging.StreamHandler(stream=sys.stdout)
formatter = logging.Formatter('%(message)s')
handler.setFormatter(formatter)
test_logger.addHandler(handler)
test_logger.setLevel(logging.INFO)
test_logger.propagate = False
# This is called from script-style iotests without a single point of entry
def script_initialize(*args, **kwargs):
"""Initialize script-style tests without running any tests."""
activate_logging()
execute_setup_common(*args, **kwargs)
# This is called from script-style iotests with a single point of entry
def script_main(test_function, *args, **kwargs):
"""Run script-style tests outside of the unittest framework"""
activate_logging()
execute_test(*args, test_function=test_function, **kwargs)
# This is called from unittest style iotests