diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a5aa0..f2b6cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Project's root `CMakeLists.txt`. * `dependencies.cmake` and `dev-dependencies.cmake` scripts. * `CPM.cmake` script that downloads specified version of [CPM](https://github.com/cpm-cmake/CPM.cmake). -* `dump()`, `dd()`, `fail_in_source_build()`, `extract_value()` and `safeguard_properties()` utils functions, in `helpers.cmake`. +* `fail_in_source_build()`, `extract_value()`, `requires_arguments()` and `safeguard_properties()` utils functions, in `helpers.cmake`. +* `dump()` and `dd()` in `debug.cmake`. * `semver_parse()`, `write_version_file` and `version_from_file()` utils, in `version.cmake`. * `git_find_version_tag()` util, in `git.cmake`. * `VERSION` file. diff --git a/cmake/rsp/cache.cmake b/cmake/rsp/cache.cmake index cabdde0..1821815 100644 --- a/cmake/rsp/cache.cmake +++ b/cmake/rsp/cache.cmake @@ -7,6 +7,8 @@ include_guard(GLOBAL) # Debug message(VERBOSE "rsp/cache module included") +include("rsp/helpers") + if (NOT DEFINED RSP_CACHE_EXPIRES_AT_KEY_AFFIX) set(RSP_CACHE_EXPIRES_AT_KEY_AFFIX "[rsp@expires_at]") endif () @@ -37,14 +39,7 @@ if (NOT COMMAND "cache_set") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY;VALUE") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY;VALUE" INPUT) # Resolve optional arguments if (NOT DEFINED INPUT_TYPE) @@ -104,14 +99,7 @@ if (NOT COMMAND "cache_get") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY" INPUT) # Resolve optional arguments if (NOT DEFINED INPUT_DEFAULT) @@ -160,14 +148,7 @@ if (NOT COMMAND "cache_has") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY;OUTPUT") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY;OUTPUT" INPUT) # Determine if entry exists in cache if (DEFINED CACHE{${INPUT_KEY}}) @@ -227,14 +208,7 @@ if (NOT COMMAND "cache_forget") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY" INPUT) # Remove entry if it exists if (DEFINED CACHE{${INPUT_KEY}}) @@ -305,14 +279,7 @@ if (NOT COMMAND "cache_remember") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY;CALLBACK") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY;CALLBACK" INPUT) # Ensure that callback exists if (NOT COMMAND ${INPUT_CALLBACK}) @@ -389,14 +356,7 @@ if (NOT COMMAND "cache_has_expired") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "KEY;OUTPUT") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("KEY;OUTPUT" INPUT) # Make expires at key... cache_make_expires_at_key(EXPIRES_AT_KEY INPUT_KEY) diff --git a/cmake/rsp/git.cmake b/cmake/rsp/git.cmake index da32d6e..4a3827a 100644 --- a/cmake/rsp/git.cmake +++ b/cmake/rsp/git.cmake @@ -7,6 +7,8 @@ include_guard(GLOBAL) # Debug message(VERBOSE "rsp/git module included") +include("rsp/helpers") + # Ensure that git is available or this module will not work find_package(Git REQUIRED) @@ -40,14 +42,7 @@ if (NOT COMMAND "git_find_version_tag") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "OUTPUT;WORKING_DIRECTORY") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("OUTPUT;WORKING_DIRECTORY" INPUT) # Resolve optional arguments if (NOT DEFINED INPUT_MATCH_PATTERN) diff --git a/cmake/rsp/helpers.cmake b/cmake/rsp/helpers.cmake index 05f6910..0d2f6e0 100644 --- a/cmake/rsp/helpers.cmake +++ b/cmake/rsp/helpers.cmake @@ -48,6 +48,42 @@ if (NOT COMMAND "extract_value") endfunction() endif () +if (NOT COMMAND "requires_arguments") + + #! requires_arguments : Ensures that specified arguments have been defined, fails otherwise + # + # Macro is intended to be used within a custom function, after `cmake_parse_arguments()` has + # been used. + # + # @see https://cmake.org/cmake/help/latest/command/cmake_parse_arguments.html#cmake-parse-arguments + # @see https://cmake.org/cmake/help/latest/command/if.html#defined + # + # @param required List of required arguments + # @param prefix The parsed input arguments prefix + # used in your cmake_parse_arguments() call. + # + # @throws If required arguments are not defined + # + macro(requires_arguments required prefix) + # Note: the "prefix" parameter cannot be made optional for this macro. + # It is unsafe to rely on any ${ARGV} in this context, ... + # @see https://cmake.org/cmake/help/latest/command/macro.html#argument-caveats + + # Append "_" to the prefix, if given. + # @see https://cmake.org/cmake/help/latest/command/cmake_parse_arguments.html#cmake-parse-arguments + set(resolved_prefix "${prefix}") + if(NOT ${prefix} EQUAL "") + set(resolved_prefix "${prefix}_") + endif () + + foreach (arg ${required}) + if (NOT DEFINED "${resolved_prefix}${arg}") + message(FATAL_ERROR "${arg} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") + endif () + endforeach () + endmacro() +endif () + if (NOT COMMAND "safeguard_properties") #! safeguard_properties : Invoke a "risky" callback whilst "safeguarding" properties @@ -76,14 +112,7 @@ if (NOT COMMAND "safeguard_properties") set(multiValueArgs PROPERTIES) cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "CALLBACK;PROPERTIES") - foreach (arg ${requiredArgs}) - if (NOT DEFINED INPUT_${arg}) - message(FATAL_ERROR "${arg} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("CALLBACK;PROPERTIES" INPUT) # ---------------------------------------------------------------------------------------------- # diff --git a/cmake/rsp/testing.cmake b/cmake/rsp/testing.cmake index 9a7af8a..2163e16 100644 --- a/cmake/rsp/testing.cmake +++ b/cmake/rsp/testing.cmake @@ -7,6 +7,7 @@ include_guard(GLOBAL) # Debug message(VERBOSE "rsp/testing module included") +include("rsp/helpers") include("rsp/cache") include("rsp/testing/asserts") @@ -90,14 +91,7 @@ if (NOT COMMAND "define_test_suite") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "DIRECTORY") - foreach (arg ${requiredArgs}) - if (NOT DEFINED INPUT_${arg}) - message(FATAL_ERROR "${arg} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("DIRECTORY" INPUT) # ---------------------------------------------------------------------------------------------- # @@ -450,14 +444,7 @@ if (NOT COMMAND "add_ctest_using_executor") set(multiValueArgs LABELS CALLBACK_ARG) # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "NAME;CALLBACK;TEST_CASE") - foreach (arg ${requiredArgs}) - if (NOT DEFINED INPUT_${arg}) - message(FATAL_ERROR "${arg} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("NAME;CALLBACK;TEST_CASE" INPUT) # ---------------------------------------------------------------------------------------------- # # Resolve optional arguments diff --git a/cmake/rsp/version.cmake b/cmake/rsp/version.cmake index 2b07922..bc9990b 100644 --- a/cmake/rsp/version.cmake +++ b/cmake/rsp/version.cmake @@ -7,6 +7,7 @@ include_guard(GLOBAL) # Debug message(VERBOSE "rsp/version module included") +include("rsp/helpers") include("rsp/git") include("rsp/version/semver") @@ -20,6 +21,7 @@ if (NOT COMMAND "write_version_file") # @example # # Defaults to version obtained via git # write_version_file(FILE "version.txt") + # # # Or, use custom version # write_version_file(FILE "version.txt" VERSION "v1.4.3") # @@ -43,14 +45,7 @@ if (NOT COMMAND "write_version_file") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "FILE") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("FILE" INPUT) # Resolve version, if none specified if (NOT DEFINED INPUT_VERSION) @@ -123,14 +118,7 @@ if (NOT COMMAND "version_from_file") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - set(requiredArgs "FILE;OUTPUT") - foreach (name ${requiredArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("FILE;OUTPUT" INPUT) # Resolve optional arguments if (NOT DEFINED INPUT_DEFAULT) diff --git a/cmake/rsp/version/semver.cmake b/cmake/rsp/version/semver.cmake index 15f1516..55c671b 100644 --- a/cmake/rsp/version/semver.cmake +++ b/cmake/rsp/version/semver.cmake @@ -7,6 +7,8 @@ include_guard(GLOBAL) # Debug message(VERBOSE "rsp/version/semver module included") +include("rsp/helpers") + if (NOT COMMAND "semver_parse") #! semver_parse : Parses a semantic version string @@ -38,13 +40,7 @@ if (NOT COMMAND "semver_parse") set(multiValueArgs "") # N/A cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Ensure required arguments are defined - foreach (name ${oneValueArgs}) - if (NOT DEFINED INPUT_${name}) - message(FATAL_ERROR "${name} argument is missing, for ${CMAKE_CURRENT_FUNCTION}()") - endif () - endforeach () + requires_arguments("VERSION;OUTPUT" INPUT) # Remove eventual "v" prefix from given version string string(REGEX REPLACE "^[v]" "" cleanVersion "${INPUT_VERSION}") diff --git a/tests/unit/helpers/requires_arguments_test.cmake b/tests/unit/helpers/requires_arguments_test.cmake new file mode 100644 index 0000000..2ea2977 --- /dev/null +++ b/tests/unit/helpers/requires_arguments_test.cmake @@ -0,0 +1,66 @@ +include("rsp/testing") +include("rsp/helpers") + +define_test_case( + "Requires Arguments Test" + LABELS "requires;args;helpers" +) + +# -------------------------------------------------------------------------------------------------------------- # +# Helpers +# -------------------------------------------------------------------------------------------------------------- # + +function(fn_with_named_args) + set(options "") # N/A + set(oneValueArgs NAME DESCRIPTION) + set(multiValueArgs "") # N/A + + cmake_parse_arguments(INPUT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + requires_arguments("NAME;DESCRIPTION" INPUT) +endfunction() + +function(fn_with_args name) + if (${ARGC} EQUAL 2) + set(description "${ARGV1}") + endif () + + # This may seem a bit strange to declare "description" as required + # here, however, this is fully intended (for the test...) + requires_arguments("name;description" "") +endfunction() + +# -------------------------------------------------------------------------------------------------------------- # +# Actual Tests +# -------------------------------------------------------------------------------------------------------------- # + +define_test("ensures required named args are defined" "ensures_required_named_args_defined") +function(ensures_required_named_args_defined) + + # If invoking method does not cause a fatal error, test passes. + fn_with_named_args(NAME "foo" DESCRIPTION "bar") + +endfunction() + +define_test("ensures required args are defined" "ensures_required_args_defined") +function(ensures_required_args_defined) + + # If invoking method does not cause a fatal error, test passes. + fn_with_args("foo" "bar") + +endfunction() + +define_test("fails when required named args are not defined" "fails_when_required_named_arg_not_defined" EXPECT_FAILURE) +function(fails_when_required_named_arg_not_defined) + + # This should cause a failure because DESCRIPTION is required + fn_with_named_args(NAME "foo") + +endfunction() + +define_test("fails when required args are not defined" "fails_when_required_arg_not_defined" EXPECT_FAILURE) +function(fails_when_required_arg_not_defined) + + # This should cause a failure because second argument is required + fn_with_args("foo") + +endfunction() \ No newline at end of file