1# 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 32 33import random 34import time 35 36RANDSEED = int(time.time()) 37random.seed(RANDSEED) 38 39import sys 40import os 41from copy import copy 42from collections import defaultdict 43 44import argparse 45import subprocess 46from subprocess import PIPE, STDOUT 47from queue import Queue, Empty 48from threading import Thread, Event, Lock 49 50from test.support.import_helper import import_fresh_module 51from randdec import randfloat, all_unary, all_binary, all_ternary 52from randdec import unary_optarg, binary_optarg, ternary_optarg 53from formathelper import rand_format, rand_locale 54from _pydecimal import _dec_from_triple 55 56C = import_fresh_module('decimal', fresh=['_decimal']) 57P = import_fresh_module('decimal', blocked=['_decimal']) 58EXIT_STATUS = 0 59 60 61# Contains all categories of Decimal methods. 62Functions = { 63 # Plain unary: 64 'unary': ( 65 '__abs__', '__bool__', '__ceil__', '__complex__', '__copy__', 66 '__floor__', '__float__', '__hash__', '__int__', '__neg__', 67 '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__', 68 'adjusted', 'as_integer_ratio', 'as_tuple', 'canonical', 'conjugate', 69 'copy_abs', 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite', 70 'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix' 71 ), 72 # Unary with optional context: 73 'unary_ctx': ( 74 'exp', 'is_normal', 'is_subnormal', 'ln', 'log10', 'logb', 75 'logical_invert', 'next_minus', 'next_plus', 'normalize', 76 'number_class', 'sqrt', 'to_eng_string' 77 ), 78 # Unary with optional rounding mode and context: 79 'unary_rnd_ctx': ('to_integral', 'to_integral_exact', 'to_integral_value'), 80 # Plain binary: 81 'binary': ( 82 '__add__', '__divmod__', '__eq__', '__floordiv__', '__ge__', '__gt__', 83 '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__pow__', 84 '__radd__', '__rdivmod__', '__rfloordiv__', '__rmod__', '__rmul__', 85 '__rpow__', '__rsub__', '__rtruediv__', '__sub__', '__truediv__', 86 'compare_total', 'compare_total_mag', 'copy_sign', 'quantize', 87 'same_quantum' 88 ), 89 # Binary with optional context: 90 'binary_ctx': ( 91 'compare', 'compare_signal', 'logical_and', 'logical_or', 'logical_xor', 92 'max', 'max_mag', 'min', 'min_mag', 'next_toward', 'remainder_near', 93 'rotate', 'scaleb', 'shift' 94 ), 95 # Plain ternary: 96 'ternary': ('__pow__',), 97 # Ternary with optional context: 98 'ternary_ctx': ('fma',), 99 # Special: 100 'special': ('__format__', '__reduce_ex__', '__round__', 'from_float', 101 'quantize'), 102 # Properties: 103 'property': ('real', 'imag') 104} 105 106# Contains all categories of Context methods. The n-ary classification 107# applies to the number of Decimal arguments. 108ContextFunctions = { 109 # Plain nullary: 110 'nullary': ('context.__hash__', 'context.__reduce__', 'context.radix'), 111 # Plain unary: 112 'unary': ('context.abs', 'context.canonical', 'context.copy_abs', 113 'context.copy_decimal', 'context.copy_negate', 114 'context.create_decimal', 'context.exp', 'context.is_canonical', 115 'context.is_finite', 'context.is_infinite', 'context.is_nan', 116 'context.is_normal', 'context.is_qnan', 'context.is_signed', 117 'context.is_snan', 'context.is_subnormal', 'context.is_zero', 118 'context.ln', 'context.log10', 'context.logb', 119 'context.logical_invert', 'context.minus', 'context.next_minus', 120 'context.next_plus', 'context.normalize', 'context.number_class', 121 'context.plus', 'context.sqrt', 'context.to_eng_string', 122 'context.to_integral', 'context.to_integral_exact', 123 'context.to_integral_value', 'context.to_sci_string' 124 ), 125 # Plain binary: 126 'binary': ('context.add', 'context.compare', 'context.compare_signal', 127 'context.compare_total', 'context.compare_total_mag', 128 'context.copy_sign', 'context.divide', 'context.divide_int', 129 'context.divmod', 'context.logical_and', 'context.logical_or', 130 'context.logical_xor', 'context.max', 'context.max_mag', 131 'context.min', 'context.min_mag', 'context.multiply', 132 'context.next_toward', 'context.power', 'context.quantize', 133 'context.remainder', 'context.remainder_near', 'context.rotate', 134 'context.same_quantum', 'context.scaleb', 'context.shift', 135 'context.subtract' 136 ), 137 # Plain ternary: 138 'ternary': ('context.fma', 'context.power'), 139 # Special: 140 'special': ('context.__reduce_ex__', 'context.create_decimal_from_float') 141} 142 143# Functions that set no context flags but whose result can differ depending 144# on prec, Emin and Emax. 145MaxContextSkip = ['is_normal', 'is_subnormal', 'logical_invert', 'next_minus', 146 'next_plus', 'number_class', 'logical_and', 'logical_or', 147 'logical_xor', 'next_toward', 'rotate', 'shift'] 148 149# Functions that require a restricted exponent range for reasonable runtimes. 150UnaryRestricted = [ 151 '__ceil__', '__floor__', '__int__', '__trunc__', 152 'as_integer_ratio', 'to_integral', 'to_integral_value' 153] 154 155BinaryRestricted = ['__round__'] 156 157TernaryRestricted = ['__pow__', 'context.power'] 158 159 160# ====================================================================== 161# Unified Context 162# ====================================================================== 163 164# Translate symbols. 165CondMap = { 166 C.Clamped: P.Clamped, 167 C.ConversionSyntax: P.ConversionSyntax, 168 C.DivisionByZero: P.DivisionByZero, 169 C.DivisionImpossible: P.InvalidOperation, 170 C.DivisionUndefined: P.DivisionUndefined, 171 C.Inexact: P.Inexact, 172 C.InvalidContext: P.InvalidContext, 173 C.InvalidOperation: P.InvalidOperation, 174 C.Overflow: P.Overflow, 175 C.Rounded: P.Rounded, 176 C.Subnormal: P.Subnormal, 177 C.Underflow: P.Underflow, 178 C.FloatOperation: P.FloatOperation, 179} 180 181RoundModes = [C.ROUND_UP, C.ROUND_DOWN, C.ROUND_CEILING, C.ROUND_FLOOR, 182 C.ROUND_HALF_UP, C.ROUND_HALF_DOWN, C.ROUND_HALF_EVEN, 183 C.ROUND_05UP] 184 185 186class Context(object): 187 """Provides a convenient way of syncing the C and P contexts""" 188 189 __slots__ = ['c', 'p'] 190 191 def __init__(self, c_ctx=None, p_ctx=None): 192 """Initialization is from the C context""" 193 self.c = C.getcontext() if c_ctx is None else c_ctx 194 self.p = P.getcontext() if p_ctx is None else p_ctx 195 self.p.prec = self.c.prec 196 self.p.Emin = self.c.Emin 197 self.p.Emax = self.c.Emax 198 self.p.rounding = self.c.rounding 199 self.p.capitals = self.c.capitals 200 self.settraps([sig for sig in self.c.traps if self.c.traps[sig]]) 201 self.setstatus([sig for sig in self.c.flags if self.c.flags[sig]]) 202 self.p.clamp = self.c.clamp 203 204 def __str__(self): 205 return str(self.c) + '\n' + str(self.p) 206 207 def getprec(self): 208 assert(self.c.prec == self.p.prec) 209 return self.c.prec 210 211 def setprec(self, val): 212 self.c.prec = val 213 self.p.prec = val 214 215 def getemin(self): 216 assert(self.c.Emin == self.p.Emin) 217 return self.c.Emin 218 219 def setemin(self, val): 220 self.c.Emin = val 221 self.p.Emin = val 222 223 def getemax(self): 224 assert(self.c.Emax == self.p.Emax) 225 return self.c.Emax 226 227 def setemax(self, val): 228 self.c.Emax = val 229 self.p.Emax = val 230 231 def getround(self): 232 assert(self.c.rounding == self.p.rounding) 233 return self.c.rounding 234 235 def setround(self, val): 236 self.c.rounding = val 237 self.p.rounding = val 238 239 def getcapitals(self): 240 assert(self.c.capitals == self.p.capitals) 241 return self.c.capitals 242 243 def setcapitals(self, val): 244 self.c.capitals = val 245 self.p.capitals = val 246 247 def getclamp(self): 248 assert(self.c.clamp == self.p.clamp) 249 return self.c.clamp 250 251 def setclamp(self, val): 252 self.c.clamp = val 253 self.p.clamp = val 254 255 prec = property(getprec, setprec) 256 Emin = property(getemin, setemin) 257 Emax = property(getemax, setemax) 258 rounding = property(getround, setround) 259 clamp = property(getclamp, setclamp) 260 capitals = property(getcapitals, setcapitals) 261 262 def clear_traps(self): 263 self.c.clear_traps() 264 for trap in self.p.traps: 265 self.p.traps[trap] = False 266 267 def clear_status(self): 268 self.c.clear_flags() 269 self.p.clear_flags() 270 271 def settraps(self, lst): 272 """lst: C signal list""" 273 self.clear_traps() 274 for signal in lst: 275 self.c.traps[signal] = True 276 self.p.traps[CondMap[signal]] = True 277 278 def setstatus(self, lst): 279 """lst: C signal list""" 280 self.clear_status() 281 for signal in lst: 282 self.c.flags[signal] = True 283 self.p.flags[CondMap[signal]] = True 284 285 def assert_eq_status(self): 286 """assert equality of C and P status""" 287 for signal in self.c.flags: 288 if self.c.flags[signal] == (not self.p.flags[CondMap[signal]]): 289 return False 290 return True 291 292 293# We don't want exceptions so that we can compare the status flags. 294context = Context() 295context.Emin = C.MIN_EMIN 296context.Emax = C.MAX_EMAX 297context.clear_traps() 298 299# When creating decimals, _decimal is ultimately limited by the maximum 300# context values. We emulate this restriction for decimal.py. 301maxcontext = P.Context( 302 prec=C.MAX_PREC, 303 Emin=C.MIN_EMIN, 304 Emax=C.MAX_EMAX, 305 rounding=P.ROUND_HALF_UP, 306 capitals=1 307) 308maxcontext.clamp = 0 309 310def RestrictedDecimal(value): 311 maxcontext.traps = copy(context.p.traps) 312 maxcontext.clear_flags() 313 if isinstance(value, str): 314 value = value.strip() 315 dec = maxcontext.create_decimal(value) 316 if maxcontext.flags[P.Inexact] or \ 317 maxcontext.flags[P.Rounded] or \ 318 maxcontext.flags[P.Clamped] or \ 319 maxcontext.flags[P.InvalidOperation]: 320 return context.p._raise_error(P.InvalidOperation) 321 if maxcontext.flags[P.FloatOperation]: 322 context.p.flags[P.FloatOperation] = True 323 return dec 324 325 326# ====================================================================== 327# TestSet: Organize data and events during a single test case 328# ====================================================================== 329 330class RestrictedList(list): 331 """List that can only be modified by appending items.""" 332 def __getattribute__(self, name): 333 if name != 'append': 334 raise AttributeError("unsupported operation") 335 return list.__getattribute__(self, name) 336 def unsupported(self, *_): 337 raise AttributeError("unsupported operation") 338 __add__ = __delattr__ = __delitem__ = __iadd__ = __imul__ = unsupported 339 __mul__ = __reversed__ = __rmul__ = __setattr__ = __setitem__ = unsupported 340 341class TestSet(object): 342 """A TestSet contains the original input operands, converted operands, 343 Python exceptions that occurred either during conversion or during 344 execution of the actual function, and the final results. 345 346 For safety, most attributes are lists that only support the append 347 operation. 348 349 If a function name is prefixed with 'context.', the corresponding 350 context method is called. 351 """ 352 def __init__(self, funcname, operands): 353 if funcname.startswith("context."): 354 self.funcname = funcname.replace("context.", "") 355 self.contextfunc = True 356 else: 357 self.funcname = funcname 358 self.contextfunc = False 359 self.op = operands # raw operand tuple 360 self.context = context # context used for the operation 361 self.cop = RestrictedList() # converted C.Decimal operands 362 self.cex = RestrictedList() # Python exceptions for C.Decimal 363 self.cresults = RestrictedList() # C.Decimal results 364 self.pop = RestrictedList() # converted P.Decimal operands 365 self.pex = RestrictedList() # Python exceptions for P.Decimal 366 self.presults = RestrictedList() # P.Decimal results 367 368 # If the above results are exact, unrounded and not clamped, repeat 369 # the operation with a maxcontext to ensure that huge intermediate 370 # values do not cause a MemoryError. 371 self.with_maxcontext = False 372 self.maxcontext = context.c.copy() 373 self.maxcontext.prec = C.MAX_PREC 374 self.maxcontext.Emax = C.MAX_EMAX 375 self.maxcontext.Emin = C.MIN_EMIN 376 self.maxcontext.clear_flags() 377 378 self.maxop = RestrictedList() # converted C.Decimal operands 379 self.maxex = RestrictedList() # Python exceptions for C.Decimal 380 self.maxresults = RestrictedList() # C.Decimal results 381 382 383# ====================================================================== 384# SkipHandler: skip known discrepancies 385# ====================================================================== 386 387class SkipHandler: 388 """Handle known discrepancies between decimal.py and _decimal.so. 389 These are either ULP differences in the power function or 390 extremely minor issues.""" 391 392 def __init__(self): 393 self.ulpdiff = 0 394 self.powmod_zeros = 0 395 self.maxctx = P.Context(Emax=10**18, Emin=-10**18) 396 397 def default(self, t): 398 return False 399 __ge__ = __gt__ = __le__ = __lt__ = __ne__ = __eq__ = default 400 __reduce__ = __format__ = __repr__ = __str__ = default 401 402 def harrison_ulp(self, dec): 403 """ftp://ftp.inria.fr/INRIA/publication/publi-pdf/RR/RR-5504.pdf""" 404 a = dec.next_plus() 405 b = dec.next_minus() 406 return abs(a - b) 407 408 def standard_ulp(self, dec, prec): 409 return _dec_from_triple(0, '1', dec._exp+len(dec._int)-prec) 410 411 def rounding_direction(self, x, mode): 412 """Determine the effective direction of the rounding when 413 the exact result x is rounded according to mode. 414 Return -1 for downwards, 0 for undirected, 1 for upwards, 415 2 for ROUND_05UP.""" 416 cmp = 1 if x.compare_total(P.Decimal("+0")) >= 0 else -1 417 418 if mode in (P.ROUND_HALF_EVEN, P.ROUND_HALF_UP, P.ROUND_HALF_DOWN): 419 return 0 420 elif mode == P.ROUND_CEILING: 421 return 1 422 elif mode == P.ROUND_FLOOR: 423 return -1 424 elif mode == P.ROUND_UP: 425 return cmp 426 elif mode == P.ROUND_DOWN: 427 return -cmp 428 elif mode == P.ROUND_05UP: 429 return 2 430 else: 431 raise ValueError("Unexpected rounding mode: %s" % mode) 432 433 def check_ulpdiff(self, exact, rounded): 434 # current precision 435 p = context.p.prec 436 437 # Convert infinities to the largest representable number + 1. 438 x = exact 439 if exact.is_infinite(): 440 x = _dec_from_triple(exact._sign, '10', context.p.Emax) 441 y = rounded 442 if rounded.is_infinite(): 443 y = _dec_from_triple(rounded._sign, '10', context.p.Emax) 444 445 # err = (rounded - exact) / ulp(rounded) 446 self.maxctx.prec = p * 2 447 t = self.maxctx.subtract(y, x) 448 if context.c.flags[C.Clamped] or \ 449 context.c.flags[C.Underflow]: 450 # The standard ulp does not work in Underflow territory. 451 ulp = self.harrison_ulp(y) 452 else: 453 ulp = self.standard_ulp(y, p) 454 # Error in ulps. 455 err = self.maxctx.divide(t, ulp) 456 457 dir = self.rounding_direction(x, context.p.rounding) 458 if dir == 0: 459 if P.Decimal("-0.6") < err < P.Decimal("0.6"): 460 return True 461 elif dir == 1: # directed, upwards 462 if P.Decimal("-0.1") < err < P.Decimal("1.1"): 463 return True 464 elif dir == -1: # directed, downwards 465 if P.Decimal("-1.1") < err < P.Decimal("0.1"): 466 return True 467 else: # ROUND_05UP 468 if P.Decimal("-1.1") < err < P.Decimal("1.1"): 469 return True 470 471 print("ulp: %s error: %s exact: %s c_rounded: %s" 472 % (ulp, err, exact, rounded)) 473 return False 474 475 def bin_resolve_ulp(self, t): 476 """Check if results of _decimal's power function are within the 477 allowed ulp ranges.""" 478 # NaNs are beyond repair. 479 if t.rc.is_nan() or t.rp.is_nan(): 480 return False 481 482 # "exact" result, double precision, half_even 483 self.maxctx.prec = context.p.prec * 2 484 485 op1, op2 = t.pop[0], t.pop[1] 486 if t.contextfunc: 487 exact = getattr(self.maxctx, t.funcname)(op1, op2) 488 else: 489 exact = getattr(op1, t.funcname)(op2, context=self.maxctx) 490 491 # _decimal's rounded result 492 rounded = P.Decimal(t.cresults[0]) 493 494 self.ulpdiff += 1 495 return self.check_ulpdiff(exact, rounded) 496 497 ############################ Correct rounding ############################# 498 def resolve_underflow(self, t): 499 """In extremely rare cases where the infinite precision result is just 500 below etiny, cdecimal does not set Subnormal/Underflow. Example: 501 502 setcontext(Context(prec=21, rounding=ROUND_UP, Emin=-55, Emax=85)) 503 Decimal("1.00000000000000000000000000000000000000000000000" 504 "0000000100000000000000000000000000000000000000000" 505 "0000000000000025").ln() 506 """ 507 if t.cresults != t.presults: 508 return False # Results must be identical. 509 if context.c.flags[C.Rounded] and \ 510 context.c.flags[C.Inexact] and \ 511 context.p.flags[P.Rounded] and \ 512 context.p.flags[P.Inexact]: 513 return True # Subnormal/Underflow may be missing. 514 return False 515 516 def exp(self, t): 517 """Resolve Underflow or ULP difference.""" 518 return self.resolve_underflow(t) 519 520 def log10(self, t): 521 """Resolve Underflow or ULP difference.""" 522 return self.resolve_underflow(t) 523 524 def ln(self, t): 525 """Resolve Underflow or ULP difference.""" 526 return self.resolve_underflow(t) 527 528 def __pow__(self, t): 529 """Always calls the resolve function. C.Decimal does not have correct 530 rounding for the power function.""" 531 if context.c.flags[C.Rounded] and \ 532 context.c.flags[C.Inexact] and \ 533 context.p.flags[P.Rounded] and \ 534 context.p.flags[P.Inexact]: 535 return self.bin_resolve_ulp(t) 536 else: 537 return False 538 power = __rpow__ = __pow__ 539 540 ############################## Technicalities ############################# 541 def __float__(self, t): 542 """NaN comparison in the verify() function obviously gives an 543 incorrect answer: nan == nan -> False""" 544 if t.cop[0].is_nan() and t.pop[0].is_nan(): 545 return True 546 return False 547 __complex__ = __float__ 548 549 def __radd__(self, t): 550 """decimal.py gives precedence to the first NaN; this is 551 not important, as __radd__ will not be called for 552 two decimal arguments.""" 553 if t.rc.is_nan() and t.rp.is_nan(): 554 return True 555 return False 556 __rmul__ = __radd__ 557 558 ################################ Various ################################## 559 def __round__(self, t): 560 """Exception: Decimal('1').__round__(-100000000000000000000000000) 561 Should it really be InvalidOperation?""" 562 if t.rc is None and t.rp.is_nan(): 563 return True 564 return False 565 566shandler = SkipHandler() 567def skip_error(t): 568 return getattr(shandler, t.funcname, shandler.default)(t) 569 570 571# ====================================================================== 572# Handling verification errors 573# ====================================================================== 574 575class VerifyError(Exception): 576 """Verification failed.""" 577 pass 578 579def function_as_string(t): 580 if t.contextfunc: 581 cargs = t.cop 582 pargs = t.pop 583 maxargs = t.maxop 584 cfunc = "c_func: %s(" % t.funcname 585 pfunc = "p_func: %s(" % t.funcname 586 maxfunc = "max_func: %s(" % t.funcname 587 else: 588 cself, cargs = t.cop[0], t.cop[1:] 589 pself, pargs = t.pop[0], t.pop[1:] 590 maxself, maxargs = t.maxop[0], t.maxop[1:] 591 cfunc = "c_func: %s.%s(" % (repr(cself), t.funcname) 592 pfunc = "p_func: %s.%s(" % (repr(pself), t.funcname) 593 maxfunc = "max_func: %s.%s(" % (repr(maxself), t.funcname) 594 595 err = cfunc 596 for arg in cargs: 597 err += "%s, " % repr(arg) 598 err = err.rstrip(", ") 599 err += ")\n" 600 601 err += pfunc 602 for arg in pargs: 603 err += "%s, " % repr(arg) 604 err = err.rstrip(", ") 605 err += ")" 606 607 if t.with_maxcontext: 608 err += "\n" 609 err += maxfunc 610 for arg in maxargs: 611 err += "%s, " % repr(arg) 612 err = err.rstrip(", ") 613 err += ")" 614 615 return err 616 617def raise_error(t): 618 global EXIT_STATUS 619 620 if skip_error(t): 621 return 622 EXIT_STATUS = 1 623 624 err = "Error in %s:\n\n" % t.funcname 625 err += "input operands: %s\n\n" % (t.op,) 626 err += function_as_string(t) 627 628 err += "\n\nc_result: %s\np_result: %s\n" % (t.cresults, t.presults) 629 if t.with_maxcontext: 630 err += "max_result: %s\n\n" % (t.maxresults) 631 else: 632 err += "\n" 633 634 err += "c_exceptions: %s\np_exceptions: %s\n" % (t.cex, t.pex) 635 if t.with_maxcontext: 636 err += "max_exceptions: %s\n\n" % t.maxex 637 else: 638 err += "\n" 639 640 err += "%s\n" % str(t.context) 641 if t.with_maxcontext: 642 err += "%s\n" % str(t.maxcontext) 643 else: 644 err += "\n" 645 646 raise VerifyError(err) 647 648 649# ====================================================================== 650# Main testing functions 651# 652# The procedure is always (t is the TestSet): 653# 654# convert(t) -> Initialize the TestSet as necessary. 655# 656# Return 0 for early abortion (e.g. if a TypeError 657# occurs during conversion, there is nothing to test). 658# 659# Return 1 for continuing with the test case. 660# 661# callfuncs(t) -> Call the relevant function for each implementation 662# and record the results in the TestSet. 663# 664# verify(t) -> Verify the results. If verification fails, details 665# are printed to stdout. 666# ====================================================================== 667 668def all_nan(a): 669 if isinstance(a, C.Decimal): 670 return a.is_nan() 671 elif isinstance(a, tuple): 672 return all(all_nan(v) for v in a) 673 return False 674 675def convert(t, convstr=True): 676 """ t is the testset. At this stage the testset contains a tuple of 677 operands t.op of various types. For decimal methods the first 678 operand (self) is always converted to Decimal. If 'convstr' is 679 true, string operands are converted as well. 680 681 Context operands are of type deccheck.Context, rounding mode 682 operands are given as a tuple (C.rounding, P.rounding). 683 684 Other types (float, int, etc.) are left unchanged. 685 """ 686 for i, op in enumerate(t.op): 687 688 context.clear_status() 689 t.maxcontext.clear_flags() 690 691 if op in RoundModes: 692 t.cop.append(op) 693 t.pop.append(op) 694 t.maxop.append(op) 695 696 elif not t.contextfunc and i == 0 or \ 697 convstr and isinstance(op, str): 698 try: 699 c = C.Decimal(op) 700 cex = None 701 except (TypeError, ValueError, OverflowError) as e: 702 c = None 703 cex = e.__class__ 704 705 try: 706 p = RestrictedDecimal(op) 707 pex = None 708 except (TypeError, ValueError, OverflowError) as e: 709 p = None 710 pex = e.__class__ 711 712 try: 713 C.setcontext(t.maxcontext) 714 maxop = C.Decimal(op) 715 maxex = None 716 except (TypeError, ValueError, OverflowError) as e: 717 maxop = None 718 maxex = e.__class__ 719 finally: 720 C.setcontext(context.c) 721 722 t.cop.append(c) 723 t.cex.append(cex) 724 725 t.pop.append(p) 726 t.pex.append(pex) 727 728 t.maxop.append(maxop) 729 t.maxex.append(maxex) 730 731 if cex is pex: 732 if str(c) != str(p) or not context.assert_eq_status(): 733 raise_error(t) 734 if cex and pex: 735 # nothing to test 736 return 0 737 else: 738 raise_error(t) 739 740 # The exceptions in the maxcontext operation can legitimately 741 # differ, only test that maxex implies cex: 742 if maxex is not None and cex is not maxex: 743 raise_error(t) 744 745 elif isinstance(op, Context): 746 t.context = op 747 t.cop.append(op.c) 748 t.pop.append(op.p) 749 t.maxop.append(t.maxcontext) 750 751 else: 752 t.cop.append(op) 753 t.pop.append(op) 754 t.maxop.append(op) 755 756 return 1 757 758def callfuncs(t): 759 """ t is the testset. At this stage the testset contains operand lists 760 t.cop and t.pop for the C and Python versions of decimal. 761 For Decimal methods, the first operands are of type C.Decimal and 762 P.Decimal respectively. The remaining operands can have various types. 763 For Context methods, all operands can have any type. 764 765 t.rc and t.rp are the results of the operation. 766 """ 767 context.clear_status() 768 t.maxcontext.clear_flags() 769 770 try: 771 if t.contextfunc: 772 cargs = t.cop 773 t.rc = getattr(context.c, t.funcname)(*cargs) 774 else: 775 cself = t.cop[0] 776 cargs = t.cop[1:] 777 t.rc = getattr(cself, t.funcname)(*cargs) 778 t.cex.append(None) 779 except (TypeError, ValueError, OverflowError, MemoryError) as e: 780 t.rc = None 781 t.cex.append(e.__class__) 782 783 try: 784 if t.contextfunc: 785 pargs = t.pop 786 t.rp = getattr(context.p, t.funcname)(*pargs) 787 else: 788 pself = t.pop[0] 789 pargs = t.pop[1:] 790 t.rp = getattr(pself, t.funcname)(*pargs) 791 t.pex.append(None) 792 except (TypeError, ValueError, OverflowError, MemoryError) as e: 793 t.rp = None 794 t.pex.append(e.__class__) 795 796 # If the above results are exact, unrounded, normal etc., repeat the 797 # operation with a maxcontext to ensure that huge intermediate values 798 # do not cause a MemoryError. 799 if (t.funcname not in MaxContextSkip and 800 not context.c.flags[C.InvalidOperation] and 801 not context.c.flags[C.Inexact] and 802 not context.c.flags[C.Rounded] and 803 not context.c.flags[C.Subnormal] and 804 not context.c.flags[C.Clamped] and 805 not context.clamp and # results are padded to context.prec if context.clamp==1. 806 not any(isinstance(v, C.Context) for v in t.cop)): # another context is used. 807 t.with_maxcontext = True 808 try: 809 if t.contextfunc: 810 maxargs = t.maxop 811 t.rmax = getattr(t.maxcontext, t.funcname)(*maxargs) 812 else: 813 maxself = t.maxop[0] 814 maxargs = t.maxop[1:] 815 try: 816 C.setcontext(t.maxcontext) 817 t.rmax = getattr(maxself, t.funcname)(*maxargs) 818 finally: 819 C.setcontext(context.c) 820 t.maxex.append(None) 821 except (TypeError, ValueError, OverflowError, MemoryError) as e: 822 t.rmax = None 823 t.maxex.append(e.__class__) 824 825def verify(t, stat): 826 """ t is the testset. At this stage the testset contains the following 827 tuples: 828 829 t.op: original operands 830 t.cop: C.Decimal operands (see convert for details) 831 t.pop: P.Decimal operands (see convert for details) 832 t.rc: C result 833 t.rp: Python result 834 835 t.rc and t.rp can have various types. 836 """ 837 t.cresults.append(str(t.rc)) 838 t.presults.append(str(t.rp)) 839 if t.with_maxcontext: 840 t.maxresults.append(str(t.rmax)) 841 842 if isinstance(t.rc, C.Decimal) and isinstance(t.rp, P.Decimal): 843 # General case: both results are Decimals. 844 t.cresults.append(t.rc.to_eng_string()) 845 t.cresults.append(t.rc.as_tuple()) 846 t.cresults.append(str(t.rc.imag)) 847 t.cresults.append(str(t.rc.real)) 848 t.presults.append(t.rp.to_eng_string()) 849 t.presults.append(t.rp.as_tuple()) 850 t.presults.append(str(t.rp.imag)) 851 t.presults.append(str(t.rp.real)) 852 853 if t.with_maxcontext and isinstance(t.rmax, C.Decimal): 854 t.maxresults.append(t.rmax.to_eng_string()) 855 t.maxresults.append(t.rmax.as_tuple()) 856 t.maxresults.append(str(t.rmax.imag)) 857 t.maxresults.append(str(t.rmax.real)) 858 859 nc = t.rc.number_class().lstrip('+-s') 860 stat[nc] += 1 861 else: 862 # Results from e.g. __divmod__ can only be compared as strings. 863 if not isinstance(t.rc, tuple) and not isinstance(t.rp, tuple): 864 if t.rc != t.rp: 865 raise_error(t) 866 if t.with_maxcontext and not isinstance(t.rmax, tuple): 867 if t.rmax != t.rc: 868 raise_error(t) 869 stat[type(t.rc).__name__] += 1 870 871 # The return value lists must be equal. 872 if t.cresults != t.presults: 873 raise_error(t) 874 # The Python exception lists (TypeError, etc.) must be equal. 875 if t.cex != t.pex: 876 raise_error(t) 877 # The context flags must be equal. 878 if not t.context.assert_eq_status(): 879 raise_error(t) 880 881 if t.with_maxcontext: 882 # NaN payloads etc. depend on precision and clamp. 883 if all_nan(t.rc) and all_nan(t.rmax): 884 return 885 # The return value lists must be equal. 886 if t.maxresults != t.cresults: 887 raise_error(t) 888 # The Python exception lists (TypeError, etc.) must be equal. 889 if t.maxex != t.cex: 890 raise_error(t) 891 # The context flags must be equal. 892 if t.maxcontext.flags != t.context.c.flags: 893 raise_error(t) 894 895 896# ====================================================================== 897# Main test loops 898# 899# test_method(method, testspecs, testfunc) -> 900# 901# Loop through various context settings. The degree of 902# thoroughness is determined by 'testspec'. For each 903# setting, call 'testfunc'. Generally, 'testfunc' itself 904# a loop, iterating through many test cases generated 905# by the functions in randdec.py. 906# 907# test_n-ary(method, prec, exp_range, restricted_range, itr, stat) -> 908# 909# 'test_unary', 'test_binary' and 'test_ternary' are the 910# main test functions passed to 'test_method'. They deal 911# with the regular cases. The thoroughness of testing is 912# determined by 'itr'. 913# 914# 'prec', 'exp_range' and 'restricted_range' are passed 915# to the test-generating functions and limit the generated 916# values. In some cases, for reasonable run times a 917# maximum exponent of 9999 is required. 918# 919# The 'stat' parameter is passed down to the 'verify' 920# function, which records statistics for the result values. 921# ====================================================================== 922 923def log(fmt, args=None): 924 if args: 925 sys.stdout.write(''.join((fmt, '\n')) % args) 926 else: 927 sys.stdout.write(''.join((str(fmt), '\n'))) 928 sys.stdout.flush() 929 930def test_method(method, testspecs, testfunc): 931 """Iterate a test function through many context settings.""" 932 log("testing %s ...", method) 933 stat = defaultdict(int) 934 for spec in testspecs: 935 if 'samples' in spec: 936 spec['prec'] = sorted(random.sample(range(1, 101), 937 spec['samples'])) 938 for prec in spec['prec']: 939 context.prec = prec 940 for expts in spec['expts']: 941 emin, emax = expts 942 if emin == 'rand': 943 context.Emin = random.randrange(-1000, 0) 944 context.Emax = random.randrange(prec, 1000) 945 else: 946 context.Emin, context.Emax = emin, emax 947 if prec > context.Emax: continue 948 log(" prec: %d emin: %d emax: %d", 949 (context.prec, context.Emin, context.Emax)) 950 restr_range = 9999 if context.Emax > 9999 else context.Emax+99 951 for rounding in RoundModes: 952 context.rounding = rounding 953 context.capitals = random.randrange(2) 954 if spec['clamp'] == 'rand': 955 context.clamp = random.randrange(2) 956 else: 957 context.clamp = spec['clamp'] 958 exprange = context.c.Emax 959 testfunc(method, prec, exprange, restr_range, 960 spec['iter'], stat) 961 log(" result types: %s" % sorted([t for t in stat.items()])) 962 963def test_unary(method, prec, exp_range, restricted_range, itr, stat): 964 """Iterate a unary function through many test cases.""" 965 if method in UnaryRestricted: 966 exp_range = restricted_range 967 for op in all_unary(prec, exp_range, itr): 968 t = TestSet(method, op) 969 try: 970 if not convert(t): 971 continue 972 callfuncs(t) 973 verify(t, stat) 974 except VerifyError as err: 975 log(err) 976 977 if not method.startswith('__'): 978 for op in unary_optarg(prec, exp_range, itr): 979 t = TestSet(method, op) 980 try: 981 if not convert(t): 982 continue 983 callfuncs(t) 984 verify(t, stat) 985 except VerifyError as err: 986 log(err) 987 988def test_binary(method, prec, exp_range, restricted_range, itr, stat): 989 """Iterate a binary function through many test cases.""" 990 if method in BinaryRestricted: 991 exp_range = restricted_range 992 for op in all_binary(prec, exp_range, itr): 993 t = TestSet(method, op) 994 try: 995 if not convert(t): 996 continue 997 callfuncs(t) 998 verify(t, stat) 999 except VerifyError as err: 1000 log(err) 1001 1002 if not method.startswith('__'): 1003 for op in binary_optarg(prec, exp_range, itr): 1004 t = TestSet(method, op) 1005 try: 1006 if not convert(t): 1007 continue 1008 callfuncs(t) 1009 verify(t, stat) 1010 except VerifyError as err: 1011 log(err) 1012 1013def test_ternary(method, prec, exp_range, restricted_range, itr, stat): 1014 """Iterate a ternary function through many test cases.""" 1015 if method in TernaryRestricted: 1016 exp_range = restricted_range 1017 for op in all_ternary(prec, exp_range, itr): 1018 t = TestSet(method, op) 1019 try: 1020 if not convert(t): 1021 continue 1022 callfuncs(t) 1023 verify(t, stat) 1024 except VerifyError as err: 1025 log(err) 1026 1027 if not method.startswith('__'): 1028 for op in ternary_optarg(prec, exp_range, itr): 1029 t = TestSet(method, op) 1030 try: 1031 if not convert(t): 1032 continue 1033 callfuncs(t) 1034 verify(t, stat) 1035 except VerifyError as err: 1036 log(err) 1037 1038def test_format(method, prec, exp_range, restricted_range, itr, stat): 1039 """Iterate the __format__ method through many test cases.""" 1040 for op in all_unary(prec, exp_range, itr): 1041 fmt1 = rand_format(chr(random.randrange(0, 128)), 'EeGgn') 1042 fmt2 = rand_locale() 1043 for fmt in (fmt1, fmt2): 1044 fmtop = (op[0], fmt) 1045 t = TestSet(method, fmtop) 1046 try: 1047 if not convert(t, convstr=False): 1048 continue 1049 callfuncs(t) 1050 verify(t, stat) 1051 except VerifyError as err: 1052 log(err) 1053 for op in all_unary(prec, 9999, itr): 1054 fmt1 = rand_format(chr(random.randrange(0, 128)), 'Ff%') 1055 fmt2 = rand_locale() 1056 for fmt in (fmt1, fmt2): 1057 fmtop = (op[0], fmt) 1058 t = TestSet(method, fmtop) 1059 try: 1060 if not convert(t, convstr=False): 1061 continue 1062 callfuncs(t) 1063 verify(t, stat) 1064 except VerifyError as err: 1065 log(err) 1066 1067def test_round(method, prec, exprange, restricted_range, itr, stat): 1068 """Iterate the __round__ method through many test cases.""" 1069 for op in all_unary(prec, 9999, itr): 1070 n = random.randrange(10) 1071 roundop = (op[0], n) 1072 t = TestSet(method, roundop) 1073 try: 1074 if not convert(t): 1075 continue 1076 callfuncs(t) 1077 verify(t, stat) 1078 except VerifyError as err: 1079 log(err) 1080 1081def test_from_float(method, prec, exprange, restricted_range, itr, stat): 1082 """Iterate the __float__ method through many test cases.""" 1083 for rounding in RoundModes: 1084 context.rounding = rounding 1085 for i in range(1000): 1086 f = randfloat() 1087 op = (f,) if method.startswith("context.") else ("sNaN", f) 1088 t = TestSet(method, op) 1089 try: 1090 if not convert(t): 1091 continue 1092 callfuncs(t) 1093 verify(t, stat) 1094 except VerifyError as err: 1095 log(err) 1096 1097def randcontext(exprange): 1098 c = Context(C.Context(), P.Context()) 1099 c.Emax = random.randrange(1, exprange+1) 1100 c.Emin = random.randrange(-exprange, 0) 1101 maxprec = 100 if c.Emax >= 100 else c.Emax 1102 c.prec = random.randrange(1, maxprec+1) 1103 c.clamp = random.randrange(2) 1104 c.clear_traps() 1105 return c 1106 1107def test_quantize_api(method, prec, exprange, restricted_range, itr, stat): 1108 """Iterate the 'quantize' method through many test cases, using 1109 the optional arguments.""" 1110 for op in all_binary(prec, restricted_range, itr): 1111 for rounding in RoundModes: 1112 c = randcontext(exprange) 1113 quantizeop = (op[0], op[1], rounding, c) 1114 t = TestSet(method, quantizeop) 1115 try: 1116 if not convert(t): 1117 continue 1118 callfuncs(t) 1119 verify(t, stat) 1120 except VerifyError as err: 1121 log(err) 1122 1123 1124def check_untested(funcdict, c_cls, p_cls): 1125 """Determine untested, C-only and Python-only attributes. 1126 Uncomment print lines for debugging.""" 1127 c_attr = set(dir(c_cls)) 1128 p_attr = set(dir(p_cls)) 1129 intersect = c_attr & p_attr 1130 1131 funcdict['c_only'] = tuple(sorted(c_attr-intersect)) 1132 funcdict['p_only'] = tuple(sorted(p_attr-intersect)) 1133 1134 tested = set() 1135 for lst in funcdict.values(): 1136 for v in lst: 1137 v = v.replace("context.", "") if c_cls == C.Context else v 1138 tested.add(v) 1139 1140 funcdict['untested'] = tuple(sorted(intersect-tested)) 1141 1142 # for key in ('untested', 'c_only', 'p_only'): 1143 # s = 'Context' if c_cls == C.Context else 'Decimal' 1144 # print("\n%s %s:\n%s" % (s, key, funcdict[key])) 1145 1146 1147if __name__ == '__main__': 1148 1149 parser = argparse.ArgumentParser(prog="deccheck.py") 1150 1151 group = parser.add_mutually_exclusive_group() 1152 group.add_argument('--short', dest='time', action="store_const", const='short', default='short', help="short test (default)") 1153 group.add_argument('--medium', dest='time', action="store_const", const='medium', default='short', help="medium test (reasonable run time)") 1154 group.add_argument('--long', dest='time', action="store_const", const='long', default='short', help="long test (long run time)") 1155 group.add_argument('--all', dest='time', action="store_const", const='all', default='short', help="all tests (excessive run time)") 1156 1157 group = parser.add_mutually_exclusive_group() 1158 group.add_argument('--single', dest='single', nargs=1, default=False, metavar="TEST", help="run a single test") 1159 group.add_argument('--multicore', dest='multicore', action="store_true", default=False, help="use all available cores") 1160 1161 args = parser.parse_args() 1162 assert args.single is False or args.multicore is False 1163 if args.single: 1164 args.single = args.single[0] 1165 1166 1167 # Set up the testspecs list. A testspec is simply a dictionary 1168 # that determines the amount of different contexts that 'test_method' 1169 # will generate. 1170 base_expts = [(C.MIN_EMIN, C.MAX_EMAX)] 1171 if C.MAX_EMAX == 999999999999999999: 1172 base_expts.append((-999999999, 999999999)) 1173 1174 # Basic contexts. 1175 base = { 1176 'expts': base_expts, 1177 'prec': [], 1178 'clamp': 'rand', 1179 'iter': None, 1180 'samples': None, 1181 } 1182 # Contexts with small values for prec, emin, emax. 1183 small = { 1184 'prec': [1, 2, 3, 4, 5], 1185 'expts': [(-1, 1), (-2, 2), (-3, 3), (-4, 4), (-5, 5)], 1186 'clamp': 'rand', 1187 'iter': None 1188 } 1189 # IEEE interchange format. 1190 ieee = [ 1191 # DECIMAL32 1192 {'prec': [7], 'expts': [(-95, 96)], 'clamp': 1, 'iter': None}, 1193 # DECIMAL64 1194 {'prec': [16], 'expts': [(-383, 384)], 'clamp': 1, 'iter': None}, 1195 # DECIMAL128 1196 {'prec': [34], 'expts': [(-6143, 6144)], 'clamp': 1, 'iter': None} 1197 ] 1198 1199 if args.time == 'medium': 1200 base['expts'].append(('rand', 'rand')) 1201 # 5 random precisions 1202 base['samples'] = 5 1203 testspecs = [small] + ieee + [base] 1204 elif args.time == 'long': 1205 base['expts'].append(('rand', 'rand')) 1206 # 10 random precisions 1207 base['samples'] = 10 1208 testspecs = [small] + ieee + [base] 1209 elif args.time == 'all': 1210 base['expts'].append(('rand', 'rand')) 1211 # All precisions in [1, 100] 1212 base['samples'] = 100 1213 testspecs = [small] + ieee + [base] 1214 else: # --short 1215 rand_ieee = random.choice(ieee) 1216 base['iter'] = small['iter'] = rand_ieee['iter'] = 1 1217 # 1 random precision and exponent pair 1218 base['samples'] = 1 1219 base['expts'] = [random.choice(base_expts)] 1220 # 1 random precision and exponent pair 1221 prec = random.randrange(1, 6) 1222 small['prec'] = [prec] 1223 small['expts'] = [(-prec, prec)] 1224 testspecs = [small, rand_ieee, base] 1225 1226 1227 check_untested(Functions, C.Decimal, P.Decimal) 1228 check_untested(ContextFunctions, C.Context, P.Context) 1229 1230 1231 if args.multicore: 1232 q = Queue() 1233 elif args.single: 1234 log("Random seed: %d", RANDSEED) 1235 else: 1236 log("\n\nRandom seed: %d\n\n", RANDSEED) 1237 1238 1239 FOUND_METHOD = False 1240 def do_single(method, f): 1241 global FOUND_METHOD 1242 if args.multicore: 1243 q.put(method) 1244 elif not args.single or args.single == method: 1245 FOUND_METHOD = True 1246 f() 1247 1248 # Decimal methods: 1249 for method in Functions['unary'] + Functions['unary_ctx'] + \ 1250 Functions['unary_rnd_ctx']: 1251 do_single(method, lambda: test_method(method, testspecs, test_unary)) 1252 1253 for method in Functions['binary'] + Functions['binary_ctx']: 1254 do_single(method, lambda: test_method(method, testspecs, test_binary)) 1255 1256 for method in Functions['ternary'] + Functions['ternary_ctx']: 1257 name = '__powmod__' if method == '__pow__' else method 1258 do_single(name, lambda: test_method(method, testspecs, test_ternary)) 1259 1260 do_single('__format__', lambda: test_method('__format__', testspecs, test_format)) 1261 do_single('__round__', lambda: test_method('__round__', testspecs, test_round)) 1262 do_single('from_float', lambda: test_method('from_float', testspecs, test_from_float)) 1263 do_single('quantize_api', lambda: test_method('quantize', testspecs, test_quantize_api)) 1264 1265 # Context methods: 1266 for method in ContextFunctions['unary']: 1267 do_single(method, lambda: test_method(method, testspecs, test_unary)) 1268 1269 for method in ContextFunctions['binary']: 1270 do_single(method, lambda: test_method(method, testspecs, test_binary)) 1271 1272 for method in ContextFunctions['ternary']: 1273 name = 'context.powmod' if method == 'context.power' else method 1274 do_single(name, lambda: test_method(method, testspecs, test_ternary)) 1275 1276 do_single('context.create_decimal_from_float', 1277 lambda: test_method('context.create_decimal_from_float', 1278 testspecs, test_from_float)) 1279 1280 if args.multicore: 1281 error = Event() 1282 write_lock = Lock() 1283 1284 def write_output(out, returncode): 1285 if returncode != 0: 1286 error.set() 1287 1288 with write_lock: 1289 sys.stdout.buffer.write(out + b"\n") 1290 sys.stdout.buffer.flush() 1291 1292 def tfunc(): 1293 while not error.is_set(): 1294 try: 1295 test = q.get(block=False, timeout=-1) 1296 except Empty: 1297 return 1298 1299 cmd = [sys.executable, "deccheck.py", "--%s" % args.time, "--single", test] 1300 p = subprocess.Popen(cmd, stdout=PIPE, stderr=STDOUT) 1301 out, _ = p.communicate() 1302 write_output(out, p.returncode) 1303 1304 N = os.cpu_count() 1305 t = N * [None] 1306 1307 for i in range(N): 1308 t[i] = Thread(target=tfunc) 1309 t[i].start() 1310 1311 for i in range(N): 1312 t[i].join() 1313 1314 sys.exit(1 if error.is_set() else 0) 1315 1316 elif args.single: 1317 if not FOUND_METHOD: 1318 log("\nerror: cannot find method \"%s\"" % args.single) 1319 EXIT_STATUS = 1 1320 sys.exit(EXIT_STATUS) 1321 else: 1322 sys.exit(EXIT_STATUS) 1323