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:

  1. The name of the operator_provider entry point (i.e. as specified in setup.py)
  2. 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 the node 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.