Search code examples
cmakefilec-criterion

Unable to compile with my unit tests in C


I'm trying to compile my project with the Criterion framework for unit testing. Here's how my project is organized:

.
├── include
│   └── mysh.h
├── Makefile
├── obj
├── src
│   ├── arrays_handler.c
│   ├── builtin_env.c
│   ├── delete_from_env.c
│   ├── display_env.c
│   ├── display_prompt.c
│   ├── doubly_linked_list.c
│   ├── env_handler.c
│   ├── execute_command.c
│   ├── mysh.c
│   ├── my_strcmp.c
│   ├── my_strcpy.c
│   ├── my_strdup.c
│   ├── my_strlen.c
│   ├── my_strncmp.c
│   ├── my_strndup.c
│   ├── my_strrchr.c
│   ├── my_str_to_word_array.c
│   ├── name_slice.c
│   ├── parse_command.c
│   ├── sanitize.c
│   ├── shell_handler.c
│   └── strbind.c
└── tests
    ├── Makefile
    └── sanitize_test.c

To compile my project, I use a first Makefile (the one at the root), which works very well. Here are its contents:

INCLUDE_DIR :=  include
OBJ_DIR     :=  obj
SRC_DIR     :=  src
NAME        :=  mysh
CC          :=  gcc
CFLAGS      :=  -Wall -Wextra -Werror -ggdb3
CPPFLAGS    :=  -I$(INCLUDE_DIR)

SRC_FILES := $(wildcard $(SRC_DIR)/*.c)
OBJ_FILES := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC_FILES))

all: $(NAME)

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
    @$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

$(NAME): $(OBJ_FILES)
    @$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)

clean:
    @$(MAKE) -C ./tests tests_clean
    @rm -rf $(OBJ_DIR)/*.o

fclean: clean
    @$(MAKE) -C ./tests tests_fclean
    @rm -rf $(NAME)

tests_run: fclean
    @$(MAKE) -C ./tests tests_run

re: fclean all

.PHONY: all clean fclean tests_run re

The problem seems to lie with my second Makefile (in the test folder), which is unable to compile the tests_run rule. Here are its contents:

CC          :=  gcc
CFLAGS      :=  -Wall -Wextra -Werror -ggdb3
CPPFLAGS    :=  -I../include
LDFLAGS     :=  -Lcriterion/lib
LDLIBS      :=  -lcriterion
NAME        = ../unit-tests
OBJ_DIR     = ../obj
SRC         = $(shell find ../src -name '*.c' ! -name 'mysh.c')
TEST_SRC    = $(shell find . -name '*.c')
OBJ         = $(patsubst ../src/%.c,$(OBJ_DIR)/%.o,$(SRC))
TEST_OBJ    = $(patsubst ./%.c,$(OBJ_DIR)/%.o,$(TEST_SRC))

$(OBJ_DIR)/%.o: %.c
    @mkdir -p $(OBJ_DIR)
    @$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

tests_run: $(OBJ) $(TEST_OBJ)
    @$(CC) $(CFLAGS) $(LDFLAGS) $(filter-out ../obj/mysh.o,$(TEST_SRC) $(SRC)) $(LDLIBS) -o $(NAME)
    @./$(NAME)

tests_clean:
    @rm -rf $(OBJ)
    @rm -f ../*.gcda
    @rm -f ../*.gcno

tests_fclean: tests_clean
    @rm -f $(NAME)

.PHONY: tests_run tests_clean tests_fclean

I get the following error when calling tests_run:

make[1]: enter the "[...]/tests" directory
make[1]: exit the "[...]/tests" directory
make[1]: enter the "[...]/tests" directory
make[1]: exit the "[...]/tests" directory
make[1]: enter the "[...]/tests" directory
make[1]: *** No rule to make target "../obj/display_prompt.o", required for "tests_run". Stop.
make[1]: exit directory "[...]/tests".
make: *** [Makefile:29: tests_run] Error 2

Can you help me correct this Makefile?


Solution

  • This error ...

    make[1]: *** No rule to make target "../obj/display_prompt.o", required for "tests_run". Stop.
    

    ... is emitted when make is processing your second makefile, but it is about ../obj/display_prompt.o, which is the responsibility of your first makefile. One of the drawbacks of recursive make, such as you are using, is that the separate make runs are largely independent. There are ways for them to communicate, but they do not share rules or dependency information, so although the top-level makefile knows how to build obj/display_prompt.o (relative to its own directory), the second level makefile does not know how build the same file, which for it is ../obj/display_prompt.o.

    The second-level makefile needs all the main object files, so if you want this recursive make arrangement to work then the top level makefile needs to ensure that they are built before recursing to the second-level makefile. But it doesn't. In fact, it does the exact opposite. The top-level tests_run target has fclean as a prerequisite, which in turn has clean as a prereq, which unconditionally deletes all the object files.

    Preliminary

    I see no reason at all for the top-level tests_run target to delete the object files. It is counter-productive. Perhaps your idea is to perform a clean build of these files for the tests, but if your makefile correctly captures all their dependencies then that should not be necessary. Moreover, if ever you do want to perform a clean build before testing then you can request it easily enough: make fclean; make tests_run.

    Additionally, your second-level makefile should have a separate rule for building the unit-tests executable. That build should not be buried in a recipe for running the same.

    Solution 1

    If you continue with the recursive make approach, then instead of removing the object files, the top-level tests_run rule must ensure, directly or indirectly, that they are built. That might look like this:

    tests_run: $(OBJ_FILES)
        $(MAKE) -C ./tests tests_run
    

    Solution 2

    I'm not a big fan of recursive make, though I do use it in certain situations. Here, though, I would use a single makefile, non-recursively, to build everything. If you want, you can keep it somewhat modular by redesigning the second-level Makefile to be included by the top-level one, maybe something like this:

    TESTS_LDFLAGS     :=  -Ltests/criterion/lib
    TESTS_LDLIBS      :=  -lcriterion
    TESTS_NAME   = unit-tests
    TESTS_SRC    = $(shell find tests -name '*.c')
    TESTS_OBJ    = $(patsubst tests/%.c,$(OBJ_DIR)/%.o,$(TESTS_SRC))
    
    $(TESTS_NAME): $(filter-out $(OBD_DIR)/mysh.o,$(OBJ)) $(TESTS_OBJ)
        $(CC) $(CFLAGS) -o $@ $^ $(TESTS_LDFLAGS) $(TESTS_LDLIBS)
    
    $(OBJ_DIR)/%.o: tests/%.c
        mkdir -p $(OBJ_DIR)
        $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
    
    tests_run: $(TESTS_NAME)
        ./$(TESTS_NAME)
    
    tests_clean:
        rm -rf $(TEST_OBJ)
        rm -f *.gcda
        rm -f *.gcno
    
    tests_fclean: tests_clean
        rm -f $(TEST_NAME)
    
    .PHONY: tests_run tests_clean tests_fclean
    

    In the top-level makefile, you would want to remove run_tests rule (tests_run will instead be used directly), and add include tests Makefile somewhere near the end. (Though if it were me, I would also rename the second-level file to tests.mk or similar, to clarify that it is not (any longer) a standalone makefile.