1
by Max Shinn
Initial commit |
1 |
#!/usr/bin/python
|
2 |
||
12
by Max Shinn
Added automatic unit tests |
3 |
import sys |
1
by Max Shinn
Initial commit |
4 |
import math |
2
by Max Shinn
Ranges, and, or, and cleanups |
5 |
import random |
6 |
import itertools |
|
3
by Max Shinn
Simple argument condition requirements |
7 |
import inspect |
6
by Max Shinn
Only one extra stack frame for all decorators |
8 |
import functools |
8
by Max Shinn
Better exceptions |
9 |
from exceptions import * |
2
by Max Shinn
Ranges, and, or, and cleanups |
10 |
|
9
by Max Shinn
Even better asserts |
11 |
_FUN_PROPS = "__verify__" |
12 |
||
2
by Max Shinn
Ranges, and, or, and cleanups |
13 |
def TypeFactory(v): |
14
by Max Shinn
Better documentation and code organization |
14 |
"""Ensure `v` is a valid Type.
|
15 |
||
16 |
This function is used to convert user-specified types into
|
|
17 |
internal types for the verification engine. It allows Type
|
|
18 |
subclasses, Type subclass instances, Python type, and user-defined
|
|
19 |
classes to be passed. Returns an instance of the type of `v`.
|
|
20 |
||
21 |
Users should never access this function directly.
|
|
22 |
"""
|
|
23 |
||
2
by Max Shinn
Ranges, and, or, and cleanups |
24 |
if issubclass(type(v), Type): |
25 |
return v |
|
26 |
elif issubclass(v, Type): |
|
27 |
return v() |
|
28 |
elif issubclass(type(v), type): |
|
29 |
return Generic(v) |
|
30 |
else: |
|
8
by Max Shinn
Better exceptions |
31 |
raise InvalidTypeError("Invalid type %s" % v) |
1
by Max Shinn
Initial commit |
32 |
|
33 |
class Type(): |
|
34 |
def test(self, v): |
|
35 |
pass
|
|
36 |
||
37 |
class Generic(Type): |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
38 |
def __init__(self, typ): |
39 |
super().__init__() |
|
3
by Max Shinn
Simple argument condition requirements |
40 |
assert isinstance(typ, type) |
2
by Max Shinn
Ranges, and, or, and cleanups |
41 |
self.type = typ |
42 |
def test(self, v): |
|
3
by Max Shinn
Simple argument condition requirements |
43 |
assert isinstance(v, self.type) |
2
by Max Shinn
Ranges, and, or, and cleanups |
44 |
def generate(self): |
12
by Max Shinn
Added automatic unit tests |
45 |
if hasattr(self.type, "_generate") and callable(self.type._generate): |
46 |
return self.type._generate() |
|
47 |
else: |
|
48 |
raise NoGeneratorError |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
49 |
|
7
by Max Shinn
None type |
50 |
class Nothing(Type): |
51 |
def test(self, v): |
|
52 |
assert v is None |
|
53 |
def generate(self): |
|
54 |
return [None] |
|
55 |
||
2
by Max Shinn
Ranges, and, or, and cleanups |
56 |
class Numeric(Type): |
57 |
def test(self, v): |
|
58 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
59 |
assert isinstance(v, (int, float)), "Invalid numeric" |
2
by Max Shinn
Ranges, and, or, and cleanups |
60 |
def generate(self): |
61 |
# Check infinity, nan, 0, +/- numbers, a float, a small/big number
|
|
62 |
return [math.inf, -math.inf, math.nan, 0, 1, -1, 3.141, 1e-10, 1e10] |
|
63 |
||
64 |
class Number(Numeric): |
|
1
by Max Shinn
Initial commit |
65 |
def test(self, v): |
66 |
super().test(v) |
|
67 |
assert isinstance(v, (int, float)), "Invalid number" |
|
68 |
assert not math.isnan(v), "Number cannot be nan" |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
69 |
assert not math.isinf(v), "Number must be finite" |
70 |
def generate(self): |
|
71 |
return [0, 1, -1, 3.141, 1e-10, 1e10] |
|
1
by Max Shinn
Initial commit |
72 |
|
73 |
class Integer(Number): |
|
74 |
def test(self, v): |
|
75 |
super().test(v) |
|
76 |
assert v // 1 == v, "Invalid integer" |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
77 |
def generate(self): |
78 |
return [-100, -1, 0, 1, 100] |
|
1
by Max Shinn
Initial commit |
79 |
|
80 |
class Natural0(Integer): |
|
81 |
def test(self, v): |
|
82 |
super().test(v) |
|
83 |
assert v >= 0, "Must be greater than or equal to 0" |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
84 |
def generate(self): |
85 |
return [0, 1, 10, 100] |
|
1
by Max Shinn
Initial commit |
86 |
|
87 |
class Natural1(Natural0): |
|
88 |
def test(self, v): |
|
89 |
super().test(v) |
|
90 |
assert v > 0, "Must be greater than 0" |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
91 |
def generate(self): |
92 |
return [1, 2, 10, 100] |
|
1
by Max Shinn
Initial commit |
93 |
|
94 |
class Range(Number): |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
95 |
def __init__(self, low, high): |
96 |
super().__init__() |
|
97 |
assert isinstance(low, (float, int)) and isinstance(high, (float, int)), "Invalid bounds" |
|
8
by Max Shinn
Better exceptions |
98 |
assert low < high, "Low %s must be strictly greater than high %s" % (low, high) |
2
by Max Shinn
Ranges, and, or, and cleanups |
99 |
self.low = low if low is not None else -math.inf |
100 |
self.high = high if low is not None else math.inf |
|
1
by Max Shinn
Initial commit |
101 |
def test(self, v): |
102 |
super().test(v) |
|
12
by Max Shinn
Added automatic unit tests |
103 |
assert self.low <= v <= self.high, "Value %f must be greater than %f and less than %f" % (v, self.low, self.high) |
4
by Max Shinn
Better ranges |
104 |
def _generate_quantiles(self): |
105 |
EPSILON = 1e-5 |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
106 |
if not (math.isinf(self.low) or math.isinf(self.high)): |
107 |
l = self.low |
|
108 |
h = self.high |
|
12
by Max Shinn
Added automatic unit tests |
109 |
return [(l+h)*EPSILON, (l+h)*.5, (l+h)*.25, (l+h)*.75, (l+h)*(1-EPSILON)] |
4
by Max Shinn
Better ranges |
110 |
elif math.isinf(self.low): |
111 |
return [self.high-EPSILON] |
|
112 |
elif math.isinf(self.high): |
|
113 |
return [self.low-EPSILON] |
|
9
by Max Shinn
Even better asserts |
114 |
raise AssertionError("Invalid Range bounds") |
4
by Max Shinn
Better ranges |
115 |
def generate(self): |
116 |
return [self.low, self.high] + self._generate_quantiles() |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
117 |
|
118 |
class RangeClosedOpen(Range): |
|
119 |
def test(self, v): |
|
120 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
121 |
assert v != self.high, "Value must be strictly greater than %f" % self.high |
4
by Max Shinn
Better ranges |
122 |
def generate(self): |
123 |
return [self.low] + self._generate_quantiles() |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
124 |
|
125 |
class RangeOpenClosed(Range): |
|
126 |
def test(self, v): |
|
127 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
128 |
assert v != self.low, "Value must be strictly less than %f" % self.low |
4
by Max Shinn
Better ranges |
129 |
def generate(self): |
130 |
return [self.high] + self._generate_quantiles() |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
131 |
|
132 |
class RangeOpen(RangeClosedOpen, RangeOpenClosed): |
|
133 |
def test(self, v): |
|
134 |
super().test(v) |
|
4
by Max Shinn
Better ranges |
135 |
def generate(self): |
136 |
return self._generate_quantiles() |
|
1
by Max Shinn
Initial commit |
137 |
|
138 |
class Set(Type): |
|
139 |
def __init__(self, els): |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
140 |
super().__init__() |
1
by Max Shinn
Initial commit |
141 |
assert hasattr(els, "__contains__") and callable(els.__contains__) |
142 |
self.els = els |
|
143 |
def test(self, v): |
|
144 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
145 |
assert v in self.els, "Value %v in set" % v |
2
by Max Shinn
Ranges, and, or, and cleanups |
146 |
def generate(self): |
147 |
return [e for e in self.els] |
|
1
by Max Shinn
Initial commit |
148 |
|
149 |
class String(Type): |
|
150 |
def test(self, v): |
|
151 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
152 |
assert isinstance(v, str), "Non-string passed" |
2
by Max Shinn
Ranges, and, or, and cleanups |
153 |
def generate(self): |
154 |
return ["", "a"*1000, "{100}", " ", "abc123", "two words", "\\", "%s", "1", "баклажан"] |
|
1
by Max Shinn
Initial commit |
155 |
|
156 |
class List(Type): |
|
157 |
def __init__(self, t): |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
158 |
super().__init__() |
159 |
self.type = TypeFactory(t) |
|
1
by Max Shinn
Initial commit |
160 |
def test(self, v): |
161 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
162 |
assert isinstance(v, list), "Non-list passed" |
2
by Max Shinn
Ranges, and, or, and cleanups |
163 |
for e in v: |
164 |
self.type.test(e) |
|
165 |
def generate(self): |
|
166 |
return [[], self.type.generate(), [self.type.generate()[0]]*1000] |
|
1
by Max Shinn
Initial commit |
167 |
|
168 |
class Dict(Type): |
|
169 |
def __init__(self, k, v): |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
170 |
self.valtype = TypeFactory(v) |
171 |
self.keytype = TypeFactory(k) |
|
1
by Max Shinn
Initial commit |
172 |
def test(self, v): |
173 |
super().test(v) |
|
8
by Max Shinn
Better exceptions |
174 |
assert isinstance(v, dict), "Non-dict passed" |
1
by Max Shinn
Initial commit |
175 |
for e in v.keys(): |
176 |
self.keytype.test(e) |
|
177 |
for e in v.values(): |
|
178 |
self.valtype.test(e) |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
179 |
def generate(self): |
180 |
return [{}, dict(zip(self.keytype.generate(), self.valtype.generate()))] |
|
181 |
||
182 |
class And(Type): |
|
183 |
def __init__(self, *types): |
|
184 |
self.types = [TypeFactory(a) for a in types] |
|
185 |
def test(self, v): |
|
186 |
for t in self.types: |
|
187 |
t.test(v) |
|
12
by Max Shinn
Added automatic unit tests |
188 |
def generate(self): |
13
by Max Shinn
Better generate() function for And and Or |
189 |
all_generated = [e for t in self.types for e in t.generate()] |
190 |
valid_generated = [] |
|
191 |
for g in all_generated: |
|
192 |
try: |
|
193 |
for t in self.types: |
|
194 |
t.test(g) |
|
195 |
except AssertionError: |
|
196 |
continue
|
|
197 |
else: |
|
198 |
valid_generated.append(g) |
|
199 |
return valid_generated |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
200 |
|
201 |
class Or(Type): |
|
202 |
def __init__(self, *types): |
|
203 |
self.types = [TypeFactory(a) for a in types] |
|
204 |
def test(self, v): |
|
205 |
passed = False |
|
206 |
for t in self.types: |
|
207 |
try: |
|
208 |
t.test(v) |
|
209 |
passed = True |
|
210 |
except AssertionError: |
|
211 |
continue
|
|
212 |
if passed == False: |
|
8
by Max Shinn
Better exceptions |
213 |
raise AssertionError("Neither type in Or holds") |
13
by Max Shinn
Better generate() function for And and Or |
214 |
def generate(self): |
215 |
return [e for t in self.types for e in t.generate()] |
|
1
by Max Shinn
Initial commit |
216 |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
217 |
def has_fun_prop(f, k): |
14
by Max Shinn
Better documentation and code organization |
218 |
"""Test whether function `f` has property `k`.
|
219 |
||
220 |
We define properties as annotations added to a function throughout
|
|
221 |
the process of defining a function for verification, e.g. the
|
|
222 |
argument types. If `f` is an unannotated function, this returns
|
|
223 |
False. If `f` has the property named `k`, it returns True.
|
|
224 |
Otherwise, it returns False.
|
|
225 |
||
226 |
Users should never access this function directly.
|
|
227 |
"""
|
|
6
by Max Shinn
Only one extra stack frame for all decorators |
228 |
if not hasattr(f, _FUN_PROPS): |
229 |
return False |
|
230 |
if not isinstance(getattr(f, _FUN_PROPS), dict): |
|
231 |
return False |
|
232 |
if k not in getattr(f, _FUN_PROPS).keys(): |
|
233 |
return False |
|
234 |
return True |
|
235 |
||
236 |
def get_fun_prop(f, k): |
|
14
by Max Shinn
Better documentation and code organization |
237 |
"""Get the value of property `k` from function `f`.
|
238 |
||
239 |
We define properties as annotations added to a function throughout
|
|
240 |
the process of defining a function for verification, e.g. the
|
|
241 |
argument types. If `f` does not have a property named `k`, this
|
|
242 |
throws an error. If `f` has the property named `k`, it returns
|
|
243 |
the value of it.
|
|
244 |
||
245 |
Users should never access this function directly.
|
|
246 |
"""
|
|
9
by Max Shinn
Even better asserts |
247 |
if not has_fun_prop(f, k): |
248 |
raise InternalError("Function %s has no property %s" % (str(f), k)) |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
249 |
return getattr(f, _FUN_PROPS)[k] |
250 |
||
251 |
def set_fun_prop(f, k, v): |
|
14
by Max Shinn
Better documentation and code organization |
252 |
"""Set the value of property `k` to be `v` in function `f`.
|
253 |
||
254 |
We define properties as annotations added to a function throughout
|
|
255 |
the process of defining a function for verification, e.g. the
|
|
256 |
argument types. This sets function `f`'s property named `k` to be
|
|
257 |
value `v`.
|
|
258 |
||
259 |
Users should never access this function directly.
|
|
260 |
"""
|
|
6
by Max Shinn
Only one extra stack frame for all decorators |
261 |
if not hasattr(f, _FUN_PROPS): |
262 |
setattr(f, _FUN_PROPS, {}) |
|
263 |
if not isinstance(getattr(f, _FUN_PROPS), dict): |
|
9
by Max Shinn
Even better asserts |
264 |
raise InternalError("Invalid function properties dictionary for %s" % str(f)) |
6
by Max Shinn
Only one extra stack frame for all decorators |
265 |
getattr(f, _FUN_PROPS)[k] = v |
266 |
||
14
by Max Shinn
Better documentation and code organization |
267 |
def _wrap(func): |
268 |
def _decorated(*args, **kwargs): |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
269 |
# @accepts decorator
|
270 |
if has_fun_prop(func, "argtypes") and has_fun_prop(func, "kwargtypes"): |
|
271 |
argtypes = get_fun_prop(func, "argtypes") |
|
272 |
kwargtypes = get_fun_prop(func, "kwargtypes") |
|
273 |
if argtypes: |
|
9
by Max Shinn
Even better asserts |
274 |
if len(argtypes) != len(args): |
275 |
raise ArgumentTypeError("Invalid argument specification to @accepts") |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
276 |
for i,t in enumerate(argtypes): |
9
by Max Shinn
Even better asserts |
277 |
try: |
278 |
t.test(args[i]) |
|
279 |
except AssertionError as e: |
|
280 |
raise ArgumentTypeError("Invalid argument type %s" % args[i]) |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
281 |
if kwargtypes: |
9
by Max Shinn
Even better asserts |
282 |
if all(k in kwargs.keys() for k in kwargtypes.keys()): |
283 |
raise ArgumentTypeError("Invalid keyword argument specification to @accepts") |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
284 |
for k,t in kwargtypes.items(): |
9
by Max Shinn
Even better asserts |
285 |
try: |
286 |
t.test(kwargs[k]) |
|
287 |
except AssertionError as e: |
|
288 |
raise ArgumentTypeError("Invalid argument type for %s: %s" % (k, kwargs[k])) |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
289 |
# @requires decorator
|
290 |
if has_fun_prop(func, "requires"): |
|
291 |
argspec = inspect.getargspec(func) |
|
292 |
# Function named arguments
|
|
293 |
#full_locals = locals().copy()
|
|
294 |
#full_locals.update({k : v for k,v in zip(argspec.args, args)})
|
|
295 |
full_locals = {k : v for k,v in zip(argspec.args, args)} |
|
296 |
# Unnamed positional arguments
|
|
297 |
if argspec.varargs is not None: |
|
298 |
positional_args = {argspec.varargs: args[len(argspec.args):]} |
|
299 |
full_locals.update(positional_args) |
|
300 |
# TODO kw arguments
|
|
301 |
for requirement in get_fun_prop(func, "requires"): |
|
9
by Max Shinn
Even better asserts |
302 |
if not eval(requirement, globals(), full_locals): |
303 |
raise EntryConditionsError("Function requirement '%s' failed" % requirement) |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
304 |
# The actual function
|
305 |
returnvalue = func(*args, **kwargs) |
|
306 |
# @returns decorator
|
|
307 |
if has_fun_prop(func, "returntype"): |
|
9
by Max Shinn
Even better asserts |
308 |
try: |
309 |
get_fun_prop(func, "returntype").test(returnvalue) |
|
310 |
except AssertionError as e: |
|
311 |
raise ReturnTypeError("Invalid return type of %s" % returnvalue ) |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
312 |
# @ensures decorator
|
313 |
if has_fun_prop(func, "ensures"): |
|
314 |
argspec = inspect.getargspec(func) |
|
315 |
# Function named arguments
|
|
316 |
limited_locals = {k : v for k,v in zip(argspec.args, args)} |
|
8
by Max Shinn
Better exceptions |
317 |
# Return value
|
318 |
limited_locals['__RETURN__'] = returnvalue |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
319 |
# Unnamed positional arguments
|
320 |
if argspec.varargs is not None: |
|
321 |
positional_args = {argspec.varargs: args[len(argspec.args):]} |
|
322 |
limited_locals.update(positional_args) |
|
10
by Max Shinn
Refer to previous execution values in ensures |
323 |
if any("`" in ens for ens in get_fun_prop(func, "ensures")) : # Cache if we refer to previous executions |
324 |
if has_fun_prop(func, "exec_cache"): |
|
325 |
exec_cache = get_fun_prop(func, "exec_cache") |
|
326 |
else: |
|
327 |
exec_cache = [] |
|
328 |
exec_cache.append(limited_locals.copy()) |
|
329 |
set_fun_prop(func, "exec_cache", exec_cache) #TODO is this line necessary? |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
330 |
# TODO kw arguments
|
8
by Max Shinn
Better exceptions |
331 |
for ensurement in get_fun_prop(func, "ensures"): |
332 |
e = ensurement.replace("return", "__RETURN__") |
|
11
by Max Shinn
Changed from => to --> notation for implies so that it is not as easily confused with >= or <= |
333 |
if "<-->" in e: |
334 |
e_parts = e.split("<-->") |
|
10
by Max Shinn
Refer to previous execution values in ensures |
335 |
assert len(e_parts) == 2, "Only one implies per statement" |
336 |
e = "((%s) if (%s) else True) and ((%s) if (%s) else True)" % (e_parts[1], e_parts[0], e_parts[0], e_parts[1]) |
|
11
by Max Shinn
Changed from => to --> notation for implies so that it is not as easily confused with >= or <= |
337 |
if "-->" in e: # TODO won't throw error if --> and <--> are in e |
338 |
e_parts = e.split("-->") |
|
10
by Max Shinn
Refer to previous execution values in ensures |
339 |
assert len(e_parts) == 2, "Only one implies per statement" |
340 |
e = "(%s) if (%s) else True" % (e_parts[1], e_parts[0]) |
|
341 |
if "`" in e: |
|
342 |
bt = "__BACKTICK__" |
|
343 |
exec_cache = get_fun_prop(func, "exec_cache") |
|
344 |
for cache_item in exec_cache: |
|
345 |
limited_locals.update({k+bt : v for k,v in cache_item.items()}) |
|
346 |
e = e.replace("`", bt) |
|
347 |
if not eval(e, globals(), limited_locals): |
|
348 |
raise ExitConditionsError("Ensures statement '%s' failed" % ensurement) |
|
349 |
elif not eval(e, globals(), limited_locals): |
|
9
by Max Shinn
Even better asserts |
350 |
raise ExitConditionsError("Ensures statement '%s' failed" % ensurement) |
6
by Max Shinn
Only one extra stack frame for all decorators |
351 |
return returnvalue |
352 |
if has_fun_prop(func, "active"): |
|
353 |
return func |
|
354 |
else: |
|
355 |
set_fun_prop(func, "active", True) |
|
356 |
assign = functools.WRAPPER_ASSIGNMENTS + (_FUN_PROPS,) |
|
14
by Max Shinn
Better documentation and code organization |
357 |
wrapped = functools.wraps(func, assigned=assign)(_decorated) |
358 |
if "__ALL_FUNCTIONS" in globals().keys(): |
|
359 |
global __ALL_FUNCTIONS |
|
360 |
__ALL_FUNCTIONS.append(wrapped) |
|
12
by Max Shinn
Added automatic unit tests |
361 |
return wrapped |
14
by Max Shinn
Better documentation and code organization |
362 |
|
6
by Max Shinn
Only one extra stack frame for all decorators |
363 |
|
1
by Max Shinn
Initial commit |
364 |
def accepts(*argtypes, **kwargtypes): |
14
by Max Shinn
Better documentation and code organization |
365 |
argtypes = [TypeFactory(a) for a in argtypes] |
366 |
kwargtypes = {k : TypeFactory(a) for k,a in kwargtypes.items()} |
|
367 |
def _decorator(func): |
|
368 |
# @accepts decorator
|
|
369 |
if argtypes is not None: |
|
370 |
if has_fun_prop(func, "argtypes"): |
|
371 |
raise ValueError("Cannot set argument types twice") |
|
372 |
set_fun_prop(func, "argtypes", argtypes) |
|
373 |
if kwargtypes is not None: |
|
374 |
if has_fun_prop(func, "kwargtypes"): |
|
375 |
raise ValueError("Cannot set set keyword argument types twice") |
|
376 |
set_fun_prop(func, "kwargtypes", kwargtypes) |
|
377 |
return _wrap(func) |
|
378 |
return _decorator |
|
379 |
||
1
by Max Shinn
Initial commit |
380 |
def returns(returntype): |
14
by Max Shinn
Better documentation and code organization |
381 |
returntype = TypeFactory(returntype) |
382 |
def _decorator(func): |
|
383 |
# @returns decorator
|
|
384 |
if has_fun_prop(func, "returntype"): |
|
385 |
raise ValueError("Cannot set return type twice") |
|
386 |
set_fun_prop(func, "returntype", returntype) |
|
387 |
return _wrap(func) |
|
388 |
return _decorator |
|
389 |
||
3
by Max Shinn
Simple argument condition requirements |
390 |
def requires(condition): |
14
by Max Shinn
Better documentation and code organization |
391 |
def _decorator(func): |
392 |
# @requires decorator
|
|
393 |
if has_fun_prop(func, "requires"): |
|
394 |
if not isinstance(get_fun_prop(func, "requires"), list): |
|
395 |
raise InternalError("Invalid requires structure") |
|
396 |
base_requires = get_fun_prop(func, "requires") |
|
397 |
else: |
|
398 |
base_requires = [] |
|
399 |
set_fun_prop(func, "requires", [condition]+base_requires) |
|
400 |
return _wrap(func) |
|
401 |
return _decorator |
|
402 |
||
8
by Max Shinn
Better exceptions |
403 |
def ensures(condition): |
14
by Max Shinn
Better documentation and code organization |
404 |
def _decorator(func): |
405 |
# @ensures decorator
|
|
406 |
if has_fun_prop(func, "ensures"): |
|
407 |
if not isinstance(get_fun_prop(func, "ensures"), list): |
|
408 |
raise InternalError("Invalid ensures strucutre") |
|
409 |
base_ensures = get_fun_prop(func, "ensures") |
|
410 |
else: |
|
411 |
base_ensures = [] |
|
412 |
set_fun_prop(func, "ensures", [condition]+base_ensures) |
|
413 |
return _wrap(func) |
|
414 |
return _decorator |
|
1
by Max Shinn
Initial commit |
415 |
|
2
by Max Shinn
Ranges, and, or, and cleanups |
416 |
def test_function(func): |
14
by Max Shinn
Better documentation and code organization |
417 |
"""Perform a unit test of a single function.
|
418 |
||
419 |
Create a series of test cases by finding the type of each of the
|
|
420 |
function arguments, and creating all possible combinations of test
|
|
421 |
cases for each type using the type's generate() function. Then,
|
|
422 |
test the function with each input. This is roughly equivalent to
|
|
423 |
just running the function with each input, because the exceptions
|
|
424 |
should be caught at runtime.
|
|
425 |
||
426 |
Assuming runtime checking is enabled (as it should be), running
|
|
427 |
this on a function only tests it for inputs it is likely to
|
|
428 |
encounter in the future.
|
|
429 |
"""
|
|
430 |
# Ensure the user has specified argument types using the @accepts
|
|
431 |
# decorator.
|
|
432 |
assert has_fun_prop(func, "argtypes"), "No argument annotations" |
|
433 |
# Generate all combinations of arguments as test cases. Some
|
|
434 |
# argument types cannot be generated automatically. If we
|
|
435 |
# encounter one of these, unit testing won't work.
|
|
12
by Max Shinn
Added automatic unit tests |
436 |
try: |
437 |
testcases = itertools.product(*[e.generate() for e in get_fun_prop(func, "argtypes")]) |
|
438 |
except NoGeneratorError: |
|
14
by Max Shinn
Better documentation and code organization |
439 |
testcases = [] |
440 |
if not testcases: |
|
12
by Max Shinn
Added automatic unit tests |
441 |
print("Warning: %s could not be tested" % func.__name__) |
14
by Max Shinn
Better documentation and code organization |
442 |
return
|
443 |
# If entry conditions are met, simply running the function will be
|
|
444 |
# enough of a test, since all values are checked at runtime. So
|
|
445 |
# execute the function once for each combination of arguments.
|
|
446 |
for tc in testcases: |
|
447 |
try: |
|
448 |
func(*tc) |
|
449 |
except EntryConditionsError: |
|
450 |
continue
|
|
451 |
||
452 |
# If called as "python3 -m verify script_file_name.py", then unit test
|
|
453 |
# script_file_name.py. By this, we mean call the function
|
|
454 |
# test_function on each function which has annotations. Note that we
|
|
455 |
# must execute the entire file script_file_name.py in order to do
|
|
456 |
# this, so any time-intensive code should be in a __name__ ==
|
|
457 |
# "__main__" guard, which will not be executed.
|
|
458 |
#
|
|
459 |
# We know if a function has annotations because all annotations are
|
|
460 |
# passed through the _decorator function (so that we can ensure we
|
|
461 |
# only use at most one more stack frame no matter how many function
|
|
462 |
# annotations we have). The _decorator function will look for the
|
|
463 |
# global variable __ALL_FUNCTIONS to be defined within the verify
|
|
464 |
# module. If it is defined, it will add one copy of each decorated
|
|
465 |
# function to the list. Then, we can perform the unit test by
|
|
466 |
# iterating through each of these. This has the advantage of allowing
|
|
467 |
# nested functions and object methods to be discovered for
|
|
468 |
# verification.
|
|
12
by Max Shinn
Added automatic unit tests |
469 |
if __name__ == "__main__": |
14
by Max Shinn
Better documentation and code organization |
470 |
# Pass exactly one argement: the python file to check
|
471 |
if len(sys.argv) != 2: # len == 2 because the path to the module is arg 1. |
|
472 |
exit("Invalid argument, please pass a python file") |
|
473 |
# Add the __ALL_FUNCTIONS to the global scope of the verify module
|
|
474 |
# and then run the script. Save the global variables from script
|
|
475 |
# execution so that we can find the __ALL_FUNCTIONS variable once
|
|
476 |
# the script has finished executing.
|
|
477 |
globs = {} # Global variables from script execution |
|
478 |
prefix = "import verify as __verifymod\n__verifymod.__ALL_FUNCTIONS = []\n" |
|
479 |
exec(prefix+open(sys.argv[1], "r").read(), globs) |
|
480 |
all_functions = globs["__verifymod"].__ALL_FUNCTIONS |
|
481 |
# Test each function from the script.
|
|
12
by Max Shinn
Added automatic unit tests |
482 |
for f in all_functions: |
14
by Max Shinn
Better documentation and code organization |
483 |
assert hasattr(f, _FUN_PROPS), "Internal error" |
484 |
# Some function executions might take a while, so we print to
|
|
485 |
# the screen when we begin testing a function, and then erase
|
|
486 |
# it after we finish testing the function. This is like a
|
|
487 |
# make-shift progress bar.
|
|
488 |
start_text = " Testing %s..." % f.__name__ |
|
489 |
print(start_text, end="", flush=True) |
|
12
by Max Shinn
Added automatic unit tests |
490 |
test_function(f) |
14
by Max Shinn
Better documentation and code organization |
491 |
import time |
492 |
time.sleep(1) |
|
493 |
# Extra spaces after "Tested %s" compensate for the fact that
|
|
494 |
# the string to signal the start of testing function f is
|
|
495 |
# longer than the string to signal function f has been tested,
|
|
496 |
# so this avoids leaving extra characters on the terminal.
|
|
497 |
print("\b"*len(start_text)+" Tested %s " % f.__name__, flush=True) |
|
498 |
print("Tested %i functions in %s." % (len(all_functions), sys.argv[1])) |