# Notebook from rindPHI/debuggingbook
Path: notebooks/Repairer.ipynb

# Repairing Code Automatically

So far, we have discussed how to track failures and how to locate defects in code. Let us now discuss how to _repair_ defects – that is, to correct the code such that the failure no longer occurs. We will discuss how to _repair code automatically_ – by systematically searching through possible fixes and evolving the most promising candidates._____no_output_____

from bookutils import YouTubeVideo
YouTubeVideo("UJTf7cW0idI")_____no_output_____

**Prerequisites**

* Re-read the [introduction to debugging](Intro_Debugging.ipynb), notably on how to properly fix code.
* We make use of automatic fault localization, as discussed in [the chapter on statistical debugging](StatisticalDebugger.ipynb).
* We make extensive use of code transformations, as discussed in [the chapter on tracing executions](Tracer.ipynb).
* We make use of [delta debugging](DeltaDebugger.ipynb)._____no_output_____

import bookutils_____no_output_____

## Synopsis

To [use the code provided in this chapter](Importing.ipynb), write

```python
>>> from debuggingbook.Repairer import <identifier>
```

and then make use of the following features.

This chapter provides tools and techniques for automated repair of program code. The `Repairer()` class takes a `RankingDebugger` debugger as input (such as `OchiaiDebugger` from [the chapter on statistical debugging](StatisticalDebugger.ipynb)). A typical setup looks like this:

```python
from debuggingbook.StatisticalDebugger import OchiaiDebugger

debugger = OchiaiDebugger()
for inputs in TESTCASES:
    with debugger:
        test_foo(inputs)
...

repairer = Repairer(debugger)
```
Here, `test_foo()` is a function that raises an exception if the tested function `foo()` fails. If `foo()` passes, `test_foo()` should not raise an exception.

The `repair()` method of a `Repairer` searches for a repair of the code covered in the debugger (except for methods starting or ending in `test`, such that `foo()`, not `test_foo()` is repaired). `repair()` returns the best fix candidate as a pair `(tree, fitness)` where `tree` is a [Python abstract syntax tree](http://docs.python.org/3/library/ast) (AST) of the fix candidate, and `fitness` is the fitness of the candidate (a value between 0 and 1). A `fitness` of 1.0 means that the candidate passed all tests. A typical usage looks like this:

```python
import astor

tree, fitness = repairer.repair()
print(astor.to_source(tree), fitness)
```

Here is a complete example for the `middle()` program. This is the original source code of `middle()`:

```python
def middle(x, y, z):  # type: ignore
    if y < z:
        if x < y:
            return y
        elif x < z:
            return y
    else:
        if x > y:
            return y
        elif x > z:
            return x
    return z
```
We set up a function `middle_test()` that tests it. The `middle_debugger`  collects testcases and outcomes:

```python
>>> middle_debugger = OchiaiDebugger()
>>> for x, y, z in MIDDLE_PASSING_TESTCASES + MIDDLE_FAILING_TESTCASES:
>>>     with middle_debugger:
>>>         middle_test(x, y, z)
```
The repairer attempts to repair the invoked function (`middle()`). The returned AST `tree` can be output via `astor.to_source()`:

```python
>>> middle_repairer = Repairer(middle_debugger)
>>> tree, fitness = middle_repairer.repair()
>>> print(astor.to_source(tree), fitness)
def middle(x, y, z):
    if y < z:
        if x < z:
            if x < y:
                return y
            else:
                return x
    elif x > y:
        return y
    elif x > z:
        return x
    return z
 1.0

```
Here are the classes defined in this chapter. A `Repairer` repairs a program, using a `StatementMutator` and a `CrossoverOperator` to evolve a population of candidates.

![](PICS/Repairer-synopsis-1.svg)

_____no_output_____## Automatic Code Repairs

So far, we have discussed how to locate defects in code, how to track failures back to the defects that caused them, and how to systematically determine failure conditions. Let us now address the last step in debugging – namely, how to _automatically fix code_.

Already in the [introduction to debugging](Intro_Debugging.ipynb), we have discussed how to fix code manually. Notably, we have established that a _diagnosis_ (which induces a fix) should show _causality_ (i.e., how the defect causes the failure) and _incorrectness_ (how the defect is wrong). Is it possible to obtain such a diagnosis automatically?_____no_output_____In this chapter, we introduce a technique of _automatic code repair_ – that is, for a given failure, automatically determine a fix that makes the failure go away. To do so, we randomly (but systematically) _mutate_ the program code – that is, insert, change, and delete fragments – until we find a change that actually causes the failing test to pass._____no_output_____If this sounds like an audacious idea, that is because it is. But not only is _automated program repair_ one of the hottest topics of software research in the last decade, it is also being increasingly deployed in industry. At Facebook, for instance, every failing test report comes with an automatically generated _repair suggestion_ – a suggestion that already has been validated to work. Programmers can apply the suggestion as is or use it as basis for their own fixes._____no_output_____### The middle() Function_____no_output_____Let us introduce our ongoing example. In the [chapter on statistical debugging](StatisticalDebugger.ipynb), we have introduced the `middle()` function – a function that returns the "middle" of three numbers `x`, `y`, and `z`:_____no_output_____

from StatisticalDebugger import middle_____no_output_____# ignore
from bookutils import print_content_____no_output_____# ignore
import inspect_____no_output_____# ignore
_, first_lineno = inspect.getsourcelines(middle)
middle_source = inspect.getsource(middle)
print_content(middle_source, '.py', start_line_number=first_lineno)_____no_output_____

In most cases, `middle()` just runs fine:_____no_output_____

middle(4, 5, 6)_____no_output_____

In some other cases, though, it does not work correctly:_____no_output_____

middle(2, 1, 3)_____no_output_____

### Validated Repairs_____no_output_____Now, if we only want a repair that fixes this one given failure, this would be very easy. All we have to do is to replace the entire body by a single statement:_____no_output_____

def middle_sort_of_fixed(x, y, z):  # type: ignore
    return x_____no_output_____

You will concur that the failure no longer occurs:_____no_output_____

middle_sort_of_fixed(2, 1, 3)_____no_output_____

But this, of course, is not the aim of automatic fixes, nor of fixes in general: We want our fixes not only to make the given failure go away, but we also want the resulting code to be _correct_ (which, of course, is a lot harder)._____no_output_____Automatic repair techniques therefore assume the existence of a _test suite_ that can check whether an implementation satisfies its requirements. Better yet, one can use the test suite to gradually check _how close_ one is to perfection: A piece of code that satisfies 99% of all tests is better than one that satisfies ~33% of all tests, as `middle_sort_of_fixed()` would do (assuming the test suite evenly checks the input space)._____no_output_____### Genetic Optimization_____no_output_____The master plan for automatic repair #TODO: also many other promising approaches which do not rely on genetics, so not actual the master plan more like a common approach# follows the principle of _genetic optimization_. Roughly spoken, genetic optimization is a _metaheuristic_ inspired by the process of _natural selection_. The idea is to _evolve_ a selection of _candidate solutions_ towards a maximum _fitness_:

1. Have a selection of _candidates_.
2. Determine the _fitness_ of each candidate.
3. Retain those candidates with the _highest fitness_.
4. Create new candidates from the retained candidates, by applying genetic operations:
    * _Mutation_ mutates some aspect of a candidate.
    * _CrossoverOperator_ creates new candidates combining features of two candidates.
5. Repeat until an optimal solution is found._____no_output_____Applied for automated program repair, this means the following steps:

1. Have a _test suite_ with both failing and passing tests that helps asserting correctness of possible solutions.
2. With the test suite, use [fault localization](StatisticalDebugger.ipynb) to determine potential code locations to be fixed.
3. Systematically _mutate_ the code (by adding, changing, or deleting code) and _cross_ code to create possible fix candidates.
4. Identify the _fittest_ fix candidates – that is, those that satisfy the most tests.
5. _Evolve_ the fittest candidates until a perfect fix is found, or until time resources are depleted._____no_output_____Let us illustrate these steps in the following sections._____no_output_____## A Test Suite_____no_output_____In automated repair, the larger and the more thorough the test suite, the higher the quality of the resulting fix (if any). Hence, if we want to repair `middle()` automatically, we need a good test suite – with good inputs, but also with good checks #TODO: there is a tradeoff between the size of a test suite and the runtime of the repairer, running the suite takes commonly the most time for repairing#_____no_output_____For better repair, we will use the test suites introduced in the [chapter on statistical debugging](StatisticalDebugger.ipynb):_____no_output_____

from StatisticalDebugger import MIDDLE_PASSING_TESTCASES, MIDDLE_FAILING_TESTCASES_____no_output_____

The `middle_test()` function fails whenever `middle()` returns an incorrect result:_____no_output_____

def middle_test(x: int, y: int, z: int) -> None:
    m = middle(x, y, z)
    assert m == sorted([x, y, z])[1]_____no_output_____from ExpectError import ExpectError_____no_output_____with ExpectError():
    middle_test(2, 1, 3)_____no_output_____

## Locating the Defect_____no_output_____Our next step is to find potential defect locations – that is, those locations in the code our mutations should focus upon. Since we already do have two test suites, we can make use of [statistical debugging](StatisticalDebugger.ipynb) #TODO: not actually statistical debugging but rather spectrum based fault localization# to identify likely faulty locations.  Our `OchiaiDebugger` ranks individual code lines by how frequently they are executed in failing runs (and not in passing runs)._____no_output_____

from StatisticalDebugger import OchiaiDebugger, RankingDebugger_____no_output_____middle_debugger = OchiaiDebugger()

for x, y, z in MIDDLE_PASSING_TESTCASES + MIDDLE_FAILING_TESTCASES:
    with middle_debugger:
        middle_test(x, y, z)_____no_output_____

We see that the upper half of the `middle()` code is definitely more suspicious:_____no_output_____

middle_debugger_____no_output_____

The most suspicious line is:_____no_output_____

# ignore
location = middle_debugger.rank()[0]
(func_name, lineno) = location
lines, first_lineno = inspect.getsourcelines(middle)
print(lineno, end="")
print_content(lines[lineno - first_lineno], '.py')_____no_output_____

with a suspiciousness of:_____no_output_____

# ignore
middle_debugger.suspiciousness(location)_____no_output_____

## Random Code Mutations_____no_output_____Our third step in automatic code repair is to _randomly mutate the code_. Specifically, we want to randomly _delete_, _insert_, and _replace_ statements in the program to be repaired. However, simply synthesizing code _from scratch_ is unlikely to yield anything meaningful – the number of combinations is simply far too high. Already for a three-character identifier name, we have more than 200,000 combinations: #TODO: you could also explain the concept behind this mutations, i.e. the redundancy assumption/plastic surgery hypothesis#_____no_output_____

import string_____no_output_____string.ascii_letters_____no_output_____len(string.ascii_letters + '_') * \
  len(string.ascii_letters + '_' + string.digits) * \
  len(string.ascii_letters + '_' + string.digits)_____no_output_____

Hence, we do _not_ synthesize code from scratch, but instead _reuse_ elements from the program to be fixed, hypothesizing that "a program that contains an error in one area likely implements the correct behavior elsewhere" \cite{LeGoues2012}._____no_output_____Furthermore, we do not operate on a _textual_ representation of the program, but rather on a _structural_ representation, which by construction allows us to avoid lexical and syntactical errors in the first place.

This structural representation is the _abstract syntax tree_ (AST), which we already have seen in various chapters, such as the [chapter on delta debugging](DeltaDebugger.ipynb), the [chapter on tracing](Tracer.ipynb), and excessively in the [chapter on slicing](Slicer.ipynb). The [official Python `ast` reference](http://docs.python.org/3/library/ast) is complete, but a bit brief; the documentation ["Green Tree Snakes - the missing Python AST docs"](https://greentreesnakes.readthedocs.io/en/latest/) provides an excellent introduction.

Recapitulating, an AST is a tree representation of the program, showing a hierarchical structure of the program's elements. Here is the AST for our `middle()` function._____no_output_____

import ast
import astor
import inspect_____no_output_____from bookutils import print_content, show_ast_____no_output_____def middle_tree() -> ast.AST:
    return ast.parse(inspect.getsource(middle))_____no_output_____show_ast(middle_tree())_____no_output_____

 You see that it consists of one function definition (`FunctionDef`) with three `arguments` and two statements – one `If` and one `Return`. Each `If` subtree has three branches – one for the condition (`test`), one for the body to be executed if the condition is true (`body`), and one for the `else` case (`orelse`). The `body` and `orelse` branches again are lists of statements._____no_output_____An AST can also be shown as text, which is more compact, yet reveals more information. `ast.dump()` gives not only the class names of elements, but also how they are constructed – actually, the whole expression can be used to construct an AST._____no_output_____

print(ast.dump(middle_tree()))_____no_output_____

This is the path to the first `return` statement:_____no_output_____

ast.dump(middle_tree().body[0].body[0].body[0].body[0])  # type: ignore_____no_output_____

### Picking Statements_____no_output_____For our mutation operators, we want to use statements from the program itself. Hence, we need a means to find those very statements. The `StatementVisitor` class iterates through an AST, adding all statements it finds in function definitions to its `statements` list. To do so, it subclasses the Python `ast` `NodeVisitor` class, described in the [official Python `ast` reference](http://docs.python.org/3/library/ast)._____no_output_____

from ast import NodeVisitor_____no_output_____# ignore
from typing import Any, Callable, Optional, Type, Tuple
from typing import Dict, Union, Set, List, cast_____no_output_____class StatementVisitor(NodeVisitor):
    """Visit all statements within function defs in an AST"""

    def __init__(self) -> None:
        self.statements: List[Tuple[ast.AST, str]] = []
        self.func_name = ""
        self.statements_seen: Set[Tuple[ast.AST, str]] = set()
        super().__init__()

    def add_statements(self, node: ast.AST, attr: str) -> None:
        elems: List[ast.AST] = getattr(node, attr, [])
        if not isinstance(elems, list):
            elems = [elems]  # type: ignore

        for elem in elems:
            stmt = (elem, self.func_name)
            if stmt in self.statements_seen:
                continue

            self.statements.append(stmt)
            self.statements_seen.add(stmt)

    def visit_node(self, node: ast.AST) -> None:
        # Any node other than the ones listed below
        self.add_statements(node, 'body')
        self.add_statements(node, 'orelse')

    def visit_Module(self, node: ast.Module) -> None:
        # Module children are defs, classes and globals - don't add
        super().generic_visit(node)

    def visit_ClassDef(self, node: ast.ClassDef) -> None:
        # Class children are defs and globals - don't add
        super().generic_visit(node)

    def generic_visit(self, node: ast.AST) -> None:
        self.visit_node(node)
        super().generic_visit(node)

    def visit_FunctionDef(self,
                          node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None:
        if not self.func_name:
            self.func_name = node.name

        self.visit_node(node)
        super().generic_visit(node)
        self.func_name = ""

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
        return self.visit_FunctionDef(node)_____no_output_____

The function `all_statements()` returns all statements in the given AST `tree`. If an `ast` class `tp` is given, it only returns instances of that class._____no_output_____

def all_statements_and_functions(tree: ast.AST,
                                 tp: Optional[Type] = None) -> \
                                 List[Tuple[ast.AST, str]]:
    """
    Return a list of pairs (`statement`, `function`) for all statements in `tree`.
    If `tp` is given, return only statements of that class.
    """

    visitor = StatementVisitor()
    visitor.visit(tree)
    statements = visitor.statements
    if tp is not None:
        statements = [s for s in statements if isinstance(s[0], tp)]

    return statements_____no_output_____def all_statements(tree: ast.AST, tp: Optional[Type] = None) -> List[ast.AST]:
    """
    Return a list of all statements in `tree`.
    If `tp` is given, return only statements of that class.
    """

    return [stmt for stmt, func_name in all_statements_and_functions(tree, tp)]_____no_output_____

Here are all the `return` statements in `middle()`:_____no_output_____

all_statements(middle_tree(), ast.Return)_____no_output_____all_statements_and_functions(middle_tree(), ast.If)_____no_output_____

We can randomly pick an element:_____no_output_____

import random_____no_output_____random_node = random.choice(all_statements(middle_tree()))
astor.to_source(random_node)_____no_output_____

### Mutating Statements

The main part in mutation, however, is to actually mutate the code of the program under test. To this end, we introduce a `StatementMutator` class – a subclass of `NodeTransformer`, described in the [official Python `ast` reference](http://docs.python.org/3/library/ast)._____no_output_____The constructor provides various keyword arguments to configure the mutator._____no_output_____

from ast import NodeTransformer_____no_output_____import copy_____no_output_____class StatementMutator(NodeTransformer):
    """Mutate statements in an AST for automated repair."""

    def __init__(self,
                 suspiciousness_func:
                     Optional[Callable[[Tuple[Callable, int]], float]] = None,
                 source: Optional[List[ast.AST]] = None,
                 log: bool = False) -> None:
        """
        Constructor.
        `suspiciousness_func` is a function that takes a location
        (function, line_number) and returns a suspiciousness value
        between 0 and 1.0. If not given, all locations get the same
        suspiciousness of 1.0.
        `source` is a list of statements to choose from.
        """

        super().__init__()
        self.log = log

        if suspiciousness_func is None:
            def suspiciousness_func(location: Tuple[Callable, int]) -> float:
                return 1.0
        assert suspiciousness_func is not None

        self.suspiciousness_func: Callable = suspiciousness_func

        if source is None:
            source = []
        self.source = source

        if self.log > 1:
            for i, node in enumerate(self.source):
                print(f"Source for repairs #{i}:")
                print_content(astor.to_source(node), '.py')
                print()
                print()

        self.mutations = 0_____no_output_____

#### Choosing Suspicious Statements to Mutate

We start with deciding which AST nodes to mutate. The method `node_suspiciousness()` returns the suspiciousness for a given node, by invoking the suspiciousness function `suspiciousness_func` given during initialization._____no_output_____

import warnings_____no_output_____class StatementMutator(StatementMutator):
    def node_suspiciousness(self, stmt: ast.AST, func_name: str) -> float:
        if not hasattr(stmt, 'lineno'):
            warnings.warn(f"{self.format_node(stmt)}: Expected line number")
            return 0.0

        suspiciousness = self.suspiciousness_func((func_name, stmt.lineno))
        if suspiciousness is None:  # not executed
            return 0.0

        return suspiciousness

    def format_node(self, node: ast.AST) -> str:
        ..._____no_output_____

The method `node_to_be_mutated()` picks a node (statement) to be mutated. It determines the suspiciousness of all statements, and invokes `random.choices()`, using the suspiciousness as weight. Unsuspicious statements (with zero weight) will not be chosen._____no_output_____

class StatementMutator(StatementMutator):
    def node_to_be_mutated(self, tree: ast.AST) -> ast.AST:
        statements = all_statements_and_functions(tree)
        assert len(statements) > 0, "No statements"

        weights = [self.node_suspiciousness(stmt, func_name)
                   for stmt, func_name in statements]
        stmts = [stmt for stmt, func_name in statements]

        if self.log > 1:
            print("Weights:")
            for i, stmt in enumerate(statements):
                node, func_name = stmt
                print(f"{weights[i]:.2} {self.format_node(node)}")

        if sum(weights) == 0.0:
            # No suspicious line
            return random.choice(stmts)
        else:
            return random.choices(stmts, weights=weights)[0]_____no_output_____

#### Choosing a Mutation Method_____no_output_____The method `visit()` is invoked on all nodes. For nodes marked with a `mutate_me` attribute, it randomly chooses a mutation method (`choose_op()`) and then invokes it on the node.

According to the rules of `NodeTransformer`, the mutation method can return

* a new node or a list of nodes, replacing the current node;
* `None`, deleting it; or
* the node itself, keeping things as they are._____no_output_____

import re_____no_output_____RE_SPACE = re.compile(r'[ \t\n]+')_____no_output_____class StatementMutator(StatementMutator):
    def choose_op(self) -> Callable:
        return random.choice([self.insert, self.swap, self.delete])

    def visit(self, node: ast.AST) -> ast.AST:
        super().visit(node)  # Visits (and transforms?) children

        if not node.mutate_me:  # type: ignore
            return node

        op = self.choose_op()
        new_node = op(node)
        self.mutations += 1

        if self.log:
            print(f"{node.lineno:4}:{op.__name__ + ':':7} "
                  f"{self.format_node(node)} "
                  f"becomes {self.format_node(new_node)}")

        return new_node_____no_output_____

#### Swapping Statements

Our first mutator is `swap()`, which replaces the current node NODE by a random node found in `source` (using a newly defined `choose_statement()`).

`\todo[Dominic]{I'd use typewriter font for NODE and BODY. I think it's nicer to read (distinguish the identifiers), and, what's more, you also use NODE and BODY within code samples, than also in typewriter font.}`

As a rule of thumb, we try to avoid inserting entire subtrees with all attached statements; and try to respect only the first line of a node. If the new node has the form

```python
if P:
    BODY
```

we thus only insert

```python
if P:
    pass
```

since the statements in BODY have a later chance to get inserted. The same holds for all constructs that have a BODY, i.e. `while`, `for`, `try`, `with`, and more._____no_output_____

class StatementMutator(StatementMutator):
    def choose_statement(self) -> ast.AST:
        return copy.deepcopy(random.choice(self.source))_____no_output_____class StatementMutator(StatementMutator):
    def swap(self, node: ast.AST) -> ast.AST:
        """Replace `node` with a random node from `source`"""
        new_node = self.choose_statement()

        if isinstance(new_node, ast.stmt):
            # The source `if P: X` is added as `if P: pass`
            if hasattr(new_node, 'body'):
                new_node.body = [ast.Pass()]  # type: ignore
            if hasattr(new_node, 'orelse'):
                new_node.orelse = []  # type: ignore
            if hasattr(new_node, 'finalbody'):
                new_node.finalbody = []  # type: ignore

        # ast.copy_location(new_node, node)
        return new_node_____no_output_____

#### Inserting Statements

Our next mutator is `insert()`, which randomly chooses some node from `source` and inserts it after the current node NODE. (If NODE is a `return` statement, then we insert the new node _before_ NODE.)

If the statement to be inserted has the form

```python
if P:
    BODY
```

we only insert the "header" of the `if`, resulting in

```python
if P:
    NODE
```

Again, this applies to all constructs that have a BODY, i.e., `while`, `for`, `try`, `with`, and more._____no_output_____

class StatementMutator(StatementMutator):
    def insert(self, node: ast.AST) -> Union[ast.AST, List[ast.AST]]:
        """Insert a random node from `source` after `node`"""
        new_node = self.choose_statement()

        if isinstance(new_node, ast.stmt) and hasattr(new_node, 'body'):
            # Inserting `if P: X` as `if P:`
            new_node.body = [node]  # type: ignore
            if hasattr(new_node, 'orelse'):
                new_node.orelse = []  # type: ignore
            if hasattr(new_node, 'finalbody'):
                new_node.finalbody = []  # type: ignore
            # ast.copy_location(new_node, node)
            return new_node

        # Only insert before `return`, not after it
        if isinstance(node, ast.Return):
            if isinstance(new_node, ast.Return):
                return new_node
            else:
                return [new_node, node]

        return [node, new_node]_____no_output_____

#### Deleting Statements

Our last mutator is `delete()`, which deletes the current node NODE. The standard case is to replace NODE by a `pass` statement.

If the statement to be deleted has the form

```python
if P:
    BODY
```

we only delete the "header" of the `if`, resulting in

```python
BODY
```

Again, this applies to all constructs that have a BODY, i.e., `while`, `for`, `try`, `with`, and more. If the statement to be deleted has multiple branches, a random branch is chosen (e.g., the `else` branch of an `if` statement)._____no_output_____

class StatementMutator(StatementMutator):
    def delete(self, node: ast.AST) -> None:
        """Delete `node`."""

        branches = [attr for attr in ['body', 'orelse', 'finalbody']
                    if hasattr(node, attr) and getattr(node, attr)]
        if branches:
            # Replace `if P: S` by `S`
            branch = random.choice(branches)
            new_node = getattr(node, branch)
            return new_node

        if isinstance(node, ast.stmt):
            # Avoid empty bodies; make this a `pass` statement
            new_node = ast.Pass()
            ast.copy_location(new_node, node)
            return new_node

        return None  # Just delete_____no_output_____from bookutils import quiz_____no_output_____quiz("Why are statements replaced by `pass` rather than deleted?",
     [
         "Because `if P: pass` is valid Python, while `if P:` is not",
         "Because in Python, bodies for `if`, `while`, etc. cannot be empty",
         "Because a `pass` node makes a target for future mutations",
         "Because it causes the tests to pass"
     ], '[3 ^ n for n in range(3)]')_____no_output_____

Indeed, Python's `compile()` will fail if any of the bodies is an empty list. Also, it leaves us a statement that can be evolved further._____no_output_____#### Helpers

For logging purposes, we introduce a helper function `format_node()` that returns a short string representation of the node._____no_output_____

class StatementMutator(StatementMutator):
    NODE_MAX_LENGTH = 20

    def format_node(self, node: ast.AST) -> str:
        """Return a string representation for `node`."""
        if node is None:
            return "None"

        if isinstance(node, list):
            return "; ".join(self.format_node(elem) for elem in node)

        s = RE_SPACE.sub(' ', astor.to_source(node)).strip()
        if len(s) > self.NODE_MAX_LENGTH - len("..."):
            s = s[:self.NODE_MAX_LENGTH] + "..."
        return repr(s)_____no_output_____

#### All Together

Let us now create the main entry point, which is `mutate()`. It picks the node to be mutated and marks it with a `mutate_me` attribute. By calling `visit()`, it then sets off the `NodeTransformer` transformation._____no_output_____

class StatementMutator(StatementMutator):
    def mutate(self, tree: ast.AST) -> ast.AST:
        """Mutate the given AST `tree` in place. Return mutated tree."""

        assert isinstance(tree, ast.AST)

        tree = copy.deepcopy(tree)

        if not self.source:
            self.source = all_statements(tree)

        for node in ast.walk(tree):
            node.mutate_me = False  # type: ignore

        node = self.node_to_be_mutated(tree)
        node.mutate_me = True  # type: ignore

        self.mutations = 0

        tree = self.visit(tree)

        if self.mutations == 0:
            warnings.warn("No mutations found")

        ast.fix_missing_locations(tree)
        return tree_____no_output_____

Here are a number of transformations applied by `StatementMutator`:_____no_output_____

mutator = StatementMutator(log=True)
for i in range(10):
    new_tree = mutator.mutate(middle_tree())_____no_output_____

This is the effect of the last mutator applied on `middle`:_____no_output_____

print_content(astor.to_source(new_tree), '.py')_____no_output_____

## Fitness

Now that we can apply random mutations to code, let us find out how good these mutations are. Given our test suites for `middle`, we can check for a given code candidate how many of the previously passing test cases it passes, and how many of the failing test cases it passes. The more tests pass, the higher the _fitness_ of the candidate._____no_output_____Not all passing tests have the same value, though. We want to prevent _regressions_ – that is, having a fix that breaks a previously passing test. The values of `WEIGHT_PASSING` and `WEIGHT_FAILING` set the relative weight (or importance) of passing vs. failing tests; we see that keeping passing tests passing is far more important then fixing failing tests._____no_output_____

WEIGHT_PASSING = 0.99
WEIGHT_FAILING = 0.01_____no_output_____def middle_fitness(tree: ast.AST) -> float:
    """Compute fitness of a `middle()` candidate given in `tree`"""
    original_middle = middle

    try:
        code = compile(tree, '<fitness>', 'exec')
    except ValueError:
        return 0  # Compilation error

    exec(code, globals())

    passing_passed = 0
    failing_passed = 0

    # Test how many of the passing runs pass
    for x, y, z in MIDDLE_PASSING_TESTCASES:
        try:
            middle_test(x, y, z)
            passing_passed += 1
        except AssertionError:
            pass

    passing_ratio = passing_passed / len(MIDDLE_PASSING_TESTCASES)

    # Test how many of the failing runs pass
    for x, y, z in MIDDLE_FAILING_TESTCASES:
        try:
            middle_test(x, y, z)
            failing_passed += 1
        except AssertionError:
            pass

    failing_ratio = failing_passed / len(MIDDLE_FAILING_TESTCASES)

    fitness = (WEIGHT_PASSING * passing_ratio +
               WEIGHT_FAILING * failing_ratio)

    globals()['middle'] = original_middle
    return fitness_____no_output_____

Our faulty `middle()` program has a fitness of `WEIGHT_PASSING` (99%), because it passes all the passing tests (but none of the failing ones)._____no_output_____

middle_fitness(middle_tree())_____no_output_____

Our "sort of fixed" version of `middle()` gets a much lower fitness:_____no_output_____

middle_fitness(ast.parse("def middle(x, y, z): return x"))_____no_output_____

In the [chapter on statistical debugging](StatisticalDebugger), we also defined a fixed version of `middle()`. This gets a fitness of 1.0, passing all tests. (We won't use this fixed version for automated repairs.)_____no_output_____

from StatisticalDebugger import middle_fixed_____no_output_____middle_fixed_source = \
    inspect.getsource(middle_fixed).replace('middle_fixed', 'middle').strip()_____no_output_____middle_fitness(ast.parse(middle_fixed_source))_____no_output_____

## Population

We now set up a _population_ of fix candidates to evolve over time.  A higher population size will yield more candidates to check, but also need more time to test; a lower population size will yield fewer candidates, but allow for more evolution steps.  We choose a population size of 40 (from \cite{LeGoues2012})._____no_output_____

POPULATION_SIZE = 40
middle_mutator = StatementMutator()_____no_output_____

#TODO: maybe set a seed here, sometime the program gets repaired without evolving, sometimes the repairer seems to produce a solution which does not match the quiz in the next section#_____no_output_____

MIDDLE_POPULATION = [middle_tree()] + \
    [middle_mutator.mutate(middle_tree()) for i in range(POPULATION_SIZE - 1)]_____no_output_____

We sort the fix candidates according to their fitness. This actually runs all tests on all candidates._____no_output_____

MIDDLE_POPULATION.sort(key=middle_fitness, reverse=True)_____no_output_____

The candidate with the highest fitness is still our original (faulty) `middle()` code:_____no_output_____

print(astor.to_source(MIDDLE_POPULATION[0]),
      middle_fitness(MIDDLE_POPULATION[0]))_____no_output_____

At the other end of the spectrum, the candidate with the lowest fitness has some vital functionality removed:_____no_output_____

print(astor.to_source(MIDDLE_POPULATION[-1]),
      middle_fitness(MIDDLE_POPULATION[-1]))_____no_output_____

## Evolution

To evolve our population of candidates, we fill up the population with mutations created from the population, using a `StatementMutator` as described above to create these mutations. Then we reduce the population to its original size, keeping the fittest candidates. #TODO: shouldn't there be some kind of randomness to also keep sometimes candidates with lesser fitness#_____no_output_____

def evolve_middle() -> None:
    global MIDDLE_POPULATION

    source = all_statements(middle_tree())
    mutator = StatementMutator(source=source)

    n = len(MIDDLE_POPULATION)

    offspring: List[ast.AST] = []
    while len(offspring) < n:
        parent = random.choice(MIDDLE_POPULATION)
        offspring.append(mutator.mutate(parent))

    MIDDLE_POPULATION += offspring
    MIDDLE_POPULATION.sort(key=middle_fitness, reverse=True)
    MIDDLE_POPULATION = MIDDLE_POPULATION[:n]_____no_output_____

This is what happens when evolving our population for the first time; the original source is still our best candidate._____no_output_____

evolve_middle()_____no_output_____tree = MIDDLE_POPULATION[0]
print(astor.to_source(tree), middle_fitness(tree))_____no_output_____

However, nothing keeps us from evolving for a few generations more..._____no_output_____

for i in range(50):
    evolve_middle()
    best_middle_tree = MIDDLE_POPULATION[0]
    fitness = middle_fitness(best_middle_tree)
    print(f"\rIteration {i:2}: fitness = {fitness}  ", end="")
    if fitness >= 1.0:
        break_____no_output_____

Success! We find a candidate that actually passes all tests, including the failing ones. Here is the candidate:_____no_output_____

print_content(astor.to_source(best_middle_tree), '.py', start_line_number=1)_____no_output_____

... and yes, it passes all tests:_____no_output_____

original_middle = middle
code = compile(best_middle_tree, '<string>', 'exec')
exec(code, globals())

for x, y, z in MIDDLE_PASSING_TESTCASES + MIDDLE_FAILING_TESTCASES:
    middle_test(x, y, z)

middle = original_middle_____no_output_____

As the code is already validated by hundreds of test cases, it is very valuable for the programmer. Even if the programmer decides not to use the code as is, the location gives very strong hints on which code to examine and where to apply a fix._____no_output_____However, a closer look at our fix candidate shows that there is some amount of redundancy – that is, superfluous statements._____no_output_____

quiz("Some of the lines in our fix candidate are redundant. Which are these?",
    [
        "Line 3: `if x < y`",
        "Line 4: `if x > z`",
        "Line 5: `return x`",
        "Line 13: `return z`"
    ], '[eval(chr(100 - x)) for x in [49, 50]]')_____no_output_____

## Simplifying_____no_output_____As demonstrated in the chapter on [reducing failure-inducing inputs](DeltaDebugger.ipynb), we can use delta debugging on code to get rid of these superfluous statements._____no_output_____The trick for simplification is to have the test function (`test_middle_lines()`) declare a fitness of 1.0 as a "failure". Delta debugging will then simplify the input as long as the "failure" (and hence the maximum fitness obtained) persists._____no_output_____

from DeltaDebugger import DeltaDebugger_____no_output_____middle_lines = astor.to_source(best_middle_tree).strip().split('\n')_____no_output_____def test_middle_lines(lines: List[str]) -> None:
    source = "\n".join(lines)
    tree = ast.parse(source)
    assert middle_fitness(tree) < 1.0  # "Fail" only while fitness is 1.0_____no_output_____with DeltaDebugger() as dd:
    test_middle_lines(middle_lines)_____no_output_____reduced_lines = dd.min_args()['lines']_____no_output_____# assert len(reduced_lines) < len(middle_lines)_____no_output_____reduced_source = "\n".join(reduced_lines)_____no_output_____repaired_source = astor.to_source(ast.parse(reduced_source))  # normalize
print_content(repaired_source, '.py')_____no_output_____

Success! Delta Debugging has eliminated the superfluous statements. We can present the difference to the original as a patch:_____no_output_____

original_source = astor.to_source(ast.parse(middle_source))  # normalize_____no_output_____from ChangeDebugger import diff, print_patch  # minor dependency_____no_output_____for patch in diff(original_source, repaired_source):
    print_patch(patch)_____no_output_____

We can present this patch to the programmer, who will then immediately know what to fix in the `middle()` code._____no_output_____## Crossover

So far, we have only applied one kind of genetic operators – mutation. There is a second one, though, also inspired by natural selection.

The *crossover* operation mutates two strands of genes, as illustrated in the following picture. We have two parents (red and blue), each as a sequence of genes. To create "crossed" children, we pick a _crossover point_ and exchange the strands at this very point:

![](https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/OnePointCrossover.svg/500px-OnePointCrossover.svg.png)_____no_output_____We implement a `CrossoverOperator` class that implements such an operation on two randomly chosen statement lists of two programs. It is used as

```python
crossover = CrossoverOperator()
crossover.crossover(tree_p1, tree_p2)
```

where `tree_p1` and `tree_p2` are two ASTs that are changed in place._____no_output_____### Excursion: Implementing Crossover_____no_output_____#### Crossing Statement Lists_____no_output_____Applied on programs, a crossover mutation takes two parents and "crosses" a list of statements. As an example, if our "parents" `p1()` and `p2()` are defined as follows:_____no_output_____

def p1():  # type: ignore
    a = 1
    b = 2
    c = 3_____no_output_____def p2():  # type: ignore
    x = 1
    y = 2
    z = 3_____no_output_____

Then a crossover operation would produce one child with a body

```python
a = 1
y = 2
z = 3
```

and another child with a body

```python
x = 1
b = 2
c = 3
```_____no_output_____We can easily implement this in a `CrossoverOperator` class in a method `cross_bodies()`._____no_output_____

class CrossoverOperator:
    """A class for performing statement crossover of Python programs"""

    def __init__(self, log: bool = False):
        """Constructor. If `log` is set, turn on logging."""
        self.log = log

    def cross_bodies(self, body_1: List[ast.AST], body_2: List[ast.AST]) -> \
        Tuple[List[ast.AST], List[ast.AST]]:
        """Crossover the statement lists `body_1` x `body_2`. Return new lists."""

        assert isinstance(body_1, list)
        assert isinstance(body_2, list)

        crossover_point_1 = len(body_1) // 2
        crossover_point_2 = len(body_2) // 2
        return (body_1[:crossover_point_1] + body_2[crossover_point_2:],
                body_2[:crossover_point_2] + body_1[crossover_point_1:])_____no_output_____

Here's the `CrossoverOperatorMutator` applied on `p1` and `p2`:_____no_output_____

tree_p1: ast.Module = ast.parse(inspect.getsource(p1))
tree_p2: ast.Module = ast.parse(inspect.getsource(p2))_____no_output_____body_p1 = tree_p1.body[0].body  # type: ignore
body_p2 = tree_p2.body[0].body  # type: ignore
body_p1_____no_output_____crosser = CrossoverOperator()
tree_p1.body[0].body, tree_p2.body[0].body = crosser.cross_bodies(body_p1, body_p2)  # type: ignore_____no_output_____print_content(astor.to_source(tree_p1), '.py')_____no_output_____print_content(astor.to_source(tree_p2), '.py')_____no_output_____

#### Applying Crossover on Programs

Applying the crossover operation on arbitrary programs is a bit more complex, though. We first have to _find_ lists of statements that we a actually can cross over. The `can_cross()` method returns True if we have a list of statements that we can cross. Python modules and classes are excluded, because changing the ordering of definitions will not have much impact on the program. #TODO: could actually be harmful because if class definitions depend on each other, changing the odering could crash the program#_____no_output_____

class CrossoverOperator(CrossoverOperator):
    # In modules and class defs, the ordering of elements does not matter (much)
    SKIP_LIST = {ast.Module, ast.ClassDef}

    def can_cross(self, tree: ast.AST, body_attr: str = 'body') -> bool:
        if any(isinstance(tree, cls) for cls in self.SKIP_LIST):
            return False

        body = getattr(tree, body_attr, [])
        return body and len(body) >= 2_____no_output_____

Here comes our method `crossover_attr()` which searches for crossover possibilities. It takes two ASTs `t1` and `t2` and an attribute (typically `'body'`) and retrieves the attribute lists $l_1$ (from `t1.<attr>`) and $l_2$ (from `t2.<attr>`).

If $l_1$ and $l_2$ can be crossed, it crosses them, and is done. Otherwise

* If there is a pair of elements $e_1 \in l_1$ and $e_2 \in l_2$ that has the same name – say, functions of the same name –, it applies itself to $e_1$ and $e_2$.
* Otherwise, it creates random pairs of elements $e_1 \in l_1$ and $e_2 \in l_2$ and applies itself on these very pairs.

`crossover_attr()` changes `t1` and `t2` in place and returns True if a crossover was found; it returns False otherwise._____no_output_____

class CrossoverOperator(CrossoverOperator):
    def crossover_attr(self, t1: ast.AST, t2: ast.AST, body_attr: str) -> bool:
        """
        Crossover the bodies `body_attr` of two trees `t1` and `t2`.
        Return True if successful.
        """
        assert isinstance(t1, ast.AST)
        assert isinstance(t2, ast.AST)
        assert isinstance(body_attr, str)

        if not getattr(t1, body_attr, None) or not getattr(t2, body_attr, None):
            return False

        if self.crossover_branches(t1, t2):
            return True

        if self.log > 1:
            print(f"Checking {t1}.{body_attr} x {t2}.{body_attr}")

        body_1 = getattr(t1, body_attr)
        body_2 = getattr(t2, body_attr)

        # If both trees have the attribute, we can cross their bodies
        if self.can_cross(t1, body_attr) and self.can_cross(t2, body_attr):
            if self.log:
                print(f"Crossing {t1}.{body_attr} x {t2}.{body_attr}")

            new_body_1, new_body_2 = self.cross_bodies(body_1, body_2)
            setattr(t1, body_attr, new_body_1)
            setattr(t2, body_attr, new_body_2)
            return True

        # Strategy 1: Find matches in class/function of same name
        for child_1 in body_1:
            if hasattr(child_1, 'name'):
                for child_2 in body_2:
                    if (hasattr(child_2, 'name') and
                           child_1.name == child_2.name):
                        if self.crossover_attr(child_1, child_2, body_attr):
                            return True

        # Strategy 2: Find matches anywhere
        for child_1 in random.sample(body_1, len(body_1)):
            for child_2 in random.sample(body_2, len(body_2)):
                if self.crossover_attr(child_1, child_2, body_attr):
                    return True

        return False_____no_output_____

We have a special case for `if` nodes, where we can cross their body and `else` branches. #TODO: in python the same could be possible for while..else and for..else#_____no_output_____

class CrossoverOperator(CrossoverOperator):
    def crossover_branches(self, t1: ast.AST, t2: ast.AST) -> bool:
        """Special case:
        `t1` = `if P: S1 else: S2` x `t2` = `if P': S1' else: S2'`
        becomes
        `t1` = `if P: S2' else: S1'` and `t2` = `if P': S2 else: S1`
        Returns True if successful.
        """
        assert isinstance(t1, ast.AST)
        assert isinstance(t2, ast.AST)

        if (hasattr(t1, 'body') and hasattr(t1, 'orelse') and
            hasattr(t2, 'body') and hasattr(t2, 'orelse')):

            t1 = cast(ast.If, t1)  # keep mypy happy
            t2 = cast(ast.If, t2)

            if self.log:
                print(f"Crossing branches {t1} x {t2}")

            t1.body, t1.orelse, t2.body, t2.orelse = \
                t2.orelse, t2.body, t1.orelse, t1.body
            return True

        return False_____no_output_____

The method `crossover()` is the main entry point. It checks for the special `if` case as described above; if not, it searches for possible crossover points. It raises `CrossoverError` if not successful._____no_output_____

class CrossoverOperator(CrossoverOperator):
    def crossover(self, t1: ast.AST, t2: ast.AST) -> Tuple[ast.AST, ast.AST]:
        """Do a crossover of ASTs `t1` and `t2`.
        Raises `CrossoverError` if no crossover is found."""
        assert isinstance(t1, ast.AST)
        assert isinstance(t2, ast.AST)

        for body_attr in ['body', 'orelse', 'finalbody']:
            if self.crossover_attr(t1, t2, body_attr):
                return t1, t2

        raise CrossoverError("No crossover found")_____no_output_____class CrossoverError(ValueError):
    pass_____no_output_____

### End of Excursion_____no_output_____### Crossover in Action_____no_output_____Let us put our `CrossoverOperator` in action. Here is a test case for crossover, involving more deeply nested structures:_____no_output_____

def p1():  # type: ignore
    if True:
        print(1)
        print(2)
        print(3)_____no_output_____def p2():  # type: ignore
    if True:
        print(a)
        print(b)
    else:
        print(c)
        print(d)_____no_output_____

We invoke the `crossover()` method with two ASTs from `p1` and `p2`:_____no_output_____

crossover = CrossoverOperator()
tree_p1 = ast.parse(inspect.getsource(p1))
tree_p2 = ast.parse(inspect.getsource(p2))
crossover.crossover(tree_p1, tree_p2);_____no_output_____

Here is the crossed offspring, mixing statement lists of `p1` and `p2`:_____no_output_____

print_content(astor.to_source(tree_p1), '.py')_____no_output_____print_content(astor.to_source(tree_p2), '.py')_____no_output_____

#TODO: when the orelse in the if-else case is empty you could add a pass, s.t. the crossover

```python
def p2():
    if True:
    else:
        print(1)
        print(2)
        print(3)
```

executes#_____no_output_____Here is our special case for `if` nodes in action, crossing our `middle()` tree with `p2`._____no_output_____

middle_t1, middle_t2 = crossover.crossover(middle_tree(),
                                          ast.parse(inspect.getsource(p2)))_____no_output_____

We see how the resulting offspring encompasses elements of both sources:_____no_output_____

print_content(astor.to_source(middle_t1), '.py')_____no_output_____print_content(astor.to_source(middle_t2), '.py')_____no_output_____

## A Repairer Class

So far, we have applied all our techniques on the `middle()` program only. Let us now create a `Repairer` class that applies automatic program repair on arbitrary Python programs. The idea is that you can apply it on some statistical debugger, for which you have gathered passing and failing test cases, and then invoke its `repair()` method to find a "best" fix candidate:

```python
debugger = OchiaiDebugger()
with debugger:
    <passing test>
with debugger:
    <failing test>
...
repairer = Repairer(debugger)
repairer.repair()
```_____no_output_____### Excursion: Implementing Repairer_____no_output_____The main argument to the `Repairer` constructor is the `debugger` to get information from. On top of that, it also allows to customize the classes used for mutation, crossover, and reduction. Setting `targets` allows to define a set of functions to repair; setting `sources` allows to set a set of sources to take repairs from. The constructor then sets up the environment for running tests and repairing, as described below._____no_output_____

from StackInspector import StackInspector  # minor dependency_____no_output_____class Repairer(StackInspector):
    """A class for automatic repair of Python programs"""

    def __init__(self, debugger: RankingDebugger, *,
                 targets: Optional[List[Any]] = None,
                 sources: Optional[List[Any]] = None,
                 log: Union[bool, int] = False,
                 mutator_class: Type = StatementMutator,
                 crossover_class: Type = CrossoverOperator,
                 reducer_class: Type = DeltaDebugger,
                 globals: Optional[Dict[str, Any]] = None):
        """Constructor.
`debugger`: a `RankingDebugger` to take tests and coverage from.
`targets`: a list of functions/modules to be repaired.
    (default: the covered functions in `debugger`, except tests)
`sources`: a list of functions/modules to take repairs from.
    (default: same as `targets`)
`globals`: if given, a `globals()` dict for executing targets
    (default: `globals()` of caller)"""

        assert isinstance(debugger, RankingDebugger)
        self.debugger = debugger
        self.log = log

        if targets is None:
            targets = self.default_functions()
        if not targets:
            raise ValueError("No targets to repair")

        if sources is None:
            sources = self.default_functions()
        if not sources:
            raise ValueError("No sources to take repairs from")

        if self.debugger.function() is None:
            raise ValueError("Multiple entry points observed")

        self.target_tree: ast.AST = self.parse(targets)
        self.source_tree: ast.AST = self.parse(sources)

        self.log_tree("Target code to be repaired:", self.target_tree)
        if ast.dump(self.target_tree) != ast.dump(self.source_tree):
            self.log_tree("Source code to take repairs from:",
                          self.source_tree)

        self.fitness_cache: Dict[str, float] = {}

        self.mutator: StatementMutator = \
            mutator_class(
                source=all_statements(self.source_tree),
                suspiciousness_func=self.debugger.suspiciousness,
                log=(self.log >= 3))
        self.crossover: CrossoverOperator = crossover_class(log=(self.log >= 3))
        self.reducer: DeltaDebugger = reducer_class(log=(self.log >= 3))

        if globals is None:
            globals = self.caller_globals()  # see below

        self.globals = globals_____no_output_____

When we access or execute functions, we ault_functionso \todo{What? -- BM} so in the  caller's environment, not ours. The `caller_globals()` method from `StackInspector` acts as replacement for `globals()`._____no_output_____#### Helper Functions

The constructor uses a number of helper functions to create its environment._____no_output_____

class Repairer(Repairer):
    def getsource(self, item: Union[str, Any]) -> str:
        """Get the source for `item`. Can also be a string."""

        if isinstance(item, str):
            item = self.globals[item]
        return inspect.getsource(item)_____no_output_____class Repairer(Repairer):
    def default_functions(self) -> List[Callable]:
        """Return the set of functions to be repaired.
        Functions whose names start or end in `test` are excluded."""
        def is_test(name: str) -> bool:
            return name.startswith('test') or name.endswith('test')

        return [func for func in self.debugger.covered_functions()
                if not is_test(func.__name__)]_____no_output_____class Repairer(Repairer):
    def log_tree(self, description: str, tree: Any) -> None:
        """Print out `tree` as source code prefixed by `description`."""
        if self.log:
            print(description)
            print_content(astor.to_source(tree), '.py')
            print()
            print()_____no_output_____class Repairer(Repairer):
    def parse(self, items: List[Any]) -> ast.AST:
        """Read in a list of items into a single tree"""
        tree = ast.parse("")
        for item in items:
            if isinstance(item, str):
                item = self.globals[item]

            item_lines, item_first_lineno = inspect.getsourcelines(item)

            try:
                item_tree = ast.parse("".join(item_lines))
            except IndentationError:
                # inner function or likewise
                warnings.warn(f"Can't parse {item.__name__}")
                continue

            ast.increment_lineno(item_tree, item_first_lineno - 1)
            tree.body += item_tree.body

        return tree_____no_output_____

#### Running Tests

Now that we have set the environment for `Repairer`, we can implement one step of automatic repair after the other. The method `run_test_set()` runs the given `test_set` (`DifferenceDebugger.PASS` or `DifferenceDebugger.FAIL`), returning the number of passed tests. If `validate` is set, it checks whether the outcomes are as expected._____no_output_____

class Repairer(Repairer):
    def run_test_set(self, test_set: str, validate: bool = False) -> int:
        """
        Run given `test_set`
        (`DifferenceDebugger.PASS` or `DifferenceDebugger.FAIL`).
        If `validate` is set, check expectations.
        Return number of passed tests.
        """
        passed = 0
        collectors = self.debugger.collectors[test_set]
        function = self.debugger.function()
        assert function is not None
        # FIXME: function may have been redefined

        for c in collectors:
            if self.log >= 4:
                print(f"Testing {c.id()}...", end="")

            try:
                function(**c.args())
            except Exception as err:
                if self.log >= 4:
                    print(f"failed ({err.__class__.__name__})")

                if validate and test_set == self.debugger.PASS:
                    raise err.__class__(
                        f"{c.id()} should have passed, but failed")
                continue

            passed += 1
            if self.log >= 4:
                print("passed")

            if validate and test_set == self.debugger.FAIL:
                raise FailureNotReproducedError(
                    f"{c.id()} should have failed, but passed")

        return passed_____no_output_____class FailureNotReproducedError(ValueError):
    pass_____no_output_____

Here is how we use `run_tests_set()`:_____no_output_____

repairer = Repairer(middle_debugger)
assert repairer.run_test_set(middle_debugger.PASS) == \
    len(MIDDLE_PASSING_TESTCASES)
assert repairer.run_test_set(middle_debugger.FAIL) == 0_____no_output_____

The method `run_tests()` runs passing and failing tests, weighing the passed test cases to obtain the overall fitness._____no_output_____

class Repairer(Repairer):
    def weight(self, test_set: str) -> float:
        """
        Return the weight of `test_set`
        (`DifferenceDebugger.PASS` or `DifferenceDebugger.FAIL`).
        """
        return {
            self.debugger.PASS: WEIGHT_PASSING,
            self.debugger.FAIL: WEIGHT_FAILING
        }[test_set]

    def run_tests(self, validate: bool = False) -> float:
        """Run passing and failing tests, returning weighted fitness."""
        fitness = 0.0

        for test_set in [self.debugger.PASS, self.debugger.FAIL]:
            passed = self.run_test_set(test_set, validate=validate)
            ratio = passed / len(self.debugger.collectors[test_set])
            fitness += self.weight(test_set) * ratio

        return fitness_____no_output_____

The method `validate()` ensures the observed tests can be adequately reproduced._____no_output_____

class Repairer(Repairer):
    def validate(self) -> None:
        fitness = self.run_tests(validate=True)
        assert fitness == self.weight(self.debugger.PASS)_____no_output_____repairer = Repairer(middle_debugger)
repairer.validate()_____no_output_____

#### (Re)defining Functions

Our `run_tests()` methods above do not yet redefine the function to be repaired. This is done by the `fitness()` function, which compiles and defines the given repair candidate `tree` before testing it.  It caches and returns the fitness._____no_output_____

class Repairer(Repairer):
    def fitness(self, tree: ast.AST) -> float:
        """Test `tree`, returning its fitness"""
        key = cast(str, ast.dump(tree))
        if key in self.fitness_cache:
            return self.fitness_cache[key]

        # Save defs
        original_defs: Dict[str, Any] = {}
        for name in self.toplevel_defs(tree):
            if name in self.globals:
                original_defs[name] = self.globals[name]
            else:
                warnings.warn(f"Couldn't find definition of {repr(name)}")

        assert original_defs, f"Couldn't find any definition"

        if self.log >= 3:
            print("Repair candidate:")
            print_content(astor.to_source(tree), '.py')
            print()

        # Create new definition
        try:
            code = compile(tree, '<Repairer>', 'exec')
        except ValueError:  # Compilation error
            code = None

        if code is None:
            if self.log >= 3:
                print(f"Fitness = 0.0 (compilation error)")

            fitness = 0.0
            return fitness

        # Execute new code, defining new functions in `self.globals`
        exec(code, self.globals)

        # Set new definitions in the namespace (`__globals__`)
        # of the function we will be calling.
        function = self.debugger.function()
        assert function is not None
        assert hasattr(function, '__globals__')

        for name in original_defs:
            function.__globals__[name] = self.globals[name]  # type: ignore

        fitness = self.run_tests(validate=False)

        # Restore definitions
        for name in original_defs:
            function.__globals__[name] = original_defs[name]  # type: ignore
            self.globals[name] = original_defs[name]

        if self.log >= 3:
            print(f"Fitness = {fitness}")

        self.fitness_cache[key] = fitness
        return fitness_____no_output_____

The helper function `toplevel_defs()` helps saving and restoring the environment before and after redefining the function under repair._____no_output_____

class Repairer(Repairer):
    def toplevel_defs(self, tree: ast.AST) -> List[str]:
        """Return a list of names of defined functions and classes in `tree`"""
        visitor = DefinitionVisitor()
        visitor.visit(tree)
        assert hasattr(visitor, 'definitions')
        return visitor.definitions_____no_output_____class DefinitionVisitor(NodeVisitor):
    def __init__(self) -> None:
        self.definitions: List[str] = []

    def add_definition(self, node: Union[ast.ClassDef,
                                         ast.FunctionDef,
                                         ast.AsyncFunctionDef]) -> None:
        self.definitions.append(node.name)

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
        self.add_definition(node)

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
        self.add_definition(node)

    def visit_ClassDef(self, node: ast.ClassDef) -> None:
        self.add_definition(node)_____no_output_____

Here's an example for `fitness()`:_____no_output_____

repairer = Repairer(middle_debugger, log=1)_____no_output_____good_fitness = repairer.fitness(middle_tree())
good_fitness_____no_output_____# ignore
assert good_fitness >= 0.99, "fitness() failed"_____no_output_____bad_middle_tree = ast.parse("def middle(x, y, z): return x")
bad_fitness = repairer.fitness(bad_middle_tree)
bad_fitness_____no_output_____# ignore
assert bad_fitness < 0.5, "fitness() failed"_____no_output_____

#### Repairing

Now for the actual `repair()` method, which creates a `population` and then evolves it until the fitness is 1.0 or the given number of iterations is spent._____no_output_____

import traceback_____no_output_____class Repairer(Repairer):
    def initial_population(self, size: int) -> List[ast.AST]:
        """Return an initial population of size `size`"""
        return [self.target_tree] + \
            [self.mutator.mutate(copy.deepcopy(self.target_tree))
                for i in range(size - 1)]

    def repair(self, population_size: int = POPULATION_SIZE, iterations: int = 100) -> \
        Tuple[ast.AST, float]:
        """
        Repair the function we collected test runs from.
        Use a population size of `population_size` and
        at most `iterations` iterations.
        Returns a pair (`ast`, `fitness`) where
        `ast` is the AST of the repaired function, and
        `fitness` is its fitness (between 0 and 1.0)
        """
        self.validate()

        population = self.initial_population(population_size)

        last_key = ast.dump(self.target_tree)

        for iteration in range(iterations):
            population = self.evolve(population)

            best_tree = population[0]
            fitness = self.fitness(best_tree)

            if self.log:
                print(f"Evolving population: "
                      f"iteration{iteration:4}/{iterations} "
                      f"fitness = {fitness:.5}   \r", end="")

            if self.log >= 2:
                best_key = ast.dump(best_tree)
                if best_key != last_key:
                    print()
                    print()
                    self.log_tree(f"New best code (fitness = {fitness}):",
                                  best_tree)
                    last_key = best_key

            if fitness >= 1.0:
                break

        if self.log:
            print()

        if self.log and self.log < 2:
            self.log_tree(f"Best code (fitness = {fitness}):", best_tree)

        best_tree = self.reduce(best_tree)
        fitness = self.fitness(best_tree)

        self.log_tree(f"Reduced code (fitness = {fitness}):", best_tree)

        return best_tree, fitness_____no_output_____

#### Evolving

The evolution of our population takes place in the `evolve()` method. In contrast to the `evolve_middle()` function, above, we use crossover to create the offspring, which we still mutate afterwards._____no_output_____

class Repairer(Repairer):
    def evolve(self, population: List[ast.AST]) -> List[ast.AST]:
        """Evolve the candidate population by mutating and crossover."""
        n = len(population)

        # Create offspring as crossover of parents
        offspring: List[ast.AST] = []
        while len(offspring) < n:
            parent_1 = copy.deepcopy(random.choice(population))
            parent_2 = copy.deepcopy(random.choice(population))
            try:
                self.crossover.crossover(parent_1, parent_2)
            except CrossoverError:
                pass  # Just keep parents
            offspring += [parent_1, parent_2]

        # Mutate offspring
        offspring = [self.mutator.mutate(tree) for tree in offspring]

        # Add it to population
        population += offspring

        # Keep the fitter part of the population
        population.sort(key=self.fitness_key, reverse=True)
        population = population[:n]

        return population_____no_output_____

A second difference is that we not only sort by fitness, but also by tree size – with equal fitness, a smaller tree thus will be favored. This helps keeping fixes and patches small._____no_output_____

class Repairer(Repairer):
    def fitness_key(self, tree: ast.AST) -> Tuple[float, int]:
        """Key to be used for sorting the population"""
        tree_size = len([node for node in ast.walk(tree)])
        return (self.fitness(tree), -tree_size)_____no_output_____

#### Simplifying

The last step in repairing is simplifying the code. As demonstrated in the chapter on [reducing failure-inducing inputs](DeltaDebugger.ipynb), we can use delta debugging on code to get rid of superfluous statements. To this end, we convert the tree to lines, run delta debugging on them, and then convert it back to a tree._____no_output_____

class Repairer(Repairer):
    def reduce(self, tree: ast.AST) -> ast.AST:
        """Simplify `tree` using delta debugging."""

        original_fitness = self.fitness(tree)
        source_lines = astor.to_source(tree).split('\n')

        with self.reducer:
            self.test_reduce(source_lines, original_fitness)

        reduced_lines = self.reducer.min_args()['source_lines']
        reduced_source = "\n".join(reduced_lines)

        return ast.parse(reduced_source)_____no_output_____

As dicussed above, we simplify the code by having the test function (`test_reduce()`) declare reaching the maximum fitness obtained so far as a "failure". Delta debugging will then simplify the input as long as the "failure" (and hence the maximum fitness obtained) persists._____no_output_____

class Repairer(Repairer):
    def test_reduce(self, source_lines: List[str], original_fitness: float) -> None:
        """Test function for delta debugging."""

        try:
            source = "\n".join(source_lines)
            tree = ast.parse(source)
            fitness = self.fitness(tree)
            assert fitness < original_fitness

        except AssertionError:
            raise
        except SyntaxError:
            raise
        except IndentationError:
            raise
        except Exception:
            # traceback.print_exc()  # Uncomment to see internal errors
            raise_____no_output_____

### End of Excursion_____no_output_____### Repairer in Action

Let us go and apply `Repairer` in practice. We initialize it with `middle_debugger`, which has (still) collected the passing and failing runs for `middle_test()`. We also set `log` for some diagnostics along the way._____no_output_____

repairer = Repairer(middle_debugger, log=True)_____no_output_____

We now invoke `repair()` to evolve our population. After a few iterations, we find a best tree with perfect fitness._____no_output_____

best_tree, fitness = repairer.repair()_____no_output_____print_content(astor.to_source(best_tree), '.py')_____no_output_____fitness_____no_output_____

Again, we have a perfect solution. Here, we did not even need to simplify the code in the last iteration, as our `fitness_key()` function favors smaller implementations._____no_output_____## Removing HTML Markup

Let us apply `Repairer` on our other ongoing example, namely `remove_html_markup()`._____no_output_____

def remove_html_markup(s):  # type: ignore
    tag = False
    quote = False
    out = ""

    for c in s:
        if c == '<' and not quote:
            tag = True
        elif c == '>' and not quote:
            tag = False
        elif c == '"' or c == "'" and tag:
            quote = not quote
        elif not tag:
            out = out + c

    return out_____no_output_____def remove_html_markup_tree() -> ast.AST:
    return ast.parse(inspect.getsource(remove_html_markup))_____no_output_____

To run `Repairer` on `remove_html_markup()`, we need a test and a test suite. `remove_html_markup_test()` raises an exception if applying `remove_html_markup()` on the given `html` string does not yield the `plain` string._____no_output_____

def remove_html_markup_test(html: str, plain: str) -> None:
    outcome = remove_html_markup(html)
    assert outcome == plain, \
        f"Got {repr(outcome)}, expected {repr(plain)}"_____no_output_____

Now for the test suite. We use a simple fuzzing scheme to create dozens of passing and failing test cases in `REMOVE_HTML_PASSING_TESTCASES` and `REMOVE_HTML_FAILING_TESTCASES`, respectively._____no_output_____### Excursion: Creating HTML Test Cases_____no_output_____

def random_string(length: int = 5, start: int = ord(' '), end: int = ord('~')) -> str:
    return "".join(chr(random.randrange(start, end + 1)) for i in range(length))_____no_output_____random_string()_____no_output_____def random_id(length: int = 2) -> str:
    return random_string(start=ord('a'), end=ord('z'))_____no_output_____random_id()_____no_output_____def random_plain() -> str:
    return random_string().replace('<', '').replace('>', '')_____no_output_____def random_string_noquotes() -> str:
    return random_string().replace('"', '').replace("'", '')_____no_output_____def random_html(depth: int = 0) -> Tuple[str, str]:
    prefix = random_plain()
    tag = random_id()

    if depth > 0:
        html, plain = random_html(depth - 1)
    else:
        html = plain = random_plain()

    attr = random_id()
    value = '"' + random_string_noquotes() + '"'
    postfix = random_plain()

    return f'{prefix}<{tag} {attr}={value}>{html}</{tag}>{postfix}', \
        prefix + plain + postfix_____no_output_____random_html()_____no_output_____def remove_html_testcase(expected: bool = True) -> Tuple[str, str]:
    while True:
        html, plain = random_html()
        outcome = (remove_html_markup(html) == plain)
        if outcome == expected:
            return html, plain_____no_output_____REMOVE_HTML_TESTS = 100
REMOVE_HTML_PASSING_TESTCASES = \
    [remove_html_testcase(True) for i in range(REMOVE_HTML_TESTS)]
REMOVE_HTML_FAILING_TESTCASES = \
    [remove_html_testcase(False) for i in range(REMOVE_HTML_TESTS)]_____no_output_____

### End of Excursion_____no_output_____Here is a passing test case:_____no_output_____

REMOVE_HTML_PASSING_TESTCASES[0]_____no_output_____html, plain = REMOVE_HTML_PASSING_TESTCASES[0]
remove_html_markup_test(html, plain)_____no_output_____

Here is a failing test case (containing a double quote in the plain text)_____no_output_____

REMOVE_HTML_FAILING_TESTCASES[0]_____no_output_____with ExpectError():
    html, plain = REMOVE_HTML_FAILING_TESTCASES[0]
    remove_html_markup_test(html, plain)_____no_output_____

We run our tests, collecting the outcomes in `html_debugger`._____no_output_____

html_debugger = OchiaiDebugger()_____no_output_____for html, plain in (REMOVE_HTML_PASSING_TESTCASES +
                    REMOVE_HTML_FAILING_TESTCASES):
    with html_debugger:
        remove_html_markup_test(html, plain)_____no_output_____

The suspiciousness distribution will not be of much help here – pretty much all lines in `remove_html_markup()` have the same suspiciousness._____no_output_____

html_debugger_____no_output_____

Let us create our repairer and run it._____no_output_____

html_repairer = Repairer(html_debugger, log=True)_____no_output_____best_tree, fitness = html_repairer.repair(iterations=20)_____no_output_____

We see that the "best" code is still our original code, with no changes. And we can set `iterations` to 50, 100, 200... – our `Repairer` won't be able to repair it._____no_output_____

quiz("Why couldn't `Repairer()` repair `remove_html_markup()`?",
     [
         "The population is too small!",
         "The suspiciousness is too evenly distributed!",
         "We need more test cases!",
         "We need more iterations!",
         "There is no statement in the source with a correct condition!",
         "The population is too big!",
     ], '5242880 >> 20')_____no_output_____

You can explore all of the hypotheses above by changing the appropriate parameters, but you won't be able to change the outcome. The problem is that, unlike `middle()`, there is no statement (or combination thereof) in `remove_html_markup()` that could be used to make the failure go away. For this, we need to mutate another aspect of the code, which we will explore in the next section._____no_output_____## Mutating Conditions

The `Repairer` class is very configurable. The individual steps in automated repair can all be replaced by providing own classes in the keyword arguments of its `__init__()` constructor:

* To change fault localization, pass a different `debugger` that is a subclass of `RankingDebugger`.
* To change the mutation operator, set `mutator_class` to a subclass of `StatementMutator`.
* To change the crossover operator, set `crossover_class` to a subclass of `CrossoverOperator`.
* To change the reduction algorithm, set `reducer_class` to a subclass of `Reducer`.

In this section, we will explore how to extend the mutation operator such that it can mutate _conditions_ for control constructs such as `if`, `while`, or `for`. To this end, we introduce a new class `ConditionMutator` subclassing `StatementMutator`._____no_output_____### Collecting Conditions

Let us start with a few simple supporting functions. The function `all_conditions()` retrieves all control conditions from an AST._____no_output_____

def all_conditions(trees: Union[ast.AST, List[ast.AST]],
                   tp: Optional[Type] = None) -> List[ast.expr]:
    """
    Return all conditions from the AST (or AST list) `trees`.
    If `tp` is given, return only elements of that type.
    """

    if not isinstance(trees, list):
        assert isinstance(trees, ast.AST)
        trees = [trees]

    visitor = ConditionVisitor()
    for tree in trees:
        visitor.visit(tree)
    conditions = visitor.conditions
    if tp is not None:
        conditions = [c for c in conditions if isinstance(c, tp)]

    return conditions_____no_output_____

`all_conditions()` uses a `ConditionVisitor` class to walk the tree and collect the conditions:_____no_output_____

class ConditionVisitor(NodeVisitor):
    def __init__(self) -> None:
        self.conditions: List[ast.expr] = []
        self.conditions_seen: Set[str] = set()
        super().__init__()

    def add_conditions(self, node: ast.AST, attr: str) -> None:
        elems = getattr(node, attr, [])
        if not isinstance(elems, list):
            elems = [elems]

        elems = cast(List[ast.expr], elems)

        for elem in elems:
            elem_str = astor.to_source(elem)
            if elem_str not in self.conditions_seen:
                self.conditions.append(elem)
                self.conditions_seen.add(elem_str)

    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST:
        self.add_conditions(node, 'values')
        return super().generic_visit(node)

    def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST:
        if isinstance(node.op, ast.Not):
            self.add_conditions(node, 'operand')
        return super().generic_visit(node)

    def generic_visit(self, node: ast.AST) -> ast.AST:
        if hasattr(node, 'test'):
            self.add_conditions(node, 'test')
        return super().generic_visit(node)_____no_output_____

Here are all the conditions in `remove_html_markup()`. This is some material to construct new conditions from._____no_output_____

[astor.to_source(cond).strip()
    for cond in all_conditions(remove_html_markup_tree())]_____no_output_____

### Mutating Conditions

Here comes our `ConditionMutator` class. We subclass from `StatementMutator` and set an attribute `self.conditions` containing all the conditions in the source. The method `choose_condition()` randomly picks a condition._____no_output_____

class ConditionMutator(StatementMutator):
    """Mutate conditions in an AST"""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Constructor. Arguments are as with `StatementMutator` constructor."""
        super().__init__(*args, **kwargs)
        self.conditions = all_conditions(self.source)
        if self.log:
            print("Found conditions",
                  [astor.to_source(cond).strip()
                   for cond in self.conditions])

    def choose_condition(self) -> ast.expr:
        """Return a random condition from source."""
        return copy.deepcopy(random.choice(self.conditions))_____no_output_____

The actual mutation takes place in the `swap()` method. If the node to be replaced has a `test` attribute (i.e. a controlling predicate), then we pick a random condition `cond` from the source and randomly chose from:

* **set**: We change `test` to `cond`.
* **not**: We invert `test`.
* **and**: We replace `test` by `cond and test`.
* **or**: We replace `test` by `cond or test`.

Over time, this might lead to operators propagating across the population._____no_output_____

class ConditionMutator(ConditionMutator):
    def choose_bool_op(self) -> str:
        return random.choice(['set', 'not', 'and', 'or'])

    def swap(self, node: ast.AST) -> ast.AST:
        """Replace `node` condition by a condition from `source`"""
        if not hasattr(node, 'test'):
            return super().swap(node)

        node = cast(ast.If, node)

        cond = self.choose_condition()
        new_test = None

        choice = self.choose_bool_op()

        if choice == 'set':
            new_test = cond
        elif choice == 'not':
            new_test = ast.UnaryOp(op=ast.Not(), operand=node.test)
        elif choice == 'and':
            new_test = ast.BoolOp(op=ast.And(), values=[cond, node.test])
        elif choice == 'or':
            new_test = ast.BoolOp(op=ast.Or(), values=[cond, node.test])
        else:
            raise ValueError("Unknown boolean operand")

        if new_test:
            # ast.copy_location(new_test, node)
            node.test = new_test

        return node_____no_output_____

We can use the mutator just like `StatementMutator`, except that some of the mutations will also include new conditions:_____no_output_____

mutator = ConditionMutator(source=all_statements(remove_html_markup_tree()),
                           log=True)_____no_output_____for i in range(10):
    new_tree = mutator.mutate(remove_html_markup_tree())_____no_output_____

Let us put our new mutator to action, again in a `Repairer()`. To activate it, all we need to do is to pass it as `mutator_class` keyword argument._____no_output_____

condition_repairer = Repairer(html_debugger,
                              mutator_class=ConditionMutator,
                              log=2)_____no_output_____

We might need more iterations for this one. Let us see..._____no_output_____

best_tree, fitness = condition_repairer.repair(iterations=200)_____no_output_____repaired_source = astor.to_source(best_tree)_____no_output_____print_content(repaired_source, '.py')_____no_output_____

Success again! We have automatically repaired `remove_html_markup()` – the resulting code passes all tests, including those that were previously failing._____no_output_____Again, we can present the fix as a patch:_____no_output_____

original_source = astor.to_source(remove_html_markup_tree())_____no_output_____for patch in diff(original_source, repaired_source):
    print_patch(patch)_____no_output_____

However, looking at the patch, one may come up with doubts._____no_output_____

quiz("Is this actually the best solution?",
    [
        "Yes, sure, of course. Why?",
        "Err - what happened to single quotes?"
    ], 1 << 1)_____no_output_____

Indeed – our solution does not seem to handle single quotes anymore. Why is that so? #TODO: the solution I've got still handles single quotes, so maybe again set a corresponding seed#_____no_output_____

quiz("Why aren't single quotes handled in the solution?",
    [
        "Because they're not important. I mean, who uses 'em anyway?",
        "Because they are not part of our tests? "
            "Let me look up how they are constructed..."
    ], 1 << 1)_____no_output_____

Correct! Our test cases do not include single quotes – at least not in the interior of HTML tags – and thus, automatic repair did not care to preserve their handling._____no_output_____How can we fix this? An easy way is to include an appropriate test case in our set – a test case that passes with the original `remove_html_markup()`, yet fails with the "repaired" `remove_html_markup()` as whosn above._____no_output_____

with html_debugger:
    remove_html_markup_test("<foo quote='>abc'>me</foo>", "me")_____no_output_____

Let us repeat the repair with the extended test set:_____no_output_____

best_tree, fitness = condition_repairer.repair(iterations=200)_____no_output_____

Here is the final tree:_____no_output_____

print_content(astor.to_source(best_tree), '.py')_____no_output_____

And here is its fitness:_____no_output_____

fitness_____no_output_____

The revised candidate now passes _all_ tests (including the tricky quote test we added last). Its condition now properly checks for `tag` _and_ both quotes. (The `tag` inside the parentheses is still redundant, but so be it.) From this example, we can learn a few lessons about the possibilities and risks of automated repair:

* First, automatic repair is highly dependent on the quality of the checking tests. The risk is that the repair may overspecialize towards the test.
* Second, automated repair is highly dependent on the sources that program fragments are chosen from. If there is a hint of a solution somewhere in the code, there is a chance that automated repair will catch it up. #TODO: only applies to the presented technique#
* Third, automatic repair is a deeply heuristic approach. Its behavior will vary widely with any change to the parameters (and the underlying random number generators)
* Fourth, automatic repair can take a long time. The examples we have in this chapter take less than a minute to compute, and neither Python nor our implementation is exactly fast. But as the search space grows, automated repair will take much longer.

On the other hand, even an incomplete automated repair candidate can be much better than nothing at all – it may provide all the essential ingredients (such as the location or the involved variables) for a successful fix. When users of automated repair techniques are aware of its limitations and its assumptions, there is lots of potential in automated repair. Enjoy!_____no_output_____## Limitations_____no_output_____The `Repairer` class is hardly tested. Things that do not work include

* Functions with inner functions are not repaired._____no_output_____## Synopsis_____no_output_____This chapter provides tools and techniques for automated repair of program code. The `Repairer()` class takes a `RankingDebugger` debugger as input (such as `OchiaiDebugger` from [the chapter on statistical debugging](StatisticalDebugger.ipynb). A typical setup looks like this:

```python
from debuggingbook.StatisticalDebugger import OchiaiDebugger

debugger = OchiaiDebugger()
for inputs in TESTCASES:
    with debugger:
        test_foo(inputs)
...

repairer = Repairer(debugger)
```
Here, `test_foo()` is a function that raises an exception if the tested function `foo()` fails. If `foo()` passes, `test_foo()` should not raise an exception._____no_output_____The `repair()` method of a `Repairer` searches for a repair of the code covered in the debugger (except for methods starting or ending in `test`, such that `foo()`, not `test_foo()` is repaired). `repair()` returns the best fix candidate as a pair `(tree, fitness)` where `tree` is a [Python abstract syntax tree](http://docs.python.org/3/library/ast) (AST) of the fix candidate, and `fitness` is the fitness of the candidate (a value between 0 and 1). A `fitness` of 1.0 means that the candidate passed all tests. A typical usage looks like this:

```python
import astor

tree, fitness = repairer.repair()
print(astor.to_source(tree), fitness)
```_____no_output_____Here is a complete example for the `middle()` program. This is the original source code of `middle()`:_____no_output_____

# ignore
print_content(middle_source, '.py')_____no_output_____

We set up a function `middle_test()` that tests it. The `middle_debugger`  collects testcases and outcomes:_____no_output_____

middle_debugger = OchiaiDebugger()_____no_output_____for x, y, z in MIDDLE_PASSING_TESTCASES + MIDDLE_FAILING_TESTCASES:
    with middle_debugger:
        middle_test(x, y, z)_____no_output_____

The repairer attempts to repair the invoked function (`middle()`). The returned AST `tree` can be output via `astor.to_source()`:_____no_output_____

middle_repairer = Repairer(middle_debugger)
tree, fitness = middle_repairer.repair()
print(astor.to_source(tree))_____no_output_____

The `fitness` value shows how well the repaired program fits the tests. A fitness value of 1.0 shows that the repaired program satisfies all tests._____no_output_____

fitness_____no_output_____# ignore
assert fitness >= 1.0_____no_output_____

Hence, the above program indeed is a perfect repair in the sense that all previously failing tests now pass – our repair was successful._____no_output_____Here are the classes defined in this chapter. A `Repairer` repairs a program, using a `StatementMutator` and a `CrossoverOperator` to evolve a population of candidates._____no_output_____

# ignore
from ClassDiagram import display_class_hierarchy_____no_output_____# ignore
display_class_hierarchy([Repairer, ConditionMutator, CrossoverOperator],
                        abstract_classes=[
                            NodeVisitor,
                            NodeTransformer
                        ],
                        public_methods=[
                            Repairer.__init__,
                            Repairer.repair,
                            StatementMutator.__init__,
                            StatementMutator.mutate,
                            ConditionMutator.__init__,
                            CrossoverOperator.__init__,
                            CrossoverOperator.crossover,
                        ],
                        project='debuggingbook')_____no_output_____

## Lessons Learned

* Automated repair based on genetic optimization uses five ingredients:
    1. A _test suite_ to determine passing and failing tests
    2. _Defect localization_ (typically obtained from [statistical debugging](StatisticalDebugger.ipynb) with the test suite) to determine potential locations to be fixed
    3. _Random code mutations_ and _crossover operations_ to create and evolve a population of inputs
    4. A _fitness function_ and a _selection strategy_ to determine the part of the population that should be evolved further
    5. A _reducer_ such as [delta debugging](DeltaDebugger.ipynb) to simplify the final candidate with the highest fitness.
* The result of automated repair is a _fix candidate_ with the highest fitness for the given tests.
* A _fix candidate_ is not guaranteed to be correct or optimal, but gives important hints on how to fix the program.
* All of the above ingredients offer plenty of settings and alternatives to experiment with._____no_output_____## Background

The seminal work in automated repair is [GenProg](https://squareslab.github.io/genprog-code/) \cite{LeGoues2012}, which heavily inspired our `Repairer` implementation. Major differences between GenProg and `Repairer` include:

* GenProg includes its own defect localization (which is also dynamically updated), whereas `Repairer` builds on earlier statistical debugging.
* GenProg can apply multiple mutations on programs (or none at all), whereas `Repairer` applies exactly one mutation.
* The `StatementMutator` used by `Repairer` includes various special cases for program structures (`if`, `for`, `while`...), whereas GenProg operates on statements only.
* GenProg has been tested on large production programs.

While GenProg is _the_ seminal work in the area (and arguably the most important software engineering research contribution of the 2010s), there have been a number of important extensions of automated repair. These include:

* *AutoFix* \cite{Pei2014} leverages _program contracts_ (pre- and postconditions) to generate tests and assertions automatically. Not only do such [assertions](Assertions.ipynb) help in fault localization, they also allow for much better validation of fix candidates.
* *SemFix* \cite{Nguyen2013} presents automated program repair based on _symbolic analysis_ rather than genetic optimization. This allows to leverage program semantics, which GenProg does not consider. #TODO: SemFix already have a successor Angelix ([http://angelix.io](http://angelix.io))#

To learn more about automated program repair, see [program-repair.org](http://program-repair.org), the community page dedicated to research in program repair._____no_output_____## Exercises_____no_output_____### Exercise 1: Automated Repair Parameters

Automated Repair is influenced by a large number of design choices – the size of the population, the number of iterations, the genetic optimization strategy, and more. How do changes to these design choices affect its effectiveness?

* Consider the constants defined in this chapter (such as `POPULATION_SIZE` or `WEIGHT_PASSING` vs. `WEIGHT_FAILING`). How do changes affect the effectiveness of automated repair?
* As an effectiveness metric, consider the number of iterations it takes to produce a fix candidate.
* Since genetic optimization is a random algorithm, you need to determine effectiveness averages over a large number of runs (say, 100)._____no_output_____### Exercise 2: Elitism

[_Elitism_](https://en.wikipedia.org/wiki/Genetic_algorithm#Elitism) (also known as _elitist selection_) is a variant of genetic selection in which a small fraction of the fittest candidates of the last population are included unchanged in the offspring.

* Implement elitist selection by subclassing the `evolve()` method. Experiment with various fractions (5%, 10%, 25%) of "elites" and see how this improves results._____no_output_____### Exercise 3: Evolving Values

Following the steps of `ConditionMutator`, implement a `ValueMutator` class that replaces one constant value by another one found in the source (say, `0` by `1` or `True` by `False`).

For validation, consider the following failure in the `square_root()` function from [the chapter on assertions](Assertions.ipynb):_____no_output_____

from Assertions import square_root  # minor dependency_____no_output_____with ExpectError():
    square_root_of_zero = square_root(0)_____no_output_____

Can your `ValueMutator` automatically fix this failure?_____no_output_____**Solution.** Your solution will be effective if it also includes named constants such as `None`._____no_output_____

import math_____no_output_____def square_root_fixed(x):  # type: ignore
    assert x >= 0  # precondition

    approx = 0  # <-- FIX: Change `None` to 0
    guess = x / 2
    while approx != guess:
        approx = guess
        guess = (approx + x / approx) / 2

    assert math.isclose(approx * approx, x)
    return approx_____no_output_____square_root_fixed(0)_____no_output_____

### Exercise 4: Evolving Variable Names

Following the steps of `ConditionMutator`, implement a `IdentifierMutator` class that replaces one identifier by another one found in the source (say, `y` by `x`). Does it help fixing the `middle()` error?_____no_output_____### Exercise 5: Parallel Repair

Automatic Repair is a technique that is embarrassingly parallel – all tests for one candidate can all be run in parallel, and all tests for _all_ candidates can also be run in parallel. Set up an infrastructure for running concurrent tests using Pythons [asyncio](https://docs.python.org/3/library/asyncio.html) library._____no_output_____