CS44: Database Management Systems

Casting and Testing

Two important skills that we will develop in Lab 2 (Heap Pages) are:

In lab, we will go through exercises to help us understanding how to hone these skills.

Mapping Structures into an Array of Bytes

C and C++ allow a programmer to map any type onto a chunk of memory (an array of bytes). Low-level File I/O is where you may have seen something like this before - calls to read and write take a char buffer and a size, but you can write any value (ex. int value) by re-casting its address as a char * and passing sizeof(int) as the number of bytes to write to the file. This was done in Lab 0:

infile.read((char*)&nameLen, sizeof(int));

In general, you can treat any chunk of memory as storing any type of values by recasting the address to the appropriate type, and dereferencing values from it using the syntax of the re-casted type. Here is a simple example where we want to store a record of type foo (containing two integers) into an unformatted array, byte_arr. Let’s define our struct and array:

struct foo {
  int x;
  int y;
};

char byte_arr[100];
struct foo* next;
int i =0;

Now, we want to store an instance of the struct in byte_arr; we point next to the beginning of the byte_arr; when we see the values of next, we are simultaneously writing modifying next and byte_arr since they share memory space.

next = (struct foo*)(&byte_arr[i]);
next->x = 16;
next->y = 100;

Next, we want to store a second instance of foo in byte_array, so we move our pointer to the next chunk of memory:

i = i + sizeof(struct foo);
next = (struct foo*)(&byte_arr[i]);
next->x = 200;
next->y = 18;

At this point, byte_arr has two instances of foo mapped into the beginning of its space and taking up the first 16 bytes (i.e., foo[0] through foo[15]). This assumes int is 4 bytes, sizeof(foo) is 8 bytes.

We can get value out of the buffer by mapping the type on to buffer i.e., re-casting as a type foo and then dereferencing the field values:

int val = ((struct foo*)(&byte_arr[i]))->x;
//or more cleanly
next = (struct foo*)(&byte_arr[i]);
int val2 = next->y;
// or re-cast the bytes it as what-ever type I want

In gdb you can use the same syntax to re-cast a chunk of memory as a type and then access field values of that type.

Testing Development using gcov

This lab, I have only provided a single basic test in test1(), which does some inserts and retrieves on your Page. You will need to write many tests, especially to verify the deleteRecord method. One tool that can be helpful is gcov, a linux tool that provides analysis of your test suite.

gcov is a test coverage program, meaning it will analyze how much of your code base your test suite “touches” (or analyzes). One use of this is to see if your test suite is indeed checking every corner case of your code. If it isn’t, it can help you identify new tests to write.

gcov does have its limitations; having 100% coverage of your Page implementation tells you nothing about the quality of your tests, or the correctness of your logic. It also does not test the interactions between different functions in your code.

To use gcov, follow these instructions:

  1. Add the compilation flags -fprofile-arcs and -ftest-coverage to your Makefile. Lines 10-11 should now look like this (feel free to copy and paste):

     all:
     	g++ -g -fprofile-arcs -ftest-coverage -std=c++0x lib/file.o page.cpp pageTester.cpp exceptions/*.cpp -I. -Wall -o wiscdb_main
    
  2. Run make to recompile your code with the new flags, then run the main program:

     $ make
     $ ./wiscdb_main
    
  3. Run gcov on the files you want to see coverage of. In this case, you want to know how much of your Page implementation (in page.cpp) was run by wiscdb_main:

     $ gcov page.cpp
    

    This will output coverage statistics (you only really care about the top few lines since you are not responsible for testing the libraries given to you):

     File 'page.cpp'
     Lines executed:36.03% of 136
     Creating 'page.cpp.gcov'
    
     File '/usr/include/c++/7/iostream'
     Lines executed:100.00% of 1
     Creating 'iostream.gcov'
    
     File 'exceptions/insufficient_space_exception.h'
     Lines executed:0.00% of 1
     Creating 'insufficient_space_exception.h.gcov'
     ...
    

    You can see that test code I have provided only tests 36.03% of your code.

  4. To see specifically which lines of code were run (and how many times), open up page.cpp.gcov, an output result of gcov. An example section of the code:

    -:   27:/**
    -:   28: * Construct a new, uninitialized page
    -:   29: */
    1:   30:Page::Page() {
    1:   31:  reset();
    1:   32:}
    -:   33:
    

    Dashes '-' can be ignored; these are non-executing lines of code (e.g., comments and white space). Lines 30-32 have a 1: at the beginning, meaning our tests ran the constructor one time (this checks out; we only create one page in test1()).

    At the end of the file, you will see the following (your line numbers will be different):

    #####:  282:void Page::setPageNumber(const PageId new_page_number) {
    #####:  283:  header.current_page_number = new_page_number;
    #####:  284:}
    

    The series of hash symbols ##### means that these lines of code were never used. Our test suite did not test them! Maybe we should fix that by writing a new test and starting the gcov analysis again.