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