Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Modules/_decimal/tests/deccheck.py
12 views
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
33
import random
34
import time
35
36
RANDSEED = int(time.time())
37
random.seed(RANDSEED)
38
39
import sys
40
import os
41
from copy import copy
42
from collections import defaultdict
43
44
import argparse
45
import subprocess
46
from subprocess import PIPE, STDOUT
47
from queue import Queue, Empty
48
from threading import Thread, Event, Lock
49
50
from test.support.import_helper import import_fresh_module
51
from randdec import randfloat, all_unary, all_binary, all_ternary
52
from randdec import unary_optarg, binary_optarg, ternary_optarg
53
from formathelper import rand_format, rand_locale
54
from _pydecimal import _dec_from_triple
55
56
C = import_fresh_module('decimal', fresh=['_decimal'])
57
P = import_fresh_module('decimal', blocked=['_decimal'])
58
EXIT_STATUS = 0
59
60
61
# Contains all categories of Decimal methods.
62
Functions = {
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.
108
ContextFunctions = {
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.
145
MaxContextSkip = ['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.
150
UnaryRestricted = [
151
'__ceil__', '__floor__', '__int__', '__trunc__',
152
'as_integer_ratio', 'to_integral', 'to_integral_value'
153
]
154
155
BinaryRestricted = ['__round__']
156
157
TernaryRestricted = ['__pow__', 'context.power']
158
159
160
# ======================================================================
161
# Unified Context
162
# ======================================================================
163
164
# Translate symbols.
165
CondMap = {
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
181
RoundModes = [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
186
class 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.
294
context = Context()
295
context.Emin = C.MIN_EMIN
296
context.Emax = C.MAX_EMAX
297
context.clear_traps()
298
299
# When creating decimals, _decimal is ultimately limited by the maximum
300
# context values. We emulate this restriction for decimal.py.
301
maxcontext = 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
)
308
maxcontext.clamp = 0
309
310
def 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
330
class 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
341
class 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
387
class 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
566
shandler = SkipHandler()
567
def skip_error(t):
568
return getattr(shandler, t.funcname, shandler.default)(t)
569
570
571
# ======================================================================
572
# Handling verification errors
573
# ======================================================================
574
575
class VerifyError(Exception):
576
"""Verification failed."""
577
pass
578
579
def 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
617
def 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
668
def 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
675
def 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
758
def 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
825
def 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
923
def 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
930
def 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
963
def 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
988
def 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
1013
def 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
1038
def 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
1067
def 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
1081
def 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
1097
def 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
1107
def 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
1124
def 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
1147
if __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
1324