3
3
#
4
4
# --- BEGIN_HEADER ---
5
5
#
6
- # support - helper functions for unit testing
6
+ # __init__ - package marker
7
7
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
8
8
#
9
9
# This file is part of MiG.
27
27
28
28
"""Supporting functions for the unit test framework"""
29
29
30
- from collections import defaultdict
31
30
import difflib
32
31
import errno
33
32
import io
34
33
import logging
35
34
import os
36
- import re
37
35
import shutil
38
36
import stat
39
37
import sys
40
38
from unittest import TestCase , main as testmain
41
39
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 )
47
44
48
45
# force defaults to a local environment
49
46
os .environ ['MIG_ENV' ] = 'local'
50
47
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
+
56
53
sys .path .append (MIG_BASE )
57
54
58
55
# provision an output directory up-front
63
60
shutil .rmtree (TEST_OUTPUT_DIR )
64
61
os .mkdir (TEST_OUTPUT_DIR )
65
62
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
+
66
69
# Basic global logging configuration for testing
67
70
68
71
69
72
class BlackHole :
70
73
"""Arrange a stream that ignores all logging messages"""
71
74
72
75
def write (self , message ):
76
+ """NoOp to fake write"""
73
77
pass
74
78
75
79
@@ -80,111 +84,6 @@ def write(self, message):
80
84
logging .captureWarnings (True )
81
85
82
86
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
-
188
87
class MigTestCase (TestCase ):
189
88
"""Embellished base class for MiG test cases. Provides additional commonly
190
89
used assertions as well as some basics for the standardised and idiomatic
@@ -202,11 +101,13 @@ def __init__(self, *args):
202
101
self ._skip_logging = False
203
102
204
103
def setUp (self ):
104
+ """Init before tests"""
205
105
if not self ._skip_logging :
206
106
self ._reset_logging (stream = self .logger )
207
107
self .before_each ()
208
108
209
109
def tearDown (self ):
110
+ """Clean up after tests"""
210
111
self .after_each ()
211
112
212
113
if not self ._skip_logging :
@@ -226,9 +127,11 @@ def tearDown(self):
226
127
227
128
# hooks
228
129
def after_each (self ):
130
+ """After each test action hook"""
229
131
pass
230
132
231
133
def before_each (self ):
134
+ """Before each test action hook"""
232
135
pass
233
136
234
137
def _reset_logging (self , stream ):
@@ -238,13 +141,15 @@ def _reset_logging(self, stream):
238
141
239
142
@property
240
143
def logger (self ):
144
+ """Init a fake logger if not already done"""
241
145
if self ._logger is None :
242
146
self ._logger = FakeLogger ()
243
147
return self ._logger
244
148
245
149
# custom assertions available for common use
246
150
247
151
def assertFileContentIdentical (self , file_actual , file_expected ):
152
+ """Make sure file_actual and file_expected are identical"""
248
153
with io .open (file_actual ) as f_actual , io .open (file_expected ) as f_expected :
249
154
lhs = f_actual .readlines ()
250
155
rhs = f_expected .readlines ()
@@ -263,6 +168,7 @@ def assertFileContentIdentical(self, file_actual, file_expected):
263
168
'' .join (different_lines )))
264
169
265
170
def assertPathExists (self , relative_path ):
171
+ """Make sure file in relative_path exists"""
266
172
assert not os .path .isabs (
267
173
relative_path ), "expected relative path within output folder"
268
174
absolute_path = os .path .join (TEST_OUTPUT_DIR , relative_path )
@@ -275,6 +181,7 @@ def assertPathExists(self, relative_path):
275
181
return "file"
276
182
277
183
def assertPathWithin (self , path , start = None ):
184
+ """Make sure path is within start directory"""
278
185
if not is_path_within (path , start = start ):
279
186
raise AssertionError (
280
187
"path %s is not within directory %s" % (path , start ))
@@ -288,6 +195,7 @@ def pretty_display_path(absolute_path):
288
195
289
196
290
197
def is_path_within (path , start = None , _msg = None ):
198
+ """Check if path is within start directory"""
291
199
try :
292
200
assert os .path .isabs (path ), _msg
293
201
relative = os .path .relpath (path , start = start )
@@ -297,29 +205,34 @@ def is_path_within(path, start=None, _msg=None):
297
205
298
206
299
207
def cleanpath (relative_path , test_case , ensure_dir = False ):
208
+ """Register post-test clean up of file in relative_path"""
300
209
assert isinstance (test_case , MigTestCase )
301
210
tmp_path = os .path .join (TEST_OUTPUT_DIR , relative_path )
302
211
if ensure_dir :
303
212
try :
304
213
os .mkdir (tmp_path )
305
214
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 )
307
217
test_case ._cleanup_paths .add (tmp_path )
308
218
return tmp_path
309
219
310
220
311
221
def fixturepath (relative_path ):
222
+ """Get absolute fixture path for relative_path"""
312
223
tmp_path = os .path .join (TEST_FIXTURE_DIR , relative_path )
313
224
return tmp_path
314
225
315
226
316
227
def outputpath (relative_path ):
228
+ """Get absolute output path for relative_path"""
317
229
assert not os .path .isabs (relative_path )
318
230
tmp_path = os .path .join (TEST_OUTPUT_DIR , relative_path )
319
231
return tmp_path
320
232
321
233
322
234
def temppath (relative_path , test_case , skip_clean = False ):
235
+ """Get absolute temp path for relative_path"""
323
236
assert isinstance (test_case , MigTestCase )
324
237
tmp_path = os .path .join (TEST_OUTPUT_DIR , relative_path )
325
238
if not skip_clean :
0 commit comments