diff --git a/src/cosmic_ray/cli.py b/src/cosmic_ray/cli.py index 513d1981..8f11049d 100644 --- a/src/cosmic_ray/cli.py +++ b/src/cosmic_ray/cli.py @@ -84,7 +84,7 @@ def init(config_file, session_file): executed with other commands. """ cfg = load_config(config_file) - + operators_cfg = cfg.operators_config modules = cosmic_ray.modules.find_modules(Path(cfg["module-path"])) modules = cosmic_ray.modules.filter_paths(modules, cfg.get("excluded-modules", ())) @@ -97,7 +97,7 @@ def init(config_file, session_file): log.info(" - %s: %s", directory, ", ".join(sorted(files))) with use_db(session_file) as database: - cosmic_ray.commands.init(modules, database) + cosmic_ray.commands.init(modules, database, operators_cfg) sys.exit(ExitCode.OK) diff --git a/src/cosmic_ray/commands/init.py b/src/cosmic_ray/commands/init.py index c21b46f6..ab978298 100644 --- a/src/cosmic_ray/commands/init.py +++ b/src/cosmic_ray/commands/init.py @@ -2,43 +2,54 @@ import logging from typing import Iterable import uuid - from cosmic_ray.ast import get_ast, ast_nodes import cosmic_ray.modules -from cosmic_ray.work_item import MutationSpec, ResolvedMutationSpec, WorkItem +from cosmic_ray.work_item import ResolvedMutationSpec, WorkItem from cosmic_ray.plugins import get_operator from cosmic_ray.work_db import WorkDB log = logging.getLogger() -def _all_work_items(module_paths, operator_names) -> Iterable[WorkItem]: +def _all_work_items(module_paths, operator_cfgs) -> Iterable[WorkItem]: "Iterable of all WorkItems for the given inputs." + for module_path in module_paths: module_ast = get_ast(module_path) - for op_name in operator_names: - operator = get_operator(op_name)() + for operator_cfg in operator_cfgs: + operator_name = operator_cfg["name"] + operator_args = operator_cfg.get('args', [{}]) - positions = ( - (start_pos, end_pos) - for node in ast_nodes(module_ast) - for start_pos, end_pos in operator.mutation_positions(node) - ) + for args in operator_args: + try: + operator = get_operator(operator_name)(**args) + except TypeError: + if not args: + continue + else: + raise Exception( + f"Operator arguments {args} could not be assigned to {operator_name}.") + else: + positions = ( + (start_pos, end_pos) + for node in ast_nodes(module_ast) + for start_pos, end_pos in operator.mutation_positions(node) + ) - for occurrence, (start_pos, end_pos) in enumerate(positions): - mutation = ResolvedMutationSpec( - module_path=str(module_path), - operator_name=op_name, - occurrence=occurrence, - start_pos=start_pos, - end_pos=end_pos, - ) + for occurrence, (start_pos, end_pos) in enumerate(positions): + mutation = ResolvedMutationSpec( + module_path=str(module_path), + operator_name=operator_name, + operator_args=args, + occurrence=occurrence, + start_pos=start_pos, + end_pos=end_pos, + ) + yield WorkItem.single(job_id=uuid.uuid4().hex, mutation=mutation) - yield WorkItem.single(job_id=uuid.uuid4().hex, mutation=mutation) - -def init(module_paths, work_db: WorkDB): +def init(module_paths, work_db: WorkDB, operators_cfgs=None): """Clear and initialize a work-db with work items. Any existing data in the work-db will be cleared and replaced with entirely @@ -48,9 +59,10 @@ def init(module_paths, work_db: WorkDB): Args: module_paths: iterable of pathlib.Paths of modules to mutate. work_db: A `WorkDB` instance into which the work orders will be saved. + operators_cfgs: A list of dictionaries representing operator configurations. """ - - operator_names = list(cosmic_ray.plugins.operator_names()) + if not operators_cfgs: + operators_cfgs = [{'name': name} for name in list(cosmic_ray.plugins.operator_names())] work_db.clear() - work_db.add_work_items(_all_work_items(module_paths, operator_names)) + work_db.add_work_items(_all_work_items(module_paths, operators_cfgs)) diff --git a/src/cosmic_ray/config.py b/src/cosmic_ray/config.py index 5a92de3f..ac8a92a1 100644 --- a/src/cosmic_ray/config.py +++ b/src/cosmic_ray/config.py @@ -87,6 +87,12 @@ def distributor_config(self): name = self.distributor_name return self["distributor"].get(name, ConfigDict()) + @property + def operators_config(self): + """The configuration for specified operators.""" + return self.get("operators", None) + + @contextmanager def _config_stream(filename): diff --git a/src/cosmic_ray/mutating.py b/src/cosmic_ray/mutating.py index 69c6e4c5..7b8317c8 100644 --- a/src/cosmic_ray/mutating.py +++ b/src/cosmic_ray/mutating.py @@ -11,12 +11,11 @@ import cosmic_ray.plugins from cosmic_ray.ast import Visitor, get_ast from cosmic_ray.testing import run_tests -from cosmic_ray.work_item import MutationSpec, TestOutcome, WorkerOutcome, WorkResult - +from cosmic_ray.work_item import ResolvedMutationSpec, TestOutcome, WorkerOutcome, WorkResult log = logging.getLogger(__name__) # pylint: disable=R0913 -async def mutate_and_test(mutations: Iterable[MutationSpec], test_command, timeout) -> WorkResult: +async def mutate_and_test(mutations: Iterable[ResolvedMutationSpec], test_command, timeout) -> WorkResult: """Apply a sequence of mutations, run thest tests, and reports the results. This is fundamentally the mutation(s)-and-test-run implementation at the heart of Cosmic Ray. @@ -50,7 +49,12 @@ async def mutate_and_test(mutations: Iterable[MutationSpec], test_command, timeo file_changes = {} for mutation in mutations: operator_class = cosmic_ray.plugins.get_operator(mutation.operator_name) - operator = operator_class() + try: + operator_args = mutation.operator_args + except AttributeError: + operator_args = {} + operator = operator_class(**operator_args) + (previous_code, mutated_code) = stack.enter_context( use_mutation(mutation.module_path, operator, mutation.occurrence) ) diff --git a/src/cosmic_ray/operators/binary_operator_replacement.py b/src/cosmic_ray/operators/binary_operator_replacement.py index f2c79874..38191bf1 100644 --- a/src/cosmic_ray/operators/binary_operator_replacement.py +++ b/src/cosmic_ray/operators/binary_operator_replacement.py @@ -8,7 +8,7 @@ from .operator import Operator from .util import extend_name - +from .example import Example class BinaryOperators(Enum): "All binary operators that we mutate." @@ -47,7 +47,7 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - ('x {} y'.format(from_op.value), 'x {} y'.format(to_op.value)), + Example('x {} y'.format(from_op.value), 'x {} y'.format(to_op.value)), ) return ReplaceBinaryOperator diff --git a/src/cosmic_ray/operators/boolean_replacer.py b/src/cosmic_ray/operators/boolean_replacer.py index 15422e19..b01b12e1 100644 --- a/src/cosmic_ray/operators/boolean_replacer.py +++ b/src/cosmic_ray/operators/boolean_replacer.py @@ -5,6 +5,7 @@ from .keyword_replacer import KeywordReplacementOperator from .operator import Operator +from .example import Example class ReplaceTrueWithFalse(KeywordReplacementOperator): @@ -29,7 +30,7 @@ class ReplaceAndWithOr(KeywordReplacementOperator): @classmethod def examples(cls): - return (("x and y", "x or y"),) + return (Example("x and y", "x or y"),) class ReplaceOrWithAnd(KeywordReplacementOperator): @@ -40,7 +41,7 @@ class ReplaceOrWithAnd(KeywordReplacementOperator): @classmethod def examples(cls): - return (("x or y", "x and y"),) + return (Example("x or y", "x and y"),) class AddNot(Operator): @@ -84,8 +85,8 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - ("if True or False: pass", "if not True or False: pass"), - ("A if B else C", "A if not B else C"), - ("assert isinstance(node, ast.Break)", "assert not isinstance(node, ast.Break)"), - ("while True: pass", "while not True: pass"), + Example("if True or False: pass", "if not True or False: pass"), + Example("A if B else C", "A if not B else C"), + Example("assert isinstance(node, ast.Break)", "assert not isinstance(node, ast.Break)"), + Example("while True: pass", "while not True: pass"), ) diff --git a/src/cosmic_ray/operators/comparison_operator_replacement.py b/src/cosmic_ray/operators/comparison_operator_replacement.py index 7829d7af..0170e0ec 100644 --- a/src/cosmic_ray/operators/comparison_operator_replacement.py +++ b/src/cosmic_ray/operators/comparison_operator_replacement.py @@ -9,6 +9,7 @@ from ..ast import is_none, is_number from .operator import Operator from .util import extend_name +from .example import Example class ComparisonOperators(Enum): @@ -53,7 +54,7 @@ def _mutation_points(node): @classmethod def examples(cls): return ( - ('x {} y'.format(from_op.value), 'x {} y'.format(to_op.value)), + Example('x {} y'.format(from_op.value), 'x {} y'.format(to_op.value)), ) return ReplaceComparisonOperator diff --git a/src/cosmic_ray/operators/example.py b/src/cosmic_ray/operators/example.py new file mode 100644 index 00000000..a897dcbc --- /dev/null +++ b/src/cosmic_ray/operators/example.py @@ -0,0 +1,25 @@ +"""Data class to store example applications of mutation operators. + These structures are used for testing purposes.""" +import dataclasses + +from typing import Optional + + +@dataclasses.dataclass(frozen=True) +class Example: + """A structure to store pre and post mutation operator code snippets, + including optional specification of occurrence and operator args. + + This is used for testing whether the pre-mutation code is correctly + mutated to the post-mutation code at the given occurrence (if specified) + and for the given operator args (if specified). + """ + + pre_mutation_code: str + post_mutation_code: str + occurrence: Optional[int] = 0 + operator_args: Optional[dict] = None + + def __post_init__(self): + if not self.operator_args: + object.__setattr__(self, "operator_args", {}) diff --git a/src/cosmic_ray/operators/exception_replacer.py b/src/cosmic_ray/operators/exception_replacer.py index af41dfdb..8491e2c7 100644 --- a/src/cosmic_ray/operators/exception_replacer.py +++ b/src/cosmic_ray/operators/exception_replacer.py @@ -5,7 +5,7 @@ from cosmic_ray.exceptions import CosmicRayTestingException from .operator import Operator - +from .example import Example class ExceptionReplacer(Operator): """An operator that modifies exception handlers.""" @@ -37,19 +37,19 @@ def _name_nodes(node): @classmethod def examples(cls): return ( - ( + Example( "try: raise OSError\nexcept OSError: pass", "try: raise OSError\nexcept {}: pass".format(CosmicRayTestingException.__name__), ), - ( + Example( "try: raise OSError\nexcept (OSError, ValueError): pass", "try: raise OSError\nexcept (OSError, {}): pass".format(CosmicRayTestingException.__name__), - 1, + occurrence=1, ), - ( + Example( "try: raise OSError\nexcept (OSError, ValueError, KeyError): pass", "try: raise OSError\nexcept (OSError, {}, KeyError): pass".format(CosmicRayTestingException.__name__), - 1, + occurrence=1, ), - ("try: pass\nexcept: pass", "try: pass\nexcept: pass"), + Example("try: pass\nexcept: pass", "try: pass\nexcept: pass"), ) diff --git a/src/cosmic_ray/operators/keyword_replacer.py b/src/cosmic_ray/operators/keyword_replacer.py index 3f4afcf2..25e1af86 100644 --- a/src/cosmic_ray/operators/keyword_replacer.py +++ b/src/cosmic_ray/operators/keyword_replacer.py @@ -3,6 +3,7 @@ from parso.python.tree import Keyword from .operator import Operator +from .example import Example # pylint: disable=E1101 @@ -26,5 +27,5 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - (cls.from_keyword, cls.to_keyword), + Example(cls.from_keyword, cls.to_keyword), ) diff --git a/src/cosmic_ray/operators/no_op.py b/src/cosmic_ray/operators/no_op.py index 7ef1d6a1..e461a0ca 100644 --- a/src/cosmic_ray/operators/no_op.py +++ b/src/cosmic_ray/operators/no_op.py @@ -1,7 +1,7 @@ "Implementation of the no-op operator." from .operator import Operator - +from .example import Example class NoOp(Operator): """An operator that makes no changes. @@ -20,7 +20,7 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - ('@foo\ndef bar(): pass', '@foo\ndef bar(): pass'), - ('def bar(): pass', 'def bar(): pass'), - ('1 + 1', '1 + 1'), + Example('@foo\ndef bar(): pass', '@foo\ndef bar(): pass'), + Example('def bar(): pass', 'def bar(): pass'), + Example('1 + 1', '1 + 1'), ) diff --git a/src/cosmic_ray/operators/number_replacer.py b/src/cosmic_ray/operators/number_replacer.py index ee2b0a4d..db871747 100644 --- a/src/cosmic_ray/operators/number_replacer.py +++ b/src/cosmic_ray/operators/number_replacer.py @@ -5,6 +5,7 @@ from ..ast import is_number from .operator import Operator +from .example import Example # List of offsets that we apply to numbers in the AST. Each index into the list # corresponds to single mutation. @@ -34,10 +35,10 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - ('x = 1', 'x = 2'), - ('x = 1', 'x = 0', 1), - ('x = 4.2', 'x = 5.2'), - ('x = 4.2', 'x = 3.2', 1), - ('x = 1j', 'x = (1+1j)'), - ('x = 1j', 'x = (-1+1j)', 1), + Example('x = 1', 'x = 2'), + Example('x = 1', 'x = 0', occurrence=1), + Example('x = 4.2', 'x = 5.2'), + Example('x = 4.2', 'x = 3.2', occurrence=1), + Example('x = 1j', 'x = (1+1j)'), + Example('x = 1j', 'x = (-1+1j)', occurrence=1), ) diff --git a/src/cosmic_ray/operators/operator.py b/src/cosmic_ray/operators/operator.py index 26d12d96..cb996cdf 100644 --- a/src/cosmic_ray/operators/operator.py +++ b/src/cosmic_ray/operators/operator.py @@ -39,15 +39,15 @@ def examples(cls): """Examples of the mutations that this operator can make. This is primarily for testing purposes, but it could also be used for - docmentation. + documentation. - Each example is a tuple of the form `(from-code, to-code, index)`. The - `index` is optional and will be assumed to be 0 if it's not included. - The `from-code` is a string containing some Python code prior to - mutation. The `to-code` is a string desribing the code after mutation. - `index` indicates the occurrence of the application of the operator to - the code (i.e. for when an operator can perform multiple mutation to a - piece of code). + Each example takes the following arguments: + pre_mutation_code: code prior to applying the mutation. + post_mutation_code: code after (successfully) applying the mutation. + occurrence: the index of the occurrence to which the mutation is + applied (optional, default=0). + operator_args: a dictionary of arguments to be **-unpacked to the + operator (optional, default={}). - Returns: An iterable of example tuples. + Returns: An iterable of Examples. """ diff --git a/src/cosmic_ray/operators/provider.py b/src/cosmic_ray/operators/provider.py index 3689dbab..58e3970f 100644 --- a/src/cosmic_ray/operators/provider.py +++ b/src/cosmic_ray/operators/provider.py @@ -6,7 +6,7 @@ from . import (binary_operator_replacement, boolean_replacer, break_continue, comparison_operator_replacement, exception_replacer, no_op, number_replacer, remove_decorator, unary_operator_replacement, - zero_iteration_for_loop) + zero_iteration_for_loop, variable_replacer, variable_inserter) # NB: The no_op operator gets special handling. We don't include it in iteration of the # available operators. However, you can request it from the provider by name. This lets us @@ -26,7 +26,9 @@ exception_replacer.ExceptionReplacer, number_replacer.NumberReplacer, remove_decorator.RemoveDecorator, - zero_iteration_for_loop.ZeroIterationForLoop)) + zero_iteration_for_loop.ZeroIterationForLoop, + variable_replacer.VariableReplacer, + variable_inserter.VariableInserter)) } diff --git a/src/cosmic_ray/operators/remove_decorator.py b/src/cosmic_ray/operators/remove_decorator.py index 4709b966..221c91ab 100644 --- a/src/cosmic_ray/operators/remove_decorator.py +++ b/src/cosmic_ray/operators/remove_decorator.py @@ -3,7 +3,7 @@ from parso.python.tree import Decorator from .operator import Operator - +from .example import Example class RemoveDecorator(Operator): """An operator that removes decorators.""" @@ -19,8 +19,8 @@ def mutate(self, node, index): @classmethod def examples(cls): return ( - ('@foo\ndef bar(): pass', 'def bar(): pass'), - ('@first\n@second\ndef bar(): pass', '@second\ndef bar(): pass'), - ('@first\n@second\ndef bar(): pass', '@first\ndef bar(): pass', 1), - ('@first\n@second\n@third\ndef bar(): pass', '@first\n@third\ndef bar(): pass', 1), + Example('@foo\ndef bar(): pass', 'def bar(): pass'), + Example('@first\n@second\ndef bar(): pass', '@second\ndef bar(): pass'), + Example('@first\n@second\ndef bar(): pass', '@first\ndef bar(): pass', occurrence=1), + Example('@first\n@second\n@third\ndef bar(): pass', '@first\n@third\ndef bar(): pass', occurrence=1), ) diff --git a/src/cosmic_ray/operators/unary_operator_replacement.py b/src/cosmic_ray/operators/unary_operator_replacement.py index 0bb8ce32..c3df8958 100644 --- a/src/cosmic_ray/operators/unary_operator_replacement.py +++ b/src/cosmic_ray/operators/unary_operator_replacement.py @@ -8,7 +8,7 @@ from . import operator from .util import extend_name - +from .example import Example class UnaryOperators(Enum): "All unary operators that we mutate." @@ -60,7 +60,7 @@ def examples(cls): to_code = ' ' + to_code return ( - (from_code, to_code), + Example(from_code, to_code), ) return ReplaceUnaryOperator diff --git a/src/cosmic_ray/operators/variable_inserter.py b/src/cosmic_ray/operators/variable_inserter.py new file mode 100644 index 00000000..f7196274 --- /dev/null +++ b/src/cosmic_ray/operators/variable_inserter.py @@ -0,0 +1,73 @@ +"""Implementation of the variable-inserter operator.""" +import random +import parso.python.tree + +from parso.python.tree import Name, PythonNode + +from .operator import Operator +from .example import Example + + +class VariableInserter(Operator): + """An operator that replaces adds usages of named variables to particular statements.""" + + def __init__(self, cause_variable, effect_variable): + self.cause_variable = cause_variable + self.effect_variable = effect_variable + + def mutation_positions(self, node): + """Find expressions or terms that define the effect variable. These nodes can be + mutated to introduce an effect of the cause variable. + """ + if isinstance(node, PythonNode) and (node.type == "arith_expr" or node.type == "term"): + expr_node = node.search_ancestor('expr_stmt') + if expr_node: + effect_variable_names = [v.value for v in expr_node.get_defined_names()] + if self.effect_variable in effect_variable_names: + cause_variables = list(self._get_causes_from_expr_node(expr_node)) + if node not in cause_variables: + yield (node.start_pos, node.end_pos) + + def mutate(self, node, index): + """Join the node with cause variable using a randomly sampled arithmetic operator.""" + assert isinstance(node, PythonNode) + assert (node.type == "arith_expr" or node.type == "term") + + arith_operator = random.choice(['+', '*', '-']) + arith_operator_node_start_pos = self._iterate_col(node.end_pos) + cause_node_start_pos = self._iterate_col(arith_operator_node_start_pos) + arith_operator_node = parso.python.tree.Operator(arith_operator, start_pos=arith_operator_node_start_pos) + cause_node = Name(self.cause_variable, start_pos=cause_node_start_pos) + replacement_node = parso.python.tree.PythonNode("arith_expr", [node, arith_operator_node, cause_node]) + return replacement_node + + def _get_causes_from_expr_node(self, expr_node): + rhs = expr_node.get_rhs().children + return self._flatten_expr(rhs) + + def _flatten_expr(self, expr): + for item in expr: + # Convert PythonNode to list of its children + try: + item_to_flatten = item.children + except AttributeError: + item_to_flatten = item + # + try: + yield from self._flatten_expr(item_to_flatten) + except TypeError: + yield item_to_flatten + + @staticmethod + def _iterate_col(position_tuple): + return tuple(sum(x) for x in zip(position_tuple, (0, 1))) + + @classmethod + def examples(cls): + return ( + Example('y = x + z', 'y = x + z * j', + operator_args={'cause_variable': 'j', 'effect_variable': 'y'}), + Example('j = x + z\ny = x + z', 'j = x + z + x\ny = x + z', + operator_args={'cause_variable': 'x', 'effect_variable': 'j'}), + ) + diff --git a/src/cosmic_ray/operators/variable_replacer.py b/src/cosmic_ray/operators/variable_replacer.py new file mode 100644 index 00000000..eba49d74 --- /dev/null +++ b/src/cosmic_ray/operators/variable_replacer.py @@ -0,0 +1,89 @@ +"""Implementation of the variable-replacement operator.""" +from .operator import Operator +from .example import Example +from parso.python.tree import Number, ExprStmt, Leaf +from random import randint + + +class VariableReplacer(Operator): + """An operator that replaces usages of named variables.""" + + def __init__(self, cause_variable, effect_variable=None): + self.cause_variable = cause_variable + self.effect_variable = effect_variable + + def mutation_positions(self, node): + """Mutate usages of the specified cause variable. If an effect variable is also + specified, then only mutate usages of the cause variable in definitions of the + effect variable.""" + + if isinstance(node, ExprStmt): + # Confirm that name node is used on right hand side of the expression + cause_variables = list(self._get_causes_from_expr_node(node)) + cause_variable_names = [cause_variable.value for cause_variable in cause_variables] + if self.cause_variable in cause_variable_names: + mutation_position = (node.start_pos, node.end_pos) + + # If an effect variable is specified, confirm that it appears on left hand + # side of the expression + if self.effect_variable: + effect_variable_names = [v.value for v in node.get_defined_names()] + if self.effect_variable in effect_variable_names: + yield mutation_position + + # If no effect variable is specified, any occurrence of the cause variable + # on the right hand side of an expression can be mutated + else: + yield mutation_position + + def mutate(self, node, index): + """Replace cause variable with random constant.""" + assert isinstance(node, ExprStmt) + # Find all occurrences of the cause node in the ExprStatement and replace with a random number + rhs = node.get_rhs() + new_rhs = self._replace_named_variable_in_expr(rhs, self.cause_variable) + node.children[2] = new_rhs + return node + + def _get_causes_from_expr_node(self, expr_node): + rhs = expr_node.get_rhs().children + return self._flatten_expr(rhs) + + def _flatten_expr(self, expr): + for item in expr: + # Convert PythonNode to list of its children + try: + item_to_flatten = item.children + except AttributeError: + item_to_flatten = item + # + try: + yield from self._flatten_expr(item_to_flatten) + except TypeError: + yield item_to_flatten + + def _replace_named_variable_in_expr(self, node, variable_name): + if isinstance(node, Leaf): + if node.value == variable_name: + return Number(start_pos=node.start_pos, value=str(randint(-100, 100))) + else: + return node + + updated_child_nodes = [] + for child_node in node.children: + updated_child_nodes.append(self._replace_named_variable_in_expr(child_node, variable_name)) + node.children = updated_child_nodes + return node + + @classmethod + def examples(cls): + return ( + Example('y = x + z', 'y = 10 + z', operator_args={'cause_variable': 'x'}), + Example('j = x + z\ny = x + z', 'j = x + z\ny = -2 + z', + operator_args={'cause_variable': 'x', 'effect_variable': 'y'}), + Example('j = x + z\ny = x + z', 'j = 1 + z\ny = x + z', + operator_args={'cause_variable': 'x','effect_variable': 'j'}), + Example('y = 2*x + 10 + j + x**2', 'y=2*10 + 10 + j + -4**2', + operator_args={'cause_variable': 'x'}), + ) + diff --git a/src/cosmic_ray/operators/zero_iteration_for_loop.py b/src/cosmic_ray/operators/zero_iteration_for_loop.py index ab86da28..8f54e0de 100644 --- a/src/cosmic_ray/operators/zero_iteration_for_loop.py +++ b/src/cosmic_ray/operators/zero_iteration_for_loop.py @@ -4,7 +4,7 @@ from parso.python.tree import ForStmt from .operator import Operator - +from .example import Example class ZeroIterationForLoop(Operator): """An operator that modified for-loops to have zero iterations.""" @@ -25,4 +25,4 @@ def mutate(self, node, index): @classmethod def examples(cls): - return (("for i in rang(1,2): pass", "for i in []: pass"),) + return (Example("for i in rang(1,2): pass", "for i in []: pass"),) diff --git a/src/cosmic_ray/work_db.py b/src/cosmic_ray/work_db.py index d0fc5ff3..d56853ad 100644 --- a/src/cosmic_ray/work_db.py +++ b/src/cosmic_ray/work_db.py @@ -1,9 +1,10 @@ """Implementation of the WorkDB.""" import contextlib +import json from pathlib import Path -from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, create_engine, event +from sqlalchemy import Column, Enum, ForeignKey, Integer, JSON, String, Text, create_engine, event from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import sessionmaker @@ -94,7 +95,6 @@ def add_work_items(self, work_items): work_items: an iterable of WorkItem. """ storage = (_work_item_to_storage(work_item) for work_item in work_items) - with self._session_maker.begin() as session: session.add_all(storage) @@ -198,6 +198,7 @@ class MutationSpecStorage(Base): __tablename__ = "mutation_specs" module_path = Column(String) operator_name = Column(String) + operator_args = Column(JSON) occurrence = Column(Integer) start_pos_row = Column(Integer) start_pos_col = Column(Integer) @@ -222,6 +223,7 @@ def _mutation_spec_from_storage(mutation_spec: MutationSpecStorage): return ResolvedMutationSpec( module_path=Path(mutation_spec.module_path), operator_name=mutation_spec.operator_name, + operator_args=json.loads(mutation_spec.operator_args), occurrence=mutation_spec.occurrence, start_pos=(mutation_spec.start_pos_row, mutation_spec.start_pos_col), end_pos=(mutation_spec.end_pos_row, mutation_spec.end_pos_col), @@ -233,6 +235,7 @@ def _mutation_spec_to_storage(mutation_spec: ResolvedMutationSpec, job_id: str): job_id=job_id, module_path=str(mutation_spec.module_path), operator_name=mutation_spec.operator_name, + operator_args=json.dumps(mutation_spec.operator_args), occurrence=mutation_spec.occurrence, start_pos_row=mutation_spec.start_pos[0], start_pos_col=mutation_spec.start_pos[1], diff --git a/src/cosmic_ray/work_item.py b/src/cosmic_ray/work_item.py index 6b52b974..a5a57d5c 100644 --- a/src/cosmic_ray/work_item.py +++ b/src/cosmic_ray/work_item.py @@ -4,7 +4,7 @@ import enum import pathlib from pathlib import Path -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict class StrEnum(str, enum.Enum): @@ -74,6 +74,7 @@ class ResolvedMutationSpec(MutationSpec): "A MutationSpec with the location of the mutation resolved." start_pos: Tuple[int, int] end_pos: Tuple[int, int] + operator_args: Optional[Dict] = None # pylint: disable=R0913 def __post_init__(self): @@ -85,6 +86,9 @@ def __post_init__(self): if self.start_pos[1] >= self.end_pos[1]: raise ValueError("End position must come after start position.") + # if not hasattr(self, "operator_args"): + object.__setattr__(self, "operator_args", self.operator_args) + @dataclasses.dataclass(frozen=True) class WorkItem: diff --git a/tests/unittests/test_operators.py b/tests/unittests/test_operators.py index 213d2e05..f86bd413 100644 --- a/tests/unittests/test_operators.py +++ b/tests/unittests/test_operators.py @@ -7,19 +7,18 @@ from cosmic_ray.plugins import get_operator, operator_names from cosmic_ray.operators.unary_operator_replacement import ReplaceUnaryOperator_USub_UAdd from cosmic_ray.operators.binary_operator_replacement import ReplaceBinaryOperator_Add_Mul +from cosmic_ray.operators.example import Example from cosmic_ray.mutating import MutationVisitor class Sample: - def __init__(self, operator, from_code, to_code, index=0): + def __init__(self, operator, example): self.operator = operator - self.from_code = from_code - self.to_code = to_code - self.index = index + self.example = example OPERATOR_PROVIDED_SAMPLES = tuple( - Sample(operator_class, *example) + Sample(operator_class, example) for operator_class in map(get_operator, operator_names()) for example in operator_class.examples() ) @@ -28,8 +27,8 @@ def __init__(self, operator, from_code, to_code, index=0): Sample(*args) for args in ( # Make sure unary and binary op mutators don't pick up the wrong kinds of operators - (ReplaceUnaryOperator_USub_UAdd, "x + 1", "x + 1"), - (ReplaceBinaryOperator_Add_Mul, "+1", "+1"), + (ReplaceUnaryOperator_USub_UAdd, Example("x + 1", "x + 1")), + (ReplaceBinaryOperator_Add_Mul, Example("+1", "+1")), ) ) @@ -38,17 +37,19 @@ def __init__(self, operator, from_code, to_code, index=0): @pytest.mark.parametrize("sample", OPERATOR_SAMPLES) def test_mutation_changes_ast(sample): - node = parso.parse(sample.from_code) - visitor = MutationVisitor(sample.index, sample.operator()) + node = parso.parse(sample.example.pre_mutation_code) + visitor = MutationVisitor(sample.example.occurrence, + sample.operator(**sample.example.operator_args)) mutant = visitor.walk(node) - assert mutant.get_code() == sample.to_code + assert mutant.get_code() == sample.example.post_mutation_code @pytest.mark.parametrize("sample", OPERATOR_SAMPLES) def test_no_mutation_leaves_ast_unchanged(sample): - node = parso.parse(sample.from_code) - visitor = MutationVisitor(-1, sample.operator()) + print(sample.operator, sample.example) + node = parso.parse(sample.example.pre_mutation_code) + visitor = MutationVisitor(-1, sample.operator(**sample.example.operator_args)) mutant = visitor.walk(node) - assert mutant.get_code() == sample.from_code + assert mutant.get_code() == sample.example.pre_mutation_code