Testing sequence of if == string statements

Sometimes in programs we see the following pattern

if X == "string_1":
    pass
elif X == "string_2":
    pass
elif X == "string_3":
    pass

X is compared to several allowed values of type string. It is important to notice that X accepts a descrete set of allowed values. When we forget to test with string values outside the allowed set mutation testing will produce surviving mutants.

Note

The order of if statements isn’t important.

Reproducer

$ pip install nose
$ pip install https://github.com/sixty-north/cosmic-ray/zipball/master
$ celery -A cosmic_ray.tasks.worker worker

$ cosmic-ray run --test-runner nose --baseline=10 example.json selinux_str.py -- tests_str.py:Test_mode_from_str
$ cosmic-ray report example.json

job ID 4:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 2 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -3,7 +3,7 @@

 def mode_from_str(str_mode):
     retval = None
-    if (str_mode == 'disabled'):
+    if (str_mode <= 'disabled'):
         retval = modes.SELINUX_DISABLED
     elif (str_mode == 'enforcing'):
         retval = modes.SELINUX_ENFORCING

job ID 8:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 6 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -3,7 +3,7 @@

 def mode_from_str(str_mode):
     retval = None
-    if (str_mode == 'disabled'):
+    if (str_mode in 'disabled'):
         retval = modes.SELINUX_DISABLED
     elif (str_mode == 'enforcing'):
         retval = modes.SELINUX_ENFORCING

job ID 12:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 10 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -5,7 +5,7 @@
     retval = None
     if (str_mode == 'disabled'):
         retval = modes.SELINUX_DISABLED
-    elif (str_mode == 'enforcing'):
+    elif (str_mode <= 'enforcing'):
         retval = modes.SELINUX_ENFORCING
     elif (str_mode == 'permissive'):
         retval = modes.SELINUX_PERMISSIVE

job ID 16:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 14 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -5,7 +5,7 @@
     retval = None
     if (str_mode == 'disabled'):
         retval = modes.SELINUX_DISABLED
-    elif (str_mode == 'enforcing'):
+    elif (str_mode in 'enforcing'):
         retval = modes.SELINUX_ENFORCING
     elif (str_mode == 'permissive'):
         retval = modes.SELINUX_PERMISSIVE

job ID 20:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 18 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -7,7 +7,7 @@
         retval = modes.SELINUX_DISABLED
     elif (str_mode == 'enforcing'):
         retval = modes.SELINUX_ENFORCING
-    elif (str_mode == 'permissive'):
+    elif (str_mode <= 'permissive'):
         retval = modes.SELINUX_PERMISSIVE
     return retval


job ID 22:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 20 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -7,7 +7,7 @@
         retval = modes.SELINUX_DISABLED
     elif (str_mode == 'enforcing'):
         retval = modes.SELINUX_ENFORCING
-    elif (str_mode == 'permissive'):
+    elif (str_mode >= 'permissive'):
         retval = modes.SELINUX_PERMISSIVE
     return retval


job ID 24:Outcome.SURVIVED:selinux_str
command: cosmic-ray worker selinux_str mutate_comparison_operator 22 nose -- tests_str.py:Test_mode_from_str
--- mutation diff ---
--- a/example_08/selinux_str.py
+++ b/example_08/selinux_str.py
@@ -7,7 +7,7 @@
         retval = modes.SELINUX_DISABLED
     elif (str_mode == 'enforcing'):
         retval = modes.SELINUX_ENFORCING
-    elif (str_mode == 'permissive'):
+    elif (str_mode in 'permissive'):
         retval = modes.SELINUX_PERMISSIVE
     return retval


total jobs: 25
complete: 25 (100.00%)
survival rate: 28.00%

Killing the mutants

To kill some mutants we need to test with values outside the allowed set

$ cosmic-ray run --test-runner nose --baseline=10 example.json selinux_str.py -- tests_str.py:TestCompletely
$ cosmic-ray report example.json

Remaining mutants

When testing string comparisons the ==/in mutations are equivalent and can not be killed:

-    elif (str_mode == 'permissive'):
+    elif (str_mode in 'permissive'):

Source code

modes.py
SELINUX_DISABLED = 0
SELINUX_ENFORCING = 1
SELINUX_PERMISSIVE = 2
selinux_str.py
import modes

def mode_from_str(str_mode):
    retval = None

    if str_mode == "disabled":
        retval = modes.SELINUX_DISABLED
    elif str_mode == "enforcing":
        retval = modes.SELINUX_ENFORCING
    elif str_mode == "permissive":
        retval = modes.SELINUX_PERMISSIVE

    return retval
tests_str.py
import modes
import selinux_str
import unittest

class Test_mode_from_str(unittest.TestCase):
    def test_with_disabled(self):
        m = selinux_str.mode_from_str("disabled")
        self.assertEqual(m, modes.SELINUX_DISABLED)

    def test_with_enforcing(self):
        m = selinux_str.mode_from_str("enforcing")
        self.assertEqual(m, modes.SELINUX_ENFORCING)

    def test_with_permissive(self):
        m = selinux_str.mode_from_str("permissive")
        self.assertEqual(m, modes.SELINUX_PERMISSIVE)


class TestCompletely(Test_mode_from_str):
    def test_with_values_outside_set(self):
        for mode in ['aaaaa', 'zzzzz']:
            m = selinux_str.mode_from_str(mode)
            self.assertEqual(m, None)

if __name__ == "__main__":
    unittest.main()