Learn more
Buy the book
For readers
Publishers' sites
Paperback · 423 pages
ISBN 978-0-12-374515-6 [US]
ISBN 978-3-89864-620-8 [DE]
|
|
Project 3: Cause-Effect Chains
In this project, you write a tool that isolates failure-inducing
program states.
Your Task
Extract and compare program states to isolate failure causes.
Proceed in five steps:
Step 1: Obtain states.
In Python, you can access the program state from within a program
run using a trace function. The Python method
sys.settrace(func) sets the global trace
function to func; it is invoked whenever a new
function is entered. func can return a local trace
function which is then invoked for every line of the current
function. Here's a full
description of sys.settrace().
From within a trace function, you can access all variables of the
current frame; you can also access the frames of the calling
functions. See internal
types for a discussion of frame objects.
- Start by extending an existing program (such as middle) with a tracing function. Have
your tracing function first report the called functions; then
extend it to report the local and global variables. See the
lecture slides on
isolating cause-effect chains for an example.
- Set up the tracing function such that it operates only at
given locations (say, a list of function names).
- Create a State class in which you create a copy
of the current state. (You may also want to create your own
Backtrace and Frame classes, such that each
State contains a Backtrace as a list of
Frame objects.)
Copying state is tricky. Here are some hints:
- You need to care about aliasing. If variables
a and b reference the same object, this
should also be reflected in your State object.
(If needed, you can use the Python built-in id()
function to access the "identity" of an object.)
- An important limitation of Python is that you cannot make
"deep" copies of the entire state, since modules cannot be
copied. Also, you cannot access the internal state of
modules, so if there is any difference in here, you won't be
able to detect or isolate it.
- The Python
copy.copy()
method handles all of this nicely. It creates a
shallow copy—that is, it copies the top-level
structure (that is, the mapping of global and local
variables), while preserving aliases at the deeper levels.
- A good solution is
to copy as much as you can—that is:
- to copy recursively the entire state,
- if an
exception occurs, catch that exception, and share the
object rather than copying it.
- Allow the state to be output in human-readable form
(for debugging).
Step 2: Compare states.
For simplicity, we shall only compare state per base
variables—that is, we do not (yet) examine the contents
of data structures.
- Create a class StateDelta that holds the difference
between two program states, expressed as a set of Delta
objects.
- Each Delta object represents a difference of two
values of one single (named) variable. Essentially, a
Delta will consist of
- The name of the variable
- Its stack depth (frames of different functions
can have different variables with the same name)
- Its value (as a copy of the original value)
- When designing your
Delta class, note that in Python, the set of local
variables at a frame may be different, even though the backtrace
is the same. For instance, in the Python code
if debug:
x = 1
the variable x is defined only if debug is
set. Comparing two runs with different settings of
debug results in different sets of local variables.
- If two values reference the same object (such as a module),
you need not look further for any differences. Use the Python
built-in id() function to access the "identity" of an
object.
- Set up a constructor for StateDelta that takes two
State objects and creates appropriate deltas—one
for each differing base variable. (Hint: to compare variables,
use the Python built-in cmp()
function.)
- Allow state differences to be output in human-readable form
(for debugging).
- For each of the two test programs (see
below), determine and output state differences between runs.
Step 3: Apply and isolate differences.
In this step, we use delta debugging to isolate failure-inducing state:
- Extend the Delta class by an
apply(frame) method that applies a delta to
the stack of frames starting with frame. This is
essentially done by finding the stack frame at the depth as
specified in the delta, and to assign the variable the value as
stored in the delta.
- If you change a variable
on the stack, only changes to the top-level frame are actually
copied back to the Python interpreter. Therefore, you may need
to defer application of deltas until the respective
frame is top-level again.
- Check that transferring state works. For each of the two
test programs (see below), the challenge is
to apply an entire StateDelta such that you effectively
transform one test run into another.
This is the trickiest part of the project. If you are
stuck somewhere during this process, drop me a note and we
will try to resolve the problem together.
- Apply delta
debugging on state deltas, thus isolating failure-inducing
program states at given locations. Use the dd()
algorithm to isolate individual states—for instance, by
extending your module from Project 1.
Once you are able to apply all deltas such that the
failure occurs, using delta debugging is pretty
straight-forward.
Step 4: Generalize.
Finally, we clean up the code and generalize it:
- Generalize your approach to a stand-alone function (say,
debug()) that takes as arguments
- a unit test which fails (see the unittest
framework for details)
- a unit test which passes
- a list of locations covered by both unit cases
at which to isolate failure-inducing states.
- Be sure to have
docstrings
for every function which describe its purpose. Have a README file
(or other appropriate help) that describes how to invoke the
isolation tool.
Step 5 (optional): Implement an advanced method.
Implement one of the following extensions to isolating
cause-effect chains:
- Make your deltas more fine-grained. Do not compare data
structures and mappings as a whole, but identify single
elements that can be added, deleted, or changed. In
particular, think of
- tuples such as (1, 2, 3)
- lists such as [1, 2, 3]
- mappings such as {1: 'one', 2: 'two', 3: 'three'}
- objects such as State with their attributes
This also calls for a more elaborated Delta
class—rather than changing the value of a base variable,
applying a Delta now means to change individual values in a
data structure. Consider introducing Delta
subclasses such as ListDelta, TupleDelta,
etc., that handle specific data types. (To simplify things,
you do not need to care about aliasing of elements.)
- To check the type of a value, use the Python
isinstance() function.
- To access individual attributes, use the Python
built-in dir() and getattr() functions.
- Implement automatic identification of cause
transitions—that is, moments in time at which the
set of relevant variables change. For a full description of
the algorithm, see the paper of Cleve and Zeller:
Locating
Causes of Program Failures (ICSE 2005).
It suffices to narrow down a cause transition to a specific
function which occurs in both traces; there is no need to
isolate single statements.
Demonstrate your techniques on two programs:
- The middle program and its
test runs as described in the book. You need to create
appropriate unit tests for this example.
- The XMLProc parser from Project 1 and Project 2. The XMLdata archive contains a number of
passing and failing test inputs. For each of the three failing
test inputs, isolate a cause-effect chain that lead to the error
or warning message. You need to set up unit test that check the
messages issued by the XMLProc parser.
References
Get the book at
Amazon.com
·
Amazon.de
Comments? Write to Andreas Zeller <zeller@whyprogramsfail.com>.
|