Testing and Debugging

Testing

Pre- and Post-Conditions

  1. Review the MyroC.h header file documentation.
    1. In the documentation, find at least two functions that have stated a "pre-condition" and at least two functions that have stated a "post-condition". (Note that every function should have postconditions, otherwise there would be no point in calling it.)
    2. In anticipation of later work in this lab, review the documentation for the function rMotors. The documentation does not explicitly state pre-conditions for this function, but one might infer such conditions. Write a careful statement of the implied pre-condition(s) for rMotors.
  2. The program motors-test.c sets the motor speeds of the Scribbler to the given leftspeed and rightspeed.
    1. Initialize the variables leftspeed and rightspeed to 1; compile and run the program.
    2. Initialize the variables leftspeed and rightspeed to 1 and -1 respectively. Compile and run the program.
    3. Initialize the variables leftspeed and rightspeed to 2 and -1. respectively. Compile and run the program.
    4. Now try using values 6 and 5 respectively.
    5. Try other numbers that you might need to figure out what works and what doesn't.
    6. How do these experimental results compare with the pre-conditions that you inferred in Step 1b?

The assert Function in C

  1. Modify the same code, motors-test.c, to use assert so that it will test the precondition(s) you wrote for rMotors in motors-test.c.

Note: You can read about C's assert function in the accompanying reading and/or using the command man assert in a terminal window. Better yet, read about assert in both places!

Choosing Test Cases

  1. Program reduce.c is supposed to reduce a fraction, with integers entered as numerator and (positive) denominator separately, to its simplest form.
    1. Develop two test plans for this program to determine whether the program works correctly. Apply both black-box and white-box testing strategies by identifying test cases that will cover a full range of situations that might be encountered in executing the program. Recall:
      • Black-box testing is when the problem is examined to determine the logical cases that might arise. Test cases are developed without reference to details of code.
      • White-box testing is when the code is examined to determine each of the possible conditions that may arise, and tests are developed to exercise each part of the code.
    2. Run reduce.c  with all the cases from your test plan to determine whether the program works correctly.
    3. Try to fix all the errors you found in the program.
    4. Run the program again with all the cases from your test plan to be sure that it now works correctly.

Debugging with gdb

Starting gdb

Recall from today's reading that you may run GDB from the terminal or within Emacs. While you might settle into using GDB from within Emacs (one of its great strengths as a development environment), here you will experiment with both. There was a lot of information in the reading, so you can take a look at this cheat sheet to follow some initial steps: GDB Lab Cheatsheet

  1. The program array-bug.c contains two simple functions for doing array manipulations.
    1. Copy the program to your account.
    2. Read over the program (particularly the function documentation and the main procedure) to be sure you understand what it is supposed to do, but do not make any changes to it.
    3. Compile and run the program to confirm its behavior. For this exercise, you MUST use the system default compiler (so that you do get obviously wrong output):
      cc -g -o array-bug array-bug.c
  2. Use the terminal to start GDB on your program and then run your program within GDB. (You will give one terminal command, and one GDB command within terminal.) You may then exit GDB with the quit command.
  3. Start GDB within Emacs and then run your program within the GDB buffer window. Leave GDB running after the program completes.

Setting break points

You should have noticed that your program does not yield the "expected" results. To isolate the bug (in case you haven't spotted it already), we will want to probe some values while the program is running. (That way we don't have to decide before we compile which values may be interesting to look at.) Thus we will practice setting break points in the code to get a better handle on the action.

  1. Set a breakpoint at the line just after the definition of the variable expected, (i.e., the line with the call to compute_average, then run your program.
    1. Use GDB to inspect the value of expected before the compute_average procedure runs. Does it have the value intended? (You may also want to verify the value of length.)
    2. Use GDB to "step over" the call to compute_average (that is, use the next command).
    3. Inspect the value of expected (i.e., just before the call to the halve procedure). Did it change?
    4. Inspect the value of the mean variable after calling compute_average. Is it correct?
    5. Step over the call to halve and inspect the value of expected once again. Did it change?
    6. Corruptions are often caused by mismanaging references. Determine the memory addresses of the four variables declared in main.
    7. Use kill to release the current execution of the program.

Setting watchpoints

By this point you should have identified two problematic behaviors in the program: (1) mean is computed incorrectly within compute_average, and (2) expected is somehow corrupted within halve. We will now use additional GDB tools to help isolate the cause(s).

  1. Run the program again. While stopped at the first breakpoint (within main) set a write watchpoint for the expected variable.
    1. continue executing the program. That is, don't use the step or next commands; instead, GDB should pause the program wherever expected changes value.
    2. Where does the corruption occur? What are the old and new values?

      While GDB shows you the code context, issue a backtrace request to see the execution context as well. (This particular stackframe is very short, but when a function has many possible entry points, understanding the execution context is a critical step in the debugging process.)

    3. Determine the values and addresses of the several local variables available where the program is currently paused: i, length, and values, and even values[i]. Compare these addresses to those you identified earlier.
    4. Construct a hypothesis about how (not why) the variable gets corrupted.

Stepping the source

Chances are very good now you've not only determined the nature of the bug, but also its source. Bear with us as we walk through one more important aspect of using the debugger, stepping through code line by line to monitor behavior.

  1. Set a breakpoint within the halve procedure. (Recall you may simply give a function name as an argument to break—a rather pleasing command in this instance.) Temporarily disable the first breakpoint you had set earlier and then re-run the program.
    1. Where does the program actually break? Is it where you expected? Why do you suppose it stops there?
    2. Inspect the value of the relevant variables (all those listed in Exercise 4c) in a single line using a printf statement. Here is a starting example:
      printf "i=\t%p\t=\t%d\n",&i,i
      Are any of these values surprising? Construct a hypothesis to explain them.
    3. Execute the step command once, note the output produced, and then re-issue (via command history) the printf command to help identify the side-effect and execution context.
    4. Repeat the previous instruction (step, printf, inspect) several times as you traverse the loop.

If you've been paying attention to the values and the addresses, by now you should be able to hypothesize the precise cause for the bug(s), which you may now attempt to fix. Be sure to confirm your fixes. If they do not fix the problem, restore your program to its original state and continue to use the debugger to explore the behavior while also studying the source code carefully.

Optional: Über-advanced GDB

For those with extra time, explore one or more of the following sections of the GDB Manual, identify some more advanced command(s) we have not yet discussed or used in the lab, and try them out on this or another program. There are also many different cheatsheets for GBD, this one is very concise.