# Demonstrate coverage and tolerances in test.test_math # # This program runs the regression test for the math module, # but on modified versions of it to check coverage and the # detection of inaccuracies. import math, io, random # False to demonstrate inadequacies in the stdlib test. # True to test the error message generation in the local one. USE_LOCAL_TEST_MATH = False if USE_LOCAL_TEST_MATH: import test_math RELATIVE_FUZZ = 1e-15 else: import test.test_math as test_math RELATIVE_FUZZ = 1e-10 class LoggedCallable: "Wrap s real function of one argument so that it logs calls" def __init__(self, f, name): self.f = f self.name = name self.pos = [] self.neg = [] self.other = [] def __call__(self, *args): if len(args)==1 and isinstance(args[0], float): x = args[0] if not math.isfinite(x) or x == 0.0: # Use str to finesse nan != nan and -0.0 == 0.0 self.other.append(str(x)) elif x > 0.0: self.pos.append(x) else: self.neg.append(x) return self.f(*args) def formatted_lists(self, fmt, limit=None): def format_element(x): return format(x, fmt) if isinstance(x, float) else str(x) if limit: m = limit//2 n = limit - m def format_list(list_name, args, s): args = sorted(set(args)) count = len(args) if limit and count > limit: args[n:-m] = ['...'] args = map(format_element, args) print(' {:>5s}:{:3d} args [{}]'.format( list_name, count, ', '.join(args)), file=s) with io.StringIO() as s: print(self.name, file=s) format_list( '+ve', self.pos, s) format_list( '-ve', self.neg, s) format_list( 'other', self.other, s) return s.getvalue() class LoggedMath: "A proxy for the stdlib math with logging on specified targets." def __init__(self, targets): self.targets = [] for name in targets: f = LoggedCallable(getattr(math, name), name) setattr(self, name, f) self.targets.append(f) def __getattr__(self, name): # non-targets return getattr(math, name) def print(self, fmt='.4g', limit=None): for f in self.targets: print(f.formatted_lists(fmt, limit)) class InaccurateCallable: "Wrap a function so that it becomes inaccurate" def __init__(self, f, error): self.f = f self.error = abs(error) def __call__(self, *args): r = self.f(*args) if math.isfinite(r): return r + r*random.uniform(-self.error, self.error) if USE_LOCAL_TEST_MATH: # Fuzz the inf and nan results too, to test our messages if math.isinf(r): return -r elif math.isnan(r): return 0.0 return r class InaccurateMath: "A proxy for the stdlib math where specified targets are inaccurate." def __init__(self, targets, error=1e-10): for name in targets: f = InaccurateCallable(getattr(math, name), error) setattr(self, name, f) def __getattr__(self, name): # non-targets return getattr(math, name) def check_coverage(): # Log arguments to these functions as used by test_math targets = [ 'sqrt', 'sin', 'asin', 'cos', 'acos', 'tan', 'atan', 'exp', 'expm1', 'log', 'log10', 'log1p', 'sinh', 'asinh', 'cosh', 'acosh', 'tanh', 'atanh', 'erf', 'erfc', 'gamma', 'lgamma', 'floor', 'ceil', ] m = LoggedMath(targets) test_math.math = m # run the test test_math.test_main() # dump the coverage stats m.print('.4g', 10) def check_tolerance(): # Fuzz the results of these functions tested by test_math targets = [ #'sqrt', # Exact result required in stdlib tests 'sin', 'asin', 'cos', 'acos', 'tan', 'atan', 'exp', #'expm1', # 20 ulps required in stdlib test 'log', 'log10', #'log1p', # 20 ulps required in stdlib test 'sinh', 'asinh', 'cosh', 'acosh', #'tanh', # Only testTanhSign will fail if fuzzed 'atanh', #'erf', # 20 ulps required in stdlib test #'erfc', # 2000 ulps required in stdlib test #'gamma', # 20 ulps required in stdlib test #'lgamma', # Tested with rel_err = 5e-15, abs_err = 5e-15 #'floor', 'ceil', # Fail to be int when fuzzed ] if USE_LOCAL_TEST_MATH: targets += [ 'sqrt', # Exact result required in stdlib tests 'expm1', # 20 ulps required in stdlib test 'log1p', # 20 ulps required in stdlib test 'tanh', # Only testTanhSign will fail if fuzzed 'erf', # 20 ulps required in stdlib test 'erfc', # 2000 ulps required in stdlib test 'gamma', # 20 ulps required in stdlib test 'lgamma', # Tested with rel_err = 5e-15, abs_err = 5e-15 'floor', 'ceil', # Fail to be int when fuzzed ] test_math.math = InaccurateMath(targets, RELATIVE_FUZZ) # run the test test_math.test_main() if __name__ == '__main__': #check_coverage() check_tolerance()