diff options
Diffstat (limited to 'Modules/_decimal/tests/deccheck.py')
-rw-r--r-- | Modules/_decimal/tests/deccheck.py | 1101 |
1 files changed, 1101 insertions, 0 deletions
diff --git a/Modules/_decimal/tests/deccheck.py b/Modules/_decimal/tests/deccheck.py new file mode 100644 index 0000000..a2853ad --- /dev/null +++ b/Modules/_decimal/tests/deccheck.py @@ -0,0 +1,1101 @@ +#!/usr/bin/env python + +# +# Copyright (c) 2008-2012 Stefan Krah. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# +# Usage: python deccheck.py [--short|--medium|--long|--all] +# + +import sys, random +from copy import copy +from collections import defaultdict +from test.support import import_fresh_module +from randdec import randfloat, all_unary, all_binary, all_ternary +from randdec import unary_optarg, binary_optarg, ternary_optarg +from formathelper import rand_format, rand_locale + +C = import_fresh_module('decimal', fresh=['_decimal']) +P = import_fresh_module('decimal', blocked=['_decimal']) +EXIT_STATUS = 0 + + +# Contains all categories of Decimal methods. +Functions = { + # Plain unary: + 'unary': ( + '__abs__', '__bool__', '__ceil__', '__complex__', '__copy__', + '__floor__', '__float__', '__hash__', '__int__', '__neg__', + '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__', + 'adjusted', 'as_tuple', 'canonical', 'conjugate', 'copy_abs', + 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite', + 'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix' + ), + # Unary with optional context: + 'unary_ctx': ( + 'exp', 'is_normal', 'is_subnormal', 'ln', 'log10', 'logb', + 'logical_invert', 'next_minus', 'next_plus', 'normalize', + 'number_class', 'sqrt', 'to_eng_string' + ), + # Unary with optional rounding mode and context: + 'unary_rnd_ctx': ('to_integral', 'to_integral_exact', 'to_integral_value'), + # Plain binary: + 'binary': ( + '__add__', '__divmod__', '__eq__', '__floordiv__', '__ge__', '__gt__', + '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__pow__', + '__radd__', '__rdivmod__', '__rfloordiv__', '__rmod__', '__rmul__', + '__rpow__', '__rsub__', '__rtruediv__', '__sub__', '__truediv__', + 'compare_total', 'compare_total_mag', 'copy_sign', 'quantize', + 'same_quantum' + ), + # Binary with optional context: + 'binary_ctx': ( + 'compare', 'compare_signal', 'logical_and', 'logical_or', 'logical_xor', + 'max', 'max_mag', 'min', 'min_mag', 'next_toward', 'remainder_near', + 'rotate', 'scaleb', 'shift' + ), + # Plain ternary: + 'ternary': ('__pow__',), + # Ternary with optional context: + 'ternary_ctx': ('fma',), + # Special: + 'special': ('__format__', '__reduce_ex__', '__round__', 'from_float', + 'quantize'), + # Properties: + 'property': ('real', 'imag') +} + +# Contains all categories of Context methods. The n-ary classification +# applies to the number of Decimal arguments. +ContextFunctions = { + # Plain nullary: + 'nullary': ('context.__hash__', 'context.__reduce__', 'context.radix'), + # Plain unary: + 'unary': ('context.abs', 'context.canonical', 'context.copy_abs', + 'context.copy_decimal', 'context.copy_negate', + 'context.create_decimal', 'context.exp', 'context.is_canonical', + 'context.is_finite', 'context.is_infinite', 'context.is_nan', + 'context.is_normal', 'context.is_qnan', 'context.is_signed', + 'context.is_snan', 'context.is_subnormal', 'context.is_zero', + 'context.ln', 'context.log10', 'context.logb', + 'context.logical_invert', 'context.minus', 'context.next_minus', + 'context.next_plus', 'context.normalize', 'context.number_class', + 'context.plus', 'context.sqrt', 'context.to_eng_string', + 'context.to_integral', 'context.to_integral_exact', + 'context.to_integral_value', 'context.to_sci_string' + ), + # Plain binary: + 'binary': ('context.add', 'context.compare', 'context.compare_signal', + 'context.compare_total', 'context.compare_total_mag', + 'context.copy_sign', 'context.divide', 'context.divide_int', + 'context.divmod', 'context.logical_and', 'context.logical_or', + 'context.logical_xor', 'context.max', 'context.max_mag', + 'context.min', 'context.min_mag', 'context.multiply', + 'context.next_toward', 'context.power', 'context.quantize', + 'context.remainder', 'context.remainder_near', 'context.rotate', + 'context.same_quantum', 'context.scaleb', 'context.shift', + 'context.subtract' + ), + # Plain ternary: + 'ternary': ('context.fma', 'context.power'), + # Special: + 'special': ('context.__reduce_ex__', 'context.create_decimal_from_float') +} + +# Functions that require a restricted exponent range for reasonable runtimes. +UnaryRestricted = [ + '__ceil__', '__floor__', '__int__', '__long__', '__trunc__', + 'to_integral', 'to_integral_value' +] + +BinaryRestricted = ['__round__'] + +TernaryRestricted = ['__pow__', 'context.power'] + + +# ====================================================================== +# Unified Context +# ====================================================================== + +# Translate symbols. +CondMap = { + C.Clamped: P.Clamped, + C.ConversionSyntax: P.ConversionSyntax, + C.DivisionByZero: P.DivisionByZero, + C.DivisionImpossible: P.InvalidOperation, + C.DivisionUndefined: P.DivisionUndefined, + C.Inexact: P.Inexact, + C.InvalidContext: P.InvalidContext, + C.InvalidOperation: P.InvalidOperation, + C.Overflow: P.Overflow, + C.Rounded: P.Rounded, + C.Subnormal: P.Subnormal, + C.Underflow: P.Underflow, + C.FloatOperation: P.FloatOperation, +} + +RoundModes = [C.ROUND_UP, C.ROUND_DOWN, C.ROUND_CEILING, C.ROUND_FLOOR, + C.ROUND_HALF_UP, C.ROUND_HALF_DOWN, C.ROUND_HALF_EVEN, + C.ROUND_05UP] + + +class Context(object): + """Provides a convenient way of syncing the C and P contexts""" + + __slots__ = ['c', 'p'] + + def __init__(self, c_ctx=None, p_ctx=None): + """Initialization is from the C context""" + self.c = C.getcontext() if c_ctx is None else c_ctx + self.p = P.getcontext() if p_ctx is None else p_ctx + self.p.prec = self.c.prec + self.p.Emin = self.c.Emin + self.p.Emax = self.c.Emax + self.p.rounding = self.c.rounding + self.p.capitals = self.c.capitals + self.settraps([sig for sig in self.c.traps if self.c.traps[sig]]) + self.setstatus([sig for sig in self.c.flags if self.c.flags[sig]]) + self.p.clamp = self.c.clamp + + def __str__(self): + return str(self.c) + '\n' + str(self.p) + + def getprec(self): + assert(self.c.prec == self.p.prec) + return self.c.prec + + def setprec(self, val): + self.c.prec = val + self.p.prec = val + + def getemin(self): + assert(self.c.Emin == self.p.Emin) + return self.c.Emin + + def setemin(self, val): + self.c.Emin = val + self.p.Emin = val + + def getemax(self): + assert(self.c.Emax == self.p.Emax) + return self.c.Emax + + def setemax(self, val): + self.c.Emax = val + self.p.Emax = val + + def getround(self): + assert(self.c.rounding == self.p.rounding) + return self.c.rounding + + def setround(self, val): + self.c.rounding = val + self.p.rounding = val + + def getcapitals(self): + assert(self.c.capitals == self.p.capitals) + return self.c.capitals + + def setcapitals(self, val): + self.c.capitals = val + self.p.capitals = val + + def getclamp(self): + assert(self.c.clamp == self.p.clamp) + return self.c.clamp + + def setclamp(self, val): + self.c.clamp = val + self.p.clamp = val + + prec = property(getprec, setprec) + Emin = property(getemin, setemin) + Emax = property(getemax, setemax) + rounding = property(getround, setround) + clamp = property(getclamp, setclamp) + capitals = property(getcapitals, setcapitals) + + def clear_traps(self): + self.c.clear_traps() + for trap in self.p.traps: + self.p.traps[trap] = False + + def clear_status(self): + self.c.clear_flags() + self.p.clear_flags() + + def settraps(self, lst): + """lst: C signal list""" + self.clear_traps() + for signal in lst: + self.c.traps[signal] = True + self.p.traps[CondMap[signal]] = True + + def setstatus(self, lst): + """lst: C signal list""" + self.clear_status() + for signal in lst: + self.c.flags[signal] = True + self.p.flags[CondMap[signal]] = True + + def assert_eq_status(self): + """assert equality of C and P status""" + for signal in self.c.flags: + if self.c.flags[signal] == (not self.p.flags[CondMap[signal]]): + return False + return True + + +# We don't want exceptions so that we can compare the status flags. +context = Context() +context.Emin = C.MIN_EMIN +context.Emax = C.MAX_EMAX +context.clear_traps() + +# When creating decimals, _decimal is ultimately limited by the maximum +# context values. We emulate this restriction for decimal.py. +maxcontext = P.Context( + prec=C.MAX_PREC, + Emin=C.MIN_EMIN, + Emax=C.MAX_EMAX, + rounding=P.ROUND_HALF_UP, + capitals=1 +) +maxcontext.clamp = 0 + +def RestrictedDecimal(value): + maxcontext.traps = copy(context.p.traps) + maxcontext.clear_flags() + if isinstance(value, str): + value = value.strip() + dec = maxcontext.create_decimal(value) + if maxcontext.flags[P.Inexact] or \ + maxcontext.flags[P.Rounded] or \ + maxcontext.flags[P.Clamped] or \ + maxcontext.flags[P.InvalidOperation]: + return context.p._raise_error(P.InvalidOperation) + if maxcontext.flags[P.FloatOperation]: + context.p.flags[P.FloatOperation] = True + return dec + + +# ====================================================================== +# TestSet: Organize data and events during a single test case +# ====================================================================== + +class RestrictedList(list): + """List that can only be modified by appending items.""" + def __getattribute__(self, name): + if name != 'append': + raise AttributeError("unsupported operation") + return list.__getattribute__(self, name) + def unsupported(self, *_): + raise AttributeError("unsupported operation") + __add__ = __delattr__ = __delitem__ = __iadd__ = __imul__ = unsupported + __mul__ = __reversed__ = __rmul__ = __setattr__ = __setitem__ = unsupported + +class TestSet(object): + """A TestSet contains the original input operands, converted operands, + Python exceptions that occurred either during conversion or during + execution of the actual function, and the final results. + + For safety, most attributes are lists that only support the append + operation. + + If a function name is prefixed with 'context.', the corresponding + context method is called. + """ + def __init__(self, funcname, operands): + if funcname.startswith("context."): + self.funcname = funcname.replace("context.", "") + self.contextfunc = True + else: + self.funcname = funcname + self.contextfunc = False + self.op = operands # raw operand tuple + self.context = context # context used for the operation + self.cop = RestrictedList() # converted C.Decimal operands + self.cex = RestrictedList() # Python exceptions for C.Decimal + self.cresults = RestrictedList() # C.Decimal results + self.pop = RestrictedList() # converted P.Decimal operands + self.pex = RestrictedList() # Python exceptions for P.Decimal + self.presults = RestrictedList() # P.Decimal results + + +# ====================================================================== +# SkipHandler: skip known discrepancies +# ====================================================================== + +class SkipHandler: + """Handle known discrepancies between decimal.py and _decimal.so. + These are either ULP differences in the power function or + extremely minor issues.""" + + def __init__(self): + self.ulpdiff = 0 + self.powmod_zeros = 0 + self.maxctx = P.Context(Emax=10**18, Emin=-10**18) + + def default(self, t): + return False + __ge__ = __gt__ = __le__ = __lt__ = __ne__ = __eq__ = default + __reduce__ = __format__ = __repr__ = __str__ = default + + def harrison_ulp(self, dec): + """ftp://ftp.inria.fr/INRIA/publication/publi-pdf/RR/RR-5504.pdf""" + a = dec.next_plus() + b = dec.next_minus() + return abs(a - b) + + def standard_ulp(self, dec, prec): + return P._dec_from_triple(0, '1', dec._exp+len(dec._int)-prec) + + def rounding_direction(self, x, mode): + """Determine the effective direction of the rounding when + the exact result x is rounded according to mode. + Return -1 for downwards, 0 for undirected, 1 for upwards, + 2 for ROUND_05UP.""" + cmp = 1 if x.compare_total(P.Decimal("+0")) >= 0 else -1 + + if mode in (P.ROUND_HALF_EVEN, P.ROUND_HALF_UP, P.ROUND_HALF_DOWN): + return 0 + elif mode == P.ROUND_CEILING: + return 1 + elif mode == P.ROUND_FLOOR: + return -1 + elif mode == P.ROUND_UP: + return cmp + elif mode == P.ROUND_DOWN: + return -cmp + elif mode == P.ROUND_05UP: + return 2 + else: + raise ValueError("Unexpected rounding mode: %s" % mode) + + def check_ulpdiff(self, exact, rounded): + # current precision + p = context.p.prec + + # Convert infinities to the largest representable number + 1. + x = exact + if exact.is_infinite(): + x = P._dec_from_triple(exact._sign, '10', context.p.Emax) + y = rounded + if rounded.is_infinite(): + y = P._dec_from_triple(rounded._sign, '10', context.p.Emax) + + # err = (rounded - exact) / ulp(rounded) + self.maxctx.prec = p * 2 + t = self.maxctx.subtract(y, x) + if context.c.flags[C.Clamped] or \ + context.c.flags[C.Underflow]: + # The standard ulp does not work in Underflow territory. + ulp = self.harrison_ulp(y) + else: + ulp = self.standard_ulp(y, p) + # Error in ulps. + err = self.maxctx.divide(t, ulp) + + dir = self.rounding_direction(x, context.p.rounding) + if dir == 0: + if P.Decimal("-0.6") < err < P.Decimal("0.6"): + return True + elif dir == 1: # directed, upwards + if P.Decimal("-0.1") < err < P.Decimal("1.1"): + return True + elif dir == -1: # directed, downwards + if P.Decimal("-1.1") < err < P.Decimal("0.1"): + return True + else: # ROUND_05UP + if P.Decimal("-1.1") < err < P.Decimal("1.1"): + return True + + print("ulp: %s error: %s exact: %s c_rounded: %s" + % (ulp, err, exact, rounded)) + return False + + def bin_resolve_ulp(self, t): + """Check if results of _decimal's power function are within the + allowed ulp ranges.""" + # NaNs are beyond repair. + if t.rc.is_nan() or t.rp.is_nan(): + return False + + # "exact" result, double precision, half_even + self.maxctx.prec = context.p.prec * 2 + + op1, op2 = t.pop[0], t.pop[1] + if t.contextfunc: + exact = getattr(self.maxctx, t.funcname)(op1, op2) + else: + exact = getattr(op1, t.funcname)(op2, context=self.maxctx) + + # _decimal's rounded result + rounded = P.Decimal(t.cresults[0]) + + self.ulpdiff += 1 + return self.check_ulpdiff(exact, rounded) + + ############################ Correct rounding ############################# + def resolve_underflow(self, t): + """In extremely rare cases where the infinite precision result is just + below etiny, cdecimal does not set Subnormal/Underflow. Example: + + setcontext(Context(prec=21, rounding=ROUND_UP, Emin=-55, Emax=85)) + Decimal("1.00000000000000000000000000000000000000000000000" + "0000000100000000000000000000000000000000000000000" + "0000000000000025").ln() + """ + if t.cresults != t.presults: + return False # Results must be identical. + if context.c.flags[C.Rounded] and \ + context.c.flags[C.Inexact] and \ + context.p.flags[P.Rounded] and \ + context.p.flags[P.Inexact]: + return True # Subnormal/Underflow may be missing. + return False + + def exp(self, t): + """Resolve Underflow or ULP difference.""" + return self.resolve_underflow(t) + + def log10(self, t): + """Resolve Underflow or ULP difference.""" + return self.resolve_underflow(t) + + def ln(self, t): + """Resolve Underflow or ULP difference.""" + return self.resolve_underflow(t) + + def __pow__(self, t): + """Always calls the resolve function. C.Decimal does not have correct + rounding for the power function.""" + if context.c.flags[C.Rounded] and \ + context.c.flags[C.Inexact] and \ + context.p.flags[P.Rounded] and \ + context.p.flags[P.Inexact]: + return self.bin_resolve_ulp(t) + else: + return False + power = __rpow__ = __pow__ + + ############################## Technicalities ############################# + def __float__(self, t): + """NaN comparison in the verify() function obviously gives an + incorrect answer: nan == nan -> False""" + if t.cop[0].is_nan() and t.pop[0].is_nan(): + return True + return False + __complex__ = __float__ + + def __radd__(self, t): + """decimal.py gives precedence to the first NaN; this is + not important, as __radd__ will not be called for + two decimal arguments.""" + if t.rc.is_nan() and t.rp.is_nan(): + return True + return False + __rmul__ = __radd__ + + ################################ Various ################################## + def __round__(self, t): + """Exception: Decimal('1').__round__(-100000000000000000000000000) + Should it really be InvalidOperation?""" + if t.rc is None and t.rp.is_nan(): + return True + return False + +shandler = SkipHandler() +def skip_error(t): + return getattr(shandler, t.funcname, shandler.default)(t) + + +# ====================================================================== +# Handling verification errors +# ====================================================================== + +class VerifyError(Exception): + """Verification failed.""" + pass + +def function_as_string(t): + if t.contextfunc: + cargs = t.cop + pargs = t.pop + cfunc = "c_func: %s(" % t.funcname + pfunc = "p_func: %s(" % t.funcname + else: + cself, cargs = t.cop[0], t.cop[1:] + pself, pargs = t.pop[0], t.pop[1:] + cfunc = "c_func: %s.%s(" % (repr(cself), t.funcname) + pfunc = "p_func: %s.%s(" % (repr(pself), t.funcname) + + err = cfunc + for arg in cargs: + err += "%s, " % repr(arg) + err = err.rstrip(", ") + err += ")\n" + + err += pfunc + for arg in pargs: + err += "%s, " % repr(arg) + err = err.rstrip(", ") + err += ")" + + return err + +def raise_error(t): + global EXIT_STATUS + + if skip_error(t): + return + EXIT_STATUS = 1 + + err = "Error in %s:\n\n" % t.funcname + err += "input operands: %s\n\n" % (t.op,) + err += function_as_string(t) + err += "\n\nc_result: %s\np_result: %s\n\n" % (t.cresults, t.presults) + err += "c_exceptions: %s\np_exceptions: %s\n\n" % (t.cex, t.pex) + err += "%s\n\n" % str(t.context) + + raise VerifyError(err) + + +# ====================================================================== +# Main testing functions +# +# The procedure is always (t is the TestSet): +# +# convert(t) -> Initialize the TestSet as necessary. +# +# Return 0 for early abortion (e.g. if a TypeError +# occurs during conversion, there is nothing to test). +# +# Return 1 for continuing with the test case. +# +# callfuncs(t) -> Call the relevant function for each implementation +# and record the results in the TestSet. +# +# verify(t) -> Verify the results. If verification fails, details +# are printed to stdout. +# ====================================================================== + +def convert(t, convstr=True): + """ t is the testset. At this stage the testset contains a tuple of + operands t.op of various types. For decimal methods the first + operand (self) is always converted to Decimal. If 'convstr' is + true, string operands are converted as well. + + Context operands are of type deccheck.Context, rounding mode + operands are given as a tuple (C.rounding, P.rounding). + + Other types (float, int, etc.) are left unchanged. + """ + for i, op in enumerate(t.op): + + context.clear_status() + + if op in RoundModes: + t.cop.append(op) + t.pop.append(op) + + elif not t.contextfunc and i == 0 or \ + convstr and isinstance(op, str): + try: + c = C.Decimal(op) + cex = None + except (TypeError, ValueError, OverflowError) as e: + c = None + cex = e.__class__ + + try: + p = RestrictedDecimal(op) + pex = None + except (TypeError, ValueError, OverflowError) as e: + p = None + pex = e.__class__ + + t.cop.append(c) + t.cex.append(cex) + t.pop.append(p) + t.pex.append(pex) + + if cex is pex: + if str(c) != str(p) or not context.assert_eq_status(): + raise_error(t) + if cex and pex: + # nothing to test + return 0 + else: + raise_error(t) + + elif isinstance(op, Context): + t.context = op + t.cop.append(op.c) + t.pop.append(op.p) + + else: + t.cop.append(op) + t.pop.append(op) + + return 1 + +def callfuncs(t): + """ t is the testset. At this stage the testset contains operand lists + t.cop and t.pop for the C and Python versions of decimal. + For Decimal methods, the first operands are of type C.Decimal and + P.Decimal respectively. The remaining operands can have various types. + For Context methods, all operands can have any type. + + t.rc and t.rp are the results of the operation. + """ + context.clear_status() + + try: + if t.contextfunc: + cargs = t.cop + t.rc = getattr(context.c, t.funcname)(*cargs) + else: + cself = t.cop[0] + cargs = t.cop[1:] + t.rc = getattr(cself, t.funcname)(*cargs) + t.cex.append(None) + except (TypeError, ValueError, OverflowError, MemoryError) as e: + t.rc = None + t.cex.append(e.__class__) + + try: + if t.contextfunc: + pargs = t.pop + t.rp = getattr(context.p, t.funcname)(*pargs) + else: + pself = t.pop[0] + pargs = t.pop[1:] + t.rp = getattr(pself, t.funcname)(*pargs) + t.pex.append(None) + except (TypeError, ValueError, OverflowError, MemoryError) as e: + t.rp = None + t.pex.append(e.__class__) + +def verify(t, stat): + """ t is the testset. At this stage the testset contains the following + tuples: + + t.op: original operands + t.cop: C.Decimal operands (see convert for details) + t.pop: P.Decimal operands (see convert for details) + t.rc: C result + t.rp: Python result + + t.rc and t.rp can have various types. + """ + t.cresults.append(str(t.rc)) + t.presults.append(str(t.rp)) + if isinstance(t.rc, C.Decimal) and isinstance(t.rp, P.Decimal): + # General case: both results are Decimals. + t.cresults.append(t.rc.to_eng_string()) + t.cresults.append(t.rc.as_tuple()) + t.cresults.append(str(t.rc.imag)) + t.cresults.append(str(t.rc.real)) + t.presults.append(t.rp.to_eng_string()) + t.presults.append(t.rp.as_tuple()) + t.presults.append(str(t.rp.imag)) + t.presults.append(str(t.rp.real)) + + nc = t.rc.number_class().lstrip('+-s') + stat[nc] += 1 + else: + # Results from e.g. __divmod__ can only be compared as strings. + if not isinstance(t.rc, tuple) and not isinstance(t.rp, tuple): + if t.rc != t.rp: + raise_error(t) + stat[type(t.rc).__name__] += 1 + + # The return value lists must be equal. + if t.cresults != t.presults: + raise_error(t) + # The Python exception lists (TypeError, etc.) must be equal. + if t.cex != t.pex: + raise_error(t) + # The context flags must be equal. + if not t.context.assert_eq_status(): + raise_error(t) + + +# ====================================================================== +# Main test loops +# +# test_method(method, testspecs, testfunc) -> +# +# Loop through various context settings. The degree of +# thoroughness is determined by 'testspec'. For each +# setting, call 'testfunc'. Generally, 'testfunc' itself +# a loop, iterating through many test cases generated +# by the functions in randdec.py. +# +# test_n-ary(method, prec, exp_range, restricted_range, itr, stat) -> +# +# 'test_unary', 'test_binary' and 'test_ternary' are the +# main test functions passed to 'test_method'. They deal +# with the regular cases. The thoroughness of testing is +# determined by 'itr'. +# +# 'prec', 'exp_range' and 'restricted_range' are passed +# to the test-generating functions and limit the generated +# values. In some cases, for reasonable run times a +# maximum exponent of 9999 is required. +# +# The 'stat' parameter is passed down to the 'verify' +# function, which records statistics for the result values. +# ====================================================================== + +def log(fmt, args=None): + if args: + sys.stdout.write(''.join((fmt, '\n')) % args) + else: + sys.stdout.write(''.join((str(fmt), '\n'))) + sys.stdout.flush() + +def test_method(method, testspecs, testfunc): + """Iterate a test function through many context settings.""" + log("testing %s ...", method) + stat = defaultdict(int) + for spec in testspecs: + if 'samples' in spec: + spec['prec'] = sorted(random.sample(range(1, 101), + spec['samples'])) + for prec in spec['prec']: + context.prec = prec + for expts in spec['expts']: + emin, emax = expts + if emin == 'rand': + context.Emin = random.randrange(-1000, 0) + context.Emax = random.randrange(prec, 1000) + else: + context.Emin, context.Emax = emin, emax + if prec > context.Emax: continue + log(" prec: %d emin: %d emax: %d", + (context.prec, context.Emin, context.Emax)) + restr_range = 9999 if context.Emax > 9999 else context.Emax+99 + for rounding in RoundModes: + context.rounding = rounding + context.capitals = random.randrange(2) + if spec['clamp'] == 'rand': + context.clamp = random.randrange(2) + else: + context.clamp = spec['clamp'] + exprange = context.c.Emax + testfunc(method, prec, exprange, restr_range, + spec['iter'], stat) + log(" result types: %s" % sorted([t for t in stat.items()])) + +def test_unary(method, prec, exp_range, restricted_range, itr, stat): + """Iterate a unary function through many test cases.""" + if method in UnaryRestricted: + exp_range = restricted_range + for op in all_unary(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + + if not method.startswith('__'): + for op in unary_optarg(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def test_binary(method, prec, exp_range, restricted_range, itr, stat): + """Iterate a binary function through many test cases.""" + if method in BinaryRestricted: + exp_range = restricted_range + for op in all_binary(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + + if not method.startswith('__'): + for op in binary_optarg(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def test_ternary(method, prec, exp_range, restricted_range, itr, stat): + """Iterate a ternary function through many test cases.""" + if method in TernaryRestricted: + exp_range = restricted_range + for op in all_ternary(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + + if not method.startswith('__'): + for op in ternary_optarg(prec, exp_range, itr): + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def test_format(method, prec, exp_range, restricted_range, itr, stat): + """Iterate the __format__ method through many test cases.""" + for op in all_unary(prec, exp_range, itr): + fmt1 = rand_format(chr(random.randrange(32, 128)), 'EeGgn') + fmt2 = rand_locale() + for fmt in (fmt1, fmt2): + fmtop = (op[0], fmt) + t = TestSet(method, fmtop) + try: + if not convert(t, convstr=False): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + for op in all_unary(prec, 9999, itr): + fmt1 = rand_format(chr(random.randrange(32, 128)), 'Ff%') + fmt2 = rand_locale() + for fmt in (fmt1, fmt2): + fmtop = (op[0], fmt) + t = TestSet(method, fmtop) + try: + if not convert(t, convstr=False): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def test_round(method, prec, exprange, restricted_range, itr, stat): + """Iterate the __round__ method through many test cases.""" + for op in all_unary(prec, 9999, itr): + n = random.randrange(10) + roundop = (op[0], n) + t = TestSet(method, roundop) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def test_from_float(method, prec, exprange, restricted_range, itr, stat): + """Iterate the __float__ method through many test cases.""" + for rounding in RoundModes: + context.rounding = rounding + for i in range(1000): + f = randfloat() + op = (f,) if method.startswith("context.") else ("sNaN", f) + t = TestSet(method, op) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + +def randcontext(exprange): + c = Context(C.Context(), P.Context()) + c.Emax = random.randrange(1, exprange+1) + c.Emin = random.randrange(-exprange, 0) + maxprec = 100 if c.Emax >= 100 else c.Emax + c.prec = random.randrange(1, maxprec+1) + c.clamp = random.randrange(2) + c.clear_traps() + return c + +def test_quantize_api(method, prec, exprange, restricted_range, itr, stat): + """Iterate the 'quantize' method through many test cases, using + the optional arguments.""" + for op in all_binary(prec, restricted_range, itr): + for rounding in RoundModes: + c = randcontext(exprange) + quantizeop = (op[0], op[1], rounding, c) + t = TestSet(method, quantizeop) + try: + if not convert(t): + continue + callfuncs(t) + verify(t, stat) + except VerifyError as err: + log(err) + + +def check_untested(funcdict, c_cls, p_cls): + """Determine untested, C-only and Python-only attributes. + Uncomment print lines for debugging.""" + c_attr = set(dir(c_cls)) + p_attr = set(dir(p_cls)) + intersect = c_attr & p_attr + + funcdict['c_only'] = tuple(sorted(c_attr-intersect)) + funcdict['p_only'] = tuple(sorted(p_attr-intersect)) + + tested = set() + for lst in funcdict.values(): + for v in lst: + v = v.replace("context.", "") if c_cls == C.Context else v + tested.add(v) + + funcdict['untested'] = tuple(sorted(intersect-tested)) + + #for key in ('untested', 'c_only', 'p_only'): + # s = 'Context' if c_cls == C.Context else 'Decimal' + # print("\n%s %s:\n%s" % (s, key, funcdict[key])) + + +if __name__ == '__main__': + + import time + + randseed = int(time.time()) + random.seed(randseed) + + # Set up the testspecs list. A testspec is simply a dictionary + # that determines the amount of different contexts that 'test_method' + # will generate. + base_expts = [(C.MIN_EMIN, C.MAX_EMAX)] + if C.MAX_EMAX == 999999999999999999: + base_expts.append((-999999999, 999999999)) + + # Basic contexts. + base = { + 'expts': base_expts, + 'prec': [], + 'clamp': 'rand', + 'iter': None, + 'samples': None, + } + # Contexts with small values for prec, emin, emax. + small = { + 'prec': [1, 2, 3, 4, 5], + 'expts': [(-1, 1), (-2, 2), (-3, 3), (-4, 4), (-5, 5)], + 'clamp': 'rand', + 'iter': None + } + # IEEE interchange format. + ieee = [ + # DECIMAL32 + {'prec': [7], 'expts': [(-95, 96)], 'clamp': 1, 'iter': None}, + # DECIMAL64 + {'prec': [16], 'expts': [(-383, 384)], 'clamp': 1, 'iter': None}, + # DECIMAL128 + {'prec': [34], 'expts': [(-6143, 6144)], 'clamp': 1, 'iter': None} + ] + + if '--medium' in sys.argv: + base['expts'].append(('rand', 'rand')) + # 5 random precisions + base['samples'] = 5 + testspecs = [small] + ieee + [base] + if '--long' in sys.argv: + base['expts'].append(('rand', 'rand')) + # 10 random precisions + base['samples'] = 10 + testspecs = [small] + ieee + [base] + elif '--all' in sys.argv: + base['expts'].append(('rand', 'rand')) + # All precisions in [1, 100] + base['samples'] = 100 + testspecs = [small] + ieee + [base] + else: # --short + rand_ieee = random.choice(ieee) + base['iter'] = small['iter'] = rand_ieee['iter'] = 1 + # 1 random precision and exponent pair + base['samples'] = 1 + base['expts'] = [random.choice(base_expts)] + # 1 random precision and exponent pair + prec = random.randrange(1, 6) + small['prec'] = [prec] + small['expts'] = [(-prec, prec)] + testspecs = [small, rand_ieee, base] + + check_untested(Functions, C.Decimal, P.Decimal) + check_untested(ContextFunctions, C.Context, P.Context) + + + log("\n\nRandom seed: %d\n\n", randseed) + + # Decimal methods: + for method in Functions['unary'] + Functions['unary_ctx'] + \ + Functions['unary_rnd_ctx']: + test_method(method, testspecs, test_unary) + + for method in Functions['binary'] + Functions['binary_ctx']: + test_method(method, testspecs, test_binary) + + for method in Functions['ternary'] + Functions['ternary_ctx']: + test_method(method, testspecs, test_ternary) + + test_method('__format__', testspecs, test_format) + test_method('__round__', testspecs, test_round) + test_method('from_float', testspecs, test_from_float) + test_method('quantize', testspecs, test_quantize_api) + + # Context methods: + for method in ContextFunctions['unary']: + test_method(method, testspecs, test_unary) + + for method in ContextFunctions['binary']: + test_method(method, testspecs, test_binary) + + for method in ContextFunctions['ternary']: + test_method(method, testspecs, test_ternary) + + test_method('context.create_decimal_from_float', testspecs, test_from_float) + + + sys.exit(EXIT_STATUS) |