Once we have reproduced a program failure, we must find out what is relevant:
- What does the failure actually depend on. We should simplify test cases to make debugging easier so we can identify what faults depend on, and make test cases easier to communicate.
To simplifyβ¦
- Use binary search to cut a test case in half and iterate
- We can automate this
- Using a binary search, throw away half the input and see if the output is wrong, if not go back to the previous state and discard the other half of the input. Repeat until we have simplified our complex input to our minimally simplified input
- If both halves pass, instead of a binary, we can divide our input into many subsets (our deltas), each subset having a small change. This increases the change of finding the failing input subset but is slower
- In general, start with few and large changes and increment to more and smaller changes
- Let be the set of possible inputs
- corresponds to an input that passes
- corresponds to an input that fails
- We let R denote the set of all possible inputs
- We can go from one input r1 to another input r2 by a series of changes
- A change is mapping which takes one input and changes it to another input
- A change can be decomposed to a number of elementary changes where is the composition of each sub-delta function in a left to right order (we apply to then apply to that output)
In summary
- We have an input without failure
- We have an input with failure
- We have a set of changes such that
- Each subset c of is a test case
Given a test case c we should like to know if the input generated by applying changes in c to causes the same failure as .
- We define the function such that, given , iff is a failing input
Hint
Once again, the whole point of this is to minimize our test cases so they are not so big
We want to find the smallest test case c such that , a failing test case is called the global minimum of if for all , .
The global minimum is the smallest set of changes which will make the program fail.
Finding the global minimum may require performing an exponential number of tests. Instead of looking for this global minimum, we can search for a 1-minimal input which is a set of changes that cause the failure but removing any change causes the failure to go away.
A failing test case is called a local minimum of if: for all . A failing test case is n-minimal if for all and it is 1-minimal if for all .
The main trade off here is minimality guarantees vs computational costs. 1 minimality is the sweet spot.
To find a 1-minimal subset of c:
- If for all changes in the test case, if you remove a a change (try this on each change) and the failure disappears, then c is 1-minimal
- Otherwise, if removing the element still causes a failure, we found a smaller subset, recurse
Runtime
In the worst case, we remove one element from the set per iteration, after trying every other element:
- Work is potentially This is
It is silly to remove one element at a time, we can try to Divide and Conquer by dividing the change set in 2 initially and increase the number of subsets if we canβt make progress.
Minimization Algorithm
This delta debugging algorithm finds a 1-minimal test case: The idea is:
- Partition the set to roughly equal chunks
- are pairwise disjoint and
- The complement of is defined as
- Start with n = 2
- Test each test case defined by each partition and its complement
- Reduces the test case if a smaller failure inducing set is found, otherwise it refines the partition Steps
- Start with n=2 and test set
- Test each and each
- There are three possible outcomes:
- Some causes failures: go to step 1 with and n = 2
- Some causes failures: Go to step 1 with and n = n-1
- No test causes failure: If granularity can be refined, go to step 1 with n = n*2, otherwise you found the 1 minimal set The worst case here is still quadratic but single failures converge in log N time.