There are several strategies to deal with buggy code. First there is print-debugging–printing extra information about your variables to find bugs. A fancy way of doing this involves logging that you can turn on or off. It might be more efficient to use a debugger, a program that can simultaneously run your application and keep track of the source code. Finally, you can write tests.

Print Debugging

When you’re new to programming, this is the simplest way to figure out what is going wrong. Have a bug but you’re not sure where? Just print “I got here” at one point in your program, and if this prints, you know the bug is after that point, otherwise it is before. Move this print statement around your code until you’ve narrowed down where the bug is happening. If it is not obvious why things are wrong, you can print the values of variables used in the problematic area of code. Using this process of elimination and investigation, you can usually tease out problems in your application.

Log information Systematically

To extend the print-debugging concept, you can use a boolean value to decide when to print this extra information. So you would have one variable, debug, and if it is true, then you print “Starting Step 1” and “Starting Step So-and-So”, and so on, throughout your code. Then you can set debug in a variety of ways at run-time. Logging is pretty simple, but you can leverage existing libraries to control 1) how debug is set, 2) where the program sends logging information (to the screen or to a log file), and 3) how much to print under different circumstances, e.g. different levels of logging: warning, info, debugging, etc. While searching online for “<your-favorite-language> logging library” will show you the options for your-favorite-language, you can also contact us for advice.

Debuggers

Debuggers allow you to run your application line by source code line (stepping through the source code), and with a debugger you can see the value of variables at each step in the application as well as identify at which lines errors occur. This option requires more effort, because you need to learn to use a new program. But it also helps to keep your code clean and free of extraneous print statements. It is also essential for very hard-to-solve bugs.

To use a debugger with a compiled program, you need to use a special compiler flag (-g) and you need to turn off all optimizations (typically -O0). Then you pass your program to the debugger as the first argument. It will load your program and provide you with several options: 1) run your code and stop when an error occurs, 2) inspect (i.e. print) variables, and 3) set break points, which are instructions to pause the program at a specific line of code.

We have three debuggers installed on the SCC cluster. gdb, the GNU debugger, is a command line debugger that works best with GNU (gcc, gfortran) compiled programs. pgdbg, the Portland Group debugger, is a debugger with a graphical user interface (GUI) that works best with Portland Group (pgcc, pgf90, etc.) compiled programs. There is an online guide for the pgdbg and various tutorials for the gdb.

A more powerful option, which will work with applications compiled with either GNU or Portland Group compilers, is TotalView. This debugger is excellent for debugging parallel applications (MPI or OpenMP). There is extensive documentation about totalview online.

Writing Tests

Tests are small programs that run part or all of your application and check the results for validity and consistency. Tests help when you are changing your application, because they verify that you have not accidentally broken a piece of code that used to work. Some programmers find tests so valuable that they write tests before they actually write their code; this is called test driven development. In this case the tests will fail, usually with an error, because the application source code does not yet exist. Once the tests are in place, the goal is to complete the application source code until the tests pass. Only then is the work actually complete.

There can be overhead common to all your tests, and test frameworks are libraries that provide a starting point for your testing. A quick search for “<your-favorite-language> testing framework” will provide you with several options for your-favorite-language. These tools usually take care of the overhead of tracking a series of tests and then printing passed/failed messages, etc., and they basically lower the bar for writing tests.

Test coverage is a measure of how much of your application source code is tested in your tests. An example result is 80% coverage, meaning 80% of the lines in your source code are involved in your tests. This measure often motivates programmers to write more tests.