Home | History | Annotate | Download | only in tests
      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 import sys, random
     33 from copy import copy
     34 from collections import defaultdict
     35 from test.support import import_fresh_module
     36 from randdec import randfloat, all_unary, all_binary, all_ternary
     37 from randdec import unary_optarg, binary_optarg, ternary_optarg
     38 from formathelper import rand_format, rand_locale
     39 from _pydecimal import _dec_from_triple
     40 
     41 C = import_fresh_module('decimal', fresh=['_decimal'])
     42 P = import_fresh_module('decimal', blocked=['_decimal'])
     43 EXIT_STATUS = 0
     44 
     45 
     46 # Contains all categories of Decimal methods.
     47 Functions = {
     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_integer_ratio', 'as_tuple', 'canonical', 'conjugate',
     54         'copy_abs', '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.
     93 ContextFunctions = {
     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.
    129 UnaryRestricted = [
    130   '__ceil__', '__floor__', '__int__', '__trunc__',
    131   'as_integer_ratio', 'to_integral', 'to_integral_value'
    132 ]
    133 
    134 BinaryRestricted = ['__round__']
    135 
    136 TernaryRestricted = ['__pow__', 'context.power']
    137 
    138 
    139 # ======================================================================
    140 #                            Unified Context
    141 # ======================================================================
    142 
    143 # Translate symbols.
    144 CondMap = {
    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 
    160 RoundModes = [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]
    163 
    164 
    165 class 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
    177         self.p.rounding = self.c.rounding
    178         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):
    211         assert(self.c.rounding == self.p.rounding)
    212         return self.c.rounding
    213 
    214     def setround(self, val):
    215         self.c.rounding = val
    216         self.p.rounding = val
    217 
    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.
    273 context = Context()
    274 context.Emin = C.MIN_EMIN
    275 context.Emax = C.MAX_EMAX
    276 context.clear_traps()
    277 
    278 # When creating decimals, _decimal is ultimately limited by the maximum
    279 # context values. We emulate this restriction for decimal.py.
    280 maxcontext = 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 )
    287 maxcontext.clamp = 0
    288 
    289 def 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 \
    297        maxcontext.flags[P.Clamped] or \
    298        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 
    309 class 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 
    320 class 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 
    352 class 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):
    374         return _dec_from_triple(0, '1', dec._exp+len(dec._int)-prec)
    375 
    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():
    405             x = _dec_from_triple(exact._sign, '10', context.p.Emax)
    406         y = rounded
    407         if rounded.is_infinite():
    408             y = _dec_from_triple(rounded._sign, '10', context.p.Emax)
    409 
    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 
    531 shandler = SkipHandler()
    532 def skip_error(t):
    533     return getattr(shandler, t.funcname, shandler.default)(t)
    534 
    535 
    536 # ======================================================================
    537 #                      Handling verification errors
    538 # ======================================================================
    539 
    540 class VerifyError(Exception):
    541     """Verification failed."""
    542     pass
    543 
    544 def 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 
    570 def 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 
    606 def 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 
    621         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):
    627             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 
    660         else:
    661             t.cop.append(op)
    662             t.pop.append(op)
    663 
    664     return 1
    665 
    666 def 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 
    703 def 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 
    775 def 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 
    782 def 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
    803                 for rounding in RoundModes:
    804                     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 
    815 def 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 
    829     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 
    840 def 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 
    854     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 
    865 def 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 
    879     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 
    890 def 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):
    893         fmt1 = rand_format(chr(random.randrange(0, 128)), 'EeGgn')
    894         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):
    906         fmt1 = rand_format(chr(random.randrange(0, 128)), 'Ff%')
    907         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 
    919 def 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 
    933 def test_from_float(method, prec, exprange, restricted_range, itr, stat):
    934     """Iterate the __float__ method through many test cases."""
    935     for rounding in RoundModes:
    936         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 
    949 def 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 
    959 def 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 
    976 def 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 
    999 if __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)
   1101