Casting and Testing
Two important skills that we will develop in Lab 2 (Heap Pages) are:
- casting data structures into unformatted memory
- developing a comprehensive test suite
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:
-
Add the compilation flags
-fprofile-arcs
and-ftest-coverage
to yourMakefile
. 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
-
Run
make
to recompile your code with the new flags, then run the main program:$ make $ ./wiscdb_main
-
Run
gcov
on the files you want to see coverage of. In this case, you want to know how much of yourPage
implementation (inpage.cpp
) was run bywiscdb_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.
-
To see specifically which lines of code were run (and how many times), open up
page.cpp.gcov
, an output result ofgcov
. 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 a1:
at the beginning, meaning our tests ran the constructor one time (this checks out; we only create one page intest1()
).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 thegcov
analysis again.