From 2b44ede68c94fb70d19393ff51153ba43ce972e3 Mon Sep 17 00:00:00 2001 From: Evan Kepner Date: Sat, 9 Mar 2019 16:22:09 -0500 Subject: [PATCH] 0.9.0-dev if statements tested --- CHANGELOG.rst | 9 ++++++++ README.rst | 29 ++++++++++++++++++++++++ mutatest/__init__.py | 2 +- mutatest/_devtools.py | 12 +++++----- mutatest/transformers.py | 28 +++++++++-------------- tests/conftest.py | 46 ++++++++++++++++++++++++++++++++++++++ tests/test_transformers.py | 35 +++++++++++++++++++++++++++-- 7 files changed, 135 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 758617e..64c7eaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,15 @@ Changelog not guaranteed while under development. +0.9.0 +----- + + - Added new :code:`If` mutation: + 1. Original statements are represented by :code:`If_Statement` and mutated to be either + :code:`If_True` where the statement always passes, or :code:`If_False` where the statement + is never passed. + + 0.8.0 ----- diff --git a/README.rst b/README.rst index a17d59f..5594949 100644 --- a/README.rst +++ b/README.rst @@ -340,6 +340,7 @@ Supported operations: - :code:`Compare` mutations e.g. :code:`== >= < <= !=`. - :code:`Compare In` mutations e.g. :code:`in, not in`. - :code:`Compare Is` mutations e.g. :code:`is, is not`. + - :code:`If` mutations e.g. :code:`If x > y` becomes :code:`If True` or :code:`If False`. - :code:`Index` mutations e.g. :code:`i[0]` becomes :code:`i[1]` and :code:`i[-1]`. - :code:`NameConstant` mutations e.g. :code:`True`, :code:`False`, and :code:`None`. - :code:`Slice` mutations e.g. changing :code:`x[:2]` to :code:`x[2:]` @@ -529,6 +530,34 @@ Example: x is not None +If +-- + +If mutations change :code:`if` statements to always be :code:`True` or :code:`False`. The original +statement is represented by the class :code:`If_Statement` in reporting. + +Members: + - :code:`If_False` + - :code:`If_Statement` + - :code:`If_True` + + +Example: + +.. code-block:: python + + # source code + if a > b: # If_Statement + ... + + # Mutations + if True: # If_True + ... + + if False: # If_False + ... + + Index ----- diff --git a/mutatest/__init__.py b/mutatest/__init__.py index d9bde3b..9eb48e0 100644 --- a/mutatest/__init__.py +++ b/mutatest/__init__.py @@ -1,6 +1,6 @@ """Mutation initialization. """ -__version__ = "0.8.1-dev" +__version__ = "0.9.0-dev" __title__ = "mutatest" __description__ = "Python mutation testing: test your tests!" diff --git a/mutatest/_devtools.py b/mutatest/_devtools.py index bf59b7c..6564ae4 100644 --- a/mutatest/_devtools.py +++ b/mutatest/_devtools.py @@ -47,6 +47,12 @@ def visit_Compare(self, node: ast.Compare) -> None: print(ast.dump(node)) self.generic_visit(node) + def visit_If(self, node: ast.If) -> None: + # have is, is not already, not clear If directly is a valuable mutation + print(f"If: {node}") + print(ast.dump(node)) + self.generic_visit(node) + def visit_Index(self, node: ast.Index) -> None: # actual slicing print(f"Index: {node}") @@ -73,12 +79,6 @@ def visit_Subscript(self, node: ast.Subscript) -> None: # IN DEVELOPMENT ################################################################################################ - def visit_If(self, node: ast.If) -> None: - # have is, is not already, not clear If directly is a valuable mutation - # print(f"If: {node}") - print(ast.dump(node)) - self.generic_visit(node) - # FUTURE DEVELOPMENT ################################################################################################ diff --git a/mutatest/transformers.py b/mutatest/transformers.py index 976c326..e59d693 100644 --- a/mutatest/transformers.py +++ b/mutatest/transformers.py @@ -200,18 +200,14 @@ def visit_Compare(self, node: ast.Compare) -> ast.AST: return node def visit_If(self, node: ast.If) -> ast.AST: - - # TODO: DETERMINE IF_CONSTANT TARGET TRANSFORMTION USAGE - # TODO: CURRENTLY ONLY TRUE/FALSE WORK - """If statements e.g. If x == y transformed to If True and If False.""" self.generic_visit(node) log_header = f"visit_If: {self.src_file}:" - # default for a comparison is "If_NotConstant" which will be changed to True/False - # if_NotConstand if not set as a mutation target, controlled in get_mutations function + # default for a comparison is "If_Statement" which will be changed to True/False + # If_Statement is not set as a mutation target, controlled in get_mutations function - if_type = "If_NotConstant" + if_type = "If_Statement" if_mutations = { "If_True": ast.NameConstant(value=True), @@ -224,10 +220,6 @@ def visit_If(self, node: ast.If) -> ast.AST: idx = LocIndex("If", node.lineno, node.col_offset, if_type) self.locs.add(idx) - if type(node) == ast.If: - LOGGER.info(self.target_idx) - LOGGER.info(self.mutation) - if idx == self.target_idx and self.mutation and not self.readonly: LOGGER.debug("%s mutating idx: %s with %s", log_header, self.target_idx, self.mutation) return ast.fix_missing_locations( @@ -418,7 +410,7 @@ def get_compatible_operation_sets() -> List[MutationOpSet]: # Custom references for If statement substitutions # only If_True and If_False will be applied as mutations - if_types: Set[str] = {"If_True", "If_False", "If_Compare"} + if_types: Set[str] = {"If_True", "If_False", "If_Statement"} # Custom references for subscript substitutions for slice mutations slice_bounded_types: Set[str] = {"Slice_UnboundUpper", "Slice_UnboundLower", "Slice_Unbounded"} @@ -455,7 +447,9 @@ def get_compatible_operation_sets() -> List[MutationOpSet]: name="Compare Is", desc="Comapre identity e.g. is, is not", operations=cmpop_is_types ), MutationOpSet( - name="If", desc="If statement tests e.g. Compare, True, False", operations=if_types + name="If", + desc="If statement tests e.g. original statement, True, False", + operations=if_types, ), MutationOpSet( name="Index", @@ -505,10 +499,10 @@ def get_mutations_for_target(target: LocIndex) -> Set[Any]: if target.op_type in ["Slice_UPosToZero", "Slice_UNegToZero"]: mutation_ops = {target.op_type} - # Special case for If-compare since that is a default to transform to True or False - # but not a validation mutation target by iself - if "If_Compare" in mutation_ops: - mutation_ops.remove("If_Compare") + # Special case for If_Statement since that is a default to transform to True or False + # but not a validation mutation target by itself + if "If_Statement" in mutation_ops: + mutation_ops.remove("If_Statement") break diff --git a/tests/conftest.py b/tests/conftest.py index 9334498..ed4393d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -352,6 +352,52 @@ def compare_expected_loc(): return LocIndex(ast_class="Compare", lineno=2, col_offset=11, op_type=ast.Eq) +#################################################################################################### +# TRANSFORMERS: IF FIXTURES +#################################################################################################### + + +@pytest.fixture(scope="session") +def if_file(tmp_path_factory): + """Simple file for if-statement mutations.""" + contents = dedent( + """\ + def equal_test(a, b): + if a == b: + print("Equal") + elif a < b: + print("LT") + else: + print("Else!") + + def second(): + if True: + print("true") + + if False: + print("false") + """ + ) + + fn = tmp_path_factory.mktemp("if_statement") / "if_statement.py" + + with open(fn, "w") as output_fn: + output_fn.write(contents) + + return fn + + +@pytest.fixture(scope="session") +def if_expected_locs(): + """Exepected locations in the if_statement.""" + return [ + LocIndex(ast_class="If", lineno=2, col_offset=4, op_type="If_Statement"), + LocIndex(ast_class="If", lineno=4, col_offset=9, op_type="If_Statement"), + LocIndex(ast_class="If", lineno=10, col_offset=4, op_type="If_True"), + LocIndex(ast_class="If", lineno=13, col_offset=4, op_type="If_False"), + ] + + #################################################################################################### # TRANSFORMERS: INDEX FIXTURES #################################################################################################### diff --git a/tests/test_transformers.py b/tests/test_transformers.py index f152143..d97a9a0 100644 --- a/tests/test_transformers.py +++ b/tests/test_transformers.py @@ -166,6 +166,35 @@ def test_MutateAST_visit_compare(compare_file, compare_expected_loc): assert l.op_type == test_mutation +def test_MutateAST_visit_if(if_file, if_expected_locs): + """Test mutation for nameconst: True, False, None.""" + tree = get_ast_from_src(if_file) + test_mutation = "If_True" + + testing_tree = deepcopy(tree) + # change from If_Statement to If_True + mutated_tree = MutateAST(target_idx=if_expected_locs[0], mutation=test_mutation).visit( + testing_tree + ) + + mast = MutateAST(readonly=True) + mast.visit(mutated_tree) + + # named constants will also be picked up, filter just to if_ operations + if_locs = [l for l in mast.locs if l.ast_class == "If"] + assert len(if_locs) == 4 + + for l in if_locs: + # spot check on mutation from True to False + if l.lineno == 2 and l.col_offset == 4: + print(l) + assert l.op_type == test_mutation + + # spot check on not-mutated location still being None + if l.lineno == 13 and l.col_offset == 4: + assert l.op_type == "If_False" + + INDEX_SETS = [ # idx order, lineno, col_offset, mutation to apply # change NumNeg to Pos and Zero @@ -232,9 +261,11 @@ def test_MutateAST_visit_nameconst(nameconst_file, nameconst_expected_locs): mast = MutateAST(readonly=True) mast.visit(mutated_tree) - assert len(mast.locs) == 4 + # if statement is included with this file that will be picked up + nc_locs = [l for l in mast.locs if l.ast_class == "NameConstant"] + assert len(nc_locs) == 4 - for l in mast.locs: + for l in nc_locs: # spot check on mutation from True to False if l.lineno == 1 and l.col_offset == 14: assert l.op_type == test_mutation