Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Circumvent freezegun in favor of a real clock where possible #228

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tests/builder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import xml.etree.ElementTree as ET
from xml.dom.minidom import Document
from freezegun import freeze_time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try:
import django
except ImportError:
django = None
else:
from django.test.utils import get_runner
from django.conf import settings, UserSettingsHolder
from django.apps import apps
settings.configure(DEBUG=True)
TESTS_DIR = path.dirname(__file__)
@unittest.skipIf(django is None, 'django not found')
class DjangoTest(unittest.TestCase):

please use same kind of approach to not run things that depend on freezegun if it is not available


from xmlrunner import builder
import time


class TestXMLContextTest(unittest.TestCase):
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions tests/testsuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ deps =
djangocurr: django>=2.2.0
pytest: pytest
lxml>=3.6.0
freezegun
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be the same as django tests, i.e. if freezegun is not available, don't run those tests. - see other comment.

commands =
coverage run --append setup.py test
coverage report --omit='.tox/*'
Expand Down
7 changes: 4 additions & 3 deletions xmlrunner/builder.py
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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()
Expand Down
9 changes: 4 additions & 5 deletions xmlrunner/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 5 additions & 3 deletions xmlrunner/runner.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions xmlrunner/time.py
Original file line number Diff line number Diff line change
@@ -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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this snippet is better

try:  # pragma: no cover
    import freezegun

    def real_time():
        return freezegun.api.real_time()
except ImportError:  # pragma: no cover
    def real_time():
        return time.time()

https://github.com/realpython/django-slow-tests/blob/2c5673ca5b6eb093a908e4e28143d889a1893113/django_slowtests/testrunner.py#L13-L20

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: naming with something shorter like real_time

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()