blob: ab7d5bdf4effbf1f84e625c74b3f0e66db6e15bf [file] [log] [blame]
Stefan Krah1919b7e2012-03-21 18:25:23 +01001#
2# Copyright (c) 2008-2012 Stefan Krah. All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions
6# are met:
7#
8# 1. Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#
11# 2. Redistributions in binary form must reproduce the above copyright
12# notice, this list of conditions and the following disclaimer in the
13# documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27
28#
29# Usage: python deccheck.py [--short|--medium|--long|--all]
30#
31
32import sys, random
33from copy import copy
34from collections import defaultdict
35from test.support import import_fresh_module
36from randdec import randfloat, all_unary, all_binary, all_ternary
Stefan Krah040e3112012-12-15 22:33:33 +010037from randdec import unary_optarg, binary_optarg, ternary_optarg
Stefan Krah1919b7e2012-03-21 18:25:23 +010038from formathelper import rand_format, rand_locale
Stefan Krahb578f8a2014-09-10 17:58:15 +020039from _pydecimal import _dec_from_triple
Stefan Krah1919b7e2012-03-21 18:25:23 +010040
41C = import_fresh_module('decimal', fresh=['_decimal'])
42P = import_fresh_module('decimal', blocked=['_decimal'])
43EXIT_STATUS = 0
44
45
46# Contains all categories of Decimal methods.
47Functions = {
48 # Plain unary:
49 'unary': (
50 '__abs__', '__bool__', '__ceil__', '__complex__', '__copy__',
51 '__floor__', '__float__', '__hash__', '__int__', '__neg__',
52 '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__',
53 'adjusted', 'as_tuple', 'canonical', 'conjugate', 'copy_abs',
54 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite',
55 'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix'
56 ),
57 # Unary with optional context:
58 'unary_ctx': (
59 'exp', 'is_normal', 'is_subnormal', 'ln', 'log10', 'logb',
60 'logical_invert', 'next_minus', 'next_plus', 'normalize',
61 'number_class', 'sqrt', 'to_eng_string'
62 ),
63 # Unary with optional rounding mode and context:
64 'unary_rnd_ctx': ('to_integral', 'to_integral_exact', 'to_integral_value'),
65 # Plain binary:
66 'binary': (
67 '__add__', '__divmod__', '__eq__', '__floordiv__', '__ge__', '__gt__',
68 '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__pow__',
69 '__radd__', '__rdivmod__', '__rfloordiv__', '__rmod__', '__rmul__',
70 '__rpow__', '__rsub__', '__rtruediv__', '__sub__', '__truediv__',
71 'compare_total', 'compare_total_mag', 'copy_sign', 'quantize',
72 'same_quantum'
73 ),
74 # Binary with optional context:
75 'binary_ctx': (
76 'compare', 'compare_signal', 'logical_and', 'logical_or', 'logical_xor',
77 'max', 'max_mag', 'min', 'min_mag', 'next_toward', 'remainder_near',
78 'rotate', 'scaleb', 'shift'
79 ),
80 # Plain ternary:
81 'ternary': ('__pow__',),
82 # Ternary with optional context:
83 'ternary_ctx': ('fma',),
84 # Special:
85 'special': ('__format__', '__reduce_ex__', '__round__', 'from_float',
86 'quantize'),
87 # Properties:
88 'property': ('real', 'imag')
89}
90
91# Contains all categories of Context methods. The n-ary classification
92# applies to the number of Decimal arguments.
93ContextFunctions = {
94 # Plain nullary:
95 'nullary': ('context.__hash__', 'context.__reduce__', 'context.radix'),
96 # Plain unary:
97 'unary': ('context.abs', 'context.canonical', 'context.copy_abs',
98 'context.copy_decimal', 'context.copy_negate',
99 'context.create_decimal', 'context.exp', 'context.is_canonical',
100 'context.is_finite', 'context.is_infinite', 'context.is_nan',
101 'context.is_normal', 'context.is_qnan', 'context.is_signed',
102 'context.is_snan', 'context.is_subnormal', 'context.is_zero',
103 'context.ln', 'context.log10', 'context.logb',
104 'context.logical_invert', 'context.minus', 'context.next_minus',
105 'context.next_plus', 'context.normalize', 'context.number_class',
106 'context.plus', 'context.sqrt', 'context.to_eng_string',
107 'context.to_integral', 'context.to_integral_exact',
108 'context.to_integral_value', 'context.to_sci_string'
109 ),
110 # Plain binary:
111 'binary': ('context.add', 'context.compare', 'context.compare_signal',
112 'context.compare_total', 'context.compare_total_mag',
113 'context.copy_sign', 'context.divide', 'context.divide_int',
114 'context.divmod', 'context.logical_and', 'context.logical_or',
115 'context.logical_xor', 'context.max', 'context.max_mag',
116 'context.min', 'context.min_mag', 'context.multiply',
117 'context.next_toward', 'context.power', 'context.quantize',
118 'context.remainder', 'context.remainder_near', 'context.rotate',
119 'context.same_quantum', 'context.scaleb', 'context.shift',
120 'context.subtract'
121 ),
122 # Plain ternary:
123 'ternary': ('context.fma', 'context.power'),
124 # Special:
125 'special': ('context.__reduce_ex__', 'context.create_decimal_from_float')
126}
127
128# Functions that require a restricted exponent range for reasonable runtimes.
129UnaryRestricted = [
Serhiy Storchakaa60c2fe2015-03-12 21:56:08 +0200130 '__ceil__', '__floor__', '__int__', '__trunc__',
Stefan Krah1919b7e2012-03-21 18:25:23 +0100131 'to_integral', 'to_integral_value'
132]
133
134BinaryRestricted = ['__round__']
135
136TernaryRestricted = ['__pow__', 'context.power']
137
138
139# ======================================================================
140# Unified Context
141# ======================================================================
142
143# Translate symbols.
144CondMap = {
145 C.Clamped: P.Clamped,
146 C.ConversionSyntax: P.ConversionSyntax,
147 C.DivisionByZero: P.DivisionByZero,
148 C.DivisionImpossible: P.InvalidOperation,
149 C.DivisionUndefined: P.DivisionUndefined,
150 C.Inexact: P.Inexact,
151 C.InvalidContext: P.InvalidContext,
152 C.InvalidOperation: P.InvalidOperation,
153 C.Overflow: P.Overflow,
154 C.Rounded: P.Rounded,
155 C.Subnormal: P.Subnormal,
156 C.Underflow: P.Underflow,
157 C.FloatOperation: P.FloatOperation,
158}
159
Stefan Krah59a4a932013-01-16 12:58:59 +0100160RoundModes = [C.ROUND_UP, C.ROUND_DOWN, C.ROUND_CEILING, C.ROUND_FLOOR,
161 C.ROUND_HALF_UP, C.ROUND_HALF_DOWN, C.ROUND_HALF_EVEN,
162 C.ROUND_05UP]
Stefan Krah1919b7e2012-03-21 18:25:23 +0100163
164
165class Context(object):
166 """Provides a convenient way of syncing the C and P contexts"""
167
168 __slots__ = ['c', 'p']
169
170 def __init__(self, c_ctx=None, p_ctx=None):
171 """Initialization is from the C context"""
172 self.c = C.getcontext() if c_ctx is None else c_ctx
173 self.p = P.getcontext() if p_ctx is None else p_ctx
174 self.p.prec = self.c.prec
175 self.p.Emin = self.c.Emin
176 self.p.Emax = self.c.Emax
Stefan Krah59a4a932013-01-16 12:58:59 +0100177 self.p.rounding = self.c.rounding
Stefan Krah1919b7e2012-03-21 18:25:23 +0100178 self.p.capitals = self.c.capitals
179 self.settraps([sig for sig in self.c.traps if self.c.traps[sig]])
180 self.setstatus([sig for sig in self.c.flags if self.c.flags[sig]])
181 self.p.clamp = self.c.clamp
182
183 def __str__(self):
184 return str(self.c) + '\n' + str(self.p)
185
186 def getprec(self):
187 assert(self.c.prec == self.p.prec)
188 return self.c.prec
189
190 def setprec(self, val):
191 self.c.prec = val
192 self.p.prec = val
193
194 def getemin(self):
195 assert(self.c.Emin == self.p.Emin)
196 return self.c.Emin
197
198 def setemin(self, val):
199 self.c.Emin = val
200 self.p.Emin = val
201
202 def getemax(self):
203 assert(self.c.Emax == self.p.Emax)
204 return self.c.Emax
205
206 def setemax(self, val):
207 self.c.Emax = val
208 self.p.Emax = val
209
210 def getround(self):
Stefan Krah59a4a932013-01-16 12:58:59 +0100211 assert(self.c.rounding == self.p.rounding)
Stefan Krah1919b7e2012-03-21 18:25:23 +0100212 return self.c.rounding
213
214 def setround(self, val):
215 self.c.rounding = val
Stefan Krah59a4a932013-01-16 12:58:59 +0100216 self.p.rounding = val
Stefan Krah1919b7e2012-03-21 18:25:23 +0100217
218 def getcapitals(self):
219 assert(self.c.capitals == self.p.capitals)
220 return self.c.capitals
221
222 def setcapitals(self, val):
223 self.c.capitals = val
224 self.p.capitals = val
225
226 def getclamp(self):
227 assert(self.c.clamp == self.p.clamp)
228 return self.c.clamp
229
230 def setclamp(self, val):
231 self.c.clamp = val
232 self.p.clamp = val
233
234 prec = property(getprec, setprec)
235 Emin = property(getemin, setemin)
236 Emax = property(getemax, setemax)
237 rounding = property(getround, setround)
238 clamp = property(getclamp, setclamp)
239 capitals = property(getcapitals, setcapitals)
240
241 def clear_traps(self):
242 self.c.clear_traps()
243 for trap in self.p.traps:
244 self.p.traps[trap] = False
245
246 def clear_status(self):
247 self.c.clear_flags()
248 self.p.clear_flags()
249
250 def settraps(self, lst):
251 """lst: C signal list"""
252 self.clear_traps()
253 for signal in lst:
254 self.c.traps[signal] = True
255 self.p.traps[CondMap[signal]] = True
256
257 def setstatus(self, lst):
258 """lst: C signal list"""
259 self.clear_status()
260 for signal in lst:
261 self.c.flags[signal] = True
262 self.p.flags[CondMap[signal]] = True
263
264 def assert_eq_status(self):
265 """assert equality of C and P status"""
266 for signal in self.c.flags:
267 if self.c.flags[signal] == (not self.p.flags[CondMap[signal]]):
268 return False
269 return True
270
271
272# We don't want exceptions so that we can compare the status flags.
273context = Context()
274context.Emin = C.MIN_EMIN
275context.Emax = C.MAX_EMAX
276context.clear_traps()
277
278# When creating decimals, _decimal is ultimately limited by the maximum
279# context values. We emulate this restriction for decimal.py.
280maxcontext = P.Context(
281 prec=C.MAX_PREC,
282 Emin=C.MIN_EMIN,
283 Emax=C.MAX_EMAX,
284 rounding=P.ROUND_HALF_UP,
285 capitals=1
286)
287maxcontext.clamp = 0
288
289def RestrictedDecimal(value):
290 maxcontext.traps = copy(context.p.traps)
291 maxcontext.clear_flags()
292 if isinstance(value, str):
293 value = value.strip()
294 dec = maxcontext.create_decimal(value)
295 if maxcontext.flags[P.Inexact] or \
296 maxcontext.flags[P.Rounded] or \
Stefan Krah0774e9b2012-04-05 15:21:58 +0200297 maxcontext.flags[P.Clamped] or \
Stefan Krah1919b7e2012-03-21 18:25:23 +0100298 maxcontext.flags[P.InvalidOperation]:
299 return context.p._raise_error(P.InvalidOperation)
300 if maxcontext.flags[P.FloatOperation]:
301 context.p.flags[P.FloatOperation] = True
302 return dec
303
304
305# ======================================================================
306# TestSet: Organize data and events during a single test case
307# ======================================================================
308
309class RestrictedList(list):
310 """List that can only be modified by appending items."""
311 def __getattribute__(self, name):
312 if name != 'append':
313 raise AttributeError("unsupported operation")
314 return list.__getattribute__(self, name)
315 def unsupported(self, *_):
316 raise AttributeError("unsupported operation")
317 __add__ = __delattr__ = __delitem__ = __iadd__ = __imul__ = unsupported
318 __mul__ = __reversed__ = __rmul__ = __setattr__ = __setitem__ = unsupported
319
320class TestSet(object):
321 """A TestSet contains the original input operands, converted operands,
322 Python exceptions that occurred either during conversion or during
323 execution of the actual function, and the final results.
324
325 For safety, most attributes are lists that only support the append
326 operation.
327
328 If a function name is prefixed with 'context.', the corresponding
329 context method is called.
330 """
331 def __init__(self, funcname, operands):
332 if funcname.startswith("context."):
333 self.funcname = funcname.replace("context.", "")
334 self.contextfunc = True
335 else:
336 self.funcname = funcname
337 self.contextfunc = False
338 self.op = operands # raw operand tuple
339 self.context = context # context used for the operation
340 self.cop = RestrictedList() # converted C.Decimal operands
341 self.cex = RestrictedList() # Python exceptions for C.Decimal
342 self.cresults = RestrictedList() # C.Decimal results
343 self.pop = RestrictedList() # converted P.Decimal operands
344 self.pex = RestrictedList() # Python exceptions for P.Decimal
345 self.presults = RestrictedList() # P.Decimal results
346
347
348# ======================================================================
349# SkipHandler: skip known discrepancies
350# ======================================================================
351
352class SkipHandler:
353 """Handle known discrepancies between decimal.py and _decimal.so.
354 These are either ULP differences in the power function or
355 extremely minor issues."""
356
357 def __init__(self):
358 self.ulpdiff = 0
359 self.powmod_zeros = 0
360 self.maxctx = P.Context(Emax=10**18, Emin=-10**18)
361
362 def default(self, t):
363 return False
364 __ge__ = __gt__ = __le__ = __lt__ = __ne__ = __eq__ = default
365 __reduce__ = __format__ = __repr__ = __str__ = default
366
367 def harrison_ulp(self, dec):
368 """ftp://ftp.inria.fr/INRIA/publication/publi-pdf/RR/RR-5504.pdf"""
369 a = dec.next_plus()
370 b = dec.next_minus()
371 return abs(a - b)
372
373 def standard_ulp(self, dec, prec):
Stefan Krahb578f8a2014-09-10 17:58:15 +0200374 return _dec_from_triple(0, '1', dec._exp+len(dec._int)-prec)
Stefan Krah1919b7e2012-03-21 18:25:23 +0100375
376 def rounding_direction(self, x, mode):
377 """Determine the effective direction of the rounding when
378 the exact result x is rounded according to mode.
379 Return -1 for downwards, 0 for undirected, 1 for upwards,
380 2 for ROUND_05UP."""
381 cmp = 1 if x.compare_total(P.Decimal("+0")) >= 0 else -1
382
383 if mode in (P.ROUND_HALF_EVEN, P.ROUND_HALF_UP, P.ROUND_HALF_DOWN):
384 return 0
385 elif mode == P.ROUND_CEILING:
386 return 1
387 elif mode == P.ROUND_FLOOR:
388 return -1
389 elif mode == P.ROUND_UP:
390 return cmp
391 elif mode == P.ROUND_DOWN:
392 return -cmp
393 elif mode == P.ROUND_05UP:
394 return 2
395 else:
396 raise ValueError("Unexpected rounding mode: %s" % mode)
397
398 def check_ulpdiff(self, exact, rounded):
399 # current precision
400 p = context.p.prec
401
402 # Convert infinities to the largest representable number + 1.
403 x = exact
404 if exact.is_infinite():
Stefan Krahb578f8a2014-09-10 17:58:15 +0200405 x = _dec_from_triple(exact._sign, '10', context.p.Emax)
Stefan Krah1919b7e2012-03-21 18:25:23 +0100406 y = rounded
407 if rounded.is_infinite():
Stefan Krahb578f8a2014-09-10 17:58:15 +0200408 y = _dec_from_triple(rounded._sign, '10', context.p.Emax)
Stefan Krah1919b7e2012-03-21 18:25:23 +0100409
410 # err = (rounded - exact) / ulp(rounded)
411 self.maxctx.prec = p * 2
412 t = self.maxctx.subtract(y, x)
413 if context.c.flags[C.Clamped] or \
414 context.c.flags[C.Underflow]:
415 # The standard ulp does not work in Underflow territory.
416 ulp = self.harrison_ulp(y)
417 else:
418 ulp = self.standard_ulp(y, p)
419 # Error in ulps.
420 err = self.maxctx.divide(t, ulp)
421
422 dir = self.rounding_direction(x, context.p.rounding)
423 if dir == 0:
424 if P.Decimal("-0.6") < err < P.Decimal("0.6"):
425 return True
426 elif dir == 1: # directed, upwards
427 if P.Decimal("-0.1") < err < P.Decimal("1.1"):
428 return True
429 elif dir == -1: # directed, downwards
430 if P.Decimal("-1.1") < err < P.Decimal("0.1"):
431 return True
432 else: # ROUND_05UP
433 if P.Decimal("-1.1") < err < P.Decimal("1.1"):
434 return True
435
436 print("ulp: %s error: %s exact: %s c_rounded: %s"
437 % (ulp, err, exact, rounded))
438 return False
439
440 def bin_resolve_ulp(self, t):
441 """Check if results of _decimal's power function are within the
442 allowed ulp ranges."""
443 # NaNs are beyond repair.
444 if t.rc.is_nan() or t.rp.is_nan():
445 return False
446
447 # "exact" result, double precision, half_even
448 self.maxctx.prec = context.p.prec * 2
449
450 op1, op2 = t.pop[0], t.pop[1]
451 if t.contextfunc:
452 exact = getattr(self.maxctx, t.funcname)(op1, op2)
453 else:
454 exact = getattr(op1, t.funcname)(op2, context=self.maxctx)
455
456 # _decimal's rounded result
457 rounded = P.Decimal(t.cresults[0])
458
459 self.ulpdiff += 1
460 return self.check_ulpdiff(exact, rounded)
461
462 ############################ Correct rounding #############################
463 def resolve_underflow(self, t):
464 """In extremely rare cases where the infinite precision result is just
465 below etiny, cdecimal does not set Subnormal/Underflow. Example:
466
467 setcontext(Context(prec=21, rounding=ROUND_UP, Emin=-55, Emax=85))
468 Decimal("1.00000000000000000000000000000000000000000000000"
469 "0000000100000000000000000000000000000000000000000"
470 "0000000000000025").ln()
471 """
472 if t.cresults != t.presults:
473 return False # Results must be identical.
474 if context.c.flags[C.Rounded] and \
475 context.c.flags[C.Inexact] and \
476 context.p.flags[P.Rounded] and \
477 context.p.flags[P.Inexact]:
478 return True # Subnormal/Underflow may be missing.
479 return False
480
481 def exp(self, t):
482 """Resolve Underflow or ULP difference."""
483 return self.resolve_underflow(t)
484
485 def log10(self, t):
486 """Resolve Underflow or ULP difference."""
487 return self.resolve_underflow(t)
488
489 def ln(self, t):
490 """Resolve Underflow or ULP difference."""
491 return self.resolve_underflow(t)
492
493 def __pow__(self, t):
494 """Always calls the resolve function. C.Decimal does not have correct
495 rounding for the power function."""
496 if context.c.flags[C.Rounded] and \
497 context.c.flags[C.Inexact] and \
498 context.p.flags[P.Rounded] and \
499 context.p.flags[P.Inexact]:
500 return self.bin_resolve_ulp(t)
501 else:
502 return False
503 power = __rpow__ = __pow__
504
505 ############################## Technicalities #############################
506 def __float__(self, t):
507 """NaN comparison in the verify() function obviously gives an
508 incorrect answer: nan == nan -> False"""
509 if t.cop[0].is_nan() and t.pop[0].is_nan():
510 return True
511 return False
512 __complex__ = __float__
513
514 def __radd__(self, t):
515 """decimal.py gives precedence to the first NaN; this is
516 not important, as __radd__ will not be called for
517 two decimal arguments."""
518 if t.rc.is_nan() and t.rp.is_nan():
519 return True
520 return False
521 __rmul__ = __radd__
522
523 ################################ Various ##################################
524 def __round__(self, t):
525 """Exception: Decimal('1').__round__(-100000000000000000000000000)
526 Should it really be InvalidOperation?"""
527 if t.rc is None and t.rp.is_nan():
528 return True
529 return False
530
531shandler = SkipHandler()
532def skip_error(t):
533 return getattr(shandler, t.funcname, shandler.default)(t)
534
535
536# ======================================================================
537# Handling verification errors
538# ======================================================================
539
540class VerifyError(Exception):
541 """Verification failed."""
542 pass
543
544def function_as_string(t):
545 if t.contextfunc:
546 cargs = t.cop
547 pargs = t.pop
548 cfunc = "c_func: %s(" % t.funcname
549 pfunc = "p_func: %s(" % t.funcname
550 else:
551 cself, cargs = t.cop[0], t.cop[1:]
552 pself, pargs = t.pop[0], t.pop[1:]
553 cfunc = "c_func: %s.%s(" % (repr(cself), t.funcname)
554 pfunc = "p_func: %s.%s(" % (repr(pself), t.funcname)
555
556 err = cfunc
557 for arg in cargs:
558 err += "%s, " % repr(arg)
559 err = err.rstrip(", ")
560 err += ")\n"
561
562 err += pfunc
563 for arg in pargs:
564 err += "%s, " % repr(arg)
565 err = err.rstrip(", ")
566 err += ")"
567
568 return err
569
570def raise_error(t):
571 global EXIT_STATUS
572
573 if skip_error(t):
574 return
575 EXIT_STATUS = 1
576
577 err = "Error in %s:\n\n" % t.funcname
578 err += "input operands: %s\n\n" % (t.op,)
579 err += function_as_string(t)
580 err += "\n\nc_result: %s\np_result: %s\n\n" % (t.cresults, t.presults)
581 err += "c_exceptions: %s\np_exceptions: %s\n\n" % (t.cex, t.pex)
582 err += "%s\n\n" % str(t.context)
583
584 raise VerifyError(err)
585
586
587# ======================================================================
588# Main testing functions
589#
590# The procedure is always (t is the TestSet):
591#
592# convert(t) -> Initialize the TestSet as necessary.
593#
594# Return 0 for early abortion (e.g. if a TypeError
595# occurs during conversion, there is nothing to test).
596#
597# Return 1 for continuing with the test case.
598#
599# callfuncs(t) -> Call the relevant function for each implementation
600# and record the results in the TestSet.
601#
602# verify(t) -> Verify the results. If verification fails, details
603# are printed to stdout.
604# ======================================================================
605
606def convert(t, convstr=True):
607 """ t is the testset. At this stage the testset contains a tuple of
608 operands t.op of various types. For decimal methods the first
609 operand (self) is always converted to Decimal. If 'convstr' is
610 true, string operands are converted as well.
611
612 Context operands are of type deccheck.Context, rounding mode
613 operands are given as a tuple (C.rounding, P.rounding).
614
615 Other types (float, int, etc.) are left unchanged.
616 """
617 for i, op in enumerate(t.op):
618
619 context.clear_status()
620
Stefan Krah59a4a932013-01-16 12:58:59 +0100621 if op in RoundModes:
622 t.cop.append(op)
623 t.pop.append(op)
624
625 elif not t.contextfunc and i == 0 or \
626 convstr and isinstance(op, str):
Stefan Krah1919b7e2012-03-21 18:25:23 +0100627 try:
628 c = C.Decimal(op)
629 cex = None
630 except (TypeError, ValueError, OverflowError) as e:
631 c = None
632 cex = e.__class__
633
634 try:
635 p = RestrictedDecimal(op)
636 pex = None
637 except (TypeError, ValueError, OverflowError) as e:
638 p = None
639 pex = e.__class__
640
641 t.cop.append(c)
642 t.cex.append(cex)
643 t.pop.append(p)
644 t.pex.append(pex)
645
646 if cex is pex:
647 if str(c) != str(p) or not context.assert_eq_status():
648 raise_error(t)
649 if cex and pex:
650 # nothing to test
651 return 0
652 else:
653 raise_error(t)
654
655 elif isinstance(op, Context):
656 t.context = op
657 t.cop.append(op.c)
658 t.pop.append(op.p)
659
Stefan Krah1919b7e2012-03-21 18:25:23 +0100660 else:
661 t.cop.append(op)
662 t.pop.append(op)
663
664 return 1
665
666def callfuncs(t):
667 """ t is the testset. At this stage the testset contains operand lists
668 t.cop and t.pop for the C and Python versions of decimal.
669 For Decimal methods, the first operands are of type C.Decimal and
670 P.Decimal respectively. The remaining operands can have various types.
671 For Context methods, all operands can have any type.
672
673 t.rc and t.rp are the results of the operation.
674 """
675 context.clear_status()
676
677 try:
678 if t.contextfunc:
679 cargs = t.cop
680 t.rc = getattr(context.c, t.funcname)(*cargs)
681 else:
682 cself = t.cop[0]
683 cargs = t.cop[1:]
684 t.rc = getattr(cself, t.funcname)(*cargs)
685 t.cex.append(None)
686 except (TypeError, ValueError, OverflowError, MemoryError) as e:
687 t.rc = None
688 t.cex.append(e.__class__)
689
690 try:
691 if t.contextfunc:
692 pargs = t.pop
693 t.rp = getattr(context.p, t.funcname)(*pargs)
694 else:
695 pself = t.pop[0]
696 pargs = t.pop[1:]
697 t.rp = getattr(pself, t.funcname)(*pargs)
698 t.pex.append(None)
699 except (TypeError, ValueError, OverflowError, MemoryError) as e:
700 t.rp = None
701 t.pex.append(e.__class__)
702
703def verify(t, stat):
704 """ t is the testset. At this stage the testset contains the following
705 tuples:
706
707 t.op: original operands
708 t.cop: C.Decimal operands (see convert for details)
709 t.pop: P.Decimal operands (see convert for details)
710 t.rc: C result
711 t.rp: Python result
712
713 t.rc and t.rp can have various types.
714 """
715 t.cresults.append(str(t.rc))
716 t.presults.append(str(t.rp))
717 if isinstance(t.rc, C.Decimal) and isinstance(t.rp, P.Decimal):
718 # General case: both results are Decimals.
719 t.cresults.append(t.rc.to_eng_string())
720 t.cresults.append(t.rc.as_tuple())
721 t.cresults.append(str(t.rc.imag))
722 t.cresults.append(str(t.rc.real))
723 t.presults.append(t.rp.to_eng_string())
724 t.presults.append(t.rp.as_tuple())
725 t.presults.append(str(t.rp.imag))
726 t.presults.append(str(t.rp.real))
727
728 nc = t.rc.number_class().lstrip('+-s')
729 stat[nc] += 1
730 else:
731 # Results from e.g. __divmod__ can only be compared as strings.
732 if not isinstance(t.rc, tuple) and not isinstance(t.rp, tuple):
733 if t.rc != t.rp:
734 raise_error(t)
735 stat[type(t.rc).__name__] += 1
736
737 # The return value lists must be equal.
738 if t.cresults != t.presults:
739 raise_error(t)
740 # The Python exception lists (TypeError, etc.) must be equal.
741 if t.cex != t.pex:
742 raise_error(t)
743 # The context flags must be equal.
744 if not t.context.assert_eq_status():
745 raise_error(t)
746
747
748# ======================================================================
749# Main test loops
750#
751# test_method(method, testspecs, testfunc) ->
752#
753# Loop through various context settings. The degree of
754# thoroughness is determined by 'testspec'. For each
755# setting, call 'testfunc'. Generally, 'testfunc' itself
756# a loop, iterating through many test cases generated
757# by the functions in randdec.py.
758#
759# test_n-ary(method, prec, exp_range, restricted_range, itr, stat) ->
760#
761# 'test_unary', 'test_binary' and 'test_ternary' are the
762# main test functions passed to 'test_method'. They deal
763# with the regular cases. The thoroughness of testing is
764# determined by 'itr'.
765#
766# 'prec', 'exp_range' and 'restricted_range' are passed
767# to the test-generating functions and limit the generated
768# values. In some cases, for reasonable run times a
769# maximum exponent of 9999 is required.
770#
771# The 'stat' parameter is passed down to the 'verify'
772# function, which records statistics for the result values.
773# ======================================================================
774
775def log(fmt, args=None):
776 if args:
777 sys.stdout.write(''.join((fmt, '\n')) % args)
778 else:
779 sys.stdout.write(''.join((str(fmt), '\n')))
780 sys.stdout.flush()
781
782def test_method(method, testspecs, testfunc):
783 """Iterate a test function through many context settings."""
784 log("testing %s ...", method)
785 stat = defaultdict(int)
786 for spec in testspecs:
787 if 'samples' in spec:
788 spec['prec'] = sorted(random.sample(range(1, 101),
789 spec['samples']))
790 for prec in spec['prec']:
791 context.prec = prec
792 for expts in spec['expts']:
793 emin, emax = expts
794 if emin == 'rand':
795 context.Emin = random.randrange(-1000, 0)
796 context.Emax = random.randrange(prec, 1000)
797 else:
798 context.Emin, context.Emax = emin, emax
799 if prec > context.Emax: continue
800 log(" prec: %d emin: %d emax: %d",
801 (context.prec, context.Emin, context.Emax))
802 restr_range = 9999 if context.Emax > 9999 else context.Emax+99
Stefan Krah59a4a932013-01-16 12:58:59 +0100803 for rounding in RoundModes:
Stefan Krah1919b7e2012-03-21 18:25:23 +0100804 context.rounding = rounding
805 context.capitals = random.randrange(2)
806 if spec['clamp'] == 'rand':
807 context.clamp = random.randrange(2)
808 else:
809 context.clamp = spec['clamp']
810 exprange = context.c.Emax
811 testfunc(method, prec, exprange, restr_range,
812 spec['iter'], stat)
813 log(" result types: %s" % sorted([t for t in stat.items()]))
814
815def test_unary(method, prec, exp_range, restricted_range, itr, stat):
816 """Iterate a unary function through many test cases."""
817 if method in UnaryRestricted:
818 exp_range = restricted_range
819 for op in all_unary(prec, exp_range, itr):
820 t = TestSet(method, op)
821 try:
822 if not convert(t):
823 continue
824 callfuncs(t)
825 verify(t, stat)
826 except VerifyError as err:
827 log(err)
828
Stefan Krah040e3112012-12-15 22:33:33 +0100829 if not method.startswith('__'):
830 for op in unary_optarg(prec, exp_range, itr):
831 t = TestSet(method, op)
832 try:
833 if not convert(t):
834 continue
835 callfuncs(t)
836 verify(t, stat)
837 except VerifyError as err:
838 log(err)
839
Stefan Krah1919b7e2012-03-21 18:25:23 +0100840def test_binary(method, prec, exp_range, restricted_range, itr, stat):
841 """Iterate a binary function through many test cases."""
842 if method in BinaryRestricted:
843 exp_range = restricted_range
844 for op in all_binary(prec, exp_range, itr):
845 t = TestSet(method, op)
846 try:
847 if not convert(t):
848 continue
849 callfuncs(t)
850 verify(t, stat)
851 except VerifyError as err:
852 log(err)
853
Stefan Krah040e3112012-12-15 22:33:33 +0100854 if not method.startswith('__'):
855 for op in binary_optarg(prec, exp_range, itr):
856 t = TestSet(method, op)
857 try:
858 if not convert(t):
859 continue
860 callfuncs(t)
861 verify(t, stat)
862 except VerifyError as err:
863 log(err)
864
Stefan Krah1919b7e2012-03-21 18:25:23 +0100865def test_ternary(method, prec, exp_range, restricted_range, itr, stat):
866 """Iterate a ternary function through many test cases."""
867 if method in TernaryRestricted:
868 exp_range = restricted_range
869 for op in all_ternary(prec, exp_range, itr):
870 t = TestSet(method, op)
871 try:
872 if not convert(t):
873 continue
874 callfuncs(t)
875 verify(t, stat)
876 except VerifyError as err:
877 log(err)
878
Stefan Krah040e3112012-12-15 22:33:33 +0100879 if not method.startswith('__'):
880 for op in ternary_optarg(prec, exp_range, itr):
881 t = TestSet(method, op)
882 try:
883 if not convert(t):
884 continue
885 callfuncs(t)
886 verify(t, stat)
887 except VerifyError as err:
888 log(err)
889
Stefan Krah1919b7e2012-03-21 18:25:23 +0100890def test_format(method, prec, exp_range, restricted_range, itr, stat):
891 """Iterate the __format__ method through many test cases."""
892 for op in all_unary(prec, exp_range, itr):
Stefan Krah6edda142013-05-29 15:45:38 +0200893 fmt1 = rand_format(chr(random.randrange(0, 128)), 'EeGgn')
Stefan Krah1919b7e2012-03-21 18:25:23 +0100894 fmt2 = rand_locale()
895 for fmt in (fmt1, fmt2):
896 fmtop = (op[0], fmt)
897 t = TestSet(method, fmtop)
898 try:
899 if not convert(t, convstr=False):
900 continue
901 callfuncs(t)
902 verify(t, stat)
903 except VerifyError as err:
904 log(err)
905 for op in all_unary(prec, 9999, itr):
Stefan Krah6edda142013-05-29 15:45:38 +0200906 fmt1 = rand_format(chr(random.randrange(0, 128)), 'Ff%')
Stefan Krah1919b7e2012-03-21 18:25:23 +0100907 fmt2 = rand_locale()
908 for fmt in (fmt1, fmt2):
909 fmtop = (op[0], fmt)
910 t = TestSet(method, fmtop)
911 try:
912 if not convert(t, convstr=False):
913 continue
914 callfuncs(t)
915 verify(t, stat)
916 except VerifyError as err:
917 log(err)
918
919def test_round(method, prec, exprange, restricted_range, itr, stat):
920 """Iterate the __round__ method through many test cases."""
921 for op in all_unary(prec, 9999, itr):
922 n = random.randrange(10)
923 roundop = (op[0], n)
924 t = TestSet(method, roundop)
925 try:
926 if not convert(t):
927 continue
928 callfuncs(t)
929 verify(t, stat)
930 except VerifyError as err:
931 log(err)
932
933def test_from_float(method, prec, exprange, restricted_range, itr, stat):
934 """Iterate the __float__ method through many test cases."""
Stefan Krah59a4a932013-01-16 12:58:59 +0100935 for rounding in RoundModes:
Stefan Krah1919b7e2012-03-21 18:25:23 +0100936 context.rounding = rounding
937 for i in range(1000):
938 f = randfloat()
939 op = (f,) if method.startswith("context.") else ("sNaN", f)
940 t = TestSet(method, op)
941 try:
942 if not convert(t):
943 continue
944 callfuncs(t)
945 verify(t, stat)
946 except VerifyError as err:
947 log(err)
948
949def randcontext(exprange):
950 c = Context(C.Context(), P.Context())
951 c.Emax = random.randrange(1, exprange+1)
952 c.Emin = random.randrange(-exprange, 0)
953 maxprec = 100 if c.Emax >= 100 else c.Emax
954 c.prec = random.randrange(1, maxprec+1)
955 c.clamp = random.randrange(2)
956 c.clear_traps()
957 return c
958
959def test_quantize_api(method, prec, exprange, restricted_range, itr, stat):
960 """Iterate the 'quantize' method through many test cases, using
961 the optional arguments."""
962 for op in all_binary(prec, restricted_range, itr):
963 for rounding in RoundModes:
964 c = randcontext(exprange)
965 quantizeop = (op[0], op[1], rounding, c)
966 t = TestSet(method, quantizeop)
967 try:
968 if not convert(t):
969 continue
970 callfuncs(t)
971 verify(t, stat)
972 except VerifyError as err:
973 log(err)
974
975
976def check_untested(funcdict, c_cls, p_cls):
977 """Determine untested, C-only and Python-only attributes.
978 Uncomment print lines for debugging."""
979 c_attr = set(dir(c_cls))
980 p_attr = set(dir(p_cls))
981 intersect = c_attr & p_attr
982
983 funcdict['c_only'] = tuple(sorted(c_attr-intersect))
984 funcdict['p_only'] = tuple(sorted(p_attr-intersect))
985
986 tested = set()
987 for lst in funcdict.values():
988 for v in lst:
989 v = v.replace("context.", "") if c_cls == C.Context else v
990 tested.add(v)
991
992 funcdict['untested'] = tuple(sorted(intersect-tested))
993
994 #for key in ('untested', 'c_only', 'p_only'):
995 # s = 'Context' if c_cls == C.Context else 'Decimal'
996 # print("\n%s %s:\n%s" % (s, key, funcdict[key]))
997
998
999if __name__ == '__main__':
1000
1001 import time
1002
1003 randseed = int(time.time())
1004 random.seed(randseed)
1005
1006 # Set up the testspecs list. A testspec is simply a dictionary
1007 # that determines the amount of different contexts that 'test_method'
1008 # will generate.
1009 base_expts = [(C.MIN_EMIN, C.MAX_EMAX)]
1010 if C.MAX_EMAX == 999999999999999999:
1011 base_expts.append((-999999999, 999999999))
1012
1013 # Basic contexts.
1014 base = {
1015 'expts': base_expts,
1016 'prec': [],
1017 'clamp': 'rand',
1018 'iter': None,
1019 'samples': None,
1020 }
1021 # Contexts with small values for prec, emin, emax.
1022 small = {
1023 'prec': [1, 2, 3, 4, 5],
1024 'expts': [(-1, 1), (-2, 2), (-3, 3), (-4, 4), (-5, 5)],
1025 'clamp': 'rand',
1026 'iter': None
1027 }
1028 # IEEE interchange format.
1029 ieee = [
1030 # DECIMAL32
1031 {'prec': [7], 'expts': [(-95, 96)], 'clamp': 1, 'iter': None},
1032 # DECIMAL64
1033 {'prec': [16], 'expts': [(-383, 384)], 'clamp': 1, 'iter': None},
1034 # DECIMAL128
1035 {'prec': [34], 'expts': [(-6143, 6144)], 'clamp': 1, 'iter': None}
1036 ]
1037
1038 if '--medium' in sys.argv:
1039 base['expts'].append(('rand', 'rand'))
1040 # 5 random precisions
1041 base['samples'] = 5
1042 testspecs = [small] + ieee + [base]
1043 if '--long' in sys.argv:
1044 base['expts'].append(('rand', 'rand'))
1045 # 10 random precisions
1046 base['samples'] = 10
1047 testspecs = [small] + ieee + [base]
1048 elif '--all' in sys.argv:
1049 base['expts'].append(('rand', 'rand'))
1050 # All precisions in [1, 100]
1051 base['samples'] = 100
1052 testspecs = [small] + ieee + [base]
1053 else: # --short
1054 rand_ieee = random.choice(ieee)
1055 base['iter'] = small['iter'] = rand_ieee['iter'] = 1
1056 # 1 random precision and exponent pair
1057 base['samples'] = 1
1058 base['expts'] = [random.choice(base_expts)]
1059 # 1 random precision and exponent pair
1060 prec = random.randrange(1, 6)
1061 small['prec'] = [prec]
1062 small['expts'] = [(-prec, prec)]
1063 testspecs = [small, rand_ieee, base]
1064
1065 check_untested(Functions, C.Decimal, P.Decimal)
1066 check_untested(ContextFunctions, C.Context, P.Context)
1067
1068
1069 log("\n\nRandom seed: %d\n\n", randseed)
1070
1071 # Decimal methods:
1072 for method in Functions['unary'] + Functions['unary_ctx'] + \
1073 Functions['unary_rnd_ctx']:
1074 test_method(method, testspecs, test_unary)
1075
1076 for method in Functions['binary'] + Functions['binary_ctx']:
1077 test_method(method, testspecs, test_binary)
1078
1079 for method in Functions['ternary'] + Functions['ternary_ctx']:
1080 test_method(method, testspecs, test_ternary)
1081
1082 test_method('__format__', testspecs, test_format)
1083 test_method('__round__', testspecs, test_round)
1084 test_method('from_float', testspecs, test_from_float)
1085 test_method('quantize', testspecs, test_quantize_api)
1086
1087 # Context methods:
1088 for method in ContextFunctions['unary']:
1089 test_method(method, testspecs, test_unary)
1090
1091 for method in ContextFunctions['binary']:
1092 test_method(method, testspecs, test_binary)
1093
1094 for method in ContextFunctions['ternary']:
1095 test_method(method, testspecs, test_ternary)
1096
1097 test_method('context.create_decimal_from_float', testspecs, test_from_float)
1098
1099
1100 sys.exit(EXIT_STATUS)