Mutation Operators¶
In Cosmic Ray we use mutation operators to implement the various forms of mutation that we support. For each specific kind of mutation – constant replacement, break/continue swaps, and so forth – there is an operator class that knows how to create that mutation from un-mutated code.
Implementation details¶
Cosmic Ray relies on parso to parse Python code into trees. Cosmic Ray operators work directly on this tree, and the results of modifying this tree are written to disk for each mutation.
Each operator is ultimately a subclass of
cosmic_ray.operators.operator.Operator
. We pass operators to various
parse-tree visitors that let the operator view and modify the tree. When an
operator reports that it can potentially modify a part of the tree, Cosmic Ray
notes this and, later, asks the operator to actually perform this mutation.
Implementing an operator¶
To implement a new operator you need to create a subclass of
cosmic_ray.operators.operator.Operator
. The first method an operator must implement
is Operator.mutation_positions()
which tells Cosmic Ray how the operator could mutate
a particular parse-tree node.
Second, an operator subclass must implement Operator.mutate()
which actually mutates
a parse-tree node.
Finally, an operator must implement the class method Operator.examples()
.
This provides a set of before and after code snippets showing how the operator
works. These examples are used in the test suite and potentially for
documenation purposes. An operator can choose to provide no examples simply by
returning an empty iterable from examples
, though we may decide to check
for an absence of examples in the future. In any case, it’s good form to provide
examples.
In both cases, the operator implementation works directly with the parso
parse tree objects.
Operator provider plugins¶
Cosmic Ray is designed to be extended with arbitrary operators provided by
users. It dynamically discovers operators at runtime using the stevedore
plugin system which relies on the setuptools
entry_points
concept.
Rather than having individual plugins for each operator, Cosmic Ray lets users specify operator provider plugins. An operator provider can supply any number of operators to Cosmic Ray. At a high level, Cosmic Ray finds all of the operators available to it by iterating over the operator provider plugins, and for each of those iterating over the operators that it exposes.
The operator provider API is very simple:
class OperatorProvider:
def __iter__(self):
"The sequence of operator names that this provider supplies"
pass
def __getitem__(self, name):
"Get an operator class by name."
pass
In other words, a provider must have a (locally) unique name for each operator it provides, it must provide an iterator over those names, and it must allow Cosmic Ray to look up operator classes by name.
To make a new operator provider available to Cosmic Ray you need to create a
cosmic_ray.operator_providers
entry point; this is generally done in
setup.py
. We’ll show an example of how to do this later.
Operator naming¶
All operators in Cosmic Ray have a unique name for any given session. The name of an operator is based on two elements:
- The name of the
operator_provider
entry point (i.e. as specified insetup.py
) - The name that the provider associates with the operator.
The full name of an operator is simply the provider’s name and the operator’s name joined with “/”. For example, if the provider’s name was “widget_corp” and the operator’s name was “add_whitespace”, the full name of the operator would be “widget_corp/add_whitespace”.
A full example: NumberReplacer
¶
One of the operators bundled with Cosmic Ray is implemented with the clas
cosmic_ray.operators.number_replacer.NumberReplacer
. This operator looks for
Num
nodes (number literals in source code) and replaces them with new
Num
nodes that have a different numeric value. To demonstrate how to create
a mutation operator and provider, we’ll step through how to create that operator
in a new package called example
.
Creating the operator class¶
The initial layout for our package is like this:
setup.py
example/
__init__.py
__init__.py
is empty and setup.py
has very minimal content:
from setuptools import setup
setup(
name='example',
version='0.1.0',
)
The first thing we need to do is create a new Python source file to hold
our new operator. Create a file named number_replacer.py
in the
example
directory. It has the following contents:
from cosmic_ray.operators.operator import Operator
import parso
class NumberReplacer(Operator):
"""An operator that modifies numeric constants."""
def mutation_positions(self, node):
if isinstance(node, parso.python.tree.Number):
yield (node.start_pos, node.end_pos)
def mutate(self, node, index):
"""Modify the numeric value on `node`."""
assert isinstance(node, parso.python.tree.Number)
val = eval(node.value) + 1
return parso.python.tree.Number(' ' + str(val), node.start_pos)
Let’s step through this line-by-line. We first import Operator
because we need to inherit from it:
from cosmic_ray.operators.operator import Operator
We then import parso
because we need to use it to create mutated nodes:
import parso
We define our new operator by creating a subclass of Operator
called
NumberReplacer
:
class NumberReplacer(Operator):
The mutate_positions
method is called whenever Cosmic Ray needs to know if an operator can mutate a particular
node. We implement ours to report a single mutation at each “number”:
def mutation_positions(self, node):
if isinstance(node, parso.python.tree.Number):
yield (node.start_pos, node.end_pos)
Finally we implement Operator.mutate()
which is called to actually
perform the mutation. mutate()
should return one of:
None
if thenode
argument should be removed from the tree, or- a new
parso
node to replace the original one
In this case, we simply create a new Number
node with a new value and
return it:
def mutate(self, node, index):
"""Modify the numeric value on `node`."""
assert isinstance(node, parso.python.tree.Number)
val = eval(node.value) + 1
return parso.python.tree.Number(' ' + str(val), node.start_pos)
That’s all there is to it. This mutation operator is now ready to be applied to any code you want to test.
However, before it can really be used, you need to make it available as a plugin.
Creating the provider¶
In order to expose our operator to Cosmic Ray we need to create an operator
provider plugin. In the case of a single operator like ours, the provider
implementation is very simple. We’ll put the implementation in
example/provider.py
:
# example/provider.py
from .number_replacer import NumberReplacer
class Provider:
_operators = {'number-replacer': NumberReplacer}
def __iter__(self):
return iter(Provider._operators)
def __getitem__(self, name):
return Provider._operators[name]
Creating the plugin¶
In order to make your operator available to Cosmic Ray as a plugin, you
need to define a new cosmic_ray.operator_providers
entry point. This is
generally done through setup.py
, which is what we’ll do here.
Modify setup.py
with a new entry_points
argument to setup()
:
setup(
. . .
entry_points={
'cosmic_ray.operator_providers': [
'example = example.provider:Provider'
]
})
Now when Cosmic Ray queries the cosmic_ray.operator_providers
entry point it
will see your provider - and hence your operator - along with all of the others.