1101 lines
38 KiB
Python
1101 lines
38 KiB
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
|
|
from _pydecimal import _dec_from_triple
|
|
|
|
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__', '__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 _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 = _dec_from_triple(exact._sign, '10', context.p.Emax)
|
|
y = rounded
|
|
if rounded.is_infinite():
|
|
y = _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(0, 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(0, 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)
|