Section 4: C Debugging, Testing, Formatting¶
In the previous discussion section, you learned how to use C build and test frameworks to help automate the process of compiling and verifying your programs. In this discussion, we will continue to learn about new tools that can help us better debug, test, and format our programs.
1. Logging Into ecelinux
with VS Code¶
Follow the same process as in the last section. Find a free workstation and log into the workstation using your NetID and standard NetID password. Then complete the following steps (described in more detail in the last section):
- Start VS Code
- Use View > Command Palette to execute Remote-SSH: Connect Current Window to Host...
- Enter
netid@ecelinux.ece.cornell.edu
- Use View > Explorer to open folder on
ecelinux
- Use View > Terminal to open terminal on
ecelinux
For this discussion section you will need to make a copy of the repo we will be using so you can push to it. Go to the section's repo here:
Click on the "Fork" button. Wait a few seconds and then visit the new copy of this repo in your own person GitHub workspace:
https://github.com/githubid/ece2400-sec04
Where githubid
is your GitHubID. Now clone the GitHub repo we will be
using in this section using the following commands:
1 2 3 4 5 6 | % source setup-ece2400.sh % mkdir -p ${HOME}/ece2400 % cd ${HOME}/ece2400 % git clone git@github.com:githubid/ece2400-sec04 sec04 % cd sec04 % tree |
Again, where githubid
is your GitHubID. The directory includes the
following files:
ece2400-stdlib.h
: ECE 2400 course standard library headerece2400-stdlib.c
: ECE 2400 course standard library implementationavg-test.c
: source and test foravg
functionsort-test.c
: source and test forsort
function.github/workflows/actions.yml
: GitHub Actions configuration script
2. Using GDB for Debugging¶
There are two kinds of C/C++ programmers in the world: printf debuggers and GDB debuggers. Prof. Batten used to be a printf debuggers but teaching this course has converted him to be a GDB advocate. He will share his perspectives on this in the discussion section.
Let's start by compiling a single-file program that uses a single test
case and the ECE2400_CHECK
macros to test our ubiquitous avg
function.
1 2 3 | % cd ${HOME}/ece2400/sec04 % gcc -Wall -g -o avg-test avg-test.c ece2400-stdlib.c % ./avg-test |
Notice how we include the -g
option to turn on support for debugging.
This code has a bug and should fail the test. Let's start by using printf
debugging. Add some extra printfs to observe the state of the program as
it executes.
1 2 3 4 5 6 7 | int avg( int x, int y ) { printf( "x = %d, y = %d\n", x, y ); int sum = x + x; printf( "sum = %d\n", sum ); return sum / 2; } |
You should be able to see that the value for the sum
variable is
incorrect, but the value for the x
and y
variables are correct. This
means we can narrow our focus to line 4 in the above code snippet.
Hopefully, you should be able to spot the bug. Fix the bug, recompile,
and rerun the program.
Let's now try tracing the execution of this program using GDB. First, remove the extra printfs, undo your bug fix, and then recompile. Then you can start GDB like this
1 2 | % cd ${HOME}/ece2400/sec04 % gdb -tui avg-test |
GDB will drop you into a GDB "prompt" which you can use to interactively execute your program. Your source code will show up at the top, and the GDB prompt is at the bottom. Here are some useful GDB commands:
break location
: set a breakpointrun
: start running the programrecord
: start recording the execution for reverse debuggingstep
: execute the next C statement, step into a function callnext
: execute the next C statement, do not step into a function callrs
: reserve step, undo the execution of current C statementprint var
: print a C variablecontinue
: continue on to the next breakpointquit
: exit GDBrefresh
: refresh the source code display
GDB is very sophisticated so of course there are many more commands you can use, but these are enough to get started. Let's start by just running the program in GDB:
1 | (gdb) run |
Now let's try again, but first let's set a breakpoint to tell GDB to stop
at a certain function or line in the program. The following will set a
breakpoint at the beginning of the main
function.
1 | (gdb) break main |
You can see a little b+
marker in the margin next to the first
statement in the main
function indicating the location of the
breakpoint. You might need to use refresh
to get GDB to refresh the
source code display. We can now use run
to start the program running.
The execution should stop at the beginning of the function main
. You
should see the first line of the function highlighted.
1 | (gdb) run > temp |
We are using > temp
so that any output from printf goes to a file and
does not mess up the source code display. We can use record
to turn on
recording to enable reverse debugging and then we can step through the
execution of each C statement using the step
command.
1 2 | (gdb) record (gdb) step |
Keep using step
until you get into the avg
function You can print out
the value of any variable using the print
command:
1 2 3 | (gdb) print x (gdb) print y (gdb) print sum |
You can also step backwards using the rs
command:
1 | (gdb) rs |
Try stepping forward and backward through the avg
function and print
out various variables to see how they change during the execution. You
can use quit
to exit.
1 | (gdb) quit |
Now fix the bug and rerun the test.
3. Using GCOV for Code Coverage¶
One you have developed your implementation and the corresponding directed and random tests, you can then move on to understanding the quality of your current set of tests. One way to do this is to use code coverage tools. The idea here is to use a tool which will count how many times each line of code in your program is executed when running all of your tests. If there are lines of code which have never executed, then this is a good indicator that you need more tests to verify that part of your code.
Let's start by recompiling our avg-test.c
and turning on code coverage
support.
1 2 3 | % cd ${HOME}/ece2400/sec04 % gcc -Wall -g --coverage -o avg-test avg-test.c ece2400-stdlib.c % ./avg-test |
This will generate additional data in avg-test.gcda
. To make this data
easy to read and understand we need to run two more tools: lcov
and
genhtml
like this:
1 2 3 4 5 6 7 8 9 10 11 12 | % cd ${HOME}/ece2400/sec04 % lcov --capture --directory . --output-file coverage.info % lcov --list coverage.info Reading tracefile coverage.info |Lines |Functions |Branches Filename |Rate Num|Rate Num|Rate Num ============================================================== [/home/cb535/vc/git-hub/cornell-ece2400/ece2400-sec04/] avg-test.c | 100% 12| 100% 3| - 0 ece2400-stdlib.c |20.4% 54|28.6% 7| - 0 ============================================================== Total:|34.8% 66|50.0% 10| - 0 |
The tools produce coverage information on each file in the project. You
do not need to worry about coverage in the ece2400-stdlib.c
file. We
can we see that we achieve 100% code coverage in our avg-test.c
file.
Note that 100% code coverage is not the same as 100% path coverage; ask
the instructors for more on this. Also note that 100% code coverage does
not mean your program is guaranteed to be correct!
If your code coverage is lower than expected you can drill down and see the coverage statistics for every line in your program by converting the coverage data into a set of HTML reports like this:
1 2 3 | % cd ${HOME}/ece2400/sec04 % genhtml coverage.info --output-directory coverage-html % elinks coverage-html/index.html |
You should be able to use your mouse to browse to the report for the
avg-test.c
source file and verify that every line is being executed in
our test.
4. Using clang-format
for Autoformatting¶
The course coding conventions are located here:
Following these conventions and indeed any coding conventions can be
quite tedious. Many software companies and open-source software projects
use code autoformatters to automate the process of formatting their code
according to a set of rules. One such tool is called clang-format
. This
tool takes a style file that specifies the coding conventions and then
tries to reformat your code so it adheres to the style file. We have
provided you a style file named .clang-format
that adheres to the
course coding conventions. You can run clang-format
like this;
1 2 | % cd ${HOME}/ece2400/sec04 % clang-format -style=file avg-test.c |
clang-format
will output the autoformatted source code. To really see
this in action though we need to write some poorly formatted code! Modify
the avg
function in avg-test.c
to look like this:
1 2 3 | int avg( int x, int y) { int sum = x + y; return sum/2; } |
Notice how the above code does not following our coding conventions. The
space before/after parenthesis is not consistent and the curly braces are
on the wrong lines. Run clang-format
again like this:
1 2 | % cd ${HOME}/ece2400/sec04 % clang-format -style=file avg-test.c |
Verify that the code is beautiful again. Note that we are not actually
modifying the code, just outputting the autoformatted code to the console.
If you cat
the source code it still is not formatted correctly:
1 2 | % cd ${HOME}/ece2400/sec04 % cat avg-test.c |
You can use the -i
command line option to autoformat the code
"in-place".
1 2 3 | % cd ${HOME}/ece2400/sec04 % clang-format -i -style=file avg-test.c % cat avg-test.c |
With autoformatting students no longer have any excuse for poorly formatted code! However, do keep in mind that the autoformatter does not fix everything. Students will still need to ensure:
- variable and function names following coding conventions
- no irrelevant instructor supplied comments are included
- an appropriate amount of comments are included (not too little, not too much!)
- no generated directories, binaries, etc are committed to their repo
5. Using GitHub Actions for Continuous Integration¶
Continuous integration is the process of continually integrating, testing, and evaluating your code. We will be using GitHub Actions to facilitate continuous integration. GitHub Actions will automatically run all tests for a project every time code is pushed to GitHub.
To start, you need to enable GitHub Actions for the remote repository on GitHub. Go to this page:
https://github.com/githubid/ece2400-sec04/actions
Where githubid
is your GitHubID. Click on "I understand my workflows,
go ahead and enable them". GitHub Actions will report that there are no
workflow runs for this repository yet. GitHub Actions looks for a special
workflow file in the .github/workflows
directory of your repository to
determine how to build and test your project. We have already created one
of those files for you, and you can see it here:
1 2 | % cd ${HOME}/ece2400/sec04 % cat .github/workflows/actions.yml |
Although it is not important to understand the details of this YAML file,
you can still see that it includes comments to first compile
avg-test.c
and run avg-test
:
1 2 | gcc -g -coverage -o avg-test avg-test.c ece2400-stdlib.c ./avg-test |
and then commands to generate and display coverage statistics:
1 2 | lcov --capture --directory . --output-file coverage.info lcov --list coverage.info |
Let's start by reintroducing the original bug in the avg
function, so
modify avg-test.c
like this:
1 2 3 4 5 | int avg( int x, int y ) { int sum = x + x; return sum / 2; } |
Then go ahead and commit all of the work you have done in this tutorial, then push your local commits to the remote repository on GitHub. As a reminder these are the appropriate git commands:
1 2 3 4 | % cd ${HOME}/ece2400/sec04 % git status % git commit -a -m "working on sec04" % git push |
Revisit the GitHub Actions page for this repository.
https://github.com/githubid/ece2400-sec04/actions
Click on the current commit "working on sec04", then click on SEC04, then
watch as GitHub Actions brings up a virtual machine and runs all the
steps to compile and test your code. You should see GitHub Actions
failing with a red X. If you click on "avg test" you can see the specific
failure message. Now go ahead and fix your code on ecelinux
, commit and
push your fix, and verify your code is now passing the tests on GitHub
Actions.
Note, you should always test your code directly on ecelinux
first. Do
not use GitHub Actions as the primary way to run your tests. GitHub
Actions is only for continuous integration testing.
6. Experimenting with Debugging, Testing, Formatting for PA1¶
Now that we understand some new tools that can help us better debug, test, and format our programs, we will see how we have automated the process of using these tools in the PA build system. You can use the following steps to update your PA repo to make sure you have received the latest code.
1 2 3 4 | % mkdir -p ${HOME}/ece2400 % cd ${HOME}/ece2400/netid % git pull % tree |
where netid
is your NetID. If you receive a warning from git, you might
need to commit your local changes before pulling the latest code.
By default when you use CMake it creates a debug build for you which is setup to use the right flags to enable GDB debugging. In the next discussion section we will learn more about evaluation builds. Let's make sure we have a debug build all setup:
1 2 3 4 | % cd ${HOME}/ece2400/netid/pa1-math % mkdir build % cd build % cmake .. |
We have also setup CMake with two targets to automate the process of doing code coverage and autoformatting.
Here is an example of running code coverage:
1 2 3 4 | % cd ${HOME}/ece2400/netid/pa1-math/build % make clean % make check % make coverage |
Notice how we do a make clean
first. This is because code coverage will
include coverage across every single run of all tests and evaluation
programs. We want to make sure our coverage analysis is starting from a
clean slate. Also keep in mind that analyzing code coverage only makes
sense once you pass all of your tests.
Here is an example of running autoformatting:
1 2 | % cd ${HOME}/ece2400/netid/pa1-math/build % make autoformat |
Note that you can only do autoformatting if you have committed all of
your work. This enables us to easily see the changes that autoformatting
has done using git diff
and to also cleanly undo autoformatting in case
something goes wrong.
Finally, all of your PA repos are already setup with a GitHub Actions file:
1 2 | % cd ${HOME}/ece2400/netid % cat .github/workflows/run_tests.yml |
If you look closely, you should be able to see that the workflow file
will run make check
and make coverage
. Go ahead and check the status
of your latest GitHub Actions run here:
https://github.com/cornell-ece2400/netid/actions
7. To-Do On Your Own¶
If you have time, try compiling sort-test.c
in the section 4 repository
and executing the resulting binary. The test should fail. Use GDB
debugging to find the bug and fix it. Think critically about whether or
not the sort function should swap the given inputs, and then use GDB to
trace and see if it is or is not swapping as expected.
After fixing the bug, compile sort-test.c
and execute the resulting
binary with support for code coverage. Use the code coverage reports to
verify that the test has less than 100% code coverage. Add another test
case to improve the code coverage to 100%.
After analyzing code coverage, run clang-format
with the in-place
command line option (-i
) to fix up the formatting.
Finally, update the .github/workflows/actions.yml
file to compile
sort-test.c
and run sort-test
. You can add this after the avg test
block and before the coverage
block:
1 2 3 4 | - name: sort test run: |- gcc -g -coverage -o sort-test sort-test.c ece2400-stdlib.c ./sort-test |
Then coming all of your changes, push to GitHub, and confirm that GitHub
actions is now passing both avg-test
and sort-test
.