Skip to content

Commit

Permalink
Mutation operators with arguments (Issue #528) (#529)
Browse files Browse the repository at this point in the history
* Created a variable replacer mutation operator

* Fixed typo in variable replacer operator examples

* Work item now stores parameter information

* Work DB now stores operator args and applies them

* Initialisation only tries mutation operators without args if none are specified

* Mutate and test now robust to lack of operator_args

* Added a variable inserter mutation operator

* Updated VariableInserter documentation

* Removed unneccessary comment from variable inserter

* VariableReplacer now replaces all usages of variable in statement

* Example dataclass added and tests updated

* Added exception to TypeError to catch operator args typos

* Refactored getting operator args in init

* Config is now loaded with a configuration or None
  • Loading branch information
Andrew Clark authored Sep 3, 2022
1 parent aff93d9 commit e02b8ec
Show file tree
Hide file tree
Showing 22 changed files with 316 additions and 93 deletions.
4 changes: 2 additions & 2 deletions src/cosmic_ray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", ()))

Expand All @@ -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)

Expand Down
60 changes: 36 additions & 24 deletions src/cosmic_ray/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
6 changes: 6 additions & 0 deletions src/cosmic_ray/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
12 changes: 8 additions & 4 deletions src/cosmic_ray/mutating.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
)
Expand Down
4 changes: 2 additions & 2 deletions src/cosmic_ray/operators/binary_operator_replacement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions src/cosmic_ray/operators/boolean_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .keyword_replacer import KeywordReplacementOperator
from .operator import Operator
from .example import Example


class ReplaceTrueWithFalse(KeywordReplacementOperator):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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"),
)
3 changes: 2 additions & 1 deletion src/cosmic_ray/operators/comparison_operator_replacement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions src/cosmic_ray/operators/example.py
Original file line number Diff line number Diff line change
@@ -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", {})
14 changes: 7 additions & 7 deletions src/cosmic_ray/operators/exception_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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"),
)
3 changes: 2 additions & 1 deletion src/cosmic_ray/operators/keyword_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from parso.python.tree import Keyword

from .operator import Operator
from .example import Example

# pylint: disable=E1101

Expand All @@ -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),
)
8 changes: 4 additions & 4 deletions src/cosmic_ray/operators/no_op.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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'),
)
13 changes: 7 additions & 6 deletions src/cosmic_ray/operators/number_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
)
18 changes: 9 additions & 9 deletions src/cosmic_ray/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
6 changes: 4 additions & 2 deletions src/cosmic_ray/operators/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
}


Expand Down
Loading

0 comments on commit e02b8ec

Please sign in to comment.