diff --git a/tests/builder_test.py b/tests/builder_test.py index 6203937..5501a16 100644 --- a/tests/builder_test.py +++ b/tests/builder_test.py @@ -4,8 +4,10 @@ import xml.etree.ElementTree as ET from xml.dom.minidom import Document +from freezegun import freeze_time from xmlrunner import builder +import time class TestXMLContextTest(unittest.TestCase): @@ -88,6 +90,19 @@ def test_add_timestamp_attribute_on_end_context(self): element.attributes['timestamp'].value + @freeze_time('2012-12-21') + def test_time_and_timestamp_attributes_are_not_affected_by_freezegun(self): + self.root.begin('testsuite', 'name') + time.sleep(.015) + element = self.root.end() + + current_year = int(element.attributes['timestamp'].value[:4]) + self.assertGreater(current_year, 2012) + self.assertLess(current_year, 2100) + + current_time = float(element.attributes['time'].value) + self.assertGreater(current_time, .012) + self.assertLess(current_time, .020) class TestXMLBuilderTest(unittest.TestCase): """TestXMLBuilder test cases. diff --git a/tests/testsuite.py b/tests/testsuite.py index 437c69f..edce180 100755 --- a/tests/testsuite.py +++ b/tests/testsuite.py @@ -26,6 +26,9 @@ import os import os.path from unittest import mock +from time import sleep +from freezegun import freeze_time +import re def _load_schema(version): @@ -116,6 +119,9 @@ def test_non_ascii_skip(self): def test_pass(self): pass + def test_pass_after_30ms(self): + sleep(.030) + def test_fail(self): self.assertTrue(False) @@ -733,6 +739,24 @@ def test_xmlrunner_elapsed_times(self): suite.addTest(self.DummyTest('test_pass')) self._test_xmlrunner(suite) + @freeze_time('2012-12-21') + def test_xmlrunner_time_measurement_unaffected_by_freezegun(self): + suite = unittest.TestSuite() + outdir = BytesIO() + suite.addTest(self.DummyTest('test_pass_after_30ms')) + runner = xmlrunner.XMLTestRunner( + stream=self.stream, output=outdir, verbosity=self.verbosity, + **self.runner_kwargs) + runner.run(suite) + outdir.seek(0) + output = outdir.read() + match = re.search(br'time="(.+?)"', output) + self.assertIsNotNone(match) + time_value = float(match.group(1)) + # Provide a generous margin for this 30ms test to make flakes unlikely. + self.assertGreater(time_value, 0.025) + self.assertLess(time_value, 0.050) + def test_xmlrunner_resultclass(self): class Result(_XMLTestResult): pass diff --git a/tox.ini b/tox.ini index e278917..a262c15 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ deps = djangocurr: django>=2.2.0 pytest: pytest lxml>=3.6.0 + freezegun commands = coverage run --append setup.py test coverage report --omit='.tox/*' diff --git a/xmlrunner/builder.py b/xmlrunner/builder.py index 50c469a..328d634 100644 --- a/xmlrunner/builder.py +++ b/xmlrunner/builder.py @@ -1,10 +1,11 @@ import re import sys import datetime -import time from xml.dom.minidom import Document +from .time import get_real_time_if_possible + __all__ = ('TestXMLBuilder', 'TestXMLContext') @@ -72,13 +73,13 @@ def begin(self, tag, name): """ self.element = self.xml_doc.createElement(tag) self.element.setAttribute('name', replace_nontext(name)) - self._start_time = time.time() + self._start_time = get_real_time_if_possible() def end(self): """Closes this context (started with a call to `begin`) and creates an attribute for each counter and another for the elapsed time. """ - self._stop_time = time.time() + self._stop_time = get_real_time_if_possible() self.element.setAttribute('time', self.elapsed_time()) self.element.setAttribute('timestamp', self.timestamp()) self._set_result_counters() diff --git a/xmlrunner/result.py b/xmlrunner/result.py index ab49078..b14f5e8 100644 --- a/xmlrunner/result.py +++ b/xmlrunner/result.py @@ -9,9 +9,7 @@ from os import path from io import StringIO -# use direct import to bypass freezegun -from time import time - +from .time import get_real_time_if_possible from .unittest import TestResult, _TextTestResult, failfast @@ -253,7 +251,7 @@ def startTest(self, test): """ Called before execute each test method. """ - self.start_time = time() + self.start_time = get_real_time_if_possible() TestResult.startTest(self, test) try: @@ -321,7 +319,8 @@ def stopTest(self, test): # self._stderr_data = sys.stderr.getvalue() _TextTestResult.stopTest(self, test) - self.stop_time = time() + + self.stop_time = get_real_time_if_possible() if self.callback and callable(self.callback): self.callback() diff --git a/xmlrunner/runner.py b/xmlrunner/runner.py index 538f187..968a446 100644 --- a/xmlrunner/runner.py +++ b/xmlrunner/runner.py @@ -1,8 +1,10 @@ import argparse import sys + import time +from .time import get_real_time_if_possible from .unittest import TextTestRunner, TestProgram from .result import _XMLTestResult @@ -15,7 +17,7 @@ class XMLTestRunner(TextTestRunner): """ A test runner class that outputs the results in JUnit like XML files. """ - def __init__(self, output='.', outsuffix=None, + def __init__(self, output='.', outsuffix=None, elapsed_times=True, encoding=UTF8, resultclass=None, **kwargs): @@ -62,9 +64,9 @@ def run(self, test): self.stream.writeln(result.separator2) # Execute tests - start_time = time.time() + start_time = get_real_time_if_possible() test(result) - stop_time = time.time() + stop_time = get_real_time_if_possible() time_taken = stop_time - start_time # Print results diff --git a/xmlrunner/time.py b/xmlrunner/time.py new file mode 100644 index 0000000..643e050 --- /dev/null +++ b/xmlrunner/time.py @@ -0,0 +1,15 @@ +"""Timing function that will attempt to circumvent freezegun and other common +techniques of mocking time in Python unit tests; will only succeed at this on +Unix currently. Falls back to regular time.time(). +""" + +import time + +def get_real_time_if_possible(): + if hasattr(time, 'clock_gettime'): + try: + return time.clock_gettime(time.CLOCK_REALTIME) + except OSError: + # would occur if time.CLOCK_REALTIME is not available, for example + pass + return time.time()