提交 7a56123e 编写于 作者: C Cleber Rosa 提交者: Lucas Meneghel Rodrigues

Separate test loading and introduce test state

This commit implements two very different things, but still very
much connected:

1) Separate test loading to the process that will be actually
running the test. This leads to a better separation of duties of
the process interacting with the user and the process that is
actually doing the (usually) heavy work.

2) Remove the pickling of the test instance and sending it back
and forth.

As an added bonus, it was noted during development, that both
processes load all necessary Python modules (including the test
module) when unmarshalling the pickled instance, resulting in
unecessary work. Actual data on performance improvements were not
collected though.

It has one change in behaviour (regression) though. Test timeout
now comes only from the test parameters. The reasoning is that
since there's now a better separation of roles and the fact that
the first process is not loading the test instance, it can not
look at the timeout defined there.

IMHO this is a sign of good design, since other types of test
(think of shell scripts) can not communicate their intended timeout.
Maybe a 'test.cfg' or something like that should be the solution to
that.
Signed-off-by: NCleber Rosa <crosa@redhat.com>
上级 db96c62a
...@@ -65,6 +65,8 @@ class TestRunner(object): ...@@ -65,6 +65,8 @@ class TestRunner(object):
""" """
Resolve and load the test url from the the test shortname. Resolve and load the test url from the the test shortname.
This method should now be called by the test runner process.
:param params: Dictionary with test params. :param params: Dictionary with test params.
:type params: dict :type params: dict
:return: an instance of :class:`avocado.test.Test`. :return: an instance of :class:`avocado.test.Test`.
...@@ -109,7 +111,7 @@ class TestRunner(object): ...@@ -109,7 +111,7 @@ class TestRunner(object):
return test_instance return test_instance
def run_test(self, instance, queue): def run_test(self, params, queue):
""" """
Run a test instance in a subprocess. Run a test instance in a subprocess.
...@@ -123,10 +125,13 @@ class TestRunner(object): ...@@ -123,10 +125,13 @@ class TestRunner(object):
raise exceptions.TestTimeoutError(e_msg) raise exceptions.TestTimeoutError(e_msg)
signal.signal(signal.SIGUSR1, timeout_handler) signal.signal(signal.SIGUSR1, timeout_handler)
instance = self.load_test(params)
self.result.start_test(instance.get_state())
try: try:
instance.run_avocado() instance.run_avocado()
finally: finally:
queue.put(instance) queue.put(instance.get_state())
def run(self, params_list): def run(self, params_list):
""" """
...@@ -145,32 +150,25 @@ class TestRunner(object): ...@@ -145,32 +150,25 @@ class TestRunner(object):
self.result.start_tests() self.result.start_tests()
q = multiprocessing.Queue() q = multiprocessing.Queue()
for params in params_list: for params in params_list:
test_instance = self.load_test(params)
self.result.start_test(test_instance)
p = multiprocessing.Process(target=self.run_test, p = multiprocessing.Process(target=self.run_test,
args=(test_instance, q,)) args=(params, q,))
p.start() p.start()
# The test timeout can come from: # Change in behaviour: timeout now comes *only* from test params
# 1) Test params dict (params)
# 2) Test default params dict (test_instance.params.timeout)
timeout = params.get('timeout') timeout = params.get('timeout')
if timeout is None:
if hasattr(test_instance.params, 'timeout'):
timeout = test_instance.params.timeout
if timeout is not None: if timeout is not None:
timeout = float(timeout) timeout = float(timeout)
# Wait for the test to end for [timeout] s # Wait for the test to end for [timeout] s
try: try:
test_instance = q.get(timeout=timeout) test_state = q.get(timeout=timeout)
except Exception: except Exception:
# If there's nothing inside the queue after timeout, the process # If there's nothing inside the queue after timeout, the process
# must be terminated. # must be terminated.
send_signal(p, signal.SIGUSR1) send_signal(p, signal.SIGUSR1)
test_instance = q.get() test_state = q.get()
self.result.check_test(test_instance) self.result.check_test(test_state)
if not status.mapping[test_instance.status]: if not status.mapping[test_state['status']]:
failures.append(test_instance.name) failures.append(test_state['name'])
self.result.end_tests() self.result.end_tests()
return failures return failures
......
...@@ -42,18 +42,19 @@ class JSONTestResult(TestResult): ...@@ -42,18 +42,19 @@ class JSONTestResult(TestResult):
self.json = {'debuglog': self.stream.logfile, self.json = {'debuglog': self.stream.logfile,
'tests': []} 'tests': []}
def end_test(self, test): def end_test(self, state):
""" """
Called when the given test has been run. Called when the given test has been run.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.end_test(self, test) TestResult.end_test(self, state)
t = {'test': test.tagged_name, t = {'test': state['tagged_name'],
'url': test.name, 'url': state['name'],
'time': test.time_elapsed, 'time': state['time_elapsed'],
'status': test.status, 'status': state['status'],
'whiteboard': test.whiteboard, 'whiteboard': state['whiteboard'],
} }
self.json['tests'].append(t) self.json['tests'].append(t)
......
...@@ -83,68 +83,72 @@ class XmlResult(object): ...@@ -83,68 +83,72 @@ class XmlResult(object):
self.xml.append(tc) self.xml.append(tc)
self.xml.append('</testsuite>') self.xml.append('</testsuite>')
def add_success(self, test): def add_success(self, state):
""" """
Add a testcase node of kind succeed. Add a testcase node of kind succeed.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
tc = '\t<testcase classname={class} name={name} time="{time}"/>' tc = '\t<testcase classname={class} name={name} time="{time}"/>'
values = {'class': self._escape_attr(test.__class__.__name__), values = {'class': self._escape_attr(state['class_name']),
'name': self._escape_attr(test.tagged_name), 'name': self._escape_attr(state['tagged_name']),
'time': test.time_elapsed} 'time': state['time_elapsed']}
self.testcases.append(tc.format(**values)) self.testcases.append(tc.format(**values))
def add_skip(self, test): def add_skip(self, state):
""" """
Add a testcase node of kind skipped. Add a testcase node of kind skipped.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
tc = '''\t<testcase classname={class} name={name} time="{time}"> tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<skipped /> \t\t<skipped />
\t</testcase>''' \t</testcase>'''
values = {'class': self._escape_attr(test.__class__.__name__), values = {'class': self._escape_attr(state['class_name']),
'name': self._escape_attr(test.tagged_name), 'name': self._escape_attr(state['tagged_name']),
'time': test.time_elapsed} 'time': state['time_elapsed']}
self.testcases.append(tc.format(**values)) self.testcases.append(tc.format(**values))
def add_failure(self, test): def add_failure(self, state):
""" """
Add a testcase node of kind failed. Add a testcase node of kind failed.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
tc = '''\t<testcase classname={class} name={name} time="{time}"> tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<failure type={type} message={reason}><![CDATA[{traceback}]]></failure> \t\t<failure type={type} message={reason}><![CDATA[{traceback}]]></failure>
\t\t<system-out><![CDATA[{systemout}]]></system-out> \t\t<system-out><![CDATA[{systemout}]]></system-out>
\t</testcase>''' \t</testcase>'''
values = {'class': self._escape_attr(test.__class__.__name__), values = {'class': self._escape_attr(state['class_name']),
'name': self._escape_attr(test.tagged_name), 'name': self._escape_attr(state['tagged_name']),
'time': test.time_elapsed, 'time': state['time_elapsed'],
'type': self._escape_attr(test.fail_class), 'type': self._escape_attr(state['fail_class']),
'traceback': self._escape_cdata(test.traceback), 'traceback': self._escape_cdata(state['traceback']),
'systemout': self._escape_cdata(test.text_output), 'systemout': self._escape_cdata(state['text_output']),
'reason': self._escape_attr(str(test.fail_reason))} 'reason': self._escape_attr(str(state['fail_reason']))}
self.testcases.append(tc.format(**values)) self.testcases.append(tc.format(**values))
def add_error(self, test): def add_error(self, state):
""" """
Add a testcase node of kind error. Add a testcase node of kind error.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
tc = '''\t<testcase classname={class} name={name} time="{time}"> tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<error type={type} message={reason}><![CDATA[{traceback}]]></error> \t\t<error type={type} message={reason}><![CDATA[{traceback}]]></error>
\t\t<system-out><![CDATA[{systemout}]]></system-out> \t\t<system-out><![CDATA[{systemout}]]></system-out>
\t</testcase>''' \t</testcase>'''
values = {'class': self._escape_attr(test.__class__.__name__), values = {'class': self._escape_attr(state['class_name']),
'name': self._escape_attr(test.tagged_name), 'name': self._escape_attr(state['tagged_name']),
'time': test.time_elapsed, 'time': state['time_elapsed'],
'type': self._escape_attr(test.fail_class), 'type': self._escape_attr(state['fail_class']),
'traceback': self._escape_cdata(test.traceback), 'traceback': self._escape_cdata(state['traceback']),
'systemout': self._escape_cdata(test.text_output), 'systemout': self._escape_cdata(state['text_output']),
'reason': self._escape_attr(str(test.fail_reason))} 'reason': self._escape_attr(str(state['fail_reason']))}
self.testcases.append(tc.format(**values)) self.testcases.append(tc.format(**values))
...@@ -183,19 +187,22 @@ class xUnitTestResult(TestResult): ...@@ -183,19 +187,22 @@ class xUnitTestResult(TestResult):
""" """
TestResult.start_test(self, test) TestResult.start_test(self, test)
def end_test(self, test): def end_test(self, state):
""" """
Record an end test event, accord to the given test status. Record an end test event, accord to the given test status.
"""
TestResult.end_test(self, test) :param state: result of :class:`avocado.test.Test.get_state`.
if test.status == 'PASS': :type state: dict
self.xml.add_success(test) """
if test.status == 'TEST_NA': TestResult.end_test(self, state)
self.xml.add_skip(test) if state['status'] == 'PASS':
if test.status == 'FAIL': self.xml.add_success(state)
self.xml.add_failure(test) elif state['status'] == 'TEST_NA':
if test.status == 'ERROR': self.xml.add_skip(state)
self.xml.add_error(test) elif state['status'] == 'FAIL':
self.xml.add_failure(state)
elif state['status'] == 'ERROR':
self.xml.add_error(state)
def end_tests(self): def end_tests(self):
""" """
......
...@@ -52,37 +52,37 @@ class TestResultProxy(object): ...@@ -52,37 +52,37 @@ class TestResultProxy(object):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.end_tests() output_plugin.end_tests()
def start_test(self, test): def start_test(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.start_test(test) output_plugin.start_test(state)
def end_test(self, test): def end_test(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.end_test(test) output_plugin.end_test(state)
def add_pass(self, test): def add_pass(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.add_pass(test) output_plugin.add_pass(state)
def add_error(self, test): def add_error(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.add_error(test) output_plugin.add_error(state)
def add_fail(self, test): def add_fail(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.add_fail(test) output_plugin.add_fail(state)
def add_skip(self, test): def add_skip(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.add_skip(test) output_plugin.add_skip(state)
def add_warn(self, test): def add_warn(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.add_warn(test) output_plugin.add_warn(state)
def check_test(self, test): def check_test(self, state):
for output_plugin in self.output_plugins: for output_plugin in self.output_plugins:
output_plugin.check_test(test) output_plugin.check_test(state)
class TestResult(object): class TestResult(object):
...@@ -149,64 +149,70 @@ class TestResult(object): ...@@ -149,64 +149,70 @@ class TestResult(object):
""" """
pass pass
def start_test(self, test): def start_test(self, state):
""" """
Called when the given test is about to run. Called when the given test is about to run.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
pass pass
def end_test(self, test): def end_test(self, state):
""" """
Called when the given test has been run. Called when the given test has been run.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.tests_run += 1 self.tests_run += 1
self.total_time += test.time_elapsed self.total_time += state['time_elapsed']
def add_pass(self, test): def add_pass(self, state):
""" """
Called when a test succeeded. Called when a test succeeded.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.passed.append(test) self.passed.append(state)
def add_error(self, test): def add_error(self, state):
""" """
Called when a test had a setup error. Called when a test had a setup error.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.errors.append(test) self.errors.append(state)
def add_fail(self, test): def add_fail(self, state):
""" """
Called when a test fails. Called when a test fails.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.failed.append(test) self.failed.append(state)
def add_skip(self, test): def add_skip(self, state):
""" """
Called when a test is skipped. Called when a test is skipped.
:param test: an instance of :class:`avocado.test.Test`. :param test: an instance of :class:`avocado.test.Test`.
""" """
self.skipped.append(test) self.skipped.append(state)
def add_warn(self, test): def add_warn(self, state):
""" """
Called when a test had a warning. Called when a test had a warning.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.warned.append(test) self.warned.append(state)
def check_test(self, test): def check_test(self, state):
""" """
Called once for a test to check status and report. Called once for a test to check status and report.
...@@ -217,9 +223,9 @@ class TestResult(object): ...@@ -217,9 +223,9 @@ class TestResult(object):
'FAIL': self.add_fail, 'FAIL': self.add_fail,
'TEST_NA': self.add_skip, 'TEST_NA': self.add_skip,
'WARN': self.add_warn} 'WARN': self.add_warn}
add = status_map[test.status] add = status_map[state['status']]
add(test) add(state)
self.end_test(test) self.end_test(state)
class HumanTestResult(TestResult): class HumanTestResult(TestResult):
...@@ -247,66 +253,73 @@ class HumanTestResult(TestResult): ...@@ -247,66 +253,73 @@ class HumanTestResult(TestResult):
self.stream.log_header("TOTAL WARNED: %d" % len(self.warned)) self.stream.log_header("TOTAL WARNED: %d" % len(self.warned))
self.stream.log_header("ELAPSED TIME: %.2f s" % self.total_time) self.stream.log_header("ELAPSED TIME: %.2f s" % self.total_time)
def start_test(self, test): def start_test(self, state):
""" """
Called when the given test is about to run. Called when the given test is about to run.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
self.test_label = '(%s/%s) %s: ' % (self.tests_run, self.test_label = '(%s/%s) %s: ' % (self.tests_run,
self.tests_total, self.tests_total,
test.tagged_name) state['tagged_name'])
self.stream.info(msg=self.test_label, skip_newline=True) self.stream.info(msg=self.test_label, skip_newline=True)
def end_test(self, test): def end_test(self, state):
""" """
Called when the given test has been run. Called when the given test has been run.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.end_test(self, test) TestResult.end_test(self, state)
def add_pass(self, test): def add_pass(self, state):
""" """
Called when a test succeeded. Called when a test succeeded.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.add_pass(self, test) TestResult.add_pass(self, state)
self.stream.log_pass(test.time_elapsed) self.stream.log_pass(state['time_elapsed'])
def add_error(self, test): def add_error(self, state):
""" """
Called when a test had a setup error. Called when a test had a setup error.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.add_error(self, test) TestResult.add_error(self, state)
self.stream.log_error(test.time_elapsed) self.stream.log_error(state['time_elapsed'])
def add_fail(self, test): def add_fail(self, state):
""" """
Called when a test fails. Called when a test fails.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.add_fail(self, test) TestResult.add_fail(self, state)
self.stream.log_fail(test.time_elapsed) self.stream.log_fail(state['time_elapsed'])
def add_skip(self, test): def add_skip(self, state):
""" """
Called when a test is skipped. Called when a test is skipped.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.add_skip(self, test) TestResult.add_skip(self, state)
self.stream.log_skip(test.time_elapsed) self.stream.log_skip(state['time_elapsed'])
def add_warn(self, test): def add_warn(self, state):
""" """
Called when a test had a warning. Called when a test had a warning.
:param test: an instance of :class:`avocado.test.Test`. :param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
""" """
TestResult.add_warn(self, test) TestResult.add_warn(self, state)
self.stream.log_warn(test.time_elapsed) self.stream.log_warn(state['time_elapsed'])
...@@ -187,12 +187,12 @@ class Test(unittest.TestCase): ...@@ -187,12 +187,12 @@ class Test(unittest.TestCase):
def __repr__(self): def __repr__(self):
return "Test(%r)" % self.tagged_name return "Test(%r)" % self.tagged_name
def __getstate__(self): def get_state(self):
""" """
Pickle only selected attributes of the class for serialization. Serialize selected attributes representing the test state
The fact we serialize the class means you'll have to modify this :returns: a dictionary containing relevant test state data
class if you intend to make significant changes to its structure. :rtype: dict
""" """
orig = dict(self.__dict__) orig = dict(self.__dict__)
d = {} d = {}
...@@ -205,6 +205,7 @@ class Test(unittest.TestCase): ...@@ -205,6 +205,7 @@ class Test(unittest.TestCase):
if key in preserve_attr: if key in preserve_attr:
d[key] = orig[key] d[key] = orig[key]
d['params'] = orig['_raw_params'] d['params'] = orig['_raw_params']
d['class_name'] = self.__class__.__name__
return d return d
def _set_default(self, key, default): def _set_default(self, key, default):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册