• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright Louis Dionne 2013-2017
2# Distributed under the Boost Software License, Version 1.0.
3# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
4#
5#
6# This CMake module provides a function generating a unit test to make sure
7# that every public header can be included on its own.
8#
9# When a C++ library or application has many header files, it can happen that
10# a header does not include all the other headers it depends on. When this is
11# the case, it can happen that including that header file on its own will
12# break the compilation. This CMake module generates a dummy executable
13# comprised of many .cpp files, each of which includes a header file that
14# is part of the public API. In other words, the executable is comprised
15# of .cpp files of the form:
16#
17#   #include <the/public/header.hpp>
18#
19# and then exactly one `main` function. If this succeeds to compile, it means
20# that the header can be included on its own, which is what clients expect.
21# Otherwise, you have a problem. Since writing these dumb unit tests by hand
22# is tedious and repetitive, you can use this CMake module to automate this
23# task.
24
25#   add_header_test(<target> [EXCLUDE_FROM_ALL] [EXCLUDE excludes...] HEADERS headers...)
26#
27# Generates header-inclusion unit tests for all the specified headers.
28#
29# This function creates a target which builds a dummy executable including
30# each specified header file individually. If this target builds successfully,
31# it means that all the specified header files can be included individually.
32#
33# Parameters
34# ----------
35# <target>:
36#   The name of the target to generate.
37#
38# HEADERS headers:
39#   A list of header files to generate the inclusion tests for. All headers
40#   in this list must be represented as relative paths from the root of the
41#   include directory added to the compiler's header search path. In other
42#   words, it should be possible to include all headers in this list as
43#
44#       #include <${header}>
45#
46#   For example, for a library with the following structure:
47#
48#       project/
49#           doc/
50#           test/
51#           ...
52#           include/
53#               boost/
54#                   hana.hpp
55#                   hana/
56#                       transform.hpp
57#                       tuple.hpp
58#                       pair.hpp
59#                       ...
60#
61#   When building the unit tests for that library, we'll add `-I project/include'
62#   to the compiler's arguments. The list of public headers should therefore contain
63#
64#       boost/hana.hpp
65#       boost/hana/transform.hpp
66#       boost/hana/tuple.hpp
67#       boost/hana/pair.hpp
68#       ...
69#
70#   Usually, all the 'public' header files of a library should be tested for
71#   standalone inclusion. A header is considered 'public' if a client should
72#   be able to include that header on its own.
73#
74# [EXCLUDE excludes]:
75#   An optional list of headers or regexes for which no unit test should be
76#   generated. Basically, any header in the list specified by the `HEADERS`
77#   argument that matches anything in `EXCLUDE` will be skipped.
78#
79# [EXCLUDE_FROM_ALL]:
80#   If provided, the generated target is excluded from the 'all' target.
81#
82function(add_header_test target)
83    cmake_parse_arguments(ARGS "EXCLUDE_FROM_ALL"             # options
84                               ""                             # 1 value args
85                               "HEADERS;EXCLUDE"              # multivalued args
86                               ${ARGN})
87    if (NOT ARGS_HEADERS)
88        message(FATAL_ERROR "The `HEADERS` argument must be provided.")
89    endif()
90
91    if (ARGS_EXCLUDE_FROM_ALL)
92        set(ARGS_EXCLUDE_FROM_ALL "EXCLUDE_FROM_ALL")
93    else()
94        set(ARGS_EXCLUDE_FROM_ALL "")
95    endif()
96
97    foreach(header ${ARGS_HEADERS})
98        set(skip FALSE)
99        foreach(exclude ${ARGS_EXCLUDE})
100            if (${header} MATCHES ${exclude})
101                set(skip TRUE)
102                break()
103            endif()
104        endforeach()
105        if (skip)
106            continue()
107        endif()
108
109        get_filename_component(filename "${header}" NAME_WE)
110        get_filename_component(directory "${header}" DIRECTORY)
111
112        set(source "${CMAKE_CURRENT_BINARY_DIR}/headers/${directory}/${filename}.cpp")
113        if (NOT EXISTS "${source}")
114            file(WRITE "${source}" "#include <${header}>")
115        endif()
116        list(APPEND sources "${source}")
117    endforeach()
118
119    set(standalone_main "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp")
120    if (NOT EXISTS "${standalone_main}")
121        file(WRITE "${standalone_main}" "int main() { }")
122    endif()
123    add_executable(${target}
124        ${ARGS_EXCLUDE_FROM_ALL}
125        ${sources}
126        "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp"
127    )
128endfunction()
129