Lab 10: Debugging - Fancier GJDB Tutorial

GJDB from the Command Line

In this section, we'll debug an actual bug report submitted by a student during our beloved db61b project. To start this lab, use hw init lab10 to get the lab 10 starter files (also, lab 10?? whoa...)

Doing Things the Wrong Way the Right Way

When I was a kid, there was a commercial in which Pizza Hut exhorted us to eat our pizza the wrong way, because they had cleverly stuffed the crust full of cheese. While this commercial left an indelible impression upon my labile mind, it doesn't seem to have left much of an impression on the internet. The basic gist of the commercial was that if you're doing things the wrong way, there's a better way to do this than the naive solution (namely by eating Pizza Hut's magic cheese filled rust).

Some of you are tempted to write programs like this:

This is a truly terrible idea, but you are not terrible people for trying it. After all, this has been more or less the way you've been educated in most courses other than 61B -- do a problem, check with some oracle for the answer.

In this exercise, we're going to write our project the wrong way, but we're going to do it with the cheese stuffed crust that is the debugger.

From the command line, run the make check from the bugs/bug1 folder, and you'll see that the provided code has some sort of null pointer exception when running test 1 (on line 299 of CommandInterpeter). The astute observe will observe that the standard output result is also showing errors involving Lastname, Firstname, and SID. However, in the spirit of the bug report that this lab is based upon, we'll ignore this and focus only on the null pointer exception.

The first step is to figure out how to run test 1 from within gjdb. Hopefully you saw the project 2 checklist and know that you can set the classpath so that you can run tests from within the test folder. In this case, we'd head to the bugs/testing folder as follows:

$ cd testing
$ make
$ gjdb -classpath ../:$CLASSPATH bugs.bug1.db61b.Main

Make sure to compile using before starting up gjdb.

However, we're going to use a new gjdb command that we haven't seen yet that will require us to set the sourcepath. Thus, you should instead use:

gjdb -classpath ../:$CLASSPATH -sourcepath ../ bugs.bug1.db61b.Main

If you're using Emacs, you should first set the working directory in Emacs by using 'M-x cd'. Have Emacs go to the testing lab10/testing folder. Then when you start gjdb, and Emacs asks you "Run gjdb (like this)", enter gjdb -classpath ../:$CLASSPATH -sourcepath ../ bugs.bug1.db61b.Main

To feed gjdb the test1.in file, use:

[-] run < test1.in

You'll see a few errors (which we'll ignore for now). We see that the code crashes on line 300.

To investigate why, use:

main[0] bt

This will show a trace of what the program was doing when it crashed. We see that the actual crash was on line 300 of CommandInterpreter, which was in turn called by line 240 and so on. We can show the code surrounding line 300 of CommandInterpreter as follows:

main[0] list
Since the null pointer happened on this line, it makes sense to try and figure out what tablesArr was. Using:
main[0] print tablesArr
$1 = instance of bugs.bug1.db61b.Table[1] (id=219)

We find out that tablesArr is an array of Tables of size 1. Now use:

main[0] print tablesArr[0]

You'll see that this entry of the table is null. But why? Either use the list command, e.g list 285 or open CommandIntepreter.java, and find where tablesArr is filled in (hint: on line 292). We see that tablesArr was created from an ArrayList called tables. It's possible that there is a bug in the code that converts the ArrayList to an array, but let's first make sure that the ArrayList is good.

Inspect the tables variable using the following commands:

print tables
print tables.size()
print tables.get(0)

Warning: If you copy and paste these three commands into gjdb, you may get the results out of order. You should enter them one by one.

You'll find that tables is an ArrayList of length 1 that contains only a null reference. We can see why tablesArr[0] was null! That means this ArrayList was created improperly. Looking at the code, we see that this ArrayList was created by a loop on lines 287-292.

Well, we know that at some point the code tried to get a table with name tableName, so let's try printing that:

print tableName

We see that it's "enrolled2", so that's just as we'd expect. Thus, we're going to need to go back in time to figure out what happened in this loop. gjdb does not support stepping back, so let's instead restart our program, but have it stop when it gets to line 290.

Let's set up a breakpoint for line 290 to see what's going on with this loop:

 break bugs.bug1.db61b.CommandInterpreter:290

If you get a warning that this class does not seem to exist, ignore this. Not quite sure why this is happening.

We can verify that our breakpoint actually got created using the break command, for example:

main[0] break
BP [1] bugs.bug1.db61b.CommandInterpreter:290

This tells us that we have a breakpoint, numbered 1, that will pause our program when we hit line 290 of bugs.bug1.db61b.CommandInterpreter.

Restart the test with:

main[0] run < test1.in

You'll see that it pauses. Print out tableName... uh-oh, it's not enrolled 2, so this isn't our error case. We only want to stop when we get to the situation where tableName is "enrolled2".

As it happens, we can add conditions to breakpoints using the condition command, for example:

main[0] condition 1 tableName.equals("enrolled2")

After running this command, we can see that our breakpoint has a condition associated with it by using the break command, which not only lists all breakpoints (as we saw above), but also any interesting properties of these breakpoints.

You should see something like:

main[0] break
BP [1] bugs.bug1.db61b.CommandInterpreter:290 if tableName.equals("enrolled2")

This tells us that breakpoint 1 now has a condition.

Restart your program once more, and try printing tableName. You'll see that it is "enrolled2" as we expected. So much power!

Using the 'step' button, step into the Databse get method. Then use 'next' until you get to line 28. If you mess up at some point and end up stepping into something you meant to step over, you can click the 'step return' button and it'll take you out of the current method call.

Since our problem is that the database can't find the Table "enrolled2", it seems like a good idea to keep an eye on the name of each table as it is examined. We could do this by typing print every time nextTable.getName() is called, e.g. as shown in the transcript below:

main[0] run < test1.in
Program is already running.  Restart from the beginning? [yn]: y
java bugs.bug1.db61b.Main  Loaded students.db
> Loaded enrolled.db
> Loaded schedule.db
> > ...Error: unknown column: Lastname
> > ......Error: unknown column: Firstname
> > > .........Error: unknown column: SID
> > 
Breakpoint 1: thread="main", bugs.bug1.db61b.CommandInterpreter.selectClause(), line=290, bci=145
  290             Table nextTable = _database.get(tableName);

main[0] step
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=24, bci=0
  24         Iterator<Table> iter = _tables.iterator();

main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=26, bci=8
  26         while (iter.hasNext()) {

main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=27, bci=11
  27             nextTable = iter.next();

main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=28, bci=21
  28             if ((nextTable.getName()).equals(name)) {

main[0] print nextTable.getName()
$1 = "schedule"
main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=26, bci=34
  26         while (iter.hasNext()) {

main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=27, bci=11
  27             nextTable = iter.next();

main[0] next
main[0] 
Step completed: thread="main", bugs.bug1.db61b.Database.get(), line=28, bci=21
  28             if ((nextTable.getName()).equals(name)) {

main[0] print nextTable.getName()
$2 = "students"
main[0] 

However this is going to get tedious in a hurry. Let's instead set up a special breakpoint on line 28 of Database that has the special property that it prints out the name of nextTable. We do this by using a new command called command as shown below:

main[0] break bugs.bug1.db61b.Database:28
Set BP [2] bugs.bug1.db61b.Database:28

main[0] command 2
Enter commands, terminated with a line containing just 'end'.
print nextTable.getName()
end

The line 'command 2' tells us gjdb to associate a set of commands with breakpoint #2, turning it into a breakpoint-that-does-something or BTDS (a term I just made up). In this case, we want it to print out the contents of a variable.

We can see that our breakpoint has this property by using the break command to list our breakpoints. You'll see that breakpoint 2 now issues commands every time it stops. Fancy!

BP [1] bugs.bug1.db61b.CommandInterpreter:290 if tableName.equals("enrolled2")

BP [2] bugs.bug1.db61b.Database:28 [deferred]
    Commands:
        print nextTable.getName()

Note that if you messed up at some point and created breakpoints numbered higher than 2, you may need to add conditions or commands to these breakpoints. Alternately you can clear breakpoints using the clear command. It is also possible to make breakpoints that are conditional AND issue commands, though we will not do so here.

Let's see our BTDS in action. Restart your code with run < test1.inand you'll see that we stop at line 300, just below _database.get(tabelName) is called. Use the continue command and you should see something like:

Breakpoint 2: thread="main", bugs.bug1.db61b.Database.get(), line=28, bci=21
  28             if ((nextTable.getName()).equals(name)) {

main[0] $1 = "enrolled"
main[0] cont
main[0] 
Breakpoint 2: thread="main", bugs.bug1.db61b.Database.get(), line=28, bci=21
  28             if ((nextTable.getName()).equals(name)) {

main[0] $2 = "students"
main[0] cont
main[0] 
Breakpoint 2: thread="main", bugs.bug1.db61b.Database.get(), line=28, bci=21
  28             if ((nextTable.getName()).equals(name)) {

main[0] $3 = "schedule"
main[0] cont
main[0] 
Exception occurred: java.lang.NullPointerException (uncaught) thread="main", bugs.bug1.db61b.CommandInterpreter.selectClause(), line=300, bci=238
  300             result = tablesArr[0].select(cols, conds);

The trouble is apparently that enrolled2 isn't even in the table. We could keep working our way backwards, but we're going to stop our analysis of bug1 here.

If we step back and think carefully about what we've done in lab today, we realize that we've been eating our pizza the wrong way!

In 61B (and in any large project), you'd better learn to turn that triangle of food around and orient it so that the pointy end reaches your mouth first. Or in programming terms, you shouldn't be running big multi-command system tests until you've tested the basics first. You should always start with unit tests, then simple system-level tests, then finally large composite system level tests.

Writing a bunch of code and then running large composite tests is a sure recipe for pain. If you find yourself tracing things back really far like this, it's probable that your pizza is all agley.

As it turns out, there are many bugs in bugs.bug1.db61b. Rather than subjecting you to fixing them all for this lab, let's move on to bugs.bug2.db61b, which will be relatively smaller.

There's plenty more to learn about gjdb, but this at least gives you the highlights that will give you the power to do nearly anything you'd want to do.