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