Lecture Summary: Loop Testing
Generating Test Cases from Implementations
Test Case Generation
In the preceding lectures we have seen various ways
- to trace facts through programs
- to consider programs themselves as facts
- to derive facts about executions paths of programs by symbolic execution
All of these perspectives of programs can be exploited for proof and for testing. Considering testing, we are particularly interested in obtaining test cases. In the last lecture we have looked at iteration and recursion unfolding. This technique permits us to look at testing of iteration and recursion as special cases on testing of conditionals. To generate test cases we need contracts and programs.
Conditionals
Example: Square Root Search
Consider the following function for computing a step in a square root search.
|
|
Does the function preserve x * x <= n
? That is, is it an invariant of the function body?
We can specify the question in Slang.
|
|
We can use Logika’s inter-procedural check to see whether this holds.
Let’s have a look at the fact corresponding to function sq_root_step
.
Example: Square Root Search Fact
The fact emanating from sq_root_step
framed in the contract of sq_root_lb
:
|
|
Recall the facts corresponding to conditionals of the shapes.
- C => Sfact, where
S
is the program in the if-branch - !C => Tfact, where
T
is the program in the else-branch
If we want to test this program we have to choose specific branches.
For instance, z * z <= n
to choose the if-branch.
We can conjoin this choice with the fact.
The fact emanating from sq_root_step
framed in the contract of sq_root_lb
:
|
|
Those parts corresponding to the if-branch are selected by applying modus ponens.
P P => Q / Q
Those parts corresponding to the if-branch are removed.
They do not constrain any variable because (z * z <= n)
is false.
The fact emanating from sq_root_step
framed in the contract of sq_root_lb
:
|
|
Recall that At(x, 0)
and At(y, 0)
refer to the initial values of variables x
and y
.
Starting with input x == 0
, y == 0
, n == 0
, we should obtain the output x == 0
, y == 0
.
If we added the post-condition n < y * y
, this test case would no longer be valid.
The fact emanating from sq_root_step
framed in the contract of sq_root_lb
:
|
|
Setting all variables to 0
is not true.
However, negating n < y * y
it becomes true.
So, we’ve found a counterexample!
We would also have found it, had we used the test case just with n < y * y
as post-condition.
This information can be fed into the original fact (before choosing the if-branch).
The fact emanating from sq_root_step
framed in the contract of sq_root_lb
:
|
|
We can mark those parts that are true.
And those parts that are false.
Now we can trace back the fact n < y * y
to the point where y
was modified.
And discover the we need the fact n < At(y, 0) * At(y, 0)
initially.
In other words, we must add n < y * y
as a pre-condition.
Symbolic Execution of Square Root Search
|
|
|
|
- Executing
sq_root_lb_ub()
yields (x: X0, y: Y0, n: N
), (PC:true
) - Executing
Requires(x * x <= n)
yields (x: X0, y: Y0, n: N
), (PC:X0 * X0 <= N
) - Executing
sq_root_step()
yields (x: X0, y: Y0, n: N
), (PC:X0 * X0 <= N
) - Executing
val z: Z = (x + y) / 2
yields (x: X0, y: Y0, n: N, z: Z
), (PC:X0 * X0 <= N, Z == (X0 + Y0) / 2
) - Executing
if (z * z <= n) {
yields (x: X0, y: Y0, n: N, z: Z
), (PC:X0 * X0 <= N, Z == (X0 + Y0) / 2, Z * Z <= N
) - Executing
x = z
yields (x: Z, y: Y0, n: N, z: Z
), (PC:X0 * X0 <= N, Z == (X0 + Y0) / 2, Z * Z <= N
) - Executing
} // return
yields (x: Z, y: Y0, n: N, z: Z
), (PC:X0 * X0 <= N, Z == (X0 + Y0) / 2, Z * Z <= N
) - Executing
Ensures(x * x <= n, n < y * y)
yields (x: Z, y: Y0, n: N, z: Z
), (PC:X0 * X0 <= N, Z == (X0 + Y0) / 2, Z * Z <= N, N < Y0 * Y0
)
Unfolded Iteration
Example: Iterative Square Root
We can implement the computation of an integer square root as a binary search.
|
|
We can use symbolic execution or unfolding for test case generation.
Unfolded while-loop:
|
|
This function is equivalent to the original one.
We don’t want the final while-loop to be executed. Let’s make this more precise.
|
|
We use abort()
to mark branches that we do not wish to consider.
This function is not equivalent to the original one.
Using it we set a bound on the iteration when analysing the while-loop.
This becomes difficult to handle “manually”. We can use Logika to inspect the formulas at different iteration depths. With an increasing number of iterations the formulas grow. The facts become complex. We still can understand and analyse them when we find errors. But we must rely on Logika to generate and manage those facts. Let’s look at a few iterations.
Example: Iterative Square Root Facts
|
|
|
|
|
|
|
|
Termination of the Iterative Square Root
Before, we have seen how to specify that a program terminates by way of a measure.
The body of the loop decreases
the measure at each iteration.
While the measure is bounded below by 0.
We can specify these properties of the measure using assertions.
|
|
Of course, we can unfold this function, too.
The assert
statements are now considered in the fact of the unfolded function.
Example: Iterative Square Root Measure Fact
|
|
|
|
Unfolded Recursion
Example: Recursive Square Root
In the recursive version the local variables x
and y
become accumulator arguments.
|
|
Let’s look at the fact of the unfolded recursive square root function.
Example: Recursive Square Root Facts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Termination of the Iterative Square Root
Termination of the recursion can be expressed by means of a measure. At each recursive call the measure is decreased while the measure is bounded below by 0. As in the iterative implementation, we can specify these properties of the measure using assertions.
|
|
The corresponding facts including the measures contain the asserted properties.
Example: Recursive Square Root Facts
|
|
|
|
Program Verification
Levels of Assurance
We have looked at various ways to verify programs\pause
- Full proof: This shows that any execution of a program is correct\pause
- Bounded proof: Using unfolding, this shows correctness up to a given depth\pause
- Testing: Using unfolding, this shows correctness for certain values up to a given depth\pause
In practice, one has to judge what is the most suitable approach for different parts of software. In particular, it is a matter of time and effort. Using the formal techniques discussed, testing can be made very effective by generating test cases from contracts and implementations.
Test Cases and Testing
We have looked at programming at different levels of abstraction. High-level programs are often also good specifications. For these we can generate test cases. Often high-level programs are close to specifications and follow the heuristic we have seen in equivalence partitioning. So, they will produce good test cases for implementations. Instead of proving an implementation correct with respect to a specification we can also generate test cases from the specification and use it to test the implementation.
Slang Examples
Sq Root It
|
|
Sq Root Rec
|
|
Sq Root Step
|
|
Summary
- We have seen how dealing with conditionals (and assignment) is sufficient for testing (and bounded proof)
- We can use termination measures for proof and for testing
- In practice, a mixes of verification techniques are used
- Note, that sometimes testing is necessary, e.g., when only some scenarios are known but a complete specification cannot be given