Skip to content

Commit 3148c2a

Browse files
committed
Merge remote-tracking branch 'origin/master' into edge
2 parents edbfb42 + 30359ec commit 3148c2a

File tree

4 files changed

+252
-1
lines changed

4 files changed

+252
-1
lines changed

tests/support/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262

6363
# Exports to expose at the top level from the support library.
6464

65+
from tests.support.assertover import AssertOver
6566
from tests.support.configsupp import FakeConfiguration
6667
from tests.support.loggersupp import FakeLogger
6768

@@ -96,6 +97,7 @@ class MigTestCase(TestCase):
9697

9798
def __init__(self, *args):
9899
super(MigTestCase, self).__init__(*args)
100+
self._cleanup_checks = list()
99101
self._cleanup_paths = set()
100102
self._logger = None
101103
self._skip_logging = False
@@ -112,6 +114,11 @@ def tearDown(self):
112114

113115
if not self._skip_logging:
114116
self._logger.check_empty_and_reset()
117+
118+
for check_callable in self._cleanup_checks:
119+
check_callable.__call__()
120+
self._cleanup_checks = list()
121+
115122
if self._logger is not None:
116123
self._reset_logging(stream=BLACKHOLE_STREAM)
117124

@@ -134,6 +141,9 @@ def before_each(self):
134141
"""Before each test action hook"""
135142
pass
136143

144+
def _register_check(self, check_callable):
145+
self._cleanup_checks.append(check_callable)
146+
137147
def _reset_logging(self, stream):
138148
root_logger = logging.getLogger()
139149
root_handler = root_logger.handlers[0]
@@ -146,6 +156,12 @@ def logger(self):
146156
self._logger = FakeLogger()
147157
return self._logger
148158

159+
def assert_over(self, values=None, _AssertOver=AssertOver):
160+
assert_over = _AssertOver(values=values, testcase=self)
161+
check_callable = assert_over.to_check_callable()
162+
self._register_check(check_callable)
163+
return assert_over
164+
149165
# custom assertions available for common use
150166

151167
def assertFileContentIdentical(self, file_actual, file_expected):

tests/support/assertover.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# assertover - iterating assert helper functions for unit testing
6+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23+
#
24+
# -- END_HEADER ---
25+
#
26+
27+
"""Infrastruture to support assertion over a range of values"""
28+
29+
30+
class NoBlockError(AssertionError):
31+
"""Decorate AssertionError for our own convenience"""
32+
pass
33+
34+
35+
class NoCasesError(AssertionError):
36+
"""Decorate AssertionError for our own convenience"""
37+
pass
38+
39+
40+
class AssertOver:
41+
"""Iteration helper to repeat the same overall assertion structure for a
42+
given sequence of test input.
43+
"""
44+
45+
def __init__(self, values=None, testcase=None):
46+
self._attempts = None
47+
self._consulted = False
48+
self._ended = False
49+
self._started = False
50+
self._testcase = testcase
51+
self._values = iter(values)
52+
53+
def __call__(self, block):
54+
self._attempts = []
55+
56+
try:
57+
while True:
58+
block_value = next(self._values)
59+
attempt_info = self._execute_block(block, block_value)
60+
self.record_attempt(attempt_info)
61+
except StopIteration:
62+
pass
63+
64+
self._ended = True
65+
66+
def __enter__(self):
67+
return self
68+
69+
def __exit__(self, exc_type, exc_value, traceback):
70+
if self._attempts is None:
71+
raise NoBlockError()
72+
73+
if len(self._attempts) == 0:
74+
raise NoCasesError()
75+
76+
if not any(self._attempts):
77+
return True
78+
79+
value_lines = ["- <%r> : %s" % (attempt[0], str(attempt[1])) for
80+
attempt in self._attempts if attempt]
81+
raise AssertionError("assertions raised for the following values:\n%s"
82+
% '\n'.join(value_lines))
83+
84+
def record_attempt(self, attempt_info):
85+
"""Record the result of a test attempt"""
86+
return self._attempts.append(attempt_info)
87+
88+
def to_check_callable(self):
89+
def raise_unless_consulted():
90+
if not self._consulted:
91+
raise AssertionError(
92+
"no examiniation made of assertion of multiple values")
93+
return raise_unless_consulted
94+
95+
def assert_success(self):
96+
"""Assertion with registration in consulted"""
97+
self._consulted = True
98+
assert not any(self._attempts)
99+
100+
@classmethod
101+
def _execute_block(cls, block, block_value):
102+
try:
103+
block.__call__(block_value)
104+
return None
105+
except Exception as blockexc:
106+
return (block_value, blockexc,)

tests/test_support.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,42 @@
33
import sys
44
import unittest
55

6-
from tests.support import MigTestCase, PY2, testmain, temppath
6+
from tests.support import MigTestCase, PY2, testmain, temppath, \
7+
AssertOver
8+
9+
10+
class InstrumentedAssertOver(AssertOver):
11+
def __init__(self, *args, **kwargs):
12+
AssertOver.__init__(self, *args, **kwargs)
13+
self._check_callable = None
14+
self._check_callable_called = False
15+
16+
def get_check_callable(self):
17+
return self._check_callable
18+
19+
def has_check_callable(self):
20+
return self._check_callable is not None
21+
22+
def was_check_callable_called(self):
23+
return self._check_callable_called
24+
25+
def to_check_callable(self):
26+
_check_callable = AssertOver.to_check_callable(self)
27+
def _wrapped_check_callable():
28+
self._check_callable_called = True
29+
_check_callable()
30+
self._check_callable = _wrapped_check_callable
31+
return _wrapped_check_callable
732

833

934
class SupportTestCase(MigTestCase):
35+
def _class_attribute(self, name, **kwargs):
36+
cls = type(self)
37+
if 'value' in kwargs:
38+
setattr(cls, name, kwargs['value'])
39+
else:
40+
return getattr(cls, name, None)
41+
1042
@unittest.skipIf(PY2, "Python 3 only")
1143
def test_unclosed_files_are_recorded(self):
1244
tmp_path = temppath("support-unclosed", self)
@@ -31,6 +63,30 @@ def test_unclosed_files_are_reset(self):
3163
except:
3264
self.assertTrue(False, "should not be reachable")
3365

66+
def test_when_asserting_over_multiple_values(self):
67+
def assert_is_int(value):
68+
assert isinstance(value, int)
69+
70+
attempt_wrapper = self.assert_over(values=(1, 2, 3), _AssertOver=InstrumentedAssertOver)
71+
72+
# record the wrapper on the test case so the subsequent test can assert against it
73+
self._class_attribute('surviving_attempt_wrapper', value=attempt_wrapper)
74+
75+
with attempt_wrapper as attempt:
76+
attempt(assert_is_int)
77+
attempt_wrapper.assert_success()
78+
79+
self.assertTrue(attempt_wrapper.has_check_callable())
80+
# cleanup was recorded
81+
self.assertIn(attempt_wrapper.get_check_callable(), self._cleanup_checks)
82+
83+
def test_when_asserting_over_multiple_values_after(self):
84+
# test name is purposefully after ..._recorded in sort order
85+
# such that we can check the check function was called correctly
86+
87+
attempt_wrapper = self._class_attribute('surviving_attempt_wrapper')
88+
self.assertTrue(attempt_wrapper.was_check_callable_called())
89+
3490

3591
if __name__ == '__main__':
3692
testmain()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# test_tests_support_assertover - unit test of the corresponding tests module
6+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
23+
# USA.
24+
#
25+
# --- END_HEADER ---
26+
#
27+
28+
"""Unit tests for the tests module pointed to in the filename"""
29+
30+
import unittest
31+
32+
from tests.support import AssertOver
33+
from tests.support.assertover import NoBlockError, NoCasesError
34+
35+
36+
def assert_a_thing(value):
37+
"""A simple assert helper to test with"""
38+
assert value.endswith(' thing'), "must end with a thing"
39+
40+
41+
class TestsSupportAssertOver(unittest.TestCase):
42+
"""Coverage of AssertOver helper"""
43+
44+
def test_none_failing(self):
45+
saw_raise = False
46+
try:
47+
with AssertOver(values=('some thing', 'other thing')) as value_block:
48+
value_block(lambda _: assert_a_thing(_))
49+
except Exception as exc:
50+
saw_raise = True
51+
self.assertFalse(saw_raise)
52+
53+
def test_three_total_two_failing(self):
54+
with self.assertRaises(AssertionError) as raised:
55+
with AssertOver(values=('some thing', 'other stuff', 'foobar')) as value_block:
56+
value_block(lambda _: assert_a_thing(_))
57+
58+
theexception = raised.exception
59+
self.assertEqual(str(theexception), """assertions raised for the following values:
60+
- <'other stuff'> : must end with a thing
61+
- <'foobar'> : must end with a thing""")
62+
63+
def test_no_cases(self):
64+
with self.assertRaises(AssertionError) as raised:
65+
with AssertOver(values=()) as value_block:
66+
value_block(lambda _: assert_a_thing(_))
67+
68+
theexception = raised.exception
69+
self.assertIsInstance(theexception, NoCasesError)
70+
71+
72+
if __name__ == '__main__':
73+
unittest.main()

0 commit comments

Comments
 (0)