boxed/mutmut

Generation of unary plus in exponent is un-killable

Closed this issue · 7 comments

Hello, thanks for the lovely tool!

mutmut generates mutants like the following, which are identical to the original code and thus un-killable (immortal?):

--- mvce/mvce.py
+++ mvce/mvce.py
@@ -1,3 +1,3 @@
 def multiply_by_1e100(number):
-    return number * 1e100
+    return number * 1e+100

Here's a test so that you can reproduce the whole thing:

from mvce import multiply_by_1e100


def test_multiply_by_1e100():
    assert multiply_by_1e100(4) == 4e100

1e16 seems to be the smallest number to which this happens. Smaller numbers get expanded from e.g. 1e2 to 101.0

boxed commented

Interesting! Yea this is because 1+1e16 = 1e16. The mutation code should try to increase it with exponentially bigger and bigger values until the values actually change I think. You want to give it a shot to implement this?

Sure, that could just be something like

exponent = 0
result = parsed + (10 ** exponent)
while result == parsed:
    exponent += 1
    result = parsed + (10 ** exponent)

result = repr(result)

which would always result in numbers differing by 1e-16 for any number >= 1e16. This could be a problem for numerical software which might be using numpy.isclose with a much looser precision.

So maybe something like:

if parsed == 0:
   result = repr(parsed + 1)
else:
   result = repr(parsed * 2)
boxed commented

Yea, that seems to do the right thing.

This could be a problem for numerical software which might be using numpy.isclose with a much looser precision.

Well.. maybe. But then I think we can wait for that bug report :P Seems like you'd have to have VERY loose boundaries for that big numbers to be "close" though, right?

I am preemptively reporting that bug then :)

numpy.isclose has default relative and absolute tolerances of 1e-5 and 1e-8 respectively, so my first suggestion above would not get killed. Changing the loop check to while np.isclose(result, parsed) results in 1.0001e+16.

I figure you probably don't want a dependency on numpy, so probably the simplest thing is my second suggestion

boxed commented

The second suggestion is too big of a hammer I think. It will change 7.0 to 14.0 right? That seems a bit much to me.

This is true, but the current version also changes any number smaller than 1e-16 to 1.0 which is comparatively much larger!

What is an appropriate change for numbers? For int, changing by one definitely seems like a good idea, as it helps catch off-by-one errors. For floats, I'm not so sure what would be appropriate across the entire domain -- adding a constant feels wrong for both very small and very large numbers. Multiplication by a constant feels safer to me, although zero needs special handling.

How about:

if 1e-5 < abs(parsed) < 1e5:
   result = repr(parsed + 1)
else:
   result = repr(parsed * 2)  # or 1.1 perhaps
boxed commented

Hmm. All good points! Yea ok then any of your suggestions sound great. You clearly have thought this through more than me :)