Skip to content

Commit c3dd97d

Browse files
authored
Merge pull request SciTools#2372 from pp-mo/test_timings
Test timings
2 parents 7546ec7 + e520f14 commit c3dd97d

File tree

17 files changed

+210
-81
lines changed

17 files changed

+210
-81
lines changed

lib/iris/tests/__init__.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import codecs
3838
import collections
3939
import contextlib
40+
import datetime
4041
import difflib
4142
import filecmp
4243
import functools
@@ -219,7 +220,7 @@ def get_data_path(relative_path):
219220
return data_path
220221

221222

222-
class IrisTest(unittest.TestCase):
223+
class IrisTest_nometa(unittest.TestCase):
223224
"""A subclass of unittest.TestCase which provides Iris specific testing functionality."""
224225

225226
_assertion_counts = collections.defaultdict(int)
@@ -853,10 +854,84 @@ def assertArrayShapeStats(self, result, shape, mean, std_dev, rtol=1e-6):
853854
self.assertArrayAllClose(result.data.std(), std_dev, rtol=rtol)
854855

855856

857+
# An environment variable controls whether test timings are output.
858+
#
859+
# NOTE: to run tests with timing output, nosetests cannot be used.
860+
# At present, that includes not using "python setup.py test"
861+
# The typically best way is like this :
862+
# $ export IRIS_TEST_TIMINGS=1
863+
# $ python -m unittest discover -s iris.tests
864+
# and commonly adding ...
865+
# | grep "TIMING TEST" >iris_test_output.txt
866+
#
867+
_PRINT_TEST_TIMINGS = bool(int(os.environ.get('IRIS_TEST_TIMINGS', 0)))
868+
869+
870+
def _method_path(meth):
871+
cls = meth.im_class
872+
return '.'.join([cls.__module__, cls.__name__, meth.__name__])
873+
874+
875+
def _testfunction_timing_decorator(fn):
876+
# Function decorator for making a testcase print its execution time.
877+
@functools.wraps(fn)
878+
def inner(*args, **kwargs):
879+
start_time = datetime.datetime.now()
880+
try:
881+
result = fn(*args, **kwargs)
882+
finally:
883+
end_time = datetime.datetime.now()
884+
elapsed_time = (end_time - start_time).total_seconds()
885+
msg = '\n TEST TIMING -- "{}" took : {:12.6f} sec.'
886+
name = _method_path(fn)
887+
print(msg.format(name, elapsed_time))
888+
return result
889+
return inner
890+
891+
892+
def iristest_timing_decorator(cls):
893+
# Class decorator to make all "test_.." functions print execution timings.
894+
if _PRINT_TEST_TIMINGS:
895+
# NOTE: 'dir' scans *all* class properties, including inherited ones.
896+
attr_names = dir(cls)
897+
for attr_name in attr_names:
898+
attr = getattr(cls, attr_name)
899+
if callable(attr) and attr_name.startswith('test'):
900+
attr = _testfunction_timing_decorator(attr)
901+
setattr(cls, attr_name, attr)
902+
return cls
903+
904+
905+
class _TestTimingsMetaclass(type):
906+
# An alternative metaclass for IrisTest subclasses, which makes
907+
# them print execution timings for all the testcases.
908+
# This is equivalent to applying the @iristest_timing_decorator to
909+
# every test class that inherits from IrisTest.
910+
# NOTE: however, it means you *cannot* specify a different metaclass for
911+
# your test class inheriting from IrisTest.
912+
# See below for how to solve that where needed.
913+
def __new__(cls, clsname, base_classes, attrs):
914+
result = type.__new__(cls, clsname, base_classes, attrs)
915+
if _PRINT_TEST_TIMINGS:
916+
result = iristest_timing_decorator(result)
917+
return result
918+
919+
920+
class IrisTest(six.with_metaclass(_TestTimingsMetaclass, IrisTest_nometa)):
921+
# Derive the 'ordinary' IrisTest from IrisTest_nometa, but add the
922+
# metaclass that enables test timings output.
923+
# This means that all subclasses also get the timing behaviour.
924+
# However, if a different metaclass is *wanted* for an IrisTest subclass,
925+
# this would cause a metaclass conflict.
926+
# Instead, you can inherit from IrisTest_nometa and apply the
927+
# @iristest_timing_decorator explicitly to your new testclass.
928+
pass
929+
930+
856931
get_result_path = IrisTest.get_result_path
857932

858933

859-
class GraphicsTest(IrisTest):
934+
class GraphicsTestMixin(object):
860935

861936
# nose directive: dispatch tests concurrently.
862937
_multiprocess_can_split_ = True
@@ -879,6 +954,15 @@ def tearDown(self):
879954
_lock.release()
880955

881956

957+
class GraphicsTest(GraphicsTestMixin, IrisTest):
958+
pass
959+
960+
961+
class GraphicsTest_nometa(GraphicsTestMixin, IrisTest_nometa):
962+
# Graphicstest without the metaclass providing test timings.
963+
pass
964+
965+
882966
class TestGribMessage(IrisTest):
883967
def assertGribMessageContents(self, filename, contents):
884968
"""

lib/iris/tests/test_cell.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2010 - 2015, Met Office
1+
# (C) British Crown Copyright 2010 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -22,15 +22,13 @@
2222
# import iris tests first so that some things can be initialised before importing anything else
2323
import iris.tests as tests
2424

25-
import unittest
26-
2725
import numpy as np
2826

2927
import iris.coords
3028
from iris.coords import Cell
3129

3230

33-
class TestCells(unittest.TestCase):
31+
class TestCells(tests.IrisTest):
3432
def setUp(self):
3533
self.cell1 = iris.coords.Cell(3, [2, 4])
3634
self.cell2 = iris.coords.Cell(360., [350., 370.])

lib/iris/tests/test_cf.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2010 - 2016, Met Office
1+
# (C) British Crown Copyright 2010 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -25,14 +25,12 @@
2525
# import iris tests first so that some things can be initialised before importing anything else
2626
import iris.tests as tests
2727

28-
import unittest
29-
3028
import iris
3129
import iris.fileformats.cf as cf
3230
from iris.tests import mock
3331

3432

35-
class TestCaching(unittest.TestCase):
33+
class TestCaching(tests.IrisTest):
3634
def test_cached(self):
3735
# Make sure attribute access to the underlying netCDF4.Variable
3836
# is cached.

lib/iris/tests/test_coding_standards.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2013 - 2016, Met Office
1+
# (C) British Crown Copyright 2013 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -18,14 +18,17 @@
1818
from __future__ import (absolute_import, division, print_function)
1919
from six.moves import (filter, input, map, range, zip) # noqa
2020

21+
# import iris tests first so that some things can be initialised before
22+
# importing anything else
23+
import iris.tests as tests
24+
2125
from datetime import datetime
2226
from fnmatch import fnmatch
2327
from glob import glob
2428
from itertools import chain
2529
import os
2630
import re
2731
import subprocess
28-
import unittest
2932

3033
import pep8
3134

@@ -170,7 +173,7 @@ def get_file_results(self):
170173
self).get_file_results()
171174

172175

173-
class TestCodeFormat(unittest.TestCase):
176+
class TestCodeFormat(tests.IrisTest):
174177
def test_pep8_conformance(self):
175178
#
176179
# Tests the iris codebase against the "pep8" tool.
@@ -218,7 +221,7 @@ def test_pep8_conformance(self):
218221
'{}'.format('\n '.join(unexpectedly_good)))
219222

220223

221-
class TestLicenseHeaders(unittest.TestCase):
224+
class TestLicenseHeaders(tests.IrisTest):
222225
@staticmethod
223226
def years_of_license_in_file(fh):
224227
"""
@@ -345,7 +348,7 @@ def test_license_headers(self):
345348
raise ValueError('There were license header failures. See stdout.')
346349

347350

348-
class TestFutureImports(unittest.TestCase):
351+
class TestFutureImports(tests.IrisTest):
349352
excluded = (
350353
'*/iris/fileformats/_old_pp_packing.py',
351354
'*/iris/fileformats/_pyke_rules/__init__.py',
@@ -406,4 +409,4 @@ def test_future_imports(self):
406409

407410

408411
if __name__ == '__main__':
409-
unittest.main()
412+
tests.main()

lib/iris/tests/test_coord_api.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2010 - 2016, Met Office
1+
# (C) British Crown Copyright 2010 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -22,7 +22,6 @@
2222
# import iris tests first so that some things can be initialised before importing anything else
2323
import iris.tests as tests
2424

25-
import unittest
2625
from xml.dom.minidom import Document
2726
import logging
2827

@@ -40,7 +39,7 @@
4039
logger = logging.getLogger('tests')
4140

4241

43-
class TestLazy(unittest.TestCase):
42+
class TestLazy(tests.IrisTest):
4443
def setUp(self):
4544
# Start with a coord with LazyArray points.
4645
shape = (3, 4)
@@ -107,7 +106,7 @@ def test_lazy_shared_data(self):
107106

108107

109108
@tests.skip_data
110-
class TestCoordSlicing(unittest.TestCase):
109+
class TestCoordSlicing(tests.IrisTest):
111110
def setUp(self):
112111
cube = iris.tests.stock.realistic_4d()
113112
self.lat = cube.coord('grid_latitude')
@@ -278,7 +277,7 @@ def test_AuxCoord_str(self):
278277
('coord_api', 'str_repr', 'aux_time_str.txt'))
279278

280279

281-
class TestAuxCoordCreation(unittest.TestCase):
280+
class TestAuxCoordCreation(tests.IrisTest):
282281
def test_basic(self):
283282
a = iris.coords.AuxCoord(np.arange(10), 'air_temperature',
284283
units='kelvin')
@@ -336,7 +335,7 @@ def test_AuxCoord_fromcoord(self):
336335
self.assertIsNot(a.coord_system, b.coord_system)
337336

338337

339-
class TestDimCoordCreation(unittest.TestCase):
338+
class TestDimCoordCreation(tests.IrisTest):
340339
def test_basic(self):
341340
a = iris.coords.DimCoord(np.arange(10), 'air_temperature',
342341
units='kelvin')

lib/iris/tests/test_io_init.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2010 - 2016, Met Office
1+
# (C) British Crown Copyright 2010 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -25,14 +25,13 @@
2525
# import iris tests first so that some things can be initialised before importing anything else
2626
import iris.tests as tests
2727

28-
import unittest
2928
from io import BytesIO
3029

3130
import iris.fileformats as iff
3231
import iris.io
3332

3433

35-
class TestDecodeUri(unittest.TestCase):
34+
class TestDecodeUri(tests.IrisTest):
3635
def test_decode_uri(self):
3736
tests = {
3837
'/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp': (

lib/iris/tests/test_plot.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2010 - 2016, Met Office
1+
# (C) British Crown Copyright 2010 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -581,8 +581,10 @@ def override_with_decorated_methods(attr_dict, target_dict,
581581

582582

583583
@tests.skip_data
584+
@tests.iristest_timing_decorator
584585
class TestPcolorNoBounds(six.with_metaclass(CheckForWarningsMetaclass,
585-
tests.GraphicsTest, SliceMixin)):
586+
tests.GraphicsTest_nometa,
587+
SliceMixin)):
586588
"""
587589
Test the iris.plot.pcolor routine on a cube with coordinates
588590
that have no bounds.
@@ -595,8 +597,9 @@ def setUp(self):
595597

596598

597599
@tests.skip_data
600+
@tests.iristest_timing_decorator
598601
class TestPcolormeshNoBounds(six.with_metaclass(CheckForWarningsMetaclass,
599-
tests.GraphicsTest,
602+
tests.GraphicsTest_nometa,
600603
SliceMixin)):
601604
"""
602605
Test the iris.plot.pcolormesh routine on a cube with coordinates

lib/iris/tests/test_pp_module.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2013 - 2015, Met Office
1+
# (C) British Crown Copyright 2013 - 2017, Met Office
22
#
33
# This file is part of Iris.
44
#
@@ -98,7 +98,7 @@ def check_pp(self, pp_fields, reference_filename):
9898
reference_fh.writelines(test_string)
9999

100100

101-
class TestPPHeaderDerived(unittest.TestCase):
101+
class TestPPHeaderDerived(tests.IrisTest):
102102

103103
def setUp(self):
104104
self.pp = pp.PPField2()
@@ -338,7 +338,7 @@ def test_save_single(self):
338338
os.remove(temp_filename)
339339

340340

341-
class TestBitwiseInt(unittest.TestCase):
341+
class TestBitwiseInt(tests.IrisTest):
342342

343343
def test_3(self):
344344
with mock.patch('warnings.warn') as warn:
@@ -447,7 +447,7 @@ def test_128(self):
447447
self.assertEqual(t.flag128, 1)
448448

449449

450-
class TestSplittableInt(unittest.TestCase):
450+
class TestSplittableInt(tests.IrisTest):
451451

452452
def test_3(self):
453453
t = pp.SplittableInt(3)
@@ -546,15 +546,15 @@ def test_negative_number(self):
546546
self.assertEqual(str(err), 'Negative numbers not supported with splittable integers object')
547547

548548

549-
class TestSplittableIntEquality(unittest.TestCase):
549+
class TestSplittableIntEquality(tests.IrisTest):
550550
def test_not_implemented(self):
551551
class Terry(object): pass
552552
sin = pp.SplittableInt(0)
553553
self.assertIs(sin.__eq__(Terry()), NotImplemented)
554554
self.assertIs(sin.__ne__(Terry()), NotImplemented)
555555

556556

557-
class TestPPDataProxyEquality(unittest.TestCase):
557+
class TestPPDataProxyEquality(tests.IrisTest):
558558
def test_not_implemented(self):
559559
class Terry(object): pass
560560
pox = pp.PPDataProxy("john", "michael", "eric", "graham", "brian",
@@ -563,7 +563,7 @@ class Terry(object): pass
563563
self.assertIs(pox.__ne__(Terry()), NotImplemented)
564564

565565

566-
class TestPPFieldEquality(unittest.TestCase):
566+
class TestPPFieldEquality(tests.IrisTest):
567567
def test_not_implemented(self):
568568
class Terry(object): pass
569569
pox = pp.PPField3()

0 commit comments

Comments
 (0)