From cf69431a586ce2b80043681e16a2039f25f21150 Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Fri, 9 May 2025 02:02:08 +0300 Subject: [PATCH 1/4] feat(str_mut): add .upper() .lower() and .capitalize() as mutations + tests --- mutmut/node_mutation.py | 10 +++++++++- tests/e2e/snapshots/config.json | 5 ++++- tests/e2e/snapshots/my_lib.json | 17 ++++++++++++++++- tests/test_mutation.py | 8 ++++---- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 6cd37e87..05df1fae 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -37,7 +37,15 @@ def operator_string( # that mutation is meaningless for return - yield node.with_changes(value=f"{prefix}{value[0]}XX{value[1:-1]}XX{value[-1]}") + supported_str_mutations: list[Callable[[str], str]] = [ + lambda x: "XX" + x + "XX", + lambda x: x.lower(), + lambda x: x.upper(), + lambda x: x.capitalize(), + ] + + for mut_func in supported_str_mutations: + yield node.with_changes(value=f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}") def operator_lambda( diff --git a/tests/e2e/snapshots/config.json b/tests/e2e/snapshots/config.json index 8833e5ea..a4fddddc 100644 --- a/tests/e2e/snapshots/config.json +++ b/tests/e2e/snapshots/config.json @@ -1,6 +1,9 @@ { "mutants/config_pkg/__init__.py.meta": { - "config_pkg.x_hello__mutmut_1": 1 + "config_pkg.x_hello__mutmut_1": 1, + "config_pkg.x_hello__mutmut_2": 1, + "config_pkg.x_hello__mutmut_3": 1, + "config_pkg.x_hello__mutmut_4": 0 }, "mutants/config_pkg/math.py.meta": { "config_pkg.math.x_add__mutmut_1": 0, diff --git a/tests/e2e/snapshots/my_lib.json b/tests/e2e/snapshots/my_lib.json index a75a1f7f..a7b92a96 100644 --- a/tests/e2e/snapshots/my_lib.json +++ b/tests/e2e/snapshots/my_lib.json @@ -1,11 +1,26 @@ { "mutants/src/my_lib/__init__.py.meta": { "my_lib.x_hello__mutmut_1": 1, + "my_lib.x_hello__mutmut_2": 1, + "my_lib.x_hello__mutmut_3": 1, + "my_lib.x_hello__mutmut_4": 0, "my_lib.x_badly_tested__mutmut_1": 0, + "my_lib.x_badly_tested__mutmut_2": 0, + "my_lib.x_badly_tested__mutmut_3": 0, + "my_lib.x_badly_tested__mutmut_4": 0, "my_lib.x_untested__mutmut_1": 33, + "my_lib.x_untested__mutmut_2": 33, + "my_lib.x_untested__mutmut_3": 33, + "my_lib.x_untested__mutmut_4": 33, "my_lib.x_make_greeter__mutmut_1": 1, "my_lib.x_make_greeter__mutmut_2": 1, - "my_lib.x_make_greeter__mutmut_3": 0, + "my_lib.x_make_greeter__mutmut_3": 1, + "my_lib.x_make_greeter__mutmut_4": 0, + "my_lib.x_make_greeter__mutmut_5": 1, + "my_lib.x_make_greeter__mutmut_6": 0, + "my_lib.x_make_greeter__mutmut_7": 0, + "my_lib.x_make_greeter__mutmut_8": 0, + "my_lib.x_make_greeter__mutmut_9": 0, "my_lib.x_fibonacci__mutmut_1": 1, "my_lib.x_fibonacci__mutmut_2": 0, "my_lib.x_fibonacci__mutmut_3": 0, diff --git a/tests/test_mutation.py b/tests/test_mutation.py index f469cb94..5644137c 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -58,7 +58,7 @@ def mutated_module(source: str) -> str: ('x: list[A | None]', []), ('a: Optional[int] = None', 'a: Optional[int] = ""'), ('a: int = 1', ['a: int = 2', 'a: int = None']), - ('a: str = "foo"', ['a: str = "XXfooXX"', 'a: str = None']), + ('a: str = "FoO"', ['a: str = "XXFoOXX"', 'a: str = "foo"', 'a: str = "FOO"', 'a: str = "Foo"', 'a: str = None']), ('lambda: 0', ['lambda: 1', 'lambda: None']), ("1 in (1, 2)", ['2 in (1, 2)', '1 not in (1, 2)', '1 in (2, 2)', '1 in (1, 3)']), ('1+1', ['2+1', '1 - 1', '1+2']), @@ -81,9 +81,9 @@ def mutated_module(source: str) -> str: ('1e-3', '1.001'), ('True', 'False'), ('False', 'True'), - ('"foo"', '"XXfooXX"'), - ("'foo'", "'XXfooXX'"), - ("u'foo'", "u'XXfooXX'"), + ('"FoO"', ['"XXFoOXX"', '"foo"', '"FOO"', '"Foo"']), + ("'FoO'", ["'XXFoOXX'", "'foo'", "'FOO'", "'Foo'"]), + ("u'FoO'", ["u'XXFoOXX'", "u'foo'", "u'FOO'", "u'Foo'"]), ("10", "11"), ("10.", "11.0"), ("0o10", "9"), From 95988cc635871ac8d82979cc490b9523f4106e82 Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Fri, 9 May 2025 02:16:51 +0300 Subject: [PATCH 2/4] fix(str_mut): add check on useless mutants --- mutmut/node_mutation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 05df1fae..13f32361 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -45,6 +45,8 @@ def operator_string( ] for mut_func in supported_str_mutations: + if mut_func(value[1:-1]) == value[1:-1]: # because mutant which do nothing is useless + continue yield node.with_changes(value=f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}") From 4ba180beb52286db2809f61aa33b48485eed8d89 Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Fri, 9 May 2025 15:27:13 +0300 Subject: [PATCH 3/4] style(str_mut): clear code and fix tests --- mutmut/node_mutation.py | 5 +++-- tests/e2e/snapshots/config.json | 3 +-- tests/e2e/snapshots/my_lib.json | 9 ++------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 13f32361..84a69c0d 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -45,9 +45,10 @@ def operator_string( ] for mut_func in supported_str_mutations: - if mut_func(value[1:-1]) == value[1:-1]: # because mutant which do nothing is useless + new_value = f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}" + if new_value == value: continue - yield node.with_changes(value=f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}") + yield node.with_changes(value=new_value) def operator_lambda( diff --git a/tests/e2e/snapshots/config.json b/tests/e2e/snapshots/config.json index a4fddddc..1061f7c1 100644 --- a/tests/e2e/snapshots/config.json +++ b/tests/e2e/snapshots/config.json @@ -2,8 +2,7 @@ "mutants/config_pkg/__init__.py.meta": { "config_pkg.x_hello__mutmut_1": 1, "config_pkg.x_hello__mutmut_2": 1, - "config_pkg.x_hello__mutmut_3": 1, - "config_pkg.x_hello__mutmut_4": 0 + "config_pkg.x_hello__mutmut_3": 1 }, "mutants/config_pkg/math.py.meta": { "config_pkg.math.x_add__mutmut_1": 0, diff --git a/tests/e2e/snapshots/my_lib.json b/tests/e2e/snapshots/my_lib.json index a7b92a96..31ad76ba 100644 --- a/tests/e2e/snapshots/my_lib.json +++ b/tests/e2e/snapshots/my_lib.json @@ -3,24 +3,19 @@ "my_lib.x_hello__mutmut_1": 1, "my_lib.x_hello__mutmut_2": 1, "my_lib.x_hello__mutmut_3": 1, - "my_lib.x_hello__mutmut_4": 0, "my_lib.x_badly_tested__mutmut_1": 0, "my_lib.x_badly_tested__mutmut_2": 0, "my_lib.x_badly_tested__mutmut_3": 0, - "my_lib.x_badly_tested__mutmut_4": 0, "my_lib.x_untested__mutmut_1": 33, "my_lib.x_untested__mutmut_2": 33, "my_lib.x_untested__mutmut_3": 33, - "my_lib.x_untested__mutmut_4": 33, "my_lib.x_make_greeter__mutmut_1": 1, "my_lib.x_make_greeter__mutmut_2": 1, "my_lib.x_make_greeter__mutmut_3": 1, - "my_lib.x_make_greeter__mutmut_4": 0, - "my_lib.x_make_greeter__mutmut_5": 1, + "my_lib.x_make_greeter__mutmut_4": 1, + "my_lib.x_make_greeter__mutmut_5": 0, "my_lib.x_make_greeter__mutmut_6": 0, "my_lib.x_make_greeter__mutmut_7": 0, - "my_lib.x_make_greeter__mutmut_8": 0, - "my_lib.x_make_greeter__mutmut_9": 0, "my_lib.x_fibonacci__mutmut_1": 1, "my_lib.x_fibonacci__mutmut_2": 0, "my_lib.x_fibonacci__mutmut_3": 0, From 1ed24ec891becb42f51a2d74035f3385b952c3bb Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Fri, 9 May 2025 23:58:40 +0300 Subject: [PATCH 4/4] feat(str_call_mut): add str methods call mutations and tests --- mutmut/node_mutation.py | 30 ++++++++++++++++++++++++++++++ tests/test_mutation.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 84a69c0d..a9218c87 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -96,6 +96,35 @@ def operator_arg_removal( yield node.with_changes(args=[*node.args[:i], *node.args[i + 1 :]]) +supported_str_methods_swap = [ + ("lower", "upper"), + ("upper", "lower"), + ("lstrip", "rstrip"), + ("rstrip", "lstrip"), + ("find", "rfind"), + ("rfind", "find"), + ("ljust", "rjust"), + ("rjust", "ljust"), + ("index", "rindex"), + ("rindex", "index"), + ("split", "rsplit"), + ("rsplit", "split"), + ("removeprefix", "removesuffix"), + ("removesuffix", "removeprefix"), + ("partition", "rpartition"), + ("rpartition", "partition") + ] + +def operator_string_methods_swap( + node: cst.Call +) -> Iterable[cst.Call]: + """try to swap string method to opposite e.g. a.lower() -> a.upper()""" + + for old_call, new_call in supported_str_methods_swap: + if m.matches(node.func, m.Attribute(value=m.DoNotCare(), attr=m.Name(value=old_call))): + yield node.with_changes(func=cst.Attribute(value=node.func.value, attr=cst.Name(value=new_call))) + + def operator_remove_unary_ops( node: cst.UnaryOperation ) -> Iterable[cst.BaseExpression]: @@ -208,6 +237,7 @@ def operator_match(node: cst.Match) -> Iterable[cst.CSTNode]: (cst.UnaryOperation, operator_remove_unary_ops), (cst.Call, operator_dict_arguments), (cst.Call, operator_arg_removal), + (cst.Call, operator_string_methods_swap), (cst.Lambda, operator_lambda), (cst.CSTNode, operator_keywords), (cst.CSTNode, operator_swap_op), diff --git a/tests/test_mutation.py b/tests/test_mutation.py index 5644137c..fe3e1bb1 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -40,6 +40,32 @@ def mutated_module(source: str) -> str: # ('break', 'continue'), # probably a bad idea. Can introduce infinite loops. ('break', 'return'), ('continue', 'break'), + ('a.lower()', 'a.upper()'), + ('a.upper()', 'a.lower()'), + ('a.lstrip("!")', ['a.rstrip("!")', 'a.lstrip("XX!XX")', 'a.lstrip(None)']), + ('a.rstrip("!")', ['a.lstrip("!")', 'a.rstrip("XX!XX")', 'a.rstrip(None)']), + ('a.find("!")', ['a.rfind("!")', 'a.find("XX!XX")', 'a.find(None)']), + ('a.rfind("!")', ['a.find("!")', 'a.rfind("XX!XX")', 'a.rfind(None)']), + ('a.ljust(10, "+")', [ + 'a.ljust("+")', 'a.ljust(10, "XX+XX")', + 'a.ljust(10, )', 'a.ljust(10, None)', + 'a.ljust(11, "+")', 'a.ljust(None, "+")', + 'a.rjust(10, "+")' + ]), + ('a.rjust(10, "+")', [ + 'a.ljust(10, "+")', 'a.rjust("+")', + 'a.rjust(10, "XX+XX")', 'a.rjust(10, )', + 'a.rjust(10, None)', 'a.rjust(11, "+")', + 'a.rjust(None, "+")' + ]), + ('a.index("+")', ['a.rindex("+")', 'a.index("XX+XX")', 'a.index(None)']), + ('a.rindex("+")', ['a.index("+")', 'a.rindex("XX+XX")', 'a.rindex(None)']), + ('a.split()', 'a.rsplit()'), + ('a.rsplit()', 'a.split()'), + ('a.removeprefix("+")', ['a.removesuffix("+")', 'a.removeprefix("XX+XX")', 'a.removeprefix(None)']), + ('a.removesuffix("+")', ['a.removeprefix("+")', 'a.removesuffix("XX+XX")', 'a.removesuffix(None)']), + ('a.partition("++")', ['a.rpartition("++")', 'a.partition("XX++XX")', 'a.partition(None)']), + ('a.rpartition("++")', ['a.partition("++")', 'a.rpartition("XX++XX")', 'a.rpartition(None)']), ('a(b)', 'a(None)'), ("dict(a=None)", ["dict(aXX=None)"]), ("dict(a=b)", ["dict(aXX=b)", 'dict(a=None)']), @@ -235,15 +261,15 @@ def xǁFooǁmember__mutmut_1(self): def test_function_with_annotation(): - source = "def capitalize(s : str):\n return s[0].upper() + s[1:] if s else s\n".strip() + source = "def capitalize(s : str):\n return s[0].title() + s[1:] if s else s\n".strip() mutated_code = mutated_module(source) - print(mutated_code) + print(mutated_code, open("aboba", "a")) expected_defs = [ - 'def x_capitalize__mutmut_1(s : str):\n return s[1].upper() + s[1:] if s else s', - 'def x_capitalize__mutmut_2(s : str):\n return s[0].upper() - s[1:] if s else s', - 'def x_capitalize__mutmut_3(s : str):\n return s[0].upper() + s[2:] if s else s', + 'def x_capitalize__mutmut_1(s : str):\n return s[1].title() + s[1:] if s else s', + 'def x_capitalize__mutmut_2(s : str):\n return s[0].title() - s[1:] if s else s', + 'def x_capitalize__mutmut_3(s : str):\n return s[0].title() + s[2:] if s else s', ] for expected in expected_defs: