Homework Assignment #3 — Mutation Testing

In this assignment you will improve a tool to carry out simple mutation testing on Python programs. You will also practice code review of pull requests in an AI context.

You start with an incomplete implementation, a desired specification, and a number of candidate pull requests to improve that implementation. You can think of the pull requests as being written by development team members or AI agents. Some of the pull requests are good and others are bad.

If you can identify good pull requests (via manual code review and/or AI assistance), you can apply those patches and you may not have to write much (or any) code yourself to meet the specification. Otherwise, you may have to write the rest yourself (and/or with AI assistance).

The program you are improving is a mutation testing tool. Given a subject program, the improved tool must generate mutants of that program that "kill" (cause to fail) test cases. You should generate mutants that are strong enough to kill a test case but not so strong as to kill every test case (cf. mutation adequacy score).

You may work with a partner for this assignment. If you do you must use the same partner for all sub-components of this assignment. Use the Gradescope feature to select your partner. Only one partner needs to submit the report on Gradescope, but if you both do, nothing fatal happens. (If your partner drops the class, you are still responsible for the full assignment and should plan for that risk accordingly.)

You are still allowed to use online (or "on-this-webpage") resources and the like as long as you cite them in your writeup.

Thoughts From Prior Students

My favorite part of the class so far has been doing homework 3, the mutator. I thoroughly enjoyed understanding and learning about syntax trees and it felt like I could walk away understanding how to make a real world tool.

The least favorite part was runnning project 3.

My favorite part of the class so far has been the exploration of mutation testing. It's cool how simple syntactic changes can reveal so much about test suite quality.

Cross-Platform Compatibility and Python

It is your responsibility to submit Python code that works on the grading server. This is true even if (indeed, especially if) the grading server uses a different version of Python than your development environment.

This assignment uses Python because the focus is on the mutation algorithm and not the compiler front-end. In theory, it would be just as easy to do this assignment on C++ code using LLVM (e.g., via clang) or the like. In practice that would just add out-of-scope overhead without reinforcing course concepts. If you are interested in such topics, consider the Compilers and Programming Languages electives.

Everything definitely works on the HW0 EC2 setup, including using agentic AI in the style of HW2.

HW3a — Mutation Testing (Python)

You will be given an incomplete implementation and a large set of pull requests that all claim to improve it. The rest of this section describes what the final version must do. This will be assessed by submitting it to the autograder. You must also read and analyze the pull requests and write a report. You can finish the incomplete implementation by finding some good pull requests, applying them, and doing any remaining finishing touches by hand (or with AI). If you are unable to identify a good or complete set of pull requests, you may have to write much more it by hand (or with AI).

You mutation testing tool will take the form of a Python program called mutate.py. Your mutate.py must define a mutate() method that accepts a single argument (an abstract syntax tree) and returns a single value (a mutated abstract syntax tree).

We provide driver code that calls your mutate.py multiple times to create multiple mutants. You are responsible for writing code that interfaces correctly with this driver.

The driver code will invoke your mutate() method and store the results in the current directory as 0.py, 1.py, 2.py, etc. Each mutant is a copy of the original program with one or more mutation operators (see below) applied. Any other output (e.g., logging to standard output) that your mutate.py produces is ignored.

The data structure used to represent Python source files is the abstract syntax tree (AST).

You should use the ast module as well as the astor module.

Many students report that the so-called "missing Python AST docs" are quite helpful here.

Other students and instructors have found this quick Python AST Tutorial to be quite helpful (thanks S. Ajami) for the basic concepts.

Your program can (and should) use random to decide where and how to apply mutation(s). However, the driver code is responsible for setting the random seed. This is done to ensure that your local (i.e., on the HW0 setup) testing and the grading server are as close as possible. Your code cannot set the random seed.

The ast.NodeTransformer portion of the interface is likely to be quite relevant for this assignment.

Mutation Operators

At minimum, you must implement and support the following three mutation activities:

  1. Negate any single comparison operation type (e.g., >= becomes <).
    • (You should strongly consider handling more than one comparison type. You have free choice for what you do, if anything, for other "corner-case" booleans such as is or True.)
  2. Swap binary operators + and -, as well as * and //.
    • (You have free choice of which AST node classes are relevant here, such as how or whether you deal with FloorDiv vs. regular division. Any non-empty choice is full credit. You should strongly consider handling more than binary operators as well.)
    • (You can swap operators "as you like". The most obvious approach is to swap + with -, and so on, but you can try something more creative if you like.)
  3. Delete an assignment or function call statement (i.e., cause it to have no effect).
    • (You have free choice of which AST node classes are relevant here. For example, you might decide that ``assignment'' means Assign, AnnAssign and AugAssign. You may also choose to deal with assignments, function calls, or both. Any non-empty choice is full credit.)
    • (Other edits that have the "effect" of deleting the assignment or function call but are not "officially" deletion are totally fine and are full credit.)
    • (Similarly, whether you interpret function call as func() or x = func() or both is up to you.)
    • (Be careful not to create invalid Python code!)
Notes:

Note that implementing those three actions may not be enough to get full points on the autograder, but you do have to implement those three and describe them in your report to get full credit on the report. You may also need to implement more operators or heuristics to get full credit on the autograder.

Note also that you do not have to use all three with the same frequency (or even use them at all — you could set their probabilities to zero). You do, however, have to implement them as a minimal baseline.

Mutation Testing

For this assignment we will make use of a simple target subject program called subject.py. It is intentionally short and simple so that you can read and comprehend it directly (cf. white-box testing). The focus of this assignment is on mutation analysis, not on large programs per se (there are many other places in the course where we use large programs).

Recall that one goal of mutation testing is to assess the adequacy of a test suite. We would thus like to produce mutants that give different adequacy scores to different test suites. A more complete test suite should get a higher score.

To get started, download hw3.zip, which contains three Python files. You will need to install two packages:

pip install func_timeout --break-system-packages
sudo apt install python3-astor

We invoke the program by running the driver.py code. You may see something like this:

$ python3 driver.py
using your mutate.py to create 10 mutants
        ...
---
Your mutants selectively killed 5 of the 12 tests!
+ f06(2,13)
+ f07(3,4)

You can view the mutants (e.g., 0.py) in the current directory. They are overwritten each time.

The goal is to complete mutate.py so that it tends to generate selective mutants: those that kill exactly one test. You can (and should!) read all of the provided code, including the tests.

The autograder is identical to the files you have, but it generates 50 mutants instead of ten. Once you feel comfortable, you can can edit driver.py to generate 50 mutants. This allows you to test ideas out without going to the autograder.

The most common student questions for this assignment are along the lines of "I'm not sure how to generate selective mutants that are neither too strong nor too weak. My mutation analysis cannot every kill test X alone. What should I do?" That is the key puzzle of this assignment. We explicitly will not tell you more that what you see on this page.

Using the Library — Visitor Patterns and Starter Code

A very common source of student confusion relates to how the AST library is structured and designed. It uses a visitor pattern, a way of structuring and interfacing code. We will cover such "Design Patterns" later in class. For now, a visitor pattern provides abstraction and modularity by hiding from you the exact fields and way in which a data structure is traversed or transformed. (This allows the library writer to change the internal representation of the data structure later, such as to move from Python 2 to Python 3 or whatever, without all of the client code needing to update.)

The mutate.py starter code provided shows two simple mutations implemented using a visitor pattern in two passes.

In the first pass, we visit every node in the tree and count the number of relevant nodes. In the second pass, we pick a random number between 1 and that node count, and then visit every node again. When we get to the chosen node, we edit it.

Students can also find other relevant example code online, such as wrap_integers.py, which may help with understanding the visitor pattern software design pattern.

Candidate Pull Requests

The file pull-requests.zip contains 40 pull requests. For simplicity, these are not official GitHub pull requests, but are instead paired high-level summaries and code patches. To make reasoning and report writing easier, each pull request is named after an animal (rather than a large number). So panda.md and panda.patch together form the panda pull request. Each .md file contains an AI-generated description of what the corresponding patch claims to do, why it should be applied, and so on. Each .patch file is a proposed change to the code (represented in the standard unified diff format used by tools and AI).

Each patch is guaranteed to apply to the original source. You can apply a patch manually:

patch < pr/koala.patch
This edits files in place, so you may want to unzip hw3.zip again to restore the original behavior after applying a patch. Because there are so many patches, you probably want to direct an AI agent to evaluate the patches for you (or manually write a script to iterate over them and apply them and analyze the result, etc.).

As of 2026, large companies (e.g., Microsoft, Google, etc.) are reporting that many patches are produced by AI — and that many developers use AI to assist with code review. In addition, many developers are reporting that AI is being used to produce an ever-increasing number of pull requests. This is why we are providing you with 40 pull requests, which probably feels like a large number to read and interpret.

The following things are true about the set of pull requests:

Analyzing The Pull Requests and Proceeding

You should analyze the pull requests and find a subset of them to apply as a starting point. Because there are so many pull requests, we recommend that you either use agentic AI to manipulate them directly for you, or use conversational AI to write a script to analyze them for you. However, you can also do it all manually. As a reminder, it is totally legitimate to point the AI to this webpage as part of your instructions to it.

If you can find a good starting point within those pull requests, you may be "done" or "almost done" with the HW3 coding work. If you cannot, you will have to write more code (or direct AI to write more code) until your submission both adheres to the "Mutation Operators" requirements on this page and yields a score you are happy with on the autograder.

Even if you do not want to use any of the provided pull requests, you must still analyze them for the written report in HW3b. In particular, you should:

Hints, Advice and Doing Well

After you have selected a subset of pull requests, you might ask agentic AI to apply them all for you (or apply them all manually). Then you might submit the code to the autograder. If you are happy with the result, you are done "coding". If you are not, you can then be very specific with the AI (e.g., "Starting from this new starting point, we need to edit this file so that we ..."). We recommend the Elephant-Goldfish process (as in HW2) for agentic AI interactions. Even if you do not use the full process, parts of it may be useful.

Many students report that they find it easy to get a few points on this assignment but hard to get a high score. That is, it is hard to write a mutate.py that produces mutants that are strong enough, but not too strong.

If you find that your program is not producing enough high-quality mutants to assess the adequacy of a test suite:

You may want to avoid generating "useless" or "stillborn" mutants, such as those that do not parse, always exit with an error, are equivalent to a previously-generated mutant, etc. See Sections III and IV of the Jia and Harman survey for ideas. Here are some common student pitfalls. You should check to see if you are making any of these.

HW3b — Written Report

You must also write a PDF report reflecting on your experiences creating a mutation testing tool for this assignment. Your report must include your University email address(es).

The report must contain the following sections:

  1. Summary. A three-paragraph high-level summary of your experience. This should address:
    • Which mutation operators did you implement?
    • How does your program decide which operators to apply?
    • What was harder than you expected or different than you expected?
    • How would you compare and contrast mutation analysis with statement coverage as a test suite adequacy metric.
    • What was the most useful prompt in your interactions with AI? What was the least useful? Please place each prompt on its own line. (If you did not use AI at all, instead explain what did happen.)
    Summary rubric:
    • 5 points — a three-paragraph report reflecting on your activities creating a mutation testing tool for this assignment, including a discussion of mutation operators and your experiences and expectations
    • 4 points — a reasonable report, but lacking a solid description of one or more aspects or being rote rather than insightful or significantly exceeding the length limit
    • 3 points — a brief report, detailing only half of the required information
    • 2 points — a report drawing only the bare minimum of contrasts comparisons, and explanations
    • 1 point — a terse or uninformative report, perhaps describing only one activity or operator

    • -2 point (etc.) — submitted source code does not implement the require operators (even loosely)
    • -1 point — English prose or grammatical errors
    • -2 point — Submission appears to be AI-generated without meaningful revision, at the grader’s discretion.
  2. Pull Request Ranking. What criterion did you use to rank order the pull requests? Why did you pick that criterion? Give your final ranking with the "best" pull request as #1 at the top. Format this as a numbered list (yes, it will be long with 40 items). We know which pull requests are actually good or bad or neutral: you will receive more points if your ranking generally places high-quality pull requests at the top and low-quality ones at the bottom. (This is a fuzzy comparison with partial credit.)
    [1 point for clear criteria, 2 points for justification, 2 points for high-quality generally at the top, 2 points for low-quality generally at the bottom]
  3. Pull Request Verification. Identify a pull request by name, quote a part of its summary or code comment, and show a justification or argument that it is correct. You can use AI or your own reasoning. Next, identify a pull request by name, quote a part of its summary or code comment, and show a justification or argument that it is wrong. You can use AI or your own reasoning.
    [1 point for positive identification, 2 points for positive argument, 1 point for negative identification, 2 points for negative argument]
  4. Pull Request Explanation. Explain, in your own words, what is actually happening in the emu pull request. Address all relevant functional and non-functional properties. You can use AI or any other tool or technique.
    [4 points for insightful and correct explanation]
  5. Pull Request Selection. Which pull requests would (or did) you accept? That is, "which ones pass code review?" or "which ones did you use use as the basis for your own code?" Even if you did not use any of the provided pull requests, you must still indicate which subset you would approve for simultaneous deployment.
    [2 points for true positives: list includes high-quality pull requests; 2 points for true negatives: list does not include low-quality pull requests]

The grading staff will select a small number of excerpts from particularly high-quality or instructive reports and share them with the class. If your report is selected you will receive extra credit.

Sanity Checking Your Submission

The autograder uses the same driver.py and subject.py that you are given. You can do as many tests as you like yourself before you submit to the autograder!

Submission

Submit a single mutate.py file to the autograder. Submit a single PDF report (HW3b) via Gradescope. In your PDF report, you must include your name and UM email ID (as well as your partner's name and email ID, if applicable).

FAQ and Troubleshooting

In this section we detail previous student issues and resolutions:

  1. Question: It's generally working, but I'm not getting the score I want on the autograder.

    Answer: One student suggests: "exploring out and covering more mutation operators rather than trying to get really granular on how to mutate each operator will lead to success a lot faster!"

  2. Question: When I try to create mutants the to_source function complains.

    Answer: If you create a Python AST that is missing critical information (like an important tree child, or location information) or otherwise create something totally invalid, the library won't even let you print it out. For example, if you had "x = y + z" and you remove the "z" node entirely without doing anything else, the library might not even print out the result at all because it is not valid Python. Even something as simple as saying ast.GtE instead of ast.GtE() can cause this.

  3. Question: My mutate.py somewhat works, but I see this sort of error:

    Traceback (most recent call last):
      File "0.py", line 42
        else:
           ^
    IndentationError: expected an indented block
    

    Answer: You are creating mutants that are not valid Python programs. In this example, the student is removing an entire statement right after an else: — but then Python gets confused because it is expecting a tabbed-over statement there. So the mutant is not a valid Python program. Check out the hints on how to deal with this sort of issue.

  4. Question: My submission does not seem to create any mutants on the autograder and/or it can't seem to find them:

    ls: cannot access '[0-9]*.py]': No such file or directory
    

    Answer: This almost always means that your mutate.py has an error (such as importing a library not found on the autograder, or raising an exception) that means it is not running to completion.

    This issue is, in some sense, the most common student problem. There is no silver bullet here: (1) test on your own before using the autograder, and then (2) make use of the feedback from the autograder.

  5. Question: It feels like the autograder is non-deterministic. I feel like I am submitting the same mutate.py twice and getting different results.

    Discussion: There are many possible issues here that all result in the same observed symptom.

    Answer 1: Your mutate.py may be non-deterministic. (This is rare, since student are careful about it.)

    Answer 2: Your mutate.py may be creating mutants that are, themselves, non-deterministic. This is much trickier, and happens surprisingly often.

    Answer 3: There may be differences between the autograder setup and your HW0 setup. For example, the autograder uses export PYTHONHASHSEED=0 to try to determinize the order in which hashmaps are iterated.

    Discussion: Ultimately, it may not be worth your time to track this down. Remember that we use your best submission, not your last one. Start early.

  6. Question: I am getting erors like TypeError: 'type' object is not subscriptable. Help!

    Answer: Double-check the spelling on the AST pieces you are creating or manipulating. For example, use ast.GtE() not the incorrect ast.GtE (note the parentheses, which are required).

  7. Question: When I run the program, I get

    ImportError: No module named 'astor'
    

    Answer: Double-check the installation instructions above.

  8. Question: I am trying to delete things, but I get this error:

    assert node is None, node
    AssertionError: 
    

    Answer: You have ast.Pass but want ast.Pass() instead (note the parentheses). However, you may actually not want to "delete" things like this at all (look around for other hints).

  9. Question: When I run my mutate.py, I get this error when trying to print out my mutants:

    "AttributeError: 'Assign' object has no attribute 'value' "
    

    Answer: This error occurs when the AST library tries to pretty-print your abstract syntax tree data structure back out to (ASCII) Python source. The AST library expects each node object to have certain fields (properties) filled out. Sometimes a node operation that can look totally reasonable to you (e.g., making a new node, setting it as the child of some other node) can result in a node object not having all of the fields set. If you are changing the types of nodes directly, I would recommend that you instead use their friendly constructors — that may well end up fixing this problem for you. (This is explicitly a bit of a challenge; dig around first and get used to the AST library before asking for help here.)

  10. Question: How can I get the parts of an AST node? Suppose I want to figure out the identified on the left-hand side of an assignment:

    Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=9))
    

    Answer: Check out the node documentation. In this particular example, some students report success with node.targets[0].id to extract the id from an instance of the the Assign class.

    Similarly, suppose you want to change the right-hand side of the assignment to "None", but leave the left-hand side alone. Have your node transformer do something like:

    return ast.copy_location(ast.Assign(targets=node.targets, value=ast.NameConstant(value=None)), node)
    
  11. Question: I'm getting errors like

    AttributeError: module 'ast' has no attribute 'Constant' 
    
    or
    AttributeError: 'Classdef' object has no attribute 'starargs'
     

    Answer: You'll want to make sure you're using the same versions of Python and the AST library as the autograder.

  12. Question: Some online documentation makes a big deal about generic_visit, but I don't really understand it. Can you elaborate?

    Answer: To some degree, using it or not is your choice. Suppose you want to change every assignment from "x = y" to "x = y + 1".

    In a language like C, you can write "a = b = 3". (Don't ever do this.) The interpretation of this statement is that you first assign b=3, and then that result (3 in this case) is also assigned into a.

    If you do not do the "generic visit" thing and instead replace the top-level assignment only, you'll end up with something like this:

    a = (b = 3) + 1
    

    [ After this program, b=3 and a=4. ]

    If you do the "generic visit" thing and recursively transform the child nodes as well, you'll end up with something like this:

     
    a = (b = 3 + 1) + 1
    

    [ After this program, b=4 and a=5. ]

    Which one do you want? It's your choice. If you have a theory about which one is a better mutation operator, do that. If you don't care, it's safe to default to the full recursive handling of all sub-nodes.