Skip to content

Commit

Permalink
Merge pull request arximboldi#162 from TheCoconutChef/master
Browse files Browse the repository at this point in the history
Nicer build failure messages on bad assignments
  • Loading branch information
arximboldi authored Oct 24, 2022
2 parents 2c60004 + 9a56535 commit 11b4326
Show file tree
Hide file tree
Showing 18 changed files with 465 additions and 7 deletions.
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()
5 changes: 4 additions & 1 deletion lager/cursor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ class cursor_base

cursor_base() = default;

template <typename T>
template <typename T,
std::enable_if_t<std::is_same_v<zug::meta::value_t<NodeT>,
zug::meta::value_t<T>>,
int> = 0>
cursor_base(cursor_base<T> x)
: base_t{std::move(x)}
{}
Expand Down
10 changes: 8 additions & 2 deletions lager/reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,18 @@ class reader_base

reader_base() = default;

template <typename T>
template <typename T,
std::enable_if_t<std::is_same_v<zug::meta::value_t<NodeT>,
zug::meta::value_t<T>>,
int> = 0>
reader_base(reader_base<T> x)
: base_t{std::move(x)}
{}

template <typename T>
template <typename T,
std::enable_if_t<std::is_same_v<zug::meta::value_t<NodeT>,
zug::meta::value_t<T>>,
int> = 0>
reader_base(cursor_base<T> x)
: base_t{std::move(x)}
{}
Expand Down
4 changes: 3 additions & 1 deletion lager/watch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class watchable_base : private NodeT::signal_type::forwarder_type
: node_{std::move(p)}
{}

template <typename T>
template <typename T,
std::enable_if_t<std::is_same_v<value_t, zug::meta::value_t<T>>,
int> = 0>
watchable_base(watchable_base<T> x)
: node_(std::move(x.node_))
{}
Expand Down
18 changes: 16 additions & 2 deletions lager/with.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,14 @@ class with_expr_base : public xform_mixin<Deriv>
return deriv_().zoom(lens);
}

template <typename T>
template <typename T,
typename U = Deriv,
std::enable_if_t<
std::is_same_v<
typename decltype(std::declval<U>().make())::value_type,
T
>,
int> = 0>
operator reader<T>() &&
{
return std::move(*this).make();
Expand All @@ -182,7 +189,14 @@ class with_expr_base : public xform_mixin<Deriv>
{
return std::move(*this).make();
}
template <typename T>
template <typename T,
typename U = Deriv,
std::enable_if_t<
std::is_same_v<
typename decltype(std::declval<U>().make())::value_type,
T
>,
int> = 0>
operator cursor<T>() &&
{
return std::move(*this).make();
Expand Down
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;
}
Loading

0 comments on commit 11b4326

Please sign in to comment.