• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
32import sys, random
33from copy import copy
34from collections import defaultdict
35from test.support import import_fresh_module
36from randdec import randfloat, all_unary, all_binary, all_ternary
37from randdec import unary_optarg, binary_optarg, ternary_optarg
38from formathelper import rand_format, rand_locale
39from _pydecimal import _dec_from_triple
40
41C = import_fresh_module('decimal', fresh=['_decimal'])
42P = import_fresh_module('decimal', blocked=['_decimal'])
43EXIT_STATUS = 0
44
45
46# Contains all categories of Decimal methods.
47Functions = {
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.
93ContextFunctions = {
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.
129UnaryRestricted = [
130  '__ceil__', '__floor__', '__int__', '__trunc__',
131  'as_integer_ratio', 'to_integral', 'to_integral_value'
132]
133
134BinaryRestricted = ['__round__']
135
136TernaryRestricted = ['__pow__', 'context.power']
137
138
139# ======================================================================
140#                            Unified Context
141# ======================================================================
142
143# Translate symbols.
144CondMap = {
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
160RoundModes = [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
165class 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.
273context = Context()
274context.Emin = C.MIN_EMIN
275context.Emax = C.MAX_EMAX
276context.clear_traps()
277
278# When creating decimals, _decimal is ultimately limited by the maximum
279# context values. We emulate this restriction for decimal.py.
280maxcontext = 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)
287maxcontext.clamp = 0
288
289def 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
309class 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
320class 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
352class 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
531shandler = SkipHandler()
532def skip_error(t):
533    return getattr(shandler, t.funcname, shandler.default)(t)
534
535
536# ======================================================================
537#                      Handling verification errors
538# ======================================================================
539
540class VerifyError(Exception):
541    """Verification failed."""
542    pass
543
544def 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
570def 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
606def 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
666def 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
703def 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
775def 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
782def 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
815def 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
840def 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
865def 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
890def 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
919def 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
933def 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
949def 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
959def 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
976def 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
999if __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