Skip to content

Commit 24855c3

Browse files
authored
Merge pull request #5 from JMU-CS/more_output_assertions
more output assertions
2 parents b54563a + fc4887d commit 24855c3

11 files changed

Lines changed: 205 additions & 40 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Features include:
1111
* provides a `@required` annotation that makes a test required, so
1212
that subsequent tests will automatically fail if it fails.
1313
* provide several additional assertion methods, including an
14-
`assertScriptOutputEqual` method that makes it possible to specify
14+
`assertOutputEqual` method that makes it possible to specify
1515
stdin input and check for correct stdout output for a Python
1616
script.
1717
* Some additional utility functions for checking PEP 8 compliance

doc/jmu_test_case.rst

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,3 @@ JmuTestCase
1414
:members:
1515
:private-members:
1616
:undoc-members:
17-
18-
.. automodule:: jmu_gradescope_utils.utils
19-
:members:
20-
:private-members:
21-
:undoc-members:
22-
23-
.. automodule:: jmu_gradescope_utils.coverage_utils
24-
:members:
25-
:private-members:
26-
:undoc-members:

examples/hello_world/tests/test_hello.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ def test_functionality(self):
2424

2525
string_in = ""
2626
expected = "Hello World!\n"
27-
self.assertScriptOutputEqual('hello_world.py', string_in, expected)
27+
self.assertOutputEqual('hello_world.py', string_in, expected)
28+
print('Correct output:\n' + expected)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[SUBMIT]
2+
code: hello_world.py
3+
tests:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[flake8]
2+
select = D,DAR
3+
ignore = D401
4+
5+
[pydocstyle]
6+
convention=google
7+
8+
[darglint]
9+
docstring_style=google
10+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
select = E, F, C90
3+
ignore = W, E226, E123, W504, N818, E704, E24, E121, E126
4+
max-line-length = 100
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello World!")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import unittest
2+
from gradescope_utils.autograder_utils.decorators import weight, number
3+
from jmu_gradescope_utils import JmuTestCase, required, check_submitted_files
4+
import jmu_gradescope_utils
5+
6+
7+
def to_lower(s):
8+
return s.lower()
9+
10+
11+
class TestHelloWorld(JmuTestCase):
12+
@required()
13+
@weight(0)
14+
def test_submitted_files(self):
15+
"""Check submitted files"""
16+
missing_files = check_submitted_files(["hello_world.py"])
17+
for path in missing_files:
18+
print("Missing {0}".format(path))
19+
self.assertEqual(len(missing_files), 0, "Missing some required files!")
20+
print("All required files submitted!")
21+
22+
@required()
23+
@weight(1)
24+
def test_output_retrieved(self):
25+
"""hello_world.py output retrieved."""
26+
string_in = ""
27+
expected = "Hello World!\n"
28+
actual = self.getScriptOutput("hello_world.py", string_in)
29+
self.assertEqual(
30+
expected, actual["stdout"], "didn't get expected output from script"
31+
)
32+
print("Correct output equal:\n" + expected)
33+
34+
@weight(1)
35+
def test_output_exactly(self):
36+
"""hello_world.py output equal."""
37+
string_in = ""
38+
expected = "Hello World!\n"
39+
self.assertOutputEqual("hello_world.py", string_in, expected)
40+
print("Correct output equal:\n" + expected)
41+
42+
@weight(1)
43+
def test_output_not_equal(self):
44+
"""hello_world.py output not equal."""
45+
string_in = ""
46+
expected = "Incorrect\n"
47+
self.assertOutputNotEqual("hello_world.py", string_in, expected)
48+
print("Correct output not equal:\n" + expected)
49+
50+
@weight(1)
51+
def test_output_contains(self):
52+
"""hello_world.py output contains."""
53+
string_in = ""
54+
expected = "hello"
55+
self.assertInOutput("hello_world.py", string_in, expected, processor=to_lower)
56+
print("Correct output contains:\n" + expected)
57+
58+
@weight(1)
59+
def test_output_not_containing(self):
60+
"""hello_world.py output not contains."""
61+
string_in = ""
62+
expected_not_in = "hola"
63+
self.assertNotInOutput("hello_world.py", string_in, expected_not_in)
64+
try:
65+
expected_in = "orld"
66+
self.assertNotInOutput("hello_world.py", string_in, expected_in)
67+
except Exception:
68+
print("assertNotIn failed correctly")
69+
else:
70+
self.fail(
71+
'assertNotIn must be broken, "orld" should have been considered to be in "Hello World!" and so asserting not in should have failed, but this error means it didnt'
72+
)
73+
print("Correct output not containing:\n" + expected_not_in)

jmu_gradescope_utils/jmu_test_case.py

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import sys
2121
from importlib import import_module
2222

23-
2423
_TEST_ORDER = {}
2524

2625

@@ -50,6 +49,7 @@ def wrapper(self, *args, **kwargs):
5049
else:
5150
result = func(self, *args, **kwargs)
5251
return result
52+
5353
return wrapper
5454

5555

@@ -58,6 +58,7 @@ def required():
5858
failed then all of the following methods will fail as well.
5959
6060
"""
61+
6162
def decorator(func):
6263
@wraps(func)
6364
def wrapper(self, *args, **kwargs):
@@ -68,7 +69,9 @@ def wrapper(self, *args, **kwargs):
6869
result = None
6970
raise e.__class__(str(e) + "\n This test was required. All of the following tests will fail automatically.")
7071
return result
72+
7173
return wrapper
74+
7275
return decorator
7376

7477

@@ -92,33 +95,33 @@ def __new__(cls, name, bases, local):
9295
class _JmuTestCase(unittest.TestCase):
9396
"""Additional useful assertions for grading.
9497
95-
This is the superclass for JmuTestCase. Users should subclass
98+
This is the superclass for ``JmuTestCase``. Users should subclass
9699
``JmuTestCase`` in their test code. """
97100

98101
# counts the number of dynamic modules created
99102
module_count = 0
100103

101-
def assertScriptOutputEqual(self, filename, string_in, expected,
102-
variables=None, args="", msg=None,
103-
processor=None):
104-
"""Assert correct output for the provided Python script.
104+
def getScriptOutput(self, filename, string_in, variables=None, args="",
105+
msg=None, processor=None, only_output=False):
106+
"""Get output for the provided Python script.
105107
106108
Args:
107109
filename (str): The name of the Python file to test
108110
string_in (str): A string that will be fed to stdin for the script
109-
expected (str): Expected stdout
110111
variables (dict): A dictionary mapping from variable names to
111112
values. The script will be edited with these
112113
substitutions before it is executed.
113114
args (str): Command line arguments that will be passed to the script.
114115
msg (str): Error message that will be printed if the assertion fails.
115116
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.
117+
process the script output before it is returned.
118+
only_output (bool): Return only the stdout (rather than also the stderr).
118119
119-
Raises:
120-
AssertionError: If the expected output doesn't match the actual
121-
output.
120+
Returns:
121+
dict: keys include 'stdout', and 'msg' as well as 'stderr' if there
122+
there was any output on stderr. 'stdout' and 'stderr' are the script
123+
output and 'msg' is a formatted string describing any execution errors
124+
as well as the inputs to the script.
122125
123126
"""
124127
tmpdir = None
@@ -145,29 +148,108 @@ def assertScriptOutputEqual(self, filename, string_in, expected,
145148

146149
if processor:
147150
actual_text = processor(actual_text)
151+
if only_output:
152+
return {"stdout": actual_text}
148153
stderr_text = stderr.decode()
149154

150155
if len(stderr) > 0:
151-
stderr_text = stderr_text.replace(utils.full_source_path() + "/" , '')
156+
stderr_text = stderr_text.replace(utils.full_source_path() + "/", '')
152157
err_msg = "Error during script execution:\n{}".format(stderr_text)
153158
out_msg = "\nOutput before failure:\n{}".format(actual_text)
154-
self.fail(err_msg + out_msg)
159+
return {"stdout": actual_text, "stderr": stderr_text, "msg": err_msg + out_msg}
155160

156161
show_in = string_in.encode('unicode_escape').decode()
157162
message = "Input was: '{}'".format(show_in)
158163
if len(args) > 0:
159164
message += "\nCommand line arguments: {}".format(args)
160165
if msg is not None:
161166
message += "\n" + msg
162-
163-
self.assertEqual(actual_text, expected, message)
167+
return {"stdout": actual_text, "msg": message}
164168
finally:
165169
if tmpdir is not None:
166170
# Restore the original submission in source:
167171
shutil.copy(os.path.join(tmpdir, "__tmp_backup.py"),
168172
utils.full_source_path(filename))
169173
shutil.rmtree(tmpdir)
170174

175+
def assertScriptOutputEqual(self, filename, string_in, expected,
176+
variables=None, args="", msg=None,
177+
processor=None):
178+
"""Deprecated wrapper for :meth:`~jmu_gradescope_utils.jmu_test_case._JmuTestCase.assertOutputEqual`."""
179+
self.assertOutputEqual(filename, string_in, expected, variables, args,
180+
msg, processor)
181+
182+
def assertOutputEqual(self, filename, string_in, expected,
183+
variables=None, args="", msg=None,
184+
processor=None):
185+
"""Assert correct output for the provided Python script.
186+
187+
Args:
188+
filename (str): The name of the Python file to test
189+
string_in (str): A string that will be fed to stdin for the script
190+
expected (str): Expected stdout
191+
variables (dict): A dictionary mapping from variable names to
192+
values. The script will be edited with these
193+
substitutions before it is executed.
194+
args (str): Command line arguments that will be passed to the script.
195+
msg (str): Error message that will be printed if the assertion fails.
196+
processor (func): A function mapping from string to string that will
197+
process the script output before it is compared
198+
to the expected output.
199+
200+
Raises:
201+
AssertionError: If the expected output doesn't match the actual
202+
output.
203+
204+
"""
205+
result = self.getScriptOutput(filename, string_in, variables=variables,
206+
args=args, msg=msg, processor=processor)
207+
if "stderr" in result:
208+
self.fail(result["msg"])
209+
self.assertEqual(result["stdout"], expected, result["msg"])
210+
211+
def assertOutputNotEqual(self, filename, string_in, expected,
212+
variables=None, args="", msg=None,
213+
processor=None):
214+
"""Assert script output is NOT equal to the indicated string.
215+
216+
See :meth:`~jmu_gradescope_utils.jmu_test_case._JmuTestCase.assertOutputEqual` for
217+
description of arguments.
218+
"""
219+
result = self.getScriptOutput(filename, string_in, variables=variables,
220+
args=args, msg=msg, processor=processor)
221+
if "stderr" in result:
222+
self.fail(result["msg"])
223+
self.assertNotEqual(result["stdout"], expected, result["msg"])
224+
225+
def assertInOutput(self, filename, string_in, expected,
226+
variables=None, args="", msg=None,
227+
processor=None):
228+
"""Assert script output contains the indicated string.
229+
230+
See :meth:`~jmu_gradescope_utils.jmu_test_case._JmuTestCase.assertOutputEqual` for
231+
description of arguments.
232+
"""
233+
result = self.getScriptOutput(filename, string_in, variables=variables,
234+
args=args, msg=msg, processor=processor)
235+
if "stderr" in result:
236+
self.fail(result["msg"])
237+
self.assertIn(expected, result["stdout"], result["msg"])
238+
239+
def assertNotInOutput(self, filename, string_in, expected,
240+
variables=None, args="", msg=None,
241+
processor=None):
242+
"""Assert script output does not contain the indicated string.
243+
244+
See :meth:`~jmu_gradescope_utils.jmu_test_case._JmuTestCase.assertOutputEqual` for
245+
description of arguments.
246+
"""
247+
result = self.getScriptOutput(filename, string_in, variables=variables,
248+
args=args, msg=msg, processor=processor)
249+
if "stderr" in result:
250+
self.fail(result["msg"])
251+
self.assertNotIn(expected, result["stdout"], result["msg"])
252+
171253
def assertNoLoops(self, filename, msg=None):
172254
""" Assert that the provided script has no for or while loops.
173255
@@ -312,13 +394,13 @@ def assertRequiredFilesPresent(self, required_files):
312394

313395
def assertOutputCorrect(self, filename, string_in, expected,
314396
variables=None, processor=None):
315-
""" Wrapper for assertScriptOutputEqual.
397+
""" Wrapper for assertOutputEqual.
316398
317399
I'm not sure why this exists. -NRS
318400
319401
"""
320-
self.assertScriptOutputEqual(filename, string_in, expected,
321-
variables=variables, processor=processor)
402+
self.assertOutputEqual(filename, string_in, expected,
403+
variables=variables, processor=processor)
322404
print('Correct output:\n' + expected)
323405

324406
def run_with_substitution(self, filename, variables, func):
@@ -328,7 +410,9 @@ def run_with_substitution(self, filename, variables, func):
328410
if filename[-3:] == '.py':
329411
short_filename = filename[0:-3]
330412
new_module_name = short_filename + "_" + str(_JmuTestCase.module_count)
331-
(tmpdir, new_file_name) = utils.replace_variables(filename, variables=variables, new_name=new_module_name + ".py")
413+
(tmpdir, new_file_name) = utils.replace_variables(filename,
414+
variables=variables,
415+
new_name=new_module_name + ".py")
332416
# insert the new temporary directory into the system module load path
333417
sys.path.insert(1, tmpdir)
334418
# load the module
@@ -359,12 +443,11 @@ def assertMatchCount(self, filename, regex, num_matches, msg=None):
359443

360444
class JmuTestCase(_JmuTestCase, metaclass=OrderAllTestsMeta):
361445
"""Test methods declared within subclasses will be executed in the
362-
order they are declared as long as the sortTestMethodUsing attrute
446+
order they are declared as long as the ``sortTestMethodUsing`` attribute
363447
of the defaultTestLoader has been set::
448+
unittest.defaultTestLoader.sortTestMethodsUsing = test_compare
364449
365-
unittest.defaultTestLoader.sortTestMethodsUsing = test_compare
366-
367-
They will also respect the @required decorator.
450+
They will also respect the ``@required`` decorator.
368451
369452
"""
370453
pass

jmu_gradescope_utils/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ def check_submitted_files(paths, base=SUBMISSION_BASE):
121121
def suppress_IO(in_string):
122122
"""
123123
Suppresses standard io when running a block of code, feeding in the given in_string as input
124-
and squelching all output. Use as
124+
and squelching all output. Use as::
125125
126-
```with suppress_IO("desired input"):
126+
with suppress_IO("desired input"):
127127
# my code block
128-
```
128+
129129
"""
130130
text_in = io.StringIO(in_string)
131131
text_out = io.StringIO()

0 commit comments

Comments
 (0)