-
Notifications
You must be signed in to change notification settings - Fork 0
/
test_util.py
227 lines (185 loc) · 7.96 KB
/
test_util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import ast
import builtins
import io
import re
import sys
import traceback
from contextlib import redirect_stdout
class TestFailure(AssertionError):
pass
def format_tb(exc_info):
"""Accepts *sys.exc_info()."""
tbe = traceback.TracebackException(*exc_info)
if "test_util.py" in tbe.stack[0].filename:
del tbe.stack[0]
return "".join(tbe.format(chain=True))
def test(input="", show_stdout=True):
"""
Decorator for the ZyLabs test_passed function that:
* Turns AssertionErrors into test feedback
* Catches syntax and other errors
* Allows providing arbitrary input (and fails if input() is called but no input was provided)
* Captures standard output.
* Has the test fail on any errors, pass otherwise.
* Interprets `None` returns as passing.
This can wrap an existing test_passed function unchanged also.
Usage:
@test(input='abc')
def test_passed(test_feedback):
from student_code import func
result = func(input)
assert result == 1, "Result should equal 1"
assert sys.stdout.getvalue() == '', "Code should not print any output"
"""
def wrapper(f):
def test_passed(test_feedback):
sys.stdin = io.StringIO(input)
stdout = io.StringIO()
our_feedback = io.StringIO()
if input == "":
def input_override(*a):
raise TestFailure(
"This task should not ask for user input. Comment out or remove any calls to 'input'"
)
builtins.input = input_override
try:
with redirect_stdout(stdout):
result = f(our_feedback)
our_feedback_val = our_feedback.getvalue()
test_feedback.write(our_feedback_val)
if result is None:
result = True
if len(our_feedback_val) == 0:
# If the test didn't write something, write something here.
if result:
test_feedback.write("Passed!\n")
else:
test_feedback.write("Test didn't pass. Some message should have been reported but didn't; please alert the course staff.")
return result
except TestFailure as e:
test_feedback.write(str(e))
return False
except AssertionError as e:
assertion_err = str(e).strip()
num_frames = len(traceback.extract_tb(sys.exc_info()[2]))
if num_frames == 2:
# This assertion is from our test code,
# not from the user's code.
# How can we tell? There's only 2 stack frames:
# zyLabsUnitTest (our test code) and the call within this function.
test_feedback.write(assertion_err)
else:
if assertion_err:
test_feedback.write("Assertion failed: " + assertion_err)
else:
test_feedback.write("Error running your code:")
test_feedback.write("\n\n" + format_tb(sys.exc_info()))
return False
except:
tb = format_tb(sys.exc_info())
tb = tb.replace(
"/home/runner/local/submission/unit_test_student_code/", ""
)
test_feedback.write("Error running your code:\n\n" + tb)
return False
finally:
# Show the actual output.
if show_stdout:
sys.stdout.write(stdout.getvalue())
return test_passed
return wrapper
def doctester(module_name, total_points=1):
'''
Usage:
Start with a triple-quoted doctest session. Then:
from test_util import doctester
test_passed = doctester("credit_card", total_points=3)
'''
import doctest, sys, importlib
testcase_module = sys.modules['zyLabsUnitTest']
testcase_module.__file__ = 'zyLabsUnitTest.py'
@test()
def test_passed(test_feedback):
target_module = importlib.import_module(module_name)
#assert sys.stdout.getvalue() == '', "Please comment out any input or print statements when submitting."
failure_count, test_count = doctest.testmod(
testcase_module,
extraglobs=target_module.__dict__,
optionflags=doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS, # doctest.FAIL_FAST
name="zyBooks_test",
report=True,
verbose=False
)
return total_points * (1 - (failure_count / test_count))
return test_passed
def function_testcase(module_name, function_name, *, args, expected_result):
import importlib
@test()
def test_passed(test_feedback):
module = importlib.import_module(module_name)
if not hasattr(module, function_name):
raise TestFailure("Missing function {}".format(function_name))
func = getattr(module, function_name)
result = func(*args)
if result == expected_result:
return True
pretty_args = repr(tuple(args)) if len(args) != 1 else "({!r})".format(args[0])
raise TestFailure(
"{}{} should return {!r}.".format(
function_name, pretty_args, expected_result
)
)
return test_passed
def get_global_refs(callable):
# Get inside any wrappers, like bound method objects.
while hasattr(callable, "__func__"):
callable = callable.__func__
import inspect
return inspect.getclosurevars(callable).globals.keys()
def filenames_test(*filenames):
"""
Example usage:
import test_util
test_passed = test_util.filenames_test("submission_file.py")
"""
def test_passed(test_feedback):
for FILENAME in filenames:
try:
source = open(FILENAME).read()
parsed = ast.parse(source, filename=FILENAME)
docstring = ast.get_docstring(parsed)
except SyntaxError:
test_feedback.write(traceback.format_exc(limit=0))
return False
except:
test_feedback.write("Unknown error reading documentation. Ask the course staff for help.\n\n" + traceback.format_exc())
if docstring is None or not any(x in docstring for x in ['\nAuthor', 'Author:', '@author']):
test_feedback.write(FILENAME + " documentation should include author (see the template).")
return False
if any(x in docstring for x in ['YOUR-NAME', 'yn123', 'PARTNER-NAME', 'pn31']):
test_feedback.write(FILENAME + ": Please replace the template names and usernames with your own.")
return False
if any(x in docstring for x in ["Describe the module here.", "Lab X.X"]):
test_feedback.write(FILENAME + ": Please replace the template documentation with your own.")
return False
test_feedback.write("Passed!")
return True
return test_passed
name_test = names_test = filenames_test
guard_line_re = re.compile(
r'^if\s+__name__\s*==\s*[\'"]__main__[\'"]\s*:', re.MULTILINE
)
if __name__ == "__main__":
assert guard_line_re.match('if __name__ == "__main__":')
assert guard_line_re.match("if __name__ == '__main__':")
assert guard_line_re.match("if __name__=='__main__' :")
assert guard_line_re.match("if __name__=='__main__':")
assert not guard_line_re.match("if __name__ == __main__:")
assert not guard_line_re.match("if __main__ == '__name__':")
assert guard_line_re.search('\n\nif __name__ == "__main__":\n print("test")\n')
@test(input="abc")
def test_passed(test_feedback):
assert 1 == 0, "1 should equal 0"
test_feedback = io.StringIO()
assert test_passed(test_feedback) is False
assert test_feedback.getvalue() == "1 should equal 0"