12. Some Debugging

Part of CS:2820 Object Oriented Software Development Notes, Fall 2015
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

Where were we?

The code we produced last Friday was ready to try out. When we compile it, we get no errors, so we can actually run the code. Our first few experiments are aimed at trying to evoke the error messages in the main program:

[HawkID@serv15 ~/project]$ java RoadNetwork
missing file name
[HawkID@serv15 ~/project]$ java RoadNetwork a b
too many arguments
[HawkID@serv15 ~/project]$ java RoadNetwork nothing
file not found: nothing

This series of tests was designed to evoke every error message output from the main program:

        public static void main(String[] args) {
                try {
                        if (args.length < 1) {
                                Errors.fatal( "missing file name" );
                        }
                        if (args.length > 1) {
                                Errors.fatal( "too many arguments" );
                        }
                        initializeNetwork( new Scanner(new File(args[0])) );
                } catch (FileNotFoundException e) {
                        Errors.fatal( "file not found: " + args[0] );
                }
                printNetwork();
        }

Path Testing the Main Program

Our testing above followed a careful methodology, attempting to evoke every error message in part of our program. This is a weak version of a more general test methodology called path testing, where a series of tests is constructed to force control to flow through the program along every possible path.

Because the error messages were printed by Errors.fatal() none of these calls ever return. As a result, the one path in our main program that we have not tested is the call to printNetwork(). To test this path, we need to suppress the file not found exception.

We could immediately create an extremely complex input file, but that would make debugging more difficult, so our first test involves a very simple input file -- an empty file containing no text. When we create this file, our program fails.

[HawkID@serv15 ~/project]$ java RoadNetwork roads
Exception in thread "main" java.lang.NullPointerException
        at RoadNetwork.printNetwork(RoadNetwork.java:149)
        at RoadNetwork.main(RoadNetwork.java:172)

What went wrong here? The above error message says that line 149 in our program tried to use a null pointer, and that this was called from line 172 of the program. Line 172 is the call to printNetwork(), so we have definitely finished our path coverage of the main program. Line 149 is this:

                for (Intersection i:inters) {

Here, we tried to pick an intersection out of a list of intersections, but there was no list. That is, the list inters was null, a condition quite distinct from being an empty list.

Why was inters null? The call to initializeNetwork() in the main program was supposed to build roads and inters, but since the input file was empty, it did nothing, leaving these lists as they were initially. The initial values of these lists were determined by their declarations:

        // the sets of all roads and all intersections
        static LinkedList  roads;
        static LinkedList  inters;

These declarations are, as it turns out, wrong. The default value of any object in Java (excepting built-in types like int) is null, and that is the source of our null-pointer exception. What we need to do is initialize these two lists to empty lists, not null pointers.

        // the sets of all roads and all intersections
        static LinkedList  roads
                = new LinkedList  ();
        static LinkedList  inters
                = new LinkedList  ();

A question of format: Why wrap both lines when only the second line was too long to fit in an 80 column display. The answer is that the two lines are parallel constructions, and in general, parallel constructions should be formatted similarly in order to emphasize their parallel nature. This is a significant aid in readability.

Path Testing the Input Parser

With this fix, the code compiles and we get the expected result, no output because the input file was empty. So, we can begin testing. As with the main program, we begin with something very simple, a one-line data file that defines just one intersection:

        intersection A

The program output is identical to the input, so if it works, we can build on this, adding more intersections and roads, working up to something like this:

        intersection A
        intersection B
        road A B 10
        road B A 20

This is not particularly interesting unless we uncover some bugs. The next step is to start making some errors. Consider this input file:

        intersection A
        intersection B
        intersection A
        road A B 10
        road B A 20

Here, we've deliberately inserted a duplicate intersection definition. When we run the program over this input (stored in the file roads, we get this output:

[HawkID@serv15 ~/project]$ java RoadNetwork roads
Intersection A redefined.
Intersection A
Intersection B
Intersection A
Road A B 10
Road B A 20

This is correct, in as far as it goes, but the output is not very readable. The problem is, the error message is not cleanly distinguished from the output. Our current version of the errors package is at fault:

class Errors {
        static void fatal( String message ) {
                System.err.println( message );
                System.exit( 1 );
        }
        static void warning( String message ) {
                System.err.println( message );
        }
}

What we need is simple, a standard prefix on each error message that distinguishes it from the normal output of the program. Consider this:

class Errors {
        static void fatal( String message ) {
                System.err.println( "Fatal error: " + message );
                System.exit( 1 );
        }
        static void warning( String message ) {
                System.err.println( "Error: " + message );
        }
}

Aside: Standard Error versus Standard Output

In our program, we have output error messages to System.err and normal data output to System.out

By default, when running under the Linux/Unix shell (and under the DOS command line under Windows), output to System.err is mixed in with output to System.out, but they can be separated. Here is an example that illustrates this:

[HawkID@serv15 ~/project]$ java RoadNetwork roads > t
Intersection A redefined.
[HawkID@serv15 ~/project]$ cat t
Intersection A
Intersection B
Intersection A
Road A B 10
Road B A 20
[HawkID@serv15 ~/project]$ rm t

The added > t at the end of the command running our program diverts System.out (or rather, the Linux/Unix standard output stream) to the file named t. So, when our program runs, the only thing we see on the screen is the error message. Then, we use the command cat t to dump the file t to the screen. We could just as easily have used any text editor to examine the file, and finally, although nothing required us to do so, we deleted the file with the rm command.

Under some Unix/Linux command shells, it is almost as easy to divert standard error (System.err) to a file, but this was an afterthought, so the way you do so differs from one shell to another. Initially, the designers of the Unix shell assumed that users always wanted to see the error messages immediately, while they might want to save other output. As a result, shell tools for redirecting styandard error are afterthoughts and differ from one shell to the next.

Path Testing Continued

Another obvious error to explore occurs when a road is defined in terms of an undefined intersection. Consider this input file:

intersection A
intersection B
intersection A
road A B 10
road B A 20
road A C 2000

When we run this, we get the expected error messages, but when it tries to output Road  C we get a null pointer exception.

What is the problem? There are some bug notices in our code that are closely related to this. Specifically, in the initializer for Road, when we output the warning about an undefined intersection, we wrote this:

                if (destination == null) {
                        Errors.warning(
                                "In road " + sourceName + " " + dstName +
                                ", Intersection " + dstName + " undefined"
                        );
                        // Bug:  Should we prevent creation of this object?
                }

We did not prevent creation of the object when the declaration of that object contained an undefined destination interseciton name. Instead, we left the object with a null destination field. This caused no problem until later when we tried to output the road description using the toString() method:

        public String toString() {
                return (
                        "Road " +
                        source.name + " " +
                        destination.name + " " +
                        travelTime
                );
        }

In this code, we blindly reached for the name fields of the source and destination intersections without checking to see if they exist. We need to add this check. Perhaps the uglyest but most compact way to do so is to use the embedded conditional operator from Java:

        public String toString() {
                return (
                        "Road " +
                        (source != null ? source.name : "---" ) +
                        " " +
                        (destination != null ? destination.name : "---" +
                        " " +
                        travelTime
                );
        }

This code works, substituting --- for any names that were undefined in the input file, but it is maddeningly difficult to format this code so that it is easy to read. It might be better to add a private method that is easier to read and packages the safe return of either the name or dashes if there is no name.