From 91ef5fd115bd1ba04b9f7647f6b53cd6c3ff3982 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 11 Sep 2018 09:04:14 -0700 Subject: [PATCH 01/14] Adds JUnit XML output for regression tests --- Lib/test/libregrtest/cmdline.py | 4 + Lib/test/libregrtest/main.py | 35 +++++- Lib/test/libregrtest/runtest_mp.py | 24 +++- Lib/test/support/__init__.py | 15 ++- Lib/test/support/testresult.py | 185 +++++++++++++++++++++++++++++ Lib/test/test_math.py | 1 + 6 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 Lib/test/support/testresult.py diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index bd126b33c9bdea..538ff05489ea9a 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -268,6 +268,10 @@ def _create_parser(): help='if a test file alters the environment, mark ' 'the test as failed') + group.add_argument('--junit-xml', dest='xmlpath', metavar='FILENAME', + help='writes JUnit-style XML results to the specified ' + 'file') + return parser diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index a176db59c0fb8e..944f5cac20d86c 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -100,7 +100,10 @@ def __init__(self): self.next_single_test = None self.next_single_filename = None - def accumulate_result(self, test, result): + # used by --junit-xml + self.testsuite_xml = None + + def accumulate_result(self, test, result, xml_list): ok, test_time = result if ok not in (CHILD_ERROR, INTERRUPTED): self.test_times.append((test_time, test)) @@ -118,6 +121,10 @@ def accumulate_result(self, test, result): elif ok != INTERRUPTED: raise ValueError("invalid test result: %r" % ok) + if xml_list: + import xml.etree.ElementTree as ET + self.testsuite_xml.append(ET.fromstring(xml_list)) + def display_progress(self, test_index, test): if self.ns.quiet: return @@ -164,6 +171,9 @@ def parse_args(self, kwargs): file=sys.stderr) ns.findleaks = False + if ns.xmlpath: + support.junit_xml_list = self.testsuite_xml = [] + # Strip .py extensions. removepy(ns.args) @@ -508,6 +518,26 @@ def finalize(self): if self.ns.runleaks: os.system("leaks %d" % os.getpid()) + def save_xml_result(self): + if not self.ns.xmlpath and not self.testsuite_xml: + return + + import xml.etree.ElementTree as ET + root = ET.Element("testsuites") + totals = {'tests': 0, 'errors': 0, 'failures': 0} + for suite in self.testsuite_xml: + root.append(suite) + for k in totals: + try: + totals[k] += int(suite.get(k, 0)) + except ValueError: + pass + for k, v in totals.items(): + root.set(k, str(v)) + with open(self.ns.xmlpath, 'wb') as f: + for s in ET.tostringlist(root): + f.write(s) + def main(self, tests=None, **kwargs): global TEMPDIR @@ -570,6 +600,9 @@ def _main(self, tests, kwargs): self.rerun_failed_tests() self.finalize() + + self.save_xml_result() + if self.bad: sys.exit(2) if self.interrupted: diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py index 1f07cfbd8cbe60..ad5187244e3f8e 100644 --- a/Lib/test/libregrtest/runtest_mp.py +++ b/Lib/test/libregrtest/runtest_mp.py @@ -62,6 +62,7 @@ def run_tests_worker(worker_args): ns_dict, testname = json.loads(worker_args) ns = types.SimpleNamespace(**ns_dict) + support.junit_xml_list = xml_list = [] if ns.xmlpath else None setup_tests(ns) try: @@ -74,6 +75,11 @@ def run_tests_worker(worker_args): print() # Force a newline (just in case) print(json.dumps(result), flush=True) + + if xml_list: + import xml.etree.ElementTree as ET + print('XML:' + b''.join(ET.tostring(x) for x in xml_list).decode('us-ascii'), flush=True) + sys.exit(0) @@ -110,7 +116,7 @@ def _runtest(self): try: test = next(self.pending) except StopIteration: - self.output.put((None, None, None, None)) + self.output.put((None, None, None, None, None)) return True try: @@ -129,12 +135,18 @@ def _runtest(self): stdout, _, result = stdout.strip().rpartition("\n") if not result: - self.output.put((None, None, None, None)) + self.output.put((None, None, None, None, None)) return True + if result.startswith("XML:"): + xml_list = result[4:] + stdout, _, result = stdout.strip().rpartition("\n") + else: + xml_list = None + result = json.loads(result) self.output.put((test, stdout.rstrip(), stderr.rstrip(), - result)) + result, xml_list)) return False def run(self): @@ -143,7 +155,7 @@ def run(self): while not stop: stop = self._runtest() except BaseException: - self.output.put((None, None, None, None)) + self.output.put((None, None, None, None, None)) raise @@ -188,11 +200,11 @@ def get_running(workers): print('running: %s' % ', '.join(running), flush=True) continue - test, stdout, stderr, result = item + test, stdout, stderr, result, xml_list = item if test is None: finished += 1 continue - regrtest.accumulate_result(test, result) + regrtest.accumulate_result(test, result, xml_list) # Display progress ok, test_time = result diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index de997b253a693f..2e3b5031ebda0f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -6,6 +6,7 @@ import asyncio.events import collections.abc import contextlib +import datetime import errno import faulthandler import fnmatch @@ -13,6 +14,7 @@ import gc import importlib import importlib.util +import io import logging.handlers import nntplib import os @@ -34,6 +36,8 @@ import urllib.error import warnings +from .testresult import RegressionTestRunner + try: import multiprocessing.process except ImportError: @@ -295,6 +299,7 @@ def get_attribute(obj, name): max_memuse = 0 # Disable bigmem tests (they will still be run with # small sizes, to make sure they work.) real_max_memuse = 0 +junit_xml_list = None # list of testsuite XML elements failfast = False # _original_stdout is meant to hold stdout at the time regrtest began. @@ -1891,13 +1896,13 @@ def _filter_suite(suite, pred): def _run_suite(suite): """Run tests from a unittest.TestSuite-derived class.""" - if verbose: - runner = unittest.TextTestRunner(sys.stdout, verbosity=2, - failfast=failfast) - else: - runner = BasicTestRunner() + runner = RegressionTestRunner(sys.stdout, verbosity=verbose) result = runner.run(suite) + + if junit_xml_list is not None: + junit_xml_list.append(result.get_xml_element()) + if not result.wasSuccessful(): if len(result.errors) == 1 and not result.failures: err = result.errors[0][1] diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py new file mode 100644 index 00000000000000..81f84575466e4a --- /dev/null +++ b/Lib/test/support/testresult.py @@ -0,0 +1,185 @@ +'''Test runner and result class for the regression test suite. + +''' + +import io +import sys +import time +import traceback +import unittest + +import xml.etree.ElementTree as ET + +from datetime import datetime + +class RegressionTestResult(unittest.TestResult): + separator1 = '=' * 70 + '\n' + separator2 = '-' * 70 + '\n' + + def __init__(self, stream, descriptions, verbosity): + super().__init__(stream, descriptions, verbosity) + self.stream = stream + self.descriptions = descriptions + self.buffer = True + self.__suite = ET.Element('testsuite') + self.__suite.set('start', datetime.utcnow().isoformat(' ')) + + self.__e = None + self.__start_time = None + self.__results = [] + self.__verbose = bool(verbosity) + + def getDescription(self, test): + doc_first_line = test.shortDescription() + if self.descriptions and doc_first_line: + return '\n'.join((str(test), doc_first_line)) + else: + return str(test) + + def startTest(self, test): + super().startTest(test) + self.__e = e = ET.SubElement(self.__suite, 'testcase') + self.__start_time = time.perf_counter() + if self.__verbose: + self.stream.write(f'{self.getDescription(test)} ... ') + self.stream.flush() + + def _add_result(self, test, capture=False, **args): + e = self.__e + self.__e = None + if e is None: + return + e.set('name', args.pop('name', test.id())) + e.set('status', args.pop('status', 'run')) + e.set('result', args.pop('result', 'completed')) + if self.__start_time: + e.set('time', f'{time.perf_counter() - self.__start_time:0.3f}') + + if capture: + stdout = self._stdout_buffer.getvalue().rstrip() + ET.SubElement(e, 'system-out').text = stdout + stderr = self._stderr_buffer.getvalue().rstrip() + ET.SubElement(e, 'system-err').text = stderr + + for k, v in args.items(): + if not k or not v: + continue + e2 = ET.SubElement(e, k) + if hasattr(v, 'items'): + for k2, v2 in v.items(): + if k2: + e2.set(k2, str(v2)) + else: + e2.text = str(v2) + else: + e2.text = str(v) + + def __write(self, c, word): + if self.__verbose: + self.stream.write(f'{word}\n') + + @classmethod + def __makeErrorDict(cls, err_type, err_value, err_tb): + if isinstance(err_type, type): + if err_type.__module__ == 'builtins': + typename = err_type.__name__ + else: + typename = f'{err_type.__module__}.{err_type.__name__}' + else: + typename = repr(err_type) + + # TODO: Limit err_tb + tb = traceback.format_exception(err_type, err_value, err_tb) + + return { + 'type': typename, + 'message': str(err_value), + '': ''.join(tb), + } + + def addError(self, test, err): + self._add_result(test, True, error=self.__makeErrorDict(*err)) + super().addError(test, err) + self.__write('E', 'ERROR') + + def addExpectedFailure(self, test, err): + self._add_result(test, True, output=self.__makeErrorDict(*err)) + super().addExpectedFailure(test, err) + self.__write('x', 'expected failure') + + def addFailure(self, test, err): + self._add_result(test, True, failure=self.__makeErrorDict(*err)) + super().addFailure(test, err) + self.__write('F', 'FAIL') + + def addSkip(self, test, reason): + self._add_result(test, skipped=reason) + super().addSkip(test, reason) + self.__write('S', f'skipped {reason!r}') + + def addSuccess(self, test): + self._add_result(test) + super().addSuccess(test) + self.__write('.', 'ok') + + def addUnexpectedSuccess(self, test): + self._add_result(test, outcome='UNEXPECTED_SUCCESS') + super().addUnexpectedSuccess(test) + self.__write('u', 'unexpected success') + + def printErrors(self): + if self.__verbose: + self.stream.write('\n') + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavor, errors): + for test, err in errors: + self.stream.write(self.separator1) + self.stream.write(f'{flavor}: {self.getDescription(test)}\n') + self.stream.write(self.separator2) + self.stream.write('%s\n' % err) + + def get_xml_element(self): + e = self.__suite + e.set('tests', str(self.testsRun)) + e.set('errors', str(len(self.errors))) + e.set('failures', str(len(self.failures))) + return e + +class RegressionTestRunner: + def __init__(self, stream, verbosity): + self.result = RegressionTestResult(stream, None, verbosity) + + def run(self, test): + test(self.result) + return self.result + +if __name__ == '__main__': + class TestTests(unittest.TestCase): + def test_pass(self): + pass + + def test_pass_slow(self): + time.sleep(1.0) + + def test_fail(self): + print('stdout', file=sys.stdout) + print('stderr', file=sys.stderr) + self.fail('failure message') + + def test_error(self): + print('stdout', file=sys.stdout) + print('stderr', file=sys.stderr) + raise RuntimeError('error message') + + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestTests)) + stream = io.StringIO() + runner = RegressionTestRunner(sys.stdout, sum(a == '-v' for a in sys.argv)) + result = runner.run(suite) + print('Output:', stream.getvalue()) + print('XML: ', end='') + for s in ET.tostringlist(result.get_xml_element()): + print(s.decode(), end='') + print() diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 608789f7202f03..a621d01429ed3e 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -400,6 +400,7 @@ def testCeil(self): self.ftest('ceil(-0.5)', math.ceil(-0.5), 0) self.ftest('ceil(-1.0)', math.ceil(-1.0), -1) self.ftest('ceil(-1.5)', math.ceil(-1.5), -1) + self.ftest('ceil(-1.5)', math.ceil(-1.5), -3) #self.assertEqual(math.ceil(INF), INF) #self.assertEqual(math.ceil(NINF), NINF) #self.assertTrue(math.isnan(math.ceil(NAN))) From 141289f80f062402b0fc8da344638c719b31f39c Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 11 Sep 2018 17:48:20 -0700 Subject: [PATCH 02/14] Adds test result upload for PR builds --- .vsts/linux-pr.yml | 11 ++++++++++- .vsts/macos-pr.yml | 11 ++++++++++- .vsts/windows-pr.yml | 11 ++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.vsts/linux-pr.yml b/.vsts/linux-pr.yml index 6e4ac7c65c4dc4..5dc71dac99619b 100644 --- a/.vsts/linux-pr.yml +++ b/.vsts/linux-pr.yml @@ -70,6 +70,15 @@ steps: displayName: 'Run patchcheck.py' condition: and(succeeded(), ne(variables['DocOnly'], 'true')) -- script: xvfb-run make buildbottest TESTOPTS="-j4 -uall,-cpu" +- script: xvfb-run make buildbottest TESTOPTS="-j4 -uall,-cpu --junit-xml=$(build.binariesDirectory)/test-results.xml" displayName: 'Tests' condition: and(succeeded(), ne(variables['DocOnly'], 'true')) + +- task: PublishTestResults@2 + displayName: 'Publish Test Results' + inputs: + testResultsFiles: '$(build.binariesDirectory)/test-results.xml' + mergeTestResults: true + testRunTitle: '$(system.pullRequest.targetBranch)-$(agent.os)' + platform: $(agent.os) + condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) diff --git a/.vsts/macos-pr.yml b/.vsts/macos-pr.yml index c56e66b5090b3b..0ad120869b0898 100644 --- a/.vsts/macos-pr.yml +++ b/.vsts/macos-pr.yml @@ -50,6 +50,15 @@ steps: displayName: 'Display build info' condition: and(succeeded(), ne(variables['DocOnly'], 'true')) -- script: make buildbottest TESTOPTS="-j4 -uall,-cpu" +- script: make buildbottest TESTOPTS="-j4 -uall,-cpu --junit-xml=$(build.binariesDirectory)/test-results.xml" displayName: 'Tests' condition: and(succeeded(), ne(variables['DocOnly'], 'true')) + +- task: PublishTestResults@2 + displayName: 'Publish Test Results' + inputs: + testResultsFiles: '$(build.binariesDirectory)/test-results.xml' + mergeTestResults: true + testRunTitle: '$(system.pullRequest.targetBranch)-$(agent.os)' + platform: $(agent.os) + condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) diff --git a/.vsts/windows-pr.yml b/.vsts/windows-pr.yml index 3dd5609a32e514..273d32235c57b6 100644 --- a/.vsts/windows-pr.yml +++ b/.vsts/windows-pr.yml @@ -54,8 +54,17 @@ steps: displayName: 'Display build info' condition: and(succeeded(), ne(variables['DocOnly'], 'true')) -- script: PCbuild\rt.bat -q -uall -u-cpu -rwW --slowest --timeout=1200 -j0 +- script: PCbuild\rt.bat -q -uall -u-cpu -rwW --slowest --timeout=1200 -j0 --junit-xml="$(Build.BinariesDirectory)\test-results.xml" displayName: 'Tests' env: PREFIX: $(Py_OutDir)\$(outDirSuffix) condition: and(succeeded(), ne(variables['DocOnly'], 'true')) + +- task: PublishTestResults@2 + displayName: 'Publish Test Results' + inputs: + testResultsFiles: '$(Build.BinariesDirectory)\test-results.xml' + mergeTestResults: true + testRunTitle: '$(System.PullRequest.TargetBranch)-$(Agent.OS)' + platform: $(Agent.OS) + condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) From 44d34a51163a8503db6a4409158d089571afc575 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 12 Sep 2018 07:47:35 -0700 Subject: [PATCH 03/14] Fix whitespace --- Lib/test/support/testresult.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py index 81f84575466e4a..a2716e26715b4a 100644 --- a/Lib/test/support/testresult.py +++ b/Lib/test/support/testresult.py @@ -23,7 +23,7 @@ def __init__(self, stream, descriptions, verbosity): self.buffer = True self.__suite = ET.Element('testsuite') self.__suite.set('start', datetime.utcnow().isoformat(' ')) - + self.__e = None self.__start_time = None self.__results = [] @@ -172,7 +172,7 @@ def test_error(self): print('stdout', file=sys.stdout) print('stderr', file=sys.stderr) raise RuntimeError('error message') - + suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestTests)) stream = io.StringIO() From ee6be458999a8ce47a369f1df49378441de74944 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 12 Sep 2018 12:20:24 -0700 Subject: [PATCH 04/14] Move XML data into test result tuple --- Lib/test/libregrtest/main.py | 8 ++++---- Lib/test/libregrtest/runtest.py | 17 ++++++++++++++--- Lib/test/libregrtest/runtest_mp.py | 30 ++++++++++++------------------ Lib/test/support/testresult.py | 16 ++++++++++++++-- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 944f5cac20d86c..5c3f5f6dc5ed78 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -103,8 +103,8 @@ def __init__(self): # used by --junit-xml self.testsuite_xml = None - def accumulate_result(self, test, result, xml_list): - ok, test_time = result + def accumulate_result(self, test, result): + ok, test_time, xml_data = result if ok not in (CHILD_ERROR, INTERRUPTED): self.test_times.append((test_time, test)) if ok == PASSED: @@ -121,9 +121,9 @@ def accumulate_result(self, test, result, xml_list): elif ok != INTERRUPTED: raise ValueError("invalid test result: %r" % ok) - if xml_list: + if xml_data: import xml.etree.ElementTree as ET - self.testsuite_xml.append(ET.fromstring(xml_list)) + self.testsuite_xml.append(ET.fromstring(xml_data)) def display_progress(self, test_index, test): if self.ns.quiet: diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py index 3e1afd41997aad..826f3a7bf09ce9 100644 --- a/Lib/test/libregrtest/runtest.py +++ b/Lib/test/libregrtest/runtest.py @@ -85,8 +85,8 @@ def runtest(ns, test): ns -- regrtest namespace of options test -- the name of the test - Returns the tuple (result, test_time), where result is one of the - constants: + Returns the tuple (result, test_time, xml_data), where result is one + of the constants: INTERRUPTED KeyboardInterrupt when run under -j RESOURCE_DENIED test skipped because resource denied @@ -94,6 +94,9 @@ def runtest(ns, test): ENV_CHANGED test failed because it changed the execution environment FAILED test failed PASSED test passed + + If ns.xmlpath is not None, xml_data is a string containing the + generated testsuite element containing results. """ output_on_failure = ns.verbose3 @@ -106,6 +109,7 @@ def runtest(ns, test): # reset the environment_altered flag to detect if a test altered # the environment support.environment_altered = False + support.junit_xml_list = xml_list = [] if ns.xmlpath else None if ns.failfast: support.failfast = True if output_on_failure: @@ -138,11 +142,18 @@ def runtest(ns, test): else: support.verbose = ns.verbose # Tell tests to be moderately quiet result = runtest_inner(ns, test, display_failure=not ns.verbose) - return result + + if xml_list: + import xml.etree.ElementTree as ET + xml_data = b''.join(ET.tostring(x) for x in xml_list).decode('us-ascii') + else: + xml_data = None + return result + (xml_data,) finally: if use_timeout: faulthandler.cancel_dump_traceback_later() cleanup_test_droppings(test, ns.verbose) + support.junit_xml_list = None runtest.stringio = None diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py index ad5187244e3f8e..132e00257804a8 100644 --- a/Lib/test/libregrtest/runtest_mp.py +++ b/Lib/test/libregrtest/runtest_mp.py @@ -62,7 +62,6 @@ def run_tests_worker(worker_args): ns_dict, testname = json.loads(worker_args) ns = types.SimpleNamespace(**ns_dict) - support.junit_xml_list = xml_list = [] if ns.xmlpath else None setup_tests(ns) try: @@ -76,10 +75,6 @@ def run_tests_worker(worker_args): print() # Force a newline (just in case) print(json.dumps(result), flush=True) - if xml_list: - import xml.etree.ElementTree as ET - print('XML:' + b''.join(ET.tostring(x) for x in xml_list).decode('us-ascii'), flush=True) - sys.exit(0) @@ -116,7 +111,7 @@ def _runtest(self): try: test = next(self.pending) except StopIteration: - self.output.put((None, None, None, None, None)) + self.output.put((None, None, None, None)) return True try: @@ -135,18 +130,17 @@ def _runtest(self): stdout, _, result = stdout.strip().rpartition("\n") if not result: - self.output.put((None, None, None, None, None)) + self.output.put((None, None, None, None)) return True - if result.startswith("XML:"): - xml_list = result[4:] - stdout, _, result = stdout.strip().rpartition("\n") - else: - xml_list = None + try: + result = json.loads(result) + except json.decoder.JSONDecodeError: + stdout += "\n" + result + result = None - result = json.loads(result) self.output.put((test, stdout.rstrip(), stderr.rstrip(), - result, xml_list)) + result)) return False def run(self): @@ -155,7 +149,7 @@ def run(self): while not stop: stop = self._runtest() except BaseException: - self.output.put((None, None, None, None, None)) + self.output.put((None, None, None, None)) raise @@ -200,14 +194,14 @@ def get_running(workers): print('running: %s' % ', '.join(running), flush=True) continue - test, stdout, stderr, result, xml_list = item + test, stdout, stderr, result = item if test is None: finished += 1 continue - regrtest.accumulate_result(test, result, xml_list) + regrtest.accumulate_result(test, result) # Display progress - ok, test_time = result + ok, test_time, xml_data = result text = format_test_result(test, ok) if (ok not in (CHILD_ERROR, INTERRUPTED) and test_time >= PROGRESS_MIN_TIME diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py index a2716e26715b4a..e4b658f9b7d098 100644 --- a/Lib/test/support/testresult.py +++ b/Lib/test/support/testresult.py @@ -29,6 +29,18 @@ def __init__(self, stream, descriptions, verbosity): self.__results = [] self.__verbose = bool(verbosity) + @classmethod + def __getId(cls, test): + try: + test_id = test.id + except AttributeError: + return str(test) + try: + return test_id() + except TypeError: + return str(test_id) + return repr(test) + def getDescription(self, test): doc_first_line = test.shortDescription() if self.descriptions and doc_first_line: @@ -49,11 +61,11 @@ def _add_result(self, test, capture=False, **args): self.__e = None if e is None: return - e.set('name', args.pop('name', test.id())) + e.set('name', args.pop('name', self.__getId(test))) e.set('status', args.pop('status', 'run')) e.set('result', args.pop('result', 'completed')) if self.__start_time: - e.set('time', f'{time.perf_counter() - self.__start_time:0.3f}') + e.set('time', f'{time.perf_counter() - self.__start_time:0.6f}') if capture: stdout = self._stdout_buffer.getvalue().rstrip() From 865ed6e37d50a4b288fe08292ec0ae367f659f5c Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 12 Sep 2018 12:36:32 -0700 Subject: [PATCH 05/14] Adds additional logging for XML parse error --- Lib/test/libregrtest/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 5c3f5f6dc5ed78..5a03f8958f9ebf 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -123,7 +123,11 @@ def accumulate_result(self, test, result): if xml_data: import xml.etree.ElementTree as ET - self.testsuite_xml.append(ET.fromstring(xml_data)) + try: + self.testsuite_xml.append(ET.fromstring(xml_data)) + except ET.ParseError: + print(xml_data, file=sys.__stderr__) + raise def display_progress(self, test_index, test): if self.ns.quiet: From c3beb5dee3f6c626547b9b6dce16070d87184d02 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 13 Sep 2018 08:38:16 -0700 Subject: [PATCH 06/14] Return XML results as list --- Lib/test/libregrtest/main.py | 20 +++++++++++++------- Lib/test/libregrtest/runtest.py | 2 +- Lib/test/libregrtest/runtest_mp.py | 8 +------- Lib/test/support/__init__.py | 3 +++ Lib/test/support/testresult.py | 3 ++- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 5a03f8958f9ebf..1fd41320d11193 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -123,11 +123,12 @@ def accumulate_result(self, test, result): if xml_data: import xml.etree.ElementTree as ET - try: - self.testsuite_xml.append(ET.fromstring(xml_data)) - except ET.ParseError: - print(xml_data, file=sys.__stderr__) - raise + for e in xml_data: + try: + self.testsuite_xml.append(ET.fromstring(e)) + except ET.ParseError: + print(xml_data, file=sys.__stderr__) + raise def display_progress(self, test_index, test): if self.ns.quiet: @@ -398,7 +399,7 @@ def run_tests_sequential(self): result = runtest(self.ns, test) except KeyboardInterrupt: self.interrupted = True - self.accumulate_result(test, (INTERRUPTED, None)) + self.accumulate_result(test, (INTERRUPTED, None, None)) break else: self.accumulate_result(test, result) @@ -528,6 +529,8 @@ def save_xml_result(self): import xml.etree.ElementTree as ET root = ET.Element("testsuites") + + # Manually count the totals for the overall summary totals = {'tests': 0, 'errors': 0, 'failures': 0} for suite in self.testsuite_xml: root.append(suite) @@ -536,9 +539,12 @@ def save_xml_result(self): totals[k] += int(suite.get(k, 0)) except ValueError: pass + for k, v in totals.items(): root.set(k, str(v)) - with open(self.ns.xmlpath, 'wb') as f: + + xmlpath = os.path.join(support.SAVEDCWD, self.ns.xmlpath) + with open(xmlpath, 'wb') as f: for s in ET.tostringlist(root): f.write(s) diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py index 826f3a7bf09ce9..fd9c1cc349c689 100644 --- a/Lib/test/libregrtest/runtest.py +++ b/Lib/test/libregrtest/runtest.py @@ -145,7 +145,7 @@ def runtest(ns, test): if xml_list: import xml.etree.ElementTree as ET - xml_data = b''.join(ET.tostring(x) for x in xml_list).decode('us-ascii') + xml_data = [ET.tostring(x).decode('us-ascii') for x in xml_list] else: xml_data = None return result + (xml_data,) diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py index 132e00257804a8..6c2939b21bf6fd 100644 --- a/Lib/test/libregrtest/runtest_mp.py +++ b/Lib/test/libregrtest/runtest_mp.py @@ -74,7 +74,6 @@ def run_tests_worker(worker_args): print() # Force a newline (just in case) print(json.dumps(result), flush=True) - sys.exit(0) @@ -133,12 +132,7 @@ def _runtest(self): self.output.put((None, None, None, None)) return True - try: - result = json.loads(result) - except json.decoder.JSONDecodeError: - stdout += "\n" + result - result = None - + result = json.loads(result) self.output.put((test, stdout.rstrip(), stderr.rstrip(), result)) return False diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 2e3b5031ebda0f..6522acee153bcd 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1898,6 +1898,9 @@ def _run_suite(suite): """Run tests from a unittest.TestSuite-derived class.""" runner = RegressionTestRunner(sys.stdout, verbosity=verbose) + # TODO: Remove this before merging (here for easy comparison with old impl) + #runner = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=failfast) + result = runner.run(suite) if junit_xml_list is not None: diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py index e4b658f9b7d098..aa5a72df008b1d 100644 --- a/Lib/test/support/testresult.py +++ b/Lib/test/support/testresult.py @@ -102,10 +102,11 @@ def __makeErrorDict(cls, err_type, err_value, err_tb): # TODO: Limit err_tb tb = traceback.format_exception(err_type, err_value, err_tb) + msg = traceback.format_exception(err_type, err_value, None) return { 'type': typename, - 'message': str(err_value), + 'message': ''.join(msg), '': ''.join(tb), } From 9ac843b838bae83e21ab73f1237cd7cb97d38330 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 13 Sep 2018 08:53:49 -0700 Subject: [PATCH 07/14] Fix whitespace --- Lib/test/support/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 6522acee153bcd..c983790b05dcb8 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1899,7 +1899,7 @@ def _run_suite(suite): runner = RegressionTestRunner(sys.stdout, verbosity=verbose) # TODO: Remove this before merging (here for easy comparison with old impl) - #runner = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=failfast) + #runner = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=failfast) result = runner.run(suite) From 287467e10b0078a084310383efd7f377fae68810 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 13 Sep 2018 09:49:58 -0700 Subject: [PATCH 08/14] Fix OS labelling in test runs --- .vsts/linux-pr.yml | 4 ++-- .vsts/macos-pr.yml | 4 ++-- .vsts/windows-pr.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.vsts/linux-pr.yml b/.vsts/linux-pr.yml index 5dc71dac99619b..d11a4f06e4e1ab 100644 --- a/.vsts/linux-pr.yml +++ b/.vsts/linux-pr.yml @@ -79,6 +79,6 @@ steps: inputs: testResultsFiles: '$(build.binariesDirectory)/test-results.xml' mergeTestResults: true - testRunTitle: '$(system.pullRequest.targetBranch)-$(agent.os)' - platform: $(agent.os) + testRunTitle: '$(system.pullRequest.targetBranch)-linux' + platform: linux condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) diff --git a/.vsts/macos-pr.yml b/.vsts/macos-pr.yml index 0ad120869b0898..69b619e4757748 100644 --- a/.vsts/macos-pr.yml +++ b/.vsts/macos-pr.yml @@ -59,6 +59,6 @@ steps: inputs: testResultsFiles: '$(build.binariesDirectory)/test-results.xml' mergeTestResults: true - testRunTitle: '$(system.pullRequest.targetBranch)-$(agent.os)' - platform: $(agent.os) + testRunTitle: '$(system.pullRequest.targetBranch)-macOS' + platform: macOS condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) diff --git a/.vsts/windows-pr.yml b/.vsts/windows-pr.yml index 273d32235c57b6..7134120d641467 100644 --- a/.vsts/windows-pr.yml +++ b/.vsts/windows-pr.yml @@ -65,6 +65,6 @@ steps: inputs: testResultsFiles: '$(Build.BinariesDirectory)\test-results.xml' mergeTestResults: true - testRunTitle: '$(System.PullRequest.TargetBranch)-$(Agent.OS)' - platform: $(Agent.OS) + testRunTitle: '$(System.PullRequest.TargetBranch)-$(outDirSuffix)' + platform: $(outDirSuffix) condition: and(succeededOrFailed(), ne(variables['DocOnly'], 'true')) From 0558f1ed9616b2587ec4d0a9aa5b32ac9f793f98 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 08:41:51 -0700 Subject: [PATCH 09/14] Fixes verbosity of test output --- Lib/test/libregrtest/runtest.py | 4 ++-- Lib/test/libregrtest/runtest_mp.py | 5 ++-- Lib/test/support/__init__.py | 4 ++-- Lib/test/support/testresult.py | 37 ++++++++++++++++-------------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py index fd9c1cc349c689..5dd2cfd5785d73 100644 --- a/Lib/test/libregrtest/runtest.py +++ b/Lib/test/libregrtest/runtest.py @@ -95,8 +95,8 @@ def runtest(ns, test): FAILED test failed PASSED test passed - If ns.xmlpath is not None, xml_data is a string containing the - generated testsuite element containing results. + If ns.xmlpath is not None, xml_data is a list containing each + generated testsuite element. """ output_on_failure = ns.verbose3 diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py index 6c2939b21bf6fd..6190574afdf8b3 100644 --- a/Lib/test/libregrtest/runtest_mp.py +++ b/Lib/test/libregrtest/runtest_mp.py @@ -67,7 +67,7 @@ def run_tests_worker(worker_args): try: result = runtest(ns, testname) except KeyboardInterrupt: - result = INTERRUPTED, '' + result = INTERRUPTED, '', None except BaseException as e: traceback.print_exc() result = CHILD_ERROR, str(e) @@ -122,7 +122,7 @@ def _runtest(self): self.current_test = None if retcode != 0: - result = (CHILD_ERROR, "Exit code %s" % retcode) + result = (CHILD_ERROR, "Exit code %s" % retcode, None) self.output.put((test, stdout.rstrip(), stderr.rstrip(), result)) return False @@ -133,6 +133,7 @@ def _runtest(self): return True result = json.loads(result) + assert len(result) == 3, f"Invalid result tuple: {result!r}" self.output.put((test, stdout.rstrip(), stderr.rstrip(), result)) return False diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index c983790b05dcb8..19701cf388fe4e 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -36,7 +36,7 @@ import urllib.error import warnings -from .testresult import RegressionTestRunner +from .testresult import get_test_runner try: import multiprocessing.process @@ -1896,7 +1896,7 @@ def _filter_suite(suite, pred): def _run_suite(suite): """Run tests from a unittest.TestSuite-derived class.""" - runner = RegressionTestRunner(sys.stdout, verbosity=verbose) + runner = get_test_runner(sys.stdout, verbosity=verbose) # TODO: Remove this before merging (here for easy comparison with old impl) #runner = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=failfast) diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py index aa5a72df008b1d..8988d3d1527871 100644 --- a/Lib/test/support/testresult.py +++ b/Lib/test/support/testresult.py @@ -2,6 +2,7 @@ ''' +import functools import io import sys import time @@ -12,14 +13,12 @@ from datetime import datetime -class RegressionTestResult(unittest.TestResult): +class RegressionTestResult(unittest.TextTestResult): separator1 = '=' * 70 + '\n' separator2 = '-' * 70 + '\n' def __init__(self, stream, descriptions, verbosity): - super().__init__(stream, descriptions, verbosity) - self.stream = stream - self.descriptions = descriptions + super().__init__(stream=stream, descriptions=descriptions, verbosity=0) self.buffer = True self.__suite = ET.Element('testsuite') self.__suite.set('start', datetime.utcnow().isoformat(' ')) @@ -41,13 +40,6 @@ def __getId(cls, test): return str(test_id) return repr(test) - def getDescription(self, test): - doc_first_line = test.shortDescription() - if self.descriptions and doc_first_line: - return '\n'.join((str(test), doc_first_line)) - else: - return str(test) - def startTest(self, test): super().startTest(test) self.__e = e = ET.SubElement(self.__suite, 'testcase') @@ -100,9 +92,8 @@ def __makeErrorDict(cls, err_type, err_value, err_tb): else: typename = repr(err_type) - # TODO: Limit err_tb - tb = traceback.format_exception(err_type, err_value, err_tb) msg = traceback.format_exception(err_type, err_value, None) + tb = traceback.format_exception(err_type, err_value, err_tb) return { 'type': typename, @@ -160,14 +151,25 @@ def get_xml_element(self): e.set('failures', str(len(self.failures))) return e -class RegressionTestRunner: - def __init__(self, stream, verbosity): - self.result = RegressionTestResult(stream, None, verbosity) +class QuietRegressionTestRunner: + def __init__(self, stream): + self.result = RegressionTestResult(stream, None, 0) def run(self, test): test(self.result) return self.result +def get_test_runner_class(verbosity): + if verbosity: + return functools.partial(unittest.TextTestRunner, + resultclass=RegressionTestResult, + buffer=True, + verbosity=verbosity) + return QuietRegressionTestRunner + +def get_test_runner(stream, verbosity): + return get_test_runner_class(verbosity)(stream) + if __name__ == '__main__': class TestTests(unittest.TestCase): def test_pass(self): @@ -189,7 +191,8 @@ def test_error(self): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestTests)) stream = io.StringIO() - runner = RegressionTestRunner(sys.stdout, sum(a == '-v' for a in sys.argv)) + runner_cls = get_test_runner_class(sum(a == '-v' for a in sys.argv)) + runner = runner_cls(sys.stdout) result = runner.run(suite) print('Output:', stream.getvalue()) print('XML: ', end='') From 4bbb19c86dfef0a54ff42d74fb7a62a586960d73 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 08:58:45 -0700 Subject: [PATCH 10/14] Revert deliberately failing test --- Lib/test/test_math.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index a35704bfc556bc..9b2f55e1f410f8 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -400,7 +400,6 @@ def testCeil(self): self.ftest('ceil(-0.5)', math.ceil(-0.5), 0) self.ftest('ceil(-1.0)', math.ceil(-1.0), -1) self.ftest('ceil(-1.5)', math.ceil(-1.5), -1) - self.ftest('ceil(-1.5)', math.ceil(-1.5), -3) #self.assertEqual(math.ceil(INF), INF) #self.assertEqual(math.ceil(NINF), NINF) #self.assertTrue(math.isnan(math.ceil(NAN))) From 9a60bbd179930aa727899f96cf2a4ab3c3c493fd Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 09:34:22 -0700 Subject: [PATCH 11/14] Make test_argparse not depend on std stream instances --- Lib/test/libregrtest/runtest.py | 13 +------------ Lib/test/test_argparse.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py index 5dd2cfd5785d73..4f41080d37b998 100644 --- a/Lib/test/libregrtest/runtest.py +++ b/Lib/test/libregrtest/runtest.py @@ -115,17 +115,7 @@ def runtest(ns, test): if output_on_failure: support.verbose = True - # Reuse the same instance to all calls to runtest(). Some - # tests keep a reference to sys.stdout or sys.stderr - # (eg. test_argparse). - if runtest.stringio is None: - stream = io.StringIO() - runtest.stringio = stream - else: - stream = runtest.stringio - stream.seek(0) - stream.truncate() - + stream = io.StringIO() orig_stdout = sys.stdout orig_stderr = sys.stderr try: @@ -154,7 +144,6 @@ def runtest(ns, test): faulthandler.cancel_dump_traceback_later() cleanup_test_droppings(test, ns.verbose) support.junit_xml_list = None -runtest.stringio = None def post_test_cleanup(): diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index f0802a50973116..c0c7cb05940ba9 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1459,6 +1459,16 @@ def test_r_1_replace(self): type = argparse.FileType('r', 1, errors='replace') self.assertEqual("FileType('r', 1, errors='replace')", repr(type)) +class StdStreamComparer: + def __init__(self, attr): + self.attr = attr + + def __eq__(self, other): + return other == getattr(sys, self.attr) + +eq_stdin = StdStreamComparer('stdin') +eq_stdout = StdStreamComparer('stdout') +eq_stderr = StdStreamComparer('stderr') class RFile(object): seen = {} @@ -1497,7 +1507,7 @@ def setUp(self): ('foo', NS(x=None, spam=RFile('foo'))), ('-x foo bar', NS(x=RFile('foo'), spam=RFile('bar'))), ('bar -x foo', NS(x=RFile('foo'), spam=RFile('bar'))), - ('-x - -', NS(x=sys.stdin, spam=sys.stdin)), + ('-x - -', NS(x=eq_stdin, spam=eq_stdin)), ('readonly', NS(x=None, spam=RFile('readonly'))), ] @@ -1537,7 +1547,7 @@ def setUp(self): ('foo', NS(x=None, spam=RFile('foo'))), ('-x foo bar', NS(x=RFile('foo'), spam=RFile('bar'))), ('bar -x foo', NS(x=RFile('foo'), spam=RFile('bar'))), - ('-x - -', NS(x=sys.stdin, spam=sys.stdin)), + ('-x - -', NS(x=eq_stdin, spam=eq_stdin)), ] @@ -1576,7 +1586,7 @@ def setUp(self): ('foo', NS(x=None, spam=WFile('foo'))), ('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))), ('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))), - ('-x - -', NS(x=sys.stdout, spam=sys.stdout)), + ('-x - -', NS(x=eq_stdout, spam=eq_stdout)), ] @@ -1591,7 +1601,7 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase): ('foo', NS(x=None, spam=WFile('foo'))), ('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))), ('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))), - ('-x - -', NS(x=sys.stdout, spam=sys.stdout)), + ('-x - -', NS(x=eq_stdout, spam=eq_stdout)), ] From 7f1257965cefc51e8a8d0982933ad86a312f4639 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 09:54:08 -0700 Subject: [PATCH 12/14] Adds blurb file --- Misc/NEWS.d/next/Build/2018-09-14-09-53-21.bpo-34582.j3omgk.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Build/2018-09-14-09-53-21.bpo-34582.j3omgk.rst diff --git a/Misc/NEWS.d/next/Build/2018-09-14-09-53-21.bpo-34582.j3omgk.rst b/Misc/NEWS.d/next/Build/2018-09-14-09-53-21.bpo-34582.j3omgk.rst new file mode 100644 index 00000000000000..582c15f27c8c4e --- /dev/null +++ b/Misc/NEWS.d/next/Build/2018-09-14-09-53-21.bpo-34582.j3omgk.rst @@ -0,0 +1 @@ +Add JUnit XML output for regression tests and update Azure DevOps builds. From 71a7211c31ce551a1db710fc9baf604073933e7a Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 10:34:20 -0700 Subject: [PATCH 13/14] Fix call to dump_traceback_later --- Lib/test/eintrdata/eintr_tester.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/eintrdata/eintr_tester.py b/Lib/test/eintrdata/eintr_tester.py index bc308fe107b311..c90471b28c3d42 100644 --- a/Lib/test/eintrdata/eintr_tester.py +++ b/Lib/test/eintrdata/eintr_tester.py @@ -52,7 +52,12 @@ def setUpClass(cls): # Issue #25277: Use faulthandler to try to debug a hang on FreeBSD if hasattr(faulthandler, 'dump_traceback_later'): - faulthandler.dump_traceback_later(10 * 60, exit=True) + try: + preserve_stderr = sys.stderr + sys.stderr = sys.__stderr__ + faulthandler.dump_traceback_later(10 * 60, exit=True) + finally: + sys.stderr = preserve_stderr @classmethod def stop_alarm(cls): From 7abd3914928fd554a9467aa996a0fd332efd84a1 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 14 Sep 2018 10:35:13 -0700 Subject: [PATCH 14/14] Better fix for dump_traceback_later --- Lib/test/eintrdata/eintr_tester.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/test/eintrdata/eintr_tester.py b/Lib/test/eintrdata/eintr_tester.py index c90471b28c3d42..1caeafe25d9b72 100644 --- a/Lib/test/eintrdata/eintr_tester.py +++ b/Lib/test/eintrdata/eintr_tester.py @@ -52,12 +52,8 @@ def setUpClass(cls): # Issue #25277: Use faulthandler to try to debug a hang on FreeBSD if hasattr(faulthandler, 'dump_traceback_later'): - try: - preserve_stderr = sys.stderr - sys.stderr = sys.__stderr__ - faulthandler.dump_traceback_later(10 * 60, exit=True) - finally: - sys.stderr = preserve_stderr + faulthandler.dump_traceback_later(10 * 60, exit=True, + file=sys.__stderr__) @classmethod def stop_alarm(cls):