Advanced C Programming

Spring 2020 ECE 264 :: Purdue University

⚠ This is a PAST SEMESTER (Spring 2020).
Due 2/17

Mini-unit: #define + make

Learning goals

You will learn the following concepts/skills:

  1. unit testing – systematically test your code
    1. framework – create a simple framework for unit-testing future C code
    2. organization – learn how to organize a test suite
    3. test coverage – measure how thoroughly your tests exercise your code
  2. C preprocessor − how to use several of the most commonly used directives
    1. #define symbols – not just for defining constants
    2. #define macros – like functions but with special capabilities
    3. #ifdef/#ifndef – disable sections of code with a #define or GCC flag
    4. #include guards – enable more versatile use of header (.h) files.
  3. Build systems – Create make files for compilation and testing.
  4. ANSI color codes – Print text in color on the terminal

Overview

Real-world software projects typically comprise tens, hundreds, or many thousands of files. Even in your ECE courses, your programming assignments will be getting bigger and more complex. Testing code by hand (i.e., by playing with it) is useless since there are so many components, and ways things can go wrong. can be complex—and slow.

Most serious software projects use two tools: a unit testing framework (e.g., CUnit) to organize and coordinate tests, and a build system (e.g., make) to manage the process of compiling, testing, and deploying the project. In this homework, you will get a light introduction to both. In addition, you will learn to use console logging effectively (as a complement to gdb), without littering your code with printf(…) statements.

You will create the following:

Starter code

You will create your Makefile, clog.h, and miniunit.h from scratch (i.e., i.e., starting with a blank file).

We are providing a simple program for counting words that you can use to test your HW05. You will be modifying this program to use miniunit.h and clog.h. There is also a script called print256colors.sh (created by Tom Hale). It will not be used directly for the assignment, but running this script will give you some exposure to the range of colors your terminal can display.

Run 264get hw05 to fetch these files.

Part 1: Makefile

Create a file called Makefile that will be used as input too the make command. This file will have instructions for how to not only compile a program, but also run your tests, submit, run the pretester, and even run a code coverage tester to search for areas of your code that your tests may not be covering.

The Makefile must be specific to a particular project. For HW05, you will create a Makefile for the count_words project that is included in the starter. On future assignments, you can easily change a few variables at the top of your Makefile to make it work for future assignments.

Structure of a Makefile

A Makefile is a text file named “Makefile” (exactly). It may contain variables, rules, and phony targets.

# VARIABLES
LHS=RHS
LHS=RHS


# RULES
target: prerequisites…
    action
    action
    

# .PHONY TARGET - declare targets that do not correspond directly to an output file
.PHONY: target_name target_name 

Variables. The variables section (at the top) contains filenames, and other information that you will use in the commands that build and manage your project. In this homework, we have specified exactly what variables you need in the Requirements table.

Rules. A rule consists of a target, ≥0 prerequisites, and ≥0 actions. Normally, most rules in make files describe how to compile (or generate) a particular file. We will also use so-called phony targets, which are essentially just scripts for performing actions related to your project (e.g., submit, run tests, etc.).

Targets. The target of a rule is usually a file that will be created by the actions in that rule. For example, the test_count_words rule will create the test_count_words executable file. For phony targets, the target is the name of the command.

Prerequisites. The prerequisites of a rule are the targets that it The prerequisites… are a list of targets upon which that rule depends. For example, test_count_words depends on count_words.c, count_words.h. If you were to use your new console log macros in your count_words.c, then clog.h would also be a prerequisite of test_count_words.

Actions. The actions in each rule consist of commands that could be entered in your shell (bash). Your bash aliases will not work here. That means simply typing gcc would not include the flags we require for the class. Therefore, you will create some variables to make sure you get those flags. (These are given in the Requirements table below.)

Actions must be indented with a tab character, not four spaces. If you are using Vim with the .vimrc we provided, this is taken care of for you.

Phony targets. At the end of your Makefile you should have a rule called .PHONY. The prerequisites of .PHONY should be every target that is not a real file target.

Running make. From bash, type make target to build that target. For example, typing make test_count_words would compile the count_words project to create or update the executable file test_count_words. If the rule has any prerequisites—e.g., files must be up-to-date or commands to be run before creating test_count_words—those are built first.

The first rule in the Makefile will be the default. When you run make from bash (with no arguments), it builds the default target. Calling make target builds the specified target.

The specifics on what your Makefile must contain are in the requirements table. These slides from Zhiliang Xu have a good explanation of makefiles.

Instructions

You may hand-copy snippets from HW05, but do not copy-paste. You won't learn as well, and it won't work anyway.)

  1. Create a very simple Makefile to just compile test_count_words
    1. Create a blank file called Makefile.
    2. Add one rule to compile test_count_words as follows.
      test_count_words: count_words.c count_words.h gcc -o test_count_words count_words.c test_count_words.c -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic -Wno-unused-function
  2. Test the Makefile from bash.
    1. From bash:
      you@ecegrid-thin1 ~/hw05 $ make
    2. From Vim:
      :make
    Since those commands do not specify a target, the first rule in your Makefile is used.
  3. Modify your Makefile to use variables where possible.
    1. Add the CC and CFLAGS variables at the top. (See the Requirements table.) Here's a start:
      EXECUTABLE=test count words SRC C=count words.c TEST C=test count words.c SRC H=count words.h CC=gcc CFLAGS=-g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic -Wno-unused-function SHELL=/bin/bash ASG NICKNAME=HW55
      CFLAGS should not include -DDEBUG or -DNDEBUG.
    2. Modify your rule to use the variables.
      $(EXECUTABLE): $(SRC C) $(TEST C) $(SRC H) $(CC) -o test count words count words.c test count words.c $(CFLAGS)
    3. Test the Makefile in the same manner as before.
  4. Add a phony rule to submit your code
    1. Add a rule called submit that submits the assignment. Use the variables you defined. You can add more if needed. Your submit rule does not need any prerequisites.
    2. Add the following line to the bottom of your Makefile: .PHONY: submit
    3. Type make submit in bash or :make submit in Vim to test your rule for the submit phony target. . You may submit to it to test your Makefile, but will not be scored and will not affect your grade.
  5. Add a phony rule to run your tests.
    1. Add a rule called test. Hand-copy the following code. We will be adding a bit more to this rule later in HW05.
    2. Add test to your .PHONY rule.
    3. test: debug $(TEST_EXPECTED) @# If actual output matches expected output then count it as a success @if diff -a -B <("./$(EXECUTABLE)") $(TEST EXPECTED) &> /dev/null ; then \ echo "Test passed: output of $(EXECUTABLE) matches $(TEST_EXPECTED)" ; \ else \ echo "Test failed: output of $(EXECUTABLE) does NOT match $(TEST_EXPECTED)" ; \ fi
    4. Type make test in bash or :make test in Vim to test your rule for the test phony target.
  6. Add a rule to run the pretester.
    1. Add a rule called pretest that runs the pretester. It should call your submit rule first (using a prerequisite) and then call 264test. Use the variables you created (including ASG_NICKNAME) in the actions for this rule. Do not enter “HW55”, “count_words.c”, “test_count_words.c”, or “expected.txt” directly.
    2. Add pretest to your .PHONY rule.
    3. Type make pretest in bash or :make pretest in Vim to test your rule for the pretest phony target. It is a dummy pretester that gives everyone 100%.
  7. Add a rule called clean that deletes the executable file (test_count_words). The command to delete a file(s) is rm files…. When working at the terminal, we have that command set to prompt for confirmation. To ensure that make does not prompt for confirmation, you add the -f flag. Thus, the whole command will be rm -f files….
    make clean should delete the executables (test_count_words and any data files created by the code coverage check (if any). To delete file(s), use rm -f ▒▒▒. The -f tells the rm command not to prompt for confirmation or print an error if no such file exists.
    Be careful not to delete your .c or .h files, or your Makefile.
  8. Add the remaining rules, as specified in the Requirements table. Some additional hints are below. Be sure all rules except for $(EXECUTABLE) are listed in your .PHONY: line.

Part 2. Test coverage checking

Code coverage testing—including statement coverage—can help you find bugs in your code by discovering aspects of the functionality that your tests are not exercising. In this section, you will learn how to use test coverage checking, and integrate that functionality into your Makefile.

We will be using one kind of coverage, called line coverage. It runs your tests and monitors which statements in your implementation code were executed. If any statements were not executed, it typically indicates that either your tests are not exercising all of the functionality. It is also possible that you have some dead code. Either way, it should be rectified.

We will be using a tool called GCOV, which works in conjunction with GCC. To check test coverage using GCOV, use this:

# Compile a special version of executable with coverage checking
gcc -o executable source files... -ftest-coverage -fprofile-arcs -DNDEBUG

# Run the executable to record coverage statistics
./executable

# Print a coverage summary
gcov -f count_words.c

Detailed information, such as which lines were executed, can be found by opening the count_words.c.gcov in your editor.

Instructions

  1. Try running GCOV from bash. Later, you will be adding this to your Makefile but it is important to first make sure it works for you, and make sure you understand what it does.
    You could do this with the included count_words program, but to show this in context of something more familiar, we will demonstrate using and implementation of HW04.
    you@ecegrid-thin1 ~/HW05 $ gcc -o test_mintf mintf.c test_mintf.c -fprofile-arcs -ftest-coverage -DNDEBUG
    you@ecegrid-thin1 ~/HW05 $ ./test_mintf
    ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒
    you@ecegrid-thin1 ~/HW05 $ gcov -f mintf.c
    Function 'mintf' Lines executed:61.29% of 31 Function 'print_integer' Lines executed:75.00% of 8 Function '_print_integer_digit' Lines executed:81.82% of 11 File 'mintf.c' Lines executed:68.00% of 50 Creating 'mintf.c.gcov'
    The example above is what you would see if you have not tested adequately. In this case, only 68% of the lines in count_words.c were executed at all. None of the three functions was tested thoroughly. Hopefully, the stats for your code will report 100% coverage.
    This metric excludes blank lines, comments, and lines containing only braces, so 100% line coverage should be attainable.
  2. Add a rule called coverage to your Makefile to check code coverage. Use the variables you defined (e.g., CC, CFLAGS, etc.) wherever possible. Hint: It should have two .c files as prerequisites, and three actions. Don't forget to add coverage to the list of phony targets (i.e., .PHONY submit test ▒▒▒▒▒▒▒▒▒▒▒▒▒).
  3. Type make coverage in bash or :make coverage in Vim to test your rule for the coverage phony target. Verify that it created five new files: count_words.c.gcov, count_words.gcda, count_words.gcno, test_count_words.gcda, test_count_words.gcno.
  4. Modify the clean rule in your Makefile to remove the data files created by GCOV. You should use wildcards (*.c.gcov *.gcda *.gcno).
  5. Type make clean in bash or :make clean in Vim to test your rule for the clean phony target. Verify that the five files created by GCOV (count_words.c.gcov, count_words.gcda, count_words.gcno, test_count_words.gcda, test_count_words.gcno) were deleted.

Part 3: clog.h

For this part, you will create a reusable library for debugging any C project. In the process, you will learn about the C preprocessor and ANSI control codes.

Instructions

In a new file called clog.h, create each of the log_▒▒▒(…) macros specified in the Requirements table. We recommend writing them in the order shown (e.g., log_msg(…) first, then log_int(…), and so on).

To test, make a separate test file called test_clog.c with calls to each of the macros.

Use #ifdef DEBUG (and #endif) to ensure that your log_▒▒▒(…) macros only work when the program was compiled with gcc -DDEBUG ▒▒▒. Without gcc -DDEBUG, your program should still compile, but they should have no effect. That can be done by adding an empty macro.

Here is a skeleton as a starting point. You are welcome (and encouraged) to modify this, as you see fit.

#ifdef DEBUG
#define log_msg(msg) fprintf(stderr, "%s", (msg)) // enabled
#else
#define log_msg(msg)                              // disabled
#endif

For readability, you may wish to indent like this (below). The # must be the first character on the line. Indenting macros like this is not required by the code quality standards. Use whichever you prefer.

#ifdef DEBUG
#    define log_msg(msg) fprintf(stderr, "%s", (msg)) // enabled
#else
#    define log_msg(msg)                              // disabled
#endif

Wrap your entire clog.h in an include guard like this:

#ifndef __CLOG_H__
#define __CLOG_H__
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
#endif

See the article linked above for details on include guards.

Part 4. Modify Makefile to report success/failure in color

This will be a small change. You will use the same ANSI color codes that you used for clog.h. The success message should be printed in green. The failure message should be printed in red.

  1. Test printing in color from bash, to make sure it works for you.
    1. Type echo -e '\x1b[31mTOMATO\x1b[0m' to print “TOMATO” in red.
    2. Type echo -e '\x1b[32mCELERY\x1b[0m' to print “CELERY” in green.
  2. Modify your test rule to use the ANSI color code in a similar manner.
    1. Change the echo statements in your test rule to use echo -e instead of just echo.
    2. Modify the messages so they print the color code before the message—green for success, or red for failure—and then print the reset code after the message.
  3. Test.
    1. Type make test in bash or :make test in Vim to test your rule for the test phony target.

Part 5. Use clog.h in test_count_words.c, and Makefile

  1. In test_count_words.c, modify the printf(…) statements to use log_int(…) instead.
  2. In Makefile, add clog.h to the SRC_H variable. It should now be SRC_H=count_words.h clog.h
  3. Test.

Part 6. miniunit.h

Until now, we have used a very low-tech method of structuring our tests with the expected.txt and the diff command. That method is based on the more general principle of unit testing.

In unit testing, programmers write collections of small functions that test some aspect of a program. For example, in mintf(…) in HW04, you might have a unit test for small positive integers in base 10, another unit for small negative numbers, one for extreme bases, extreme positive values, extreme negative values, and so on. Each test is its own function.

As the number and complexity of tests grows, it becomes necessary to have a foundation of code for running and organizing them. This is called a unit test framework. There are unit test frameworks for every major programming language. For C, one example is CUnit. These typically have special-purpose macros for checking assumptions, similarly to the assert(…) macro, which we have already covered this semester, but with a unit test framework, we may allow the code to continue, even if an assumption is not met, since the goal is simply to check. In addition to the checking macros, real-world testing frameworks have mechanisms for setting up the needed environment that the program-in-test depends on (e.g., files, network connections, etc.), and sophisticated interfaces for visualizing the results and scheduling test runs.

In this homework, you will create a very simple unit test framework… let's call it a unit test library. You may use this to test future assignments. We will illustrate by example.

Using miniunit.h to test — minimal example

Suppose you have the following (very trivial) module for doing arithmetic calculations involving the number 3.

three.c
 1 #include "three.h"
 2
 3 int times_3(int n) {
 4     return n * 3;
 5 }
 6
 7 int divided_by_3(int n) {
 8     return n % 3;  // BUG
 9 }
10
11 int plus_3(int n) {
12     return n * 3;  // BUG
13 }
14
15 int minus_3(int n) {
16     return n - 3;
17 }
three.h
 1 int times_3(int n);
 2 int divided_by_3(int n);
 3 int plus_3(int n);
 4 int minus_3(int n);

Here is the general format for every unit test function (e.g., in test_three.c). You will typically have several of these in your test_▒▒▒.c.

int test_▒▒▒() {
    mu_start();  // set up the test
    // …
    mu_check(condition);
    // …
    mu_check(condition);
    // …
    mu_end();    // finish the test
}

Then, to call all of your unit test functions, you will have a runner function (e.g., main(…)) like this:

int main(int argc, char* argv[]) {
    mu_run(test_▒▒▒);
    mu_run(test_▒▒▒);
    mu_run(test_▒▒▒);
    mu_run(test_▒▒▒);
    return EXIT_SUCCESS;
}

Using the above format, we create the following unit test suite (group of tests) for our three-arithmetic module.

// test_three.c

#include <stdio.h>
#include <stdlib.h>
#include "miniunit.h"
#include "three.h"

int test_plus_3() {
    mu_start();
    mu_check(plus_3(1) == 4);  // will FAIL
    mu_end();
}

int test_minus_3() {
    mu_start();
    mu_check(minus_3(1) == -2);
    mu_end();
}

int test_times_3() {
    mu_start();
    mu_check(times_3(1) == 3);
    mu_end();
}

int test_divided_by_3() {
    mu_start();
    mu_check(divided_by_3(1) == 0);  // will FAIL
    mu_end();
}

int main(int argc, char* argv[]) {
    mu_run(test_plus_3);        // will FAIL
    mu_run(test_minus_3);
    mu_run(test_times_3);
    mu_run(test_divided_by_3);  // will FAIL
    return EXIT_SUCCESS;
}

Your unit test library, using #define macros will convert that seemingly simple test code into a colored summary, including the name of the test function and the line where it failed. Here is the expected out put for the test above:

$ gcc -o test_three three.c test_three.c

$ ./test_three
Test failed (test_plus_3) at line 8
Test passed (test_minus_3)
Test passed (test_times_3)
Test failed (test_divided_by_3) at line 26

$

Instructions

Create the four #define macros as specified in the Requirements table.

Success and failure messages should be printed by mu_run(…) (not mu_check(…)), and should not rely on log_green(…) and log_red(…). Those will be disabled when DEBUG is not defined (i.e., when -DDEBUG was not passed to the compiler); miniunit.h is expected to work either way.

Update Makefile

Add miniunit.h to the SRC_H variable. It should now be SRC_H=count_words.h clog.h miniunit.h

Part 7. Use miniunit.h to improve test_count_words.c

This part will teach you how to use miniunit.h in practice for future assignments. We are giving you the code for free, so it shouldn't take more than about 15 minutes to type and test.

  1. At the top of test_count_words.c, include miniunit.h. (You can also remove the #include "clog.h" since your miniunit.h will include that.)
  2. Add three test functions above main(…): int _test_empty(), int _test_simple(), and int _test_hard().
  3. Inside each test function, add mu_start() at the beginning, and mu_end(…) just before the end.
  4. Add at least one call to mu_check(…) in between mu_start(…) and mu_end(…).
  5. At the top of main(…), add mu_run(_test_empty), mu_run(_test_simple), and mu_run(_test_hard).
  6. The result may look something like this:
    #include <stdio.h>
    #include <stdlib.h>
    #include "count_words.h"
    #include "miniunit.h"
    
    int _test_empty() {
      mu_start();
      //-------------------------------
      mu_check(count_words("") == 0);
      //-------------------------------
      mu_end();
    }
    
    int _test_simple() {
      mu_start();
      //-------------------------------
      mu_check(count_words("apple") == 1);
      mu_check(count_words("boring boxes") == 2);
      mu_check(count_words("apple banana") == 2);
      mu_check(count_words("apple banana cherry") == 3);
      //-------------------------------
      mu_end();
    }
    
    int _test_hard() {
      mu_start();
      //-------------------------------
      mu_check(count_words("famigerate fiddle-faddle") == 2);
      mu_check(count_words("Mary's mongoose") == 2);
      mu_check(count_words("plumbers' platitudes") == 2);
      //-------------------------------
      mu_end();
    }
    
    int main(int argc, char* argv[]) {
      mu_run(_test_empty);
      mu_run(_test_simple);
      mu_run(_test_hard);
    
      log_int(count_words("My apples are sweet."));
      log_int(count_words("My friend's apples are sweeter."));
      log_int(count_words("A pear is sweeter than an apple.."));
    
      return EXIT_SUCCESS;
    }
    
    Note: Due to the intentially planted bugs in count_words.c, _test_simple(…) will fail on "apple banana" (due to 'a'), and _test_hard(…) will fail on "familgerate fiddle-faddle" (due to '-').
  7. Run the tests from bash using make test.
    you@ecegrid-thin1 ~/HW05 $ make test
    gcc -std=c11 -g -Wall -Wshadow --pedantic -Wvla -Werror count_words.c test_count_words.c -o test_count_words -DDEBUG Test passed (_test_empty) Test failed (_test_simple) at line 20 Test failed (_test_hard) at line 29 count_words("My apples are sweet.") == 4 count_words("My friend's apples are sweeter.") == 6 count_words("A pear is sweeter than an apple..") == 8 Test failed: output of {{ test_exe }} does NOT match {{ test_txt }}
    Reminder: count_words(…) has intentionally planted bugs. It is okay that some of the results are obviously incorrect and the tests fail. The purpose of tests is to detect flaws. If the flaws are detected, then the test worked correctly.
    We are giving you this code for free. You are welcome to make changes, or hand-copy as is, as long as you understand what it is doing. If not, please ask.
  8. Try running all of the rules in your Makefile with your new test code.
  9. You must submit your test_count_words.c, even if it is identical to the code above.

Requirements

  1. Your submission must contain each of the following files, as specified:
    file contents
    miniunit.h macros
    mu start()
    1. Declare a local variable and initialize it to 0.
    2. The purpose of this variable is to store the line number of the first "check".
      • By “check” we mean a call to mu_check(…) in the test code that uses miniunit.h.
    3. Name of variable must begin with __mu_ (e.g., __mu_line_number_of_first_failure).
    4. Initial value of this variable should be 0.
    5. Hint: mu_start(…) will be only one line; there should be no semicolon.
    mu check(condition)
    1. If condition is false, store the line number of this mu_check(…) call in the variable that you created in mu_start(…)—but only for the first call to mu_check(…) with a condition that is false.
      • Store the line number only on the first check that fails.
      • If you have multiple calls to mu_check(…) that fail, you want to keep only the line number of the first one that failed.
    2. You can get the current line number with __LINE__.
    mu run(function)
    • Call function() (with no parameters).
    • If it returns 0, then print (in green on stderr):
      Test passed: function
    • If it returns a line number (≥1), then print (in red on stderr):
      Test failed: function at line line#
    • This should work even when the program is not compiled with -DDEBUG.
      • This may require minor changes to your clog.h to ensure that mu_run(…) works with or without -DDEBUG, but the log_▒▒▒(…) macros only work when compiled with -DDEBUG.
      • Hint: This means mu_run(…) should not call log_green(…) or log_red(…) directly. This is a big hint! The snippet given in Q12 should help with this.
    mu end()
    Return the line number of the first check (mu_check(…)) that failed, or 0 if all checks succeeded.
    • Hint: This will be just one line; there should be no semicolon.
    clog.h constants
    ANSI color codes
    • 7 codes: red, green, yellow, blue, magenta, cyan, and reset
    • The first (ANSI_RED) is provided below.
    • Search for the rest of the codes online. This is so you can see that these are standard, and not part of ECE 264. As long your source agrees with our ANSI_RED below, the rest should be fine.
    • #define ANSI_RED "\x1b[31m" // ← OK to copy
    • #define ANSI_GREEN "▒▒▒▒▒▒▒▒"
    • #define ANSI_YELLOW "▒▒▒▒▒▒▒▒"
    • #define ANSI_BLUE "▒▒▒▒▒▒▒▒"
    • #define ANSI_MAGENTA "▒▒▒▒▒▒▒▒"
    • #define ANSI_CYAN "▒▒▒▒▒▒▒▒"
    • #define ANSI_RESET "▒▒▒▒▒▒▒▒"
    macros
    log msg(msg)
    • fprintf(stderr, "%s", msg)
    log int(n)
    • Calling log_int(3+3) should print this:
      3+3 == 6
    • Here's a naïve way to do it:
      #define log_int(n) fprintf(stderr, "%s == %d\n", (#n), (n))
      You may copy/adapt that, but you will need a way to selectively enable this (and other log_▒▒▒(…)) macros only when the symbol DEBUG is defined.
    log str(s)
    • log_str(s)fprintf(stderr, "s == %s\n", s)
    • Calling char* s = "abc"; and then log_str(s) should print this:
      s == "abc"
    • Calling log_str("xyx") should print this:
      "xyx" == "xyx"
    log char(c)
    • log_char(ch)fprintf(stderr, "ch == '%c'\n", ch)
    log addr(addr)
    • log_addr(addr)fprintf(stderr, "addr == %p\n", addr)
    • Examples:
      • int n = 5; log_addr(&n) // should print something like &n == 0x7fff938d
      • int* a_n = &n; log_addr(a_n) // should print something like a_n == 0x7fff938d
    • Note: You need a cast when passing an address to printf("… %p …")
    log red(format, ...)
    • Equivalent to fprintf(stderr, format, ...) except text is printed in red.
    • To do this, print ANSI_RED, then call fprintf(…), and finally print ANSI_RESET.
    • The specification for this macro above is not code. For your actual #define you will need something like log_red(...). In the RHS, use __VA_ARGS__. Search the web for “variadic macros” for more information on this.
    log green(format, ...)
    • Like log_red(…) but in green
    • See the warning in log_red(…).
    log yellow(format, ...)
    • Like log_red(…) but in yellow
    • See the warning in log_red(…).
    log blue(format, ...)
    • Like log_red(…) but in blue
    • See the warning in log_red(…).
    log magenta(format, ...)
    • Like log_red(…) but in magenta
    • See the warning in log_red(…).
    log cyan(format, ...)
    • Like log_red(…) but in cyan
    • See the warning in log_red(…).
    Makefile makefile
    Variable definitions (top section)
    name value (exactly)
    SRC_C count_words.c
    SRC_H count_words.h clog.h miniunit.h
    TEST_C test_count_words.c
    TEST_EXPECTED expected.txt
    TEST_ACTUAL actual.txt
    EXECUTABLE test_count_words
    ASG_NICKNAME HW55
    CC gcc
    CFLAGS -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic -Wno-unused-function
    CFLAGS_GCOV $(CFLAGS) -fprofile-arcs -ftest-coverage
    SHELL /bin/bash
    Rules
    target name action(s)
    $(EXECUTABLE) Compile the executable.
    • Reminder: Use variables in all rules, wherever possible.
    test
    Run executable (test_count_words).
    If output matches expected.txt), print…
    Test passed: output of test_count_words matches expected.txt
    If output does not match expected.txt, print…
    Test failed: output of test_count_words does NOT match expected.txt
    submit Submit the assignment using 264submit.
    • Rule will look like this:
      submit: ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
          264submit ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
      
    • Reminder: Use variables. There should be no raw filenames in this rule.
    pretest Pretest the assignment using 264test.
    • Rule should submit before pretesting.
    • Simply make submit a prerequisite of pretest (i.e., pretest: submit)
    • This rule is simple, so we'll give it to you:
      pretest: submit
          264test $(ASG_NICKNAME)
      
    coverage Check the code coverage of your tests.
    debug Compile test_count_words with DEBUG defined.
    • Hint: This rule will be exactly the same as $(EXECUTABLE), except the name is debug and you will add a GCC flag to the end to set the DEBUG symbol.
    clean Delete all files created by any of the above rules.
    1. Use the variables wherever possible to avoid duplicating information.
      • Information in the variables (above) should not be duplicated elsewhere. ◀◀◀◀◀◀
    2. Use prerequisites to reduce duplication and ensure each action has what it needs.
      • A rule should depend (directly or indirectly) on every file it uses.
      • Do not repeat exactly the same compilation command in multiple places.
      • Ex: test should depend on debug.
    3. Do not add pointless (unnecessary) prerequisites.
      • Ex: Any rule that depends on test_count_words should not list count_words.c or test_count_words.c as prerequisites since they are implied by test_count_words and thus unnecessary.
      • Ex: make clean should not depend on anything.
    4. Do not define DEBUG or NDEBUG in your compilation commands unless otherwise specified in these requirements.
      • make debug: Compile with DEBUG.
      • make coverage: Compile with NDEBUG but not DEBUG to reduce the effect of logging macros (e.g., log_int(…), etc.) and assertions (e.g., assert(…)) on the metrics.
    5. miniunit.h should use clog.h to the extent possible—at least use the ANSI_▒▒▒ constants.
    6. make submit should result in:
      264submit HW55 count_words.c test_count_words.c expected.txt miniunit.h clog.h
  2. You may hand-copy any code snippets you find in this homework description into your HW05 submission.
    • Do not use copy-paste. You learn more from hand-copying unfamiliar syntax. Expect problems if you ignore this.
    • Adaptation is strongly recommended. Some snippets may not work in your file as is.
    • Be sure you understand what you are copying. Correct functioning of your code is your responsibility.
    • Copying from this page is not necessary. This permission is given as a convenience, since some of the syntax may be unfamiliar, and this homework is more tightly specified than most others.
  3. Names of helper macros (if any) must begin with “__mu_”.
  4. Do not repeat the ANSI codes anywhere other than in clog.h and Makefile.
  5. Required macros in clog.h (i.e., log_▒▒▒(…)) should work ONLY when the symbol DEBUG is defined (i.e., when you compile with gcc -DDEBUG).
    • Hint: You will need to use some combination of #ifdef, #ifndef, #else, and #define
    • If you use any helper macros (e.g., __mu_log_color(…)), they may work even when DEBUG is not defined.
  6. Macros in miniunit.h should work regardless of the symbols defined (e.g., DEBUG).
  7. Do not print ANSI codes when output is being directed to a file or other application.
    • This applies to all parts of this homework, including test_count_words.c, Makefile, clog.h, and miniunit.h.
    • In C, you can use isatty(STDERR_FILENO) to determine if the output is going to a real terminal ("TTY"), versus being redirected to a file or something else.
  8. You may use any of the following:
    header functions/symbols allowed in…
    stdbool.h bool, true, false *.c, *.h
    stdio.h fputs, fprintf, stderr *.c, *.h
    string.h strcmp test_count_words.c
    unistd.h isatty, STDERR_FILENO *.c, *.h
    stdlib.h EXIT_SUCCESS test_count_words.c
    Check with the instructor if you are using others. For HW05, we do not anticipate prohibiting any external functions or constants (within reason), but if you find yourself in need of something else, there's a decent chance you are on the wrong track. In particular, you should not need to use printf(…) at all, since all output is going to stderr.
  9. Code that includes clog.h and/or miniunit.h and uses macros from them must compile and run whether or not DEBUG was defined.
  10. miniunit.h should have #include "clog.h" so that users of miniunit.h don't need to include both.
  11. Submissions must meet the code quality standards and the course policies on homework and academic integrity.
    • That means everything must compile successfully, even when compiled with the usual compiler flags (gcc -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic -DNDEBUG -Wno-unused-function)—and without -DDEBUG. Furthermore, tests using your miniunit.h should work properly with or without -DDEBUG.

Submit

To submit HW05 from within your hw05 directory, type 264submit HW05 miniunit.h clog.h Makefile test_count_words.c

Be sure to submit test_count_words.c (as well as the others).

Pre-tester

The pre-tester for HW05 has not yet been released. As soon as it is ready, this note will be changed.

Q&A

  1. Shouldn't a Makefile refer to .o files?
    We are using makefiles in a simplified manner that does not require awareness of .o files.
  2. How do I print a double quotation mark using printf(…)?
    printf("\"")
  3. How do I use a variadic macro to pass arguments from log_red(…) to fprintf(…)?
    In the LHS of the #define, use “...” to indicate 1 or more arguments. In the RHS, use __VA_ARGS__ to pass those same arguments to the next function (e.g., fprintf(…)). Note that “...” stands for one or more, not zero or more.
    Here's a simple example:
    #define printf_to_stderr(...) fprintf(stderr, __VA_ARGS__)
    That macro would convert printf_to_stderr("%d", 3) to fprintf(stderr, "%d", 3).
  4. Should I have a semicolon at the end of the RHS of a #define macro?
    No. The person using the macro will normally include the semicolon.
  5. How can I continue the RHS of a #define macro definition onto the next line?
    Put a backslash ("\") at the end of the line.
    #define profess_love_for_food(food) printf("I love %s", \
            food)
  6. Can I have a macro with multiple C statements?
    Yes. In theory, you could just have the two statements separated by a semicolon like this:
    // BAD
    #define profess_love_for_two_foods(food1, food2) \
          printf("I love %s", food1);                \
          printf("I love %s", food2)
    However, that would lead to surprising results if someone who doesn't follow the code quality standards calls that macro in an if statement like this:
    if(1 == 0)
        profess_love_for_two_foods("soap", "poison");
    Only the first statment would be connected to the if statement.
    if(1 == 0)
        printf("I love %s", "soap");
    
    printf("I love %s", "poison");
    It is tempting to just put curly braces around the two statements, but that also causes problems.
    // BAD
    #define profess_love_for_two_foods(food1, food2) { \
        printf("I love %s", food1); \
        printf("I love %s", food2); \
    }

    The problem comes back to uncivilized oafs who write if statements without curly braces, like this:

    if(age >= 30)
        profess_love_for_two_foods("chocolate", "pizza");
    else
        profess_love_for_two_foods("spinach", "broccoli");

    The above example would result in this:

    if(age >= 30) {
        printf("I love %s", "chocolate");
        printf("I love %s", "pizza");
    };  ← PROBLEM
    else {
        printf("I love %s", "spinach");
        printf("I love %s", "broccoli");
    };
    The standard solution is to wrap the statements in a do { … } while(false) block. Because the do…while requires a semicolon, this actually works out like we want.
    Yes, it is ugly. Hacks like this are not something the instructor would normally condone, but it is standard practice because there are very few truly versatile options for this.
    // USE THIS WAY
    #define profess_love_for_two_foods(food1, food2) do { \
        printf("I love %s", food1);                       \
        printf("I love %s", food2);                       \
    } while(false)
  7. What is HW55? Is that a typo?
    HW55 is a dummy assignment that you will use to test the rules in your Makefile that deal with assignment submission and pretesting. It will not be scored and will not affect your grade.
    HW05 is the “real” assignment you are doing here.
  8. Why does miniunit.h need mu_start(…) and mu_end(…)?
    These give a relatively clean and consistent form to your unit tests, so you can focus on the code that matters for each particular test.
    Without mu_start(…) and mu_end(…), one might resort to a naïve approach, like this:
    YUK!
    int test_count_words_▒▒▒▒▒() { int test__mu_failure_line_num = __MU_SUCCESS; mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); return __mu_failure_line_num; }
    The programmer should not have to know the specific internal names used by miniunit.h! If they get it wrong, then it won't work properly with mu_check(…).
    Of course, they could avoid the problem of consistency if they do the whole thing without any macros—i.e., without miniunit.h—but then the above skeleton would be even messier!
    YUK! YUK! YUK! YUK! YUK!
    int test_count_words_▒▒▒▒▒() { const int __MU_SUCCESS = 0; int __mu_failure_line_num = __MU_SUCCESS; if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } if(!(▒▒▒▒▒▒▒▒▒▒) && __mu_failure_line_num == __MU_SUCCESS) { __mu_failure_line_num = __LINE__; } return __mu_failure_line_num; }
    With mu_start(…) and mu_end(…)—and adding a divider comment (optional)—you get this:
    GOOD
    int test_count_words_▒▒▒▒▒() { mu_start(); //──────────────────────────────────────── mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); mu_check(▒▒▒▒▒▒▒▒▒▒); //──────────────────────────────────────── mu_end(); }
    mu_start(…) and mu_end(…) also make your unit testing library more extensible. If you wanted to change how your tests are organized and/or reported, you could do so without changing all of your test code that uses it.
  9. How do I convert the printf(…) statements in test_count_words.c to use log_int(…)?
    Here is an unrelated example that prints the output of a function using printf(…).
    #include <stdio.h>
    #include <stdlib.h>
    
    int triple(int n) {
      return n * 3;
    }
    
    int main(int argc, char* argv[]) {
        // BAD (… or less good)
        printf("triple(5) == %d\n", triple(5));
        printf("triple(4) == %d\n", triple(4));
        printf("triple(3) == %d\n", triple(3));
        printf("triple(2) == %d\n", triple(2));
        printf("triple(1) == %d\n", triple(1));
    
        return EXIT_SUCCESS;
    }
    
    Here is the same example converted to use log_int(…).
    #include <stdio.h>
    #include <stdlib.h>
    #include "clog.h"
    
    int triple(int n) {
        return n * 3;
    }
    
    int main(int argc, char* argv[]) {
        // GOOD
        log_int(triple(5));
        log_int(triple(4));
        log_int(triple(3));
        log_int(triple(2));
        log_int(triple(1));
        // Advantages over raw printf(…)
        // ∙ Easy to "turn off" when it is time to submit your code.
        // ∙ Less duplication means fewer oppportunities for bugs.
    
        return EXIT_SUCCESS;
    }
    
    With log_int(…), you can print the expression itself, along with its value.
  10. What does #x do in a #define macro?
    It expands to the text of the expression, instead of its value. This is easiest to see if you test using the /usr/bin/cpp command.
    Here is an example, which uses the log_int(…) snippet given in the Requirements table.
    // demonstrate_hash.c
    #include <stdio.h>
    #include <stdlib.h>
    
    #define log_int(n) fprintf(stderr, "%s == %d\n", (#n), (n))
    
    int main(int argc, char* argv[]) {
        log_int(3 + 3);
        return EXIT_SUCCESS;
    }
    
    If we process that with the preprocessor directly (instead of via gcc), we can see what it becomes.
    you@ecegrid-thin1 ~/HW05 $ /usr/bin/cpp demonstrate_hash.c
    … int main(int argc, char* argv[]) { fprintf(stderr, "%s == %d\n", ("3 + 3"), (3 + 3)); return 0; }
    Notice that the third argument to fprintf(…) is a string literal, "3 + 3"—the text of the argument that was passed to log_int(…). That is different from the fourth argument, which is the value of that parameter, 3 + 3 (= 6).
  11. How do I test my clog.h?
    Here's an example. As with all other code snippets in this homework description (for HW05 only), you may hand-copy or adapt this. You may also test in some other way of your choice.
    // test_clog.c
    #include "clog.h"
    
    int main(int argc, char* argv[]) {
        log_msg("COLORS\n");
        log_green("green\n");
        log_red("red\n");
        log_yellow("yellow\n");
        log_blue("blue\n");
        log_magenta("magenta\n");
        log_cyan("cyan\n");
    
        log_msg("\n");
    
        log_msg("SHOPPING LIST\n");
        log_green("bok choy x %d bunches\n", 3);
        log_red("tomatos x %d\n", 5);
        log_yellow("lemon x %d\n", 3);
        log_blue("borage x %d flowers\n", 100);
        log_magenta("rambutan x %d\n", 15);
        log_cyan("Peeps x %d boxes\n", 5);
    
        return EXIT_SUCCESS;
    }
    
    When you compile with -DDEBUG, the output should look like this:
    you@ecegrid-thin1 ~/HW05 $ gcc -o test_clog test_clog.c -DDEBUG
    you@ecegrid-thin1 ~/HW05 $ ./test_clog
    COLORS green red yellow blue magenta cyan SHOPPING LIST bok choy x 3 bunches tomatos x 5 lemon x 3 borage x 100 flowers rambutan x 15 Peeps x 5 boxes
    you@ecegrid-thin1 ~/HW05 $
    When you compile without -DDEBUG, there should be no output, like this:
    you@ecegrid-thin1 ~/HW05 $ gcc -o test_clog test_clog.c
    you@ecegrid-thin1 ~/HW05 $
  12. How can I stop ANSI codes when redirecting to a file or another program?
    This refers to the line in the Requirements table that states, “Do not print ANSI codes when output is being directed to a file or other application.”
    As mentioned just below that requirement, you can use the isatty(STDERR_FILENO) function. It returns true if the output is going directly to the terminal, and false if the output is being redirected to a file or another program.
    The following snippet illustrates how that would work. This is one of several ways you could this. This is included only to help those who may be stuck on this part.
    #define __mu_log_color(color, ...)         \
    do {                                       \
        if(isatty(STDERR_FILENO)) {            \
            fprintf(stderr, "%s", color);      \
        }                                      \
        fprintf(stderr, __VA_ARGS__);          \
        if(isatty(STDERR_FILENO)) {            \
            fprintf(stderr, "%s", ANSI_RESET); \
        }                                      \
    } while(false) // unavoidable hack for function-like macros (See Q6.)
    
    Then, each of your log_COLOR(…) functions would follow the following form:
    #define log_red(...) __mu_log_color(ANSI_RED, __VA_ARGS__)
    
    … and so on.
  13. How can I test that it is working (i.e., no ANSI codes when output is redirected to a file or another program)?
    One way to check is to redirect the output to a file and then open that file in Vim, or print its contents using the cat command.
    you@ecegrid-thin1 ~/HW05 $ gcc -o test_clog test_clog.c -DDEBUG
    you@ecegrid-thin1 ~/HW05 $ ./test_clog &> actual.txt
    you@ecegrid-thin1 ~/HW05 $ cat actual.txt
    COLORS green red yellow blue magenta cyan SHOPPING LIST bok choy x 3 bunches tomatos x 5 lemon x 3 borage x 100 flowers rambutan x 15 Peeps x 5 boxes
    you@ecegrid-thin1 ~/HW05 $
    Another way to check this is to redirect the output to the cat command.
    you@ecegrid-thin1 ~/HW05 $ gcc -o test_clog test_clog.c -DDEBUG
    you@ecegrid-thin1 ~/HW05 $ ./test_clog | cat
    COLORS green red yellow blue magenta cyan SHOPPING LIST bok choy x 3 bunches tomatos x 5 lemon x 3 borage x 100 flowers rambutan x 15 Peeps x 5 boxes
    you@ecegrid-thin1 ~/HW05 $
  14. I'm confused about isatty(…) and/or how to omit the ANSI color codes when output is going to a file or another program. What should I do?
    This will be a small part of the score. If it isn't working for you, get the other parts working before worrying about this.
  15. Why is this even a requirement?
    Vim (and other editors) can call make directly, and show you the output right in the editor. However, the color codes make a mess. More generally, this issue comes up in many programs that print in color (e.g., ls); not printing the ANSI codes to a non-terminal is standard behavior for command-line programs.
  16. How do I make mu_run(…) print the success/failure messages in color even when the DEBUG symbols is not defined (i.e., -DDEBUG not passed to GCC), and without duplicating the ANSI codes in miniunit.h?
    Define a helper macro for printing in color outside the #ifdef/#endif. You can use the one in Q12 or make your own. You will need to define the ANSI codes (#define ANSI_RED …, etc.) outside the #ifdef/#endif, as well.
  17. GCC: “ISO C99 requires rest arguments to be used.” ⋯???
    In variadic macros (see Q3), the ... can stand for 1 or more arguments. If your log_red(…) looks like #define log_red(format, ...) ▒▒▒▒▒▒▒▒▒▒, it will likely work as long as you pass ≥2 arguments (e.g., log_red("I like %d", 5)), but if you pass only a string (e.g., log_red("orange sky")), then there are 0 arguments for the .... It needs ≥1. The solution for our purposes is to remove the first argument (format). The correct form was given in Q3 and Q12.
  18. GCC: “error: ‘true’ undeclared” ⋯???
    GCC: “error: ‘false’ undeclared” ⋯???

    The true and false constants are defined in a standard header called stdbool.h; you need to include it (i.e., #include <stdbool.h>) in any file where you use them (e.g., clog.h). This was communicated in #8 of the Requirements table, as well as the Code Quality Standards, which have more about bool, true, and false.
  19. GCC: “implicit declaration of function ‘isatty’” ⋯???
    GCC “‘STDERR_FILENO’ undeclared” ⋯???

    The isatty(…) function and STDERR_FILENO symbol are defined in a standard header called unistd.h you need to include it (i.e., #include <unistd.h>). This was communicated in #7 and #8 of the Requirements table.
  20. What is the purpose of the debug rule in the Makefile?
    Compiling with make debug will enable your log_▒▒▒(…) macros by passing the -DDEBUG flag to GCC (i.e., to define the DEBUG symbol). With just make (or make test_count_words) DEBUG will not be defined, so your log_▒▒▒(…) macros will be silent. How you call make determines whether they are on or off.
    After HW05, you are welcome to modify this (e.g., if you want your log_▒▒▒(…) macros to be enabled by default.)
  21. Make: “No rule to make target `miniunit.h', needed by `debug'”?
    You do not have a file called miniunit.h, but it is listed as a prerequisite of that file.
  22. After editing miniunit.h or clog.h, nothing changes when I run make test?
    Make sure your make test rule depends on debug.
  23. GCC: “__mu_failure_line_num is not defined” ⋯ Why?
    If you get that within main(…), make sure you are not trying to use the variable declared in mu_start(…) in your mu_run(…). Your mu_start(…) is expanded in the context of a _test_▒▒▒(…) function, while mu_run(…) is expanded in the context of main(…).
  24. CPP: "Unexpected EOF (end-of-file)" (or similar). Why?
    You probably have unmatched braces somewhere in your clog.h or miniunit.h. First, fix your indents. It will make the errors more obvious. Then, be sure to use /usr/bin/cpp test_count_words.c | indent to make the output readable.
  25. After changing the printf(…) statements in test_count_words.c to log_int(…), running make test prints the messages and the test itself doesn't work right.
    It's okay. log_int(…) prints to stderr so the diff testing isn't seeing the output. It is going to the terminal. (This answer might be expanded later.)
  26. What else could be wrong with my clog.h?
    Make sure you have parenthesized the condition parameter. The rationale was covered in both sections' lectures. See the slides. Failure to follow this has caused many students' errors and incorrect behavior.

Updates

Clarifications or corrections may be posted here.
2/9/2020
  • Many clarifications, mainly to the explanation of how to do the assignment.
  • Replaced code snippet for make test.
  • Correction: Remove -DNDEBUG from GCC flags. (It disables assert(…).)
2/10/2020
  • Correction: HEADERS_HSRC_H
  • Correction: ▒.gdno▒.gcno
  • Correction: GCOV creates five files, not three.
  • Correction: Rename test_count_words.txt to expected.txt.
  • Correction: “To test, make a separate test file called test_clog.c with calls to each of the macros.
  • Correction: output messages from make test.
  • Clarification: miniunit.h should be added to SRC_H in the Makefile.
2/11/2020
  • Correction: HEADERS_HSRC_H ⋯ removed some lingering references.
  • Correction: ANSI_▒▒▒ should use #define not const char*.
  • Added Q8 to Q15 in Q&A with hints and free snippets.
  • Added Part 7. Code is given to you free. This should teach you how to use miniunit to test future homework assignments.
2/12/2020
  • Extension to deadline (+1 day)
  • Correction: Submit test_count_words.c.
  • Correction: make test should depend on debug not $(EXECUTABLE) (since your test code needs -DDEBUG to print the messages).
  • Clarification: mu_check(…) should return line number of the first failure (false condition) in a given test function.
  • Clarification: The macros in miniunit.h (mu_▒▒▒(…)) should work regardless of whether the DEBUG symbol exists (i.e., -DDEBUG passed to gcc).
  • Emphasis: Added "◀◀◀◀◀" in Requirements to draw attention to a requirement that some have missed.
  • Correction: “Test passed” and “Test failed” messages should be printed in mu_run(…) not mu_check(…).
  • Correction: mu_run(…) should not use log_green(…) and log_red(…) directly. (Hint: See Q12.)
2/13/2020
  • In Q13: ./test_clog &> actual.txt