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