Skip to content

Commit a803f15

Browse files
committed
Write harness for testing labs in
1 parent 9573a71 commit a803f15

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed

harness.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""
2+
File: harness.py
3+
----------------
4+
5+
The lab harness for CS 41 that allows the user to interact with the testable
6+
functions in their lab assignment.
7+
8+
History
9+
-------
10+
8/9/2020 -- Created by @psarin
11+
8/12/2020 -- Looks like we're using cmd!
12+
"""
13+
import cmd # For the terminal interface
14+
import inspect # For grabbing information about student functions
15+
import os # For the files in the current dir
16+
from typing import * # For enforcing and displaying type information
17+
import traceback # For displaying errors to students
18+
19+
20+
def get_testable_functions():
21+
"""
22+
Returns a list of the testable functions by extracting them from the Python
23+
files in the current directory.
24+
"""
25+
# Check the files that aren't the current file.
26+
curr_file = os.path.basename(__file__)
27+
files_to_check = [
28+
f for f in os.listdir() if f.endswith('.py') and f != curr_file
29+
]
30+
31+
# Collect a list of functions to test
32+
output = []
33+
for file in files_to_check:
34+
module_name = file[:-3]
35+
module = __import__(module_name)
36+
37+
try:
38+
output += module.TESTABLE
39+
except AttributeError:
40+
# No testable functions in the file
41+
pass
42+
43+
return output
44+
45+
46+
class Harness(cmd.Cmd):
47+
prompt = '🦄 > '
48+
49+
def __init__(self, choices, *args, **kwargs):
50+
super().__init__(*args, **kwargs)
51+
self.choices = choices
52+
self.intro = (
53+
"Welcome to the CS 41 Lab Harness! Type ? or help to get a list of "
54+
"commands or type \na number to execute tests on that function.\n\n"
55+
f"Functions\n=========\n{self._make_choices_lst(choices)}\n"
56+
)
57+
58+
59+
@staticmethod
60+
def _get_signature(fn):
61+
"""
62+
Returns the signature of fn as a string, where fn is a callable object.
63+
"""
64+
return f"{fn.__name__}{inspect.signature(fn)}"
65+
66+
67+
@staticmethod
68+
def _make_choices_lst(choices):
69+
"""
70+
Join function choices togeher into an indexed list
71+
"""
72+
return '\n'.join([
73+
"{}: {}".format(i+1, Harness._get_signature(choice))
74+
for i, choice in enumerate(choices)
75+
])
76+
77+
78+
def default(self, arg):
79+
"""
80+
The default interpretation of arg.
81+
"""
82+
try:
83+
chosen_i = int(arg) - 1
84+
chosen_f = self.choices[chosen_i]
85+
except ValueError:
86+
print("Please enter a valid command.")
87+
return
88+
except IndexError:
89+
print(f"Please enter a number between 1 and {len(self.choices)}.")
90+
return
91+
92+
self.test(chosen_f)
93+
94+
95+
@staticmethod
96+
def _get_arg(arg_name: str, arg_type=None):
97+
"""
98+
Prompts the user for an arg_name of type arg_type and returns the result
99+
once they enter valid Python code.
100+
"""
101+
type_hint = ''
102+
type_comparison = None
103+
104+
# Define the type hint and find the highest object we can compare to
105+
if arg_type:
106+
type_hint = f' ({arg_type})' if arg_type else ''
107+
try:
108+
type_comparison = arg_type.__origin__
109+
except AttributeError:
110+
type_comparison = arg_type
111+
112+
# Combine those to the prompt
113+
prompt = f"{arg_name}{type_hint.replace('typing.', '')}? "
114+
115+
# Prompt until the user inputs a good value
116+
while True:
117+
try:
118+
res = eval(input(prompt))
119+
except Exception as e:
120+
print(f"Your input raised an error: {e}. Try again.")
121+
continue
122+
123+
if not isinstance(res, type_comparison):
124+
print("You didn't enter an object with the correct type.")
125+
continue
126+
127+
break
128+
129+
return res
130+
131+
132+
@staticmethod
133+
def test(fn):
134+
"""
135+
Initiates tests on the function fn.
136+
"""
137+
print(f"Testing {Harness._get_signature(fn)} \nPlease enter valid "
138+
"Python code as inputs for each of the arguments.")
139+
140+
argspec = inspect.getfullargspec(fn)
141+
annotations = argspec.annotations
142+
143+
# Collect information about what to pass to the funciton
144+
args = []
145+
for arg in argspec.args:
146+
args.append(Harness._get_arg(arg, annotations.get(arg)))
147+
148+
varargs = ()
149+
if argspec.varargs:
150+
varargs = Harness._get_arg(argspec.varargs, tuple)
151+
152+
kwonlyargs = {}
153+
for arg in argspec.kwonlyargs:
154+
kwonlyargs[arg] = Harness._get_arg(arg, annotations.get(arg))
155+
156+
varkw = {}
157+
if argspec.varkw:
158+
varkw = Harness._get_arg(argspec.varkw, dict)
159+
160+
# Collect the args into a string
161+
str_arg_lst = [str(a) for a in args + list(varargs)] \
162+
+ [f"{k}={repr(v)}" for k, v in kwonlyargs.items()] \
163+
+ [f"{k}={repr(v)}" for k, v in varkw.items()]
164+
str_rep = ', '.join(str_arg_lst)
165+
166+
# Run the function
167+
print()
168+
print(f"Calling {fn.__name__}({str_rep})...")
169+
try:
170+
retval = fn(*args, *varargs, **kwonlyargs, **varkw)
171+
except Exception as e:
172+
print(f"The function raised an error: {e}.")
173+
print(traceback.format_exc(chain=False))
174+
else:
175+
print(f"Out: {repr(retval)}")
176+
print()
177+
178+
179+
def do_exit(self, arg):
180+
'Closes the CS 41 lab harness.'
181+
print("Good bye! Have a lovely, unicorn-filled day.")
182+
return True
183+
184+
185+
def do_list(self, arg):
186+
'Prints a list of functions that are available for testing.'
187+
print(f"Functions\n=========\n{self._make_choices_lst(choices)}\n")
188+
189+
190+
if __name__ == '__main__':
191+
Harness(get_testable_functions()).cmdloop()

0 commit comments

Comments
 (0)