def test_create_mutant(binop_file, stdoutIO): """Basic mutant creation to modify the add_five() function from add to mult.""" tree = get_ast_from_src(binop_file) # this target is the add_five() function, changing add to mult target_idx = LocIndex(ast_class="BinOp", lineno=10, col_offset=11, op_type=ast.Add) mutation_op = ast.Mult mutant = create_mutant(tree=tree, src_file=binop_file, target_idx=target_idx, mutation_op=mutation_op) # uses the redirection for stdout to capture the value from the final output of binop_file with stdoutIO() as s: exec(mutant.mutant_code) assert int(s.getvalue()) == 25 tag = sys.implementation.cache_tag expected_cfile = binop_file.parent / "__pycache__" / ".".join( [binop_file.stem, tag, "pyc"]) assert mutant.src_file == binop_file assert mutant.cfile == expected_cfile assert mutant.src_idx == target_idx
def test_MutateAST_visit_nameconst(nameconst_file, nameconst_expected_locs): """Test mutation for nameconst: True, False, None.""" tree = get_ast_from_src(nameconst_file) test_mutation = False testing_tree = deepcopy(tree) mutated_tree = MutateAST(target_idx=nameconst_expected_locs[0], mutation=test_mutation).visit( testing_tree ) mast = MutateAST(readonly=True) mast.visit(mutated_tree) # 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 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 # spot check on not-mutated location still being None if l.lineno == 7 and l.col_offset == 22: assert l.op_type is None
def test_get_mutation_targets(binop_file, binop_expected_locs): """Test mutation target retrieval from the bin_op test fixture.""" tree = get_ast_from_src(binop_file) targets = get_mutation_targets(tree, binop_file) assert len(targets) == 4 assert targets == binop_expected_locs
def test_create_mutation_and_run_trial(returncode, expected_status, monkeypatch, binop_file): """Mocked trial to ensure mutated cache files are removed after running.""" tree = get_ast_from_src(binop_file) # this target is the add_five() function, changing add to mult target_idx = LocIndex(ast_class="BinOp", lineno=10, col_offset=11, op_type=ast.Add) mutation_op = ast.Mult tag = sys.implementation.cache_tag expected_cfile = binop_file.parent / "__pycache__" / ".".join( [binop_file.stem, tag, "pyc"]) def mock_subprocess_run(*args, **kwargs): return CompletedProcess(args="pytest", returncode=returncode) monkeypatch.setattr(subprocess, "run", mock_subprocess_run) trial = create_mutation_and_run_trial( src_tree=tree, src_file=binop_file, target_idx=target_idx, mutation_op=mutation_op, test_cmds=["pytest"], tree_inplace=False, ) # mutated cache files should be removed after trial run assert not expected_cfile.exists() assert trial.status == expected_status
def test_MutateAST_visit_index_neg( i_order, lineno, col_offset, mut, index_file, index_expected_locs ): """Test mutation for Index: i[0], i[1], i[-1].""" tree = get_ast_from_src(index_file) test_mutation = mut testing_tree = deepcopy(tree) mutated_tree = MutateAST(target_idx=index_expected_locs[i_order], mutation=test_mutation).visit( testing_tree ) mast = MutateAST(readonly=True) mast.visit(mutated_tree) assert len(mast.locs) == 4 for l in mast.locs: # spot check on mutation from Index_NumNeg to Index_NumPos if l.lineno == lineno and l.col_offset == col_offset: assert l.op_type == test_mutation # spot check on not-mutated location still being None if l.lineno == 4 and l.col_offset == 23: assert l.op_type == "Index_NumPos"
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"
def test_get_ast_from_src(binop_file): """Basic assurance that the AST matches expectations in type and body size.""" tree = get_ast_from_src(binop_file) assert type(tree) == ast.Module # 4 functions will be 4 body entries in the AST, plus 1 for print statement, 5 total assert len(tree.body) == 5
def test_MutateAST_visit_read_only(binop_file): """Read only test to ensure locations are aggregated.""" tree = get_ast_from_src(binop_file) mast = MutateAST(readonly=True) testing_tree = deepcopy(tree) mast.visit(testing_tree) # four locations from the binary operations in binop_file assert len(mast.locs) == 4 # tree should be unmodified assert ast.dump(tree) == ast.dump(testing_tree)
def add_five_to_mult_mutant(binop_file, stdoutIO): """Mutant that takes add_five op ADD to MULT. Fails if mutation code does not work.""" tree = get_ast_from_src(binop_file) # this target is the add_five() function, changing add to mult target_idx = LocIndex(ast_class="BinOp", lineno=10, col_offset=11, op_type=ast.Add) mutation_op = ast.Mult mutant = create_mutant(tree=tree, src_file=binop_file, target_idx=target_idx, mutation_op=mutation_op) # uses the redirection for stdout to capture the value from the final output of binop_file with stdoutIO() as s: exec(mutant.mutant_code) assert int(s.getvalue()) == 25 return mutant
def build_src_trees_and_targets( src_loc: Union[str, Path], exclude_files: Optional[List[Path]] = None ) -> Tuple[Dict[str, ast.Module], Dict[str, List[LocIndex]]]: """Build the source AST references and find all mutatest target locations for each. Args: src_loc: the source code package directory to scan or file location Returns: Tuple(source trees, source targets) """ src_trees: Dict[str, ast.Module] = {} src_targets: Dict[str, List[LocIndex]] = {} for src_file in get_py_files(src_loc): # if the src_file is in the exclusion list then reset to the next iteration if exclude_files: if src_file in exclude_files: LOGGER.info( "%s", colorize_output(f"Exclusion: {src_file}", "yellow")) continue tree = get_ast_from_src(src_file) targets = get_mutation_targets(tree, src_file) LOGGER.info( "%s", colorize_output( f"{len(targets)} mutation targets found in {src_file} AST.", "green" if len(targets) > 0 else "yellow", ), ) # only add files that have at least one valid target for mutatest if targets: src_trees[str(src_file)] = tree src_targets[str(src_file)] = [tgt for tgt in targets] return src_trees, src_targets
def test_MutateAST_visit_binop(binop_file): """Read only test to ensure locations are aggregated.""" tree = get_ast_from_src(binop_file) test_idx = LocIndex(ast_class="BinOp", lineno=6, col_offset=11, op_type=ast.Add) test_mutation = ast.Pow # apply the mutation to the original tree copy testing_tree = deepcopy(tree) mutated_tree = MutateAST(target_idx=test_idx, mutation=test_mutation).visit(testing_tree) # revisit in read-only mode to gather the locations of the new nodes mast = MutateAST(readonly=True) mast.visit(mutated_tree) # four locations from the binary operations in binop_file assert len(mast.locs) == 4 # locs is an unordered set, cycle through to thd target and check the mutation for l in mast.locs: if l.lineno == 6 and l.col_offset == 11: assert l.op_type == test_mutation
def test_MutateAST_visit_augassign(augassign_file, augassign_expected_locs): """Test mutation for AugAssign: +=, -=, /=, *=.""" tree = get_ast_from_src(augassign_file) test_mutation = "AugAssign_Div" testing_tree = deepcopy(tree) mutated_tree = MutateAST(target_idx=augassign_expected_locs[0], mutation=test_mutation).visit( testing_tree ) mast = MutateAST(readonly=True) mast.visit(mutated_tree) assert len(mast.locs) == 4 for l in mast.locs: # spot check on mutation from Add tp Div if l.lineno == 1 and l.col_offset == 4: assert l.op_type == test_mutation # spot check on not-mutated location still being Mult if l.lineno == 5 and l.col_offset == 4: assert l.op_type == "AugAssign_Mult"
def test_MutateAST_visit_subscript(slice_file, slice_expected_locs): """Test Slice references within subscript.""" tree = get_ast_from_src(slice_file) mast = MutateAST(readonly=True) mast.visit(tree) assert len(mast.locs) == len(slice_expected_locs) test_mutation = "Slice_UNegToZero" # loc index 3 is the Slice_NegShrink operation in the fixture mutated_tree = MutateAST(target_idx=slice_expected_locs[3], mutation=test_mutation).visit(tree) mast.visit(mutated_tree) assert len(mast.locs) == len(slice_expected_locs) for l in mast.locs: if l.lineno == 5 and l.col_offset == 15: assert l.op_type == test_mutation # test one unmodified location if l.lineno == 4 and l.col_offset == 14: assert l.op_type == "Slice_UnboundUpper"
def test_MutateAST_visit_compare(compare_file, compare_expected_loc): """Test mutation of the == to != in the compare op.""" tree = get_ast_from_src(compare_file) test_mutation = ast.NotEq # apply the mutation to the original tree copy testing_tree = deepcopy(tree) mutated_tree = MutateAST(target_idx=compare_expected_loc, mutation=test_mutation).visit( testing_tree ) # revisit in read-only mode to gather the locations of the new nodes mast = MutateAST(readonly=True) mast.visit(mutated_tree) # four locations from the binary operations in binop_file assert len(mast.locs) == 1 # there will only be one loc, but this still works # basedon the col and line offset in the fixture for compare_expected_loc for l in mast.locs: if l.lineno == 2 and l.col_offset == 11: assert l.op_type == test_mutation