Skip to content

Commit

Permalink
test: add compile time test
Browse files Browse the repository at this point in the history
  • Loading branch information
TheCoconutChef committed Oct 24, 2022
1 parent ba9d8e4 commit 9a56535
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 1 deletion.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
endif()

option(lager_BUILD_TESTS "Build tests" ON)
option(lager_BUILD_FAILURE_TESTS "Build failure tests" ON)
option(lager_BUILD_EXAMPLES "Build examples" ON)
option(lager_BUILD_DEBUGGER_EXAMPLES "Build examples that showcase the web based debugger" ON)
option(lager_BUILD_DOCS "Build docs" ON)
Expand Down
232 changes: 232 additions & 0 deletions cmake/icm_build_failure_testing.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# icm_build_failure_testing
#
# SPDX-License-Identifier: MIT
# MIT License:
# Copyright (c) 2022 Borislav Stanimirov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files(the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and / or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions :
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# VERSION HISTORY
#
# 1.00 (2022-xx-xx) Initial release
#
# NOTES
# This file is bundled with icm_build_failure_parse_and_run.cmake and expects
# it to be in the same directory
include_guard(GLOBAL)

# store current dir to find icm_build_failure_parse_and_run
set(ICM_BUILD_FAILURE_TEST_SCRIPT_DIR "${CMAKE_CURRENT_LIST_DIR}")

# icm_add_build_failure_test
#
# Add a build failure test to a project
#
# Args:
# * NAME name - Name of the test (for CTest)
# * TARGET target - Optional.
# Name of the target executable (whose build will fail).
# Defaults to '<name>-test' if not provided
#
# * SOURCES sources - Sources for the executable
# * PARSE src - Optional. One of the sources can be marked as PARSE. If
# such is present, it will be parsed for expected errors
#
# * LIBRARIES libs - Optional. Link libraries for the executable
# * LABELS labels - Optional. CTest labels for the test
# If none are provided, "build-failure" will be added
# * FOLDER folder - Optional. MSVC solution folder for the target
#
# * ERROR_MATCHES mathes - Optional. Strings with which to match the build
# output of the executable
#
# Notes:
# * If neither ERROR_MATCHES nor a PARSE source is present, the test will only
# check that the build of the target fails. This is not recommended
# * If both are present, the PARSE source takes precedence
# * ERROR_MATCHES are a list of strings. The test will check that at least one
# of provided strings matches the output of the build
# * To list errors in the PARSE source, it must be on a line prepended with:
# "// build error:". Example:
#
# // x.cpp:
# // build error: custom error was triggered
# static_assert(false, "custom error was triggered")
#
# If multimple "// build error: lines" are present, at least one of them
# needs to match for a successful test.
#
# Example:
# icm_add_build_failure_test(
# NAME mylib-bf-foo-func-first-arg-trivial
# LIBRARIES mylib
# SOURCES
# test-helper.cpp
# PARSE bf-foo-func-first-arg-trivial.cpp
# LABELS bf mylib
# )
function(icm_add_build_failure_test)
cmake_parse_arguments(ARG "" "NAME;TARGET;FOLDER" "SOURCES;LIBRARIES;ERROR_MATCHES;LABELS" ${ARGN})
if(DEFINED ARG_UNPARSED_ARGUMENTS)
message(NOTICE "icm_add_build_failure_test called with unknown arguments")
endif()

# check sources for a file to parse
cmake_parse_arguments(ARG "" "PARSE" "" ${ARG_SOURCES})
set(ARG_SOURCES "${ARG_UNPARSED_ARGUMENTS}")

if(NOT DEFINED ARG_TARGET)
set(ARG_TARGET "${ARG_NAME}-test")
endif()

# add an executable
# an object library will be sufficient for compilation errors,
# but an executable will help with linker errors as well
add_executable(${ARG_TARGET} ${ARG_SOURCES})
set_target_properties(${ARG_TARGET} PROPERTIES
EXCLUDE_FROM_ALL TRUE
EXCLUDE_FROM_DEFAULT_BUILD TRUE
)
if(DEFINED ARG_LIBRARIES)
target_link_libraries(${ARG_TARGET} PRIVATE ${ARG_LIBRARIES})
endif()
if(DEFINED ARG_FOLDER)
set_target_properties(${ARG_TARGET} PROPERTIES FOLDER ${ARG_FOLDER})
endif()

if(DEFINED ARG_PARSE)
# we find error matches from a parsed file

# also add the parsed source to the executable's sources
target_sources(${ARG_TARGET} PRIVATE "${ARG_PARSE}")
# this var is used in the configured file bellow
get_filename_component(parsedSourcePath "${ARG_PARSE}" ABSOLUTE)

# configure in binary dir based on target name
# it's safe as no two targets can have the same name anyway
# TODO: when minimal supported version of CMake is 3.18, embed the
# helper script [=[here]=] and use file(CONFIGURE)
configure_file(
"${ICM_BUILD_FAILURE_TEST_SCRIPT_DIR}/icm_build_failure_parse_and_run.cmake"
"${CMAKE_BINARY_DIR}/${ARG_TARGET}.cmake"
@ONLY
)

add_test(
NAME ${ARG_NAME}
# provide the config as a command line arg here
# we cannot configure the file with a generator expression
COMMAND ${CMAKE_COMMAND} -DCFG=$<CONFIG> -P ${ARG_TARGET}.cmake
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
else()
# we look for error matches in arguments
add_test(
NAME ${ARG_NAME}
COMMAND ${CMAKE_COMMAND} --build . --target ${ARG_TARGET} --config $<CONFIG>
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
if(DEFINED ARG_ERROR_MATCHES)
# matches are provided for all test runs
set_tests_properties(${ARG_NAME} PROPERTIES
PASS_REGULAR_EXPRESSION "${ARG_ERROR_MATCHES}")
else()
# no matches are provided so just expect the build to fail
set_tests_properties(${ARG_NAME} PROPERTIES
WILL_FAIL TRUE)
endif()
endif()

if(NOT DEFINED ARG_LABELS)
# if labels are not provided, still add "build-failure"
set(ARG_LABELS "build-failure")
endif()

set_tests_properties(${ARG_NAME} PROPERTIES LABELS "${ARG_LABELS}")
endfunction()

# icm_add_multiple_build_failure_tests
#
# Add a multiple build failure tests to a project via multiple calls to
# icm_add_build_failure_test
#
# Args:
# * SOURCES sources - List of sources to add. Each source will lead to a new
# test being added
# * PREFIX prefix - Optional. Prefix string to add to each test name
#
# * LIBRARIES, LABELS, FOLDER, ERROR_MATCHES
# forwarded to icm_add_build_failure_test
#
# Notes:
# If ERROR_MATCHES is not present, each source is forwarded as PARSE
#
function(icm_add_multiple_build_failure_tests)
cmake_parse_arguments(ARG "" "PREFIX;FOLDER" "SOURCES;LIBRARIES;ERROR_MATCHES;LABELS" ${ARGN})
if(DEFINED ARG_UNPARSED_ARGUMENTS)
message(NOTICE "icm_add_multiple_build_failure_tests called with unknown arguments")
endif()
foreach(sourceFile ${ARG_SOURCES})
# replace spaces and path separators with '-'
string(REGEX REPLACE "[// ]" "-" testName "${sourceFile}")
# strip extenstion for target name
get_filename_component(testName ${testName} NAME_WLE)

if(NOT DEFINED ARG_ERROR_MATCHES)
set(sourceFile "PARSE;${sourceFile}")
endif()

if(DEFINED ARG_PREFIX)
set(testName ${ARG_PREFIX}-${testName})
endif()

# add test and forward args
icm_add_build_failure_test(
NAME ${testName}
SOURCES "${sourceFile}"
LIBRARIES ${ARG_LIBRARIES}
LABELS ${ARG_LABELS}
ERROR_MATCHES "${ARG_ERROR_MATCHES}"
FOLDER ${ARG_FOLDER}
)
endforeach()
endfunction()

# icm_glob_build_failure_tests
#
# add multiple sources to icm_add_multiple_build_failure_tests via a
# file(GLOB pattern)
#
# Args:
# * PATTERN pat - GLOB pattern to use
# The rest of the argument are forwarded verbatim to
# icm_add_multiple_build_failure_tests
function(icm_glob_build_failure_tests)
cmake_parse_arguments(ARG "" "PATTERN" "" ${ARGN})
file(GLOB srcs
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
LIST_DIRECTORIES OFF
${ARG_PATTERN}
)
icm_add_multiple_build_failure_tests(
SOURCES ${srcs}
${ARG_UNPARSED_ARGUMENTS}
)
endfunction()
7 changes: 6 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ find_package(Qt5 COMPONENTS Core Concurrent)
add_custom_target(tests COMMENT "Build all the unit tests.")
add_dependencies(check tests)

file(GLOB_RECURSE lager_unit_tests "*.cpp")
file(GLOB lager_unit_tests "*.cpp" "cereal/*.cpp" "event_loop/*.cpp" "extra/*.cpp")

foreach(_file IN LISTS lager_unit_tests)
message("found unit test: " ${_file})
Expand All @@ -66,3 +66,8 @@ foreach(_file IN LISTS lager_unit_tests)
target_link_libraries(${_target} PUBLIC lager-dev ${_qt_libs})
add_test("test/${_output}" ${_output})
endforeach()


if (lager_BUILD_FAILURE_TESTS)
add_subdirectory(build_failure)
endif()
19 changes: 19 additions & 0 deletions test/build_failure/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
include(icm_build_failure_testing)

set(_glob_pattern "*.cpp")

file(GLOB_RECURSE lager_build_failure_tests ${_glob_pattern})
foreach(_file IN LISTS lager_build_failure_tests)
message("found build failure test: " ${_file})
endforeach()

# We test that the build fails and the failure message
# contains the offending line.
#
# see: https://ibob.bg/blog/2022/10/04/testing-build-failure-with-cmake/
icm_glob_build_failure_tests(
PATTERN ${_glob_pattern}
LIBRARIES lager-dev
PREFIX "build-failure"
ERROR_MATCHES "offending line"
)
15 changes: 15 additions & 0 deletions test/build_failure/cursor-to-cursor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// clang-format off
#include <lager/cursor.hpp>

#include <string>

int main()
{
lager::cursor<int> a;

lager::cursor int_cursor = a; // happy path

lager::cursor<std::string> b = a; // offending line

return 0;
}
16 changes: 16 additions & 0 deletions test/build_failure/cursor-to-reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// clang-format off
#include <lager/cursor.hpp>
#include <lager/reader.hpp>

#include <string>

int main()
{
lager::cursor<int> a{};

lager::reader<int> int_reader = a; // happy path

lager::reader<std::string> b = a; // offending line

return 0;
}
18 changes: 18 additions & 0 deletions test/build_failure/lense-to-cursor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// clang-format off
#include <lager/cursor.hpp>
#include <lager/lenses.hpp>
#include <lager/state.hpp>

#include "setup.hpp"

int main()
{
auto st = lager::make_state(Model{});
auto l = lager::lenses::attr(&Model::foo);

lager::cursor<Foo> foo_cursor = st.zoom(l); // happy path

lager::cursor<std::string> a = st.zoom(l); // offending line

return 0;
}
17 changes: 17 additions & 0 deletions test/build_failure/lense-to-reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// clang-format off
#include "setup.hpp"

#include <lager/lenses.hpp>
#include <lager/reader.hpp>

int main()
{
auto s = make_dummy_store();
auto l = lager::lenses::attr(&Model::foo);

lager::reader<Foo> foo_reader = s.zoom(l); // happy path

lager::reader<std::string> b = s.zoom(l); // offending line

return 0;
}
15 changes: 15 additions & 0 deletions test/build_failure/map-to-reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// clang-format off
#include <lager/reader.hpp>

#include "setup.hpp"

int main()
{
auto s = make_dummy_store();

lager::reader<int> int_reader = s.map([](auto) { return 1; }); // happy path

lager::reader<std::string> a = s.map([](auto) { return 1; }); // offending line

return 0;
}
13 changes: 13 additions & 0 deletions test/build_failure/reader-to-reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// clang-format off
#include <lager/reader.hpp>

int main()
{
lager::reader<int> a{};

lager::reader<int> num = a; // happy path

lager::reader<std::string> b = a; // offending line

return 0;
}
Loading

0 comments on commit 9a56535

Please sign in to comment.