1
+ """JMUTestCase class and related code.
2
+
3
+ The ``JMUTestCase`` is a subclass of ``unittest.TestCase`` with the following
4
+ features:
5
+
6
+ * Some useful assertions for autograding.
7
+ * Guaranteed test method execution order based on definition order.
8
+ * ``@required`` annotation that can be used to make a particular test a requirement for all subsequent tests.
9
+
10
+ """
1
11
import types
2
12
import unittest
3
13
import tempfile
@@ -80,14 +90,37 @@ def __new__(cls, name, bases, local):
80
90
81
91
82
92
class _JmuTestCase (unittest .TestCase ):
83
- """ Additional useful assertions for grading. """
93
+ """Additional useful assertions for grading.
94
+
95
+ This is the superclass for JmuTestCase. Users should subclass
96
+ ``JmuTestCase`` in their test code. """
84
97
85
98
# counts the number of dynamic modules created
86
99
module_count = 0
87
100
88
101
def assertScriptOutputEqual (self , filename , string_in , expected ,
89
102
variables = None , args = "" , msg = None ,
90
103
processor = None ):
104
+ """Assert correct output for the provided Python script.
105
+
106
+ Args:
107
+ filename (str): The name of the Python file to test
108
+ string_in (str): A string that will be fed to stdin for the script
109
+ expected (str): Expected stdout
110
+ variables (dict): A dictionary mapping from variable names to
111
+ values. The script will be edited with these
112
+ substitutions before it is executed.
113
+ args (str): Command line arguments that will be passed to the script.
114
+ msg (str): Error message that will be printed if the assertion fails.
115
+ processor (func): A function mapping from string to string that will
116
+ process the script output before it is compared
117
+ to the expected output.
118
+
119
+ Raises:
120
+ AssertionError: If the expected output doesn't match the actual
121
+ output.
122
+
123
+ """
91
124
tmpdir = None
92
125
try :
93
126
tmpdir , new_file_name = utils .replace_variables (filename ,
@@ -136,6 +169,18 @@ def assertScriptOutputEqual(self, filename, string_in, expected,
136
169
shutil .rmtree (tmpdir )
137
170
138
171
def assertNoLoops (self , filename , msg = None ):
172
+ """ Assert that the provided script has no for or while loops.
173
+
174
+ Comments will be ignored.
175
+
176
+ Args:
177
+ filename (str): The name of the Python file to test
178
+ msg (str): Error message that will be printed if the assertion fails.
179
+
180
+ Raises:
181
+ AssertionError: If the file contains a loop.
182
+
183
+ """
139
184
loop_regex = "(^|(\r \n ?|\n ))\s*(for|while).*:\s*(#.*)*($|(\r \n ?|\n ))"
140
185
count = utils .count_regex_matches (loop_regex , filename )
141
186
message = f"It looks like the file { filename } contains at least one loop."
@@ -145,6 +190,18 @@ def assertNoLoops(self, filename, msg=None):
145
190
self .fail (message )
146
191
147
192
def assertNoForLoops (self , filename , msg = None ):
193
+ """ Assert that the provided script has no for loops.
194
+
195
+ Comments will be ignored.
196
+
197
+ Args:
198
+ filename (str): The name of the Python file to test
199
+ msg (str): Error message that will be printed if the assertion fails.
200
+
201
+ Raises:
202
+ AssertionError: If the file contains a for loop.
203
+
204
+ """
148
205
loop_regex = "(^|(\r \n ?|\n ))\s*(for).*:\s*(#.*)*($|(\r \n ?|\n ))"
149
206
count = utils .count_regex_matches (loop_regex , filename )
150
207
message = f"It looks like the file { filename } contains at least one for loop."
@@ -154,6 +211,18 @@ def assertNoForLoops(self, filename, msg=None):
154
211
self .fail (message )
155
212
156
213
def assertNoWhileLoops (self , filename , msg = None ):
214
+ """ Assert that the provided script has no while loops.
215
+
216
+ Comments will be ignored.
217
+
218
+ Args:
219
+ filename (str): The name of the Python file to test
220
+ msg (str): Error message that will be printed if the assertion fails.
221
+
222
+ Raises:
223
+ AssertionError: If the file contains a while loop.
224
+
225
+ """
157
226
loop_regex = "(^|(\r \n ?|\n ))\s*(while).*:\s*(#.*)*($|(\r \n ?|\n ))"
158
227
count = utils .count_regex_matches (loop_regex , filename )
159
228
message = f"It looks like the file { filename } contains at least one while loop."
@@ -163,6 +232,18 @@ def assertNoWhileLoops(self, filename, msg=None):
163
232
self .fail (message )
164
233
165
234
def assertNoConditionals (self , filename , msg = None ):
235
+ """ Assert that the provided script has no conditional statements.
236
+
237
+ Comments will be ignored. ``if __name__ == "__main__":`` will be ignored.
238
+
239
+ Args:
240
+ filename (str): The name of the Python file to test
241
+ msg (str): Error message that will be printed if the assertion fails.
242
+
243
+ Raises:
244
+ AssertionError: If the file contains an if.
245
+
246
+ """
166
247
if_regex = "(^|(\r \n ?|\n ))\s*if.*:\s*(#.*)*($|(\r \n ?|\n ))"
167
248
main_regex = "(^|(\r \n ?|\n ))\s*if\s*__name__.*:\s*(#.*)*($|(\r \n ?|\n ))"
168
249
count = utils .count_regex_matches (if_regex , filename )
@@ -174,18 +255,55 @@ def assertNoConditionals(self, filename, msg=None):
174
255
self .fail (message )
175
256
176
257
def assertPassesPep8 (self , filename ):
258
+ """Assert that there are no formatting errors as discovered by flake8.
259
+
260
+ This will use the config file flake8.cfg included in the autograder
261
+ folder.
262
+
263
+ Args:
264
+ filename (str): The name of the Python file to test
265
+
266
+ Raises:
267
+ AssertionError: If flake8 produces any output.
268
+
269
+ """
177
270
output = utils .run_flake8 (filename )
178
271
if len (output ) != 0 :
179
272
self .fail ("Submission does not pass pep8 checks:\n " + output )
180
273
print ('Submission passes all formatting checks!' )
181
274
182
275
def assertDocstringsCorrect (self , filename ):
276
+ """Assert that there are no formatting errors as discovered by flake8.
277
+
278
+ This will use the config file docstring.cfg included in the autograder
279
+ folder.
280
+
281
+ Args:
282
+ filename (str): The name of the Python file to test
283
+
284
+ Raises:
285
+ AssertionError: If flake8 produces any output.
286
+
287
+ """
183
288
output = utils .run_flake8_docstring (filename )
184
289
if len (output ) != 0 :
185
290
self .fail ("Submission does not pass docstring checks:\n " + output )
186
291
print ('Submission passes all docstring checks!' )
187
292
188
293
def assertRequiredFilesPresent (self , required_files ):
294
+ """Assert that all files in the provided list were submitted.
295
+
296
+ Note that this assertion won't get a chance to run if the test file
297
+ attempts to import a missing file. One workaround is to do the
298
+ imports inside the test methods.
299
+
300
+ Args:
301
+ required_files (list): A list of Python file names.
302
+
303
+ Raises:
304
+ AssertionError: If any of the indicated files are missing.
305
+
306
+ """
189
307
missing_files = utils .check_submitted_files (required_files )
190
308
for path in missing_files :
191
309
print ('Missing {0}' .format (path ))
@@ -194,6 +312,11 @@ def assertRequiredFilesPresent(self, required_files):
194
312
195
313
def assertOutputCorrect (self , filename , string_in , expected ,
196
314
variables = None , processor = None ):
315
+ """ Wrapper for assertScriptOutputEqual.
316
+
317
+ I'm not sure why this exists. -NRS
318
+
319
+ """
197
320
self .assertScriptOutputEqual (filename , string_in , expected ,
198
321
variables = variables , processor = processor )
199
322
print ('Correct output:\n ' + expected )
@@ -213,6 +336,23 @@ def run_with_substitution(self, filename, variables, func):
213
336
func (dynamic_module )
214
337
215
338
def assertMatchCount (self , filename , regex , num_matches , msg = None ):
339
+ """Assert that the regex matches exactly the correct number of times.
340
+
341
+ Ignores comments and docstrings.
342
+
343
+ Could be used if the problem instructions say something like:
344
+ "Your program must use exactly one while loop."
345
+
346
+ Args:
347
+ filename (str): The name of the Python file to test
348
+ regex (str): A Python regular expression.
349
+ num_matches (str): The expected number of matches.
350
+ msg (str): Error message that will be printed if the assertion fails.
351
+
352
+ Raises:
353
+ AssertionError: If the count doesn't match.
354
+
355
+ """
216
356
count = utils .count_regex_matches (regex , filename )
217
357
self .assertEqual (num_matches , count , msg = msg )
218
358
0 commit comments