Skip to content

Commit da943cd

Browse files
committed
manually merge PR114 to refactor unit test support helpers into a sub-package
git-svn-id: svn+ssh://svn.code.sf.net/p/migrid/code/trunk@6131 b75ad72c-e7d7-11dd-a971-7dbc132099af
1 parent 5f0aa4b commit da943cd

File tree

4 files changed

+245
-119
lines changed

4 files changed

+245
-119
lines changed

tests/support.py renamed to tests/support/__init__.py

Lines changed: 32 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# --- BEGIN_HEADER ---
55
#
6-
# support - helper functions for unit testing
6+
# __init__ - package marker
77
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
88
#
99
# This file is part of MiG.
@@ -27,32 +27,29 @@
2727

2828
"""Supporting functions for the unit test framework"""
2929

30-
from collections import defaultdict
3130
import difflib
3231
import errno
3332
import io
3433
import logging
3534
import os
36-
import re
3735
import shutil
3836
import stat
3937
import sys
4038
from unittest import TestCase, main as testmain
4139

42-
TEST_BASE = os.path.dirname(__file__)
43-
TEST_FIXTURE_DIR = os.path.join(TEST_BASE, "fixture")
44-
TEST_OUTPUT_DIR = os.path.join(TEST_BASE, "output")
45-
MIG_BASE = os.path.realpath(os.path.join(TEST_BASE, ".."))
46-
PY2 = sys.version_info[0] == 2
40+
from tests.support.suppconst import MIG_BASE, TEST_BASE, TEST_FIXTURE_DIR, \
41+
TEST_OUTPUT_DIR
42+
43+
PY2 = (sys.version_info[0] == 2)
4744

4845
# force defaults to a local environment
4946
os.environ['MIG_ENV'] = 'local'
5047

51-
# All MiG related code will at some point include bits
52-
# from the mig module namespace. Rather than have this
53-
# knowledge spread through every test file, make the
54-
# sole responsbility of test files to find the support
55-
# file and configure the rest here.
48+
# All MiG related code will at some point include bits from the mig module
49+
# namespace. Rather than have this knowledge spread through every test file,
50+
# make the sole responsbility of test files to find the support file and
51+
# configure the rest here.
52+
5653
sys.path.append(MIG_BASE)
5754

5855
# provision an output directory up-front
@@ -63,13 +60,20 @@
6360
shutil.rmtree(TEST_OUTPUT_DIR)
6461
os.mkdir(TEST_OUTPUT_DIR)
6562

63+
# Exports to expose at the top level from the support library.
64+
65+
from tests.support.configsupp import FakeConfiguration
66+
from tests.support.loggersupp import FakeLogger
67+
68+
6669
# Basic global logging configuration for testing
6770

6871

6972
class BlackHole:
7073
"""Arrange a stream that ignores all logging messages"""
7174

7275
def write(self, message):
76+
"""NoOp to fake write"""
7377
pass
7478

7579

@@ -80,111 +84,6 @@ def write(self, message):
8084
logging.captureWarnings(True)
8185

8286

83-
class FakeLogger:
84-
"""An output capturing logger suitable for being passed to the
85-
majority of MiG code by presenting an API compatible interface
86-
with the common logger module.
87-
88-
An instance of this class is made available to test cases which
89-
can pass it down into function calls and subsequenently make
90-
assertions against any output strings hat were recorded during
91-
execution while also avoiding noise hitting the console.
92-
"""
93-
94-
RE_UNCLOSEDFILE = re.compile(
95-
'unclosed file <.*? name=\'(?P<location>.*?)\'( .*?)?>')
96-
97-
def __init__(self):
98-
self.channels_dict = defaultdict(list)
99-
self.forgive_by_channel = defaultdict(lambda: False)
100-
self.unclosed_by_file = defaultdict(list)
101-
102-
def _append_as(self, channel, line):
103-
self.channels_dict[channel].append(line)
104-
105-
def check_empty_and_reset(self):
106-
channels_dict = self.channels_dict
107-
forgive_by_channel = self.forgive_by_channel
108-
unclosed_by_file = self.unclosed_by_file
109-
110-
# reset the record of any logged messages
111-
self.channels_dict = defaultdict(list)
112-
self.forgive_by_channel = defaultdict(lambda: False)
113-
self.unclosed_by_file = defaultdict(list)
114-
115-
# complain loudly (and in detail) in the case of unclosed files
116-
if len(unclosed_by_file) > 0:
117-
messages = '\n'.join({' --> %s: line=%s, file=%s' % (fname, lineno, outname)
118-
for fname, (lineno, outname) in unclosed_by_file.items()})
119-
raise RuntimeError('unclosed files encountered:\n%s' % (messages,))
120-
121-
if channels_dict['error'] and not forgive_by_channel['error']:
122-
raise RuntimeError('errors reported to logger:\n%s' % '\n'.join(channels_dict['error']))
123-
124-
125-
def forgive_errors(self):
126-
self.forgive_by_channel['error'] = True
127-
128-
# logger interface
129-
130-
def debug(self, line):
131-
self._append_as('debug', line)
132-
133-
def error(self, line):
134-
self._append_as('error', line)
135-
136-
def info(self, line):
137-
self._append_as('info', line)
138-
139-
def warning(self, line):
140-
self._append_as('warning', line)
141-
142-
def write(self, message):
143-
channel, namespace, specifics = message.split(':', 2)
144-
145-
# ignore everything except warnings sent by the python runtime
146-
if not (channel == 'WARNING' and namespace == 'py.warnings'):
147-
return
148-
149-
filename_and_datatuple = FakeLogger.identify_unclosed_file(specifics)
150-
if filename_and_datatuple is not None:
151-
self.unclosed_by_file.update((filename_and_datatuple,))
152-
153-
@staticmethod
154-
def identify_unclosed_file(specifics):
155-
filename, lineno, exc_name, message = specifics.split(':', 3)
156-
157-
exc_name = exc_name.lstrip()
158-
if exc_name != 'ResourceWarning':
159-
return
160-
161-
matched = FakeLogger.RE_UNCLOSEDFILE.match(message.lstrip())
162-
if matched is None:
163-
return
164-
165-
relative_testfile = os.path.relpath(filename, start=MIG_BASE)
166-
relative_outputfile = os.path.relpath(
167-
matched.groups('location')[0], start=TEST_BASE)
168-
return (relative_testfile, (lineno, relative_outputfile))
169-
170-
171-
class FakeConfiguration:
172-
"""A simple helper to pretend we have a real Configuration object with any
173-
required attributes explicitly passed.
174-
Automatically attaches a FakeLogger instance if no logger is provided in
175-
kwargs.
176-
"""
177-
178-
def __init__(self, **kwargs):
179-
"""Initialise instance attributes to be any named args provided and a
180-
FakeLogger instance attached if not provided.
181-
"""
182-
self.__dict__.update(kwargs)
183-
if not 'logger' in self.__dict__:
184-
dummy_logger = FakeLogger()
185-
self.__dict__.update({'logger': dummy_logger})
186-
187-
18887
class MigTestCase(TestCase):
18988
"""Embellished base class for MiG test cases. Provides additional commonly
19089
used assertions as well as some basics for the standardised and idiomatic
@@ -202,11 +101,13 @@ def __init__(self, *args):
202101
self._skip_logging = False
203102

204103
def setUp(self):
104+
"""Init before tests"""
205105
if not self._skip_logging:
206106
self._reset_logging(stream=self.logger)
207107
self.before_each()
208108

209109
def tearDown(self):
110+
"""Clean up after tests"""
210111
self.after_each()
211112

212113
if not self._skip_logging:
@@ -226,9 +127,11 @@ def tearDown(self):
226127

227128
# hooks
228129
def after_each(self):
130+
"""After each test action hook"""
229131
pass
230132

231133
def before_each(self):
134+
"""Before each test action hook"""
232135
pass
233136

234137
def _reset_logging(self, stream):
@@ -238,13 +141,15 @@ def _reset_logging(self, stream):
238141

239142
@property
240143
def logger(self):
144+
"""Init a fake logger if not already done"""
241145
if self._logger is None:
242146
self._logger = FakeLogger()
243147
return self._logger
244148

245149
# custom assertions available for common use
246150

247151
def assertFileContentIdentical(self, file_actual, file_expected):
152+
"""Make sure file_actual and file_expected are identical"""
248153
with io.open(file_actual) as f_actual, io.open(file_expected) as f_expected:
249154
lhs = f_actual.readlines()
250155
rhs = f_expected.readlines()
@@ -263,6 +168,7 @@ def assertFileContentIdentical(self, file_actual, file_expected):
263168
''.join(different_lines)))
264169

265170
def assertPathExists(self, relative_path):
171+
"""Make sure file in relative_path exists"""
266172
assert not os.path.isabs(
267173
relative_path), "expected relative path within output folder"
268174
absolute_path = os.path.join(TEST_OUTPUT_DIR, relative_path)
@@ -275,6 +181,7 @@ def assertPathExists(self, relative_path):
275181
return "file"
276182

277183
def assertPathWithin(self, path, start=None):
184+
"""Make sure path is within start directory"""
278185
if not is_path_within(path, start=start):
279186
raise AssertionError(
280187
"path %s is not within directory %s" % (path, start))
@@ -288,6 +195,7 @@ def pretty_display_path(absolute_path):
288195

289196

290197
def is_path_within(path, start=None, _msg=None):
198+
"""Check if path is within start directory"""
291199
try:
292200
assert os.path.isabs(path), _msg
293201
relative = os.path.relpath(path, start=start)
@@ -297,29 +205,34 @@ def is_path_within(path, start=None, _msg=None):
297205

298206

299207
def cleanpath(relative_path, test_case, ensure_dir=False):
208+
"""Register post-test clean up of file in relative_path"""
300209
assert isinstance(test_case, MigTestCase)
301210
tmp_path = os.path.join(TEST_OUTPUT_DIR, relative_path)
302211
if ensure_dir:
303212
try:
304213
os.mkdir(tmp_path)
305214
except FileExistsError:
306-
raise AssertionError("ABORT: use of unclean output path: %s" % relative_path)
215+
raise AssertionError(
216+
"ABORT: use of unclean output path: %s" % relative_path)
307217
test_case._cleanup_paths.add(tmp_path)
308218
return tmp_path
309219

310220

311221
def fixturepath(relative_path):
222+
"""Get absolute fixture path for relative_path"""
312223
tmp_path = os.path.join(TEST_FIXTURE_DIR, relative_path)
313224
return tmp_path
314225

315226

316227
def outputpath(relative_path):
228+
"""Get absolute output path for relative_path"""
317229
assert not os.path.isabs(relative_path)
318230
tmp_path = os.path.join(TEST_OUTPUT_DIR, relative_path)
319231
return tmp_path
320232

321233

322234
def temppath(relative_path, test_case, skip_clean=False):
235+
"""Get absolute temp path for relative_path"""
323236
assert isinstance(test_case, MigTestCase)
324237
tmp_path = os.path.join(TEST_OUTPUT_DIR, relative_path)
325238
if not skip_clean:

tests/support/configsupp.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# configsupp - configuration helpers for unit tests
7+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24+
#
25+
# -- END_HEADER ---
26+
#
27+
28+
"""Configuration related details within the test support library."""
29+
30+
from tests.support.loggersupp import FakeLogger
31+
32+
33+
class FakeConfiguration:
34+
"""A simple helper to pretend we have a real Configuration object with any
35+
required attributes explicitly passed.
36+
Automatically attaches a FakeLogger instance if no logger is provided in
37+
kwargs.
38+
"""
39+
40+
def __init__(self, **kwargs):
41+
"""Initialise instance attributes to be any named args provided and a
42+
FakeLogger instance attached if not provided.
43+
"""
44+
self.__dict__.update(kwargs)
45+
if not 'logger' in self.__dict__:
46+
dummy_logger = FakeLogger()
47+
self.__dict__.update({'logger': dummy_logger})

0 commit comments

Comments
 (0)