diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..565c55b7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.12) +project(cppcoro LANGUAGES CXX) + +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +add_subdirectory(lib) + +enable_testing() +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +export(EXPORT cppcoroTargets + FILE "${PROJECT_BINARY_DIR}/cppcoro/cppcoroTargets.cmake" + NAMESPACE cppcoro::) +configure_file(cmake/cppcoroConfig.cmake + "${PROJECT_BINARY_DIR}/cppcoro/cppcoroConfig.cmake" + COPYONLY) + +set(config_package_location lib/cmake/cppcoro) +install(DIRECTORY include/cppcoro + DESTINATION include + COMPONENT Devel) +install(FILES cmake/FindCoroutines.cmake + DESTINATION ${config_package_location} + COMPONENT Devel) +install(EXPORT cppcoroTargets + FILE cppcoroTargets.cmake + NAMESPACE cppcoro:: + DESTINATION ${config_package_location}) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/cppcoro/cppcoroConfig.cmake + DESTINATION ${config_package_location} + COMPONENT Devel) diff --git a/README.md b/README.md index 0dce93d5..443c2db6 100644 --- a/README.md +++ b/README.md @@ -2859,7 +2859,7 @@ Given a type, `S`, that implements the `DelayedScheduler` and an instance, `s` o The cppcoro library supports building under Windows with Visual Studio 2017 and Linux with Clang 5.0+. -This library makes use of the [Cake build system](https://github.com/lewissbaker/cake) (no, not the [C# one](http://cakebuild.net/)). +This library makes use of either the [Cake build system](https://github.com/lewissbaker/cake) (no, not the [C# one](http://cakebuild.net/)) or CMake. The cake build system is checked out automatically as a git submodule so you don't need to download or install it separately. @@ -2867,10 +2867,12 @@ The cake build system is checked out automatically as a git submodule so you don This library currently requires Visual Studio 2017 or later and the Windows 10 SDK. -Support for Clang ([#3](https://github.com/lewissbaker/cppcoro/issues/3)) and Linux ([#15](https://github.com/lewissbaker/cppcoro/issues/15)) is planned. +Support for Linux ([#15](https://github.com/lewissbaker/cppcoro/issues/15)) is planned. ### Prerequisites +The CMakeLists requires version 3.13 or later. + The Cake build-system is implemented in Python and requires Python 2.7 to be installed. Ensure Python 2.7 interpreter is in your PATH and available as 'python'. @@ -2903,6 +2905,68 @@ c:\Code\cppcoro> git submodule update --init --recursive ### Building from the command-line +#### With CMake + +Cppcoro follows the usual CMake workflow with no custom options added. Notable [standard CMake options](https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html): + +| Flag | Description | Default Value | +|----------------------|------------------------------|------------------------| +| BUILD_TESTING | Build the unit tests | ON | +| BUILD_SHARED_LIBS | Build as a shared library | OFF | +| CMAKE_BUILD_TYPE | Build as `Debug`/`Release` | | +| CMAKE_INSTALL_PREFIX | Where to install the library | `/usr/local` (on Unix) | + +CMake also respects the [conventional environment variables](https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html): + +| Environment Variable | Description | +|----------------------|-------------------------------| +| CXX | Path to the C++ compiler | +| CXXFLAGS | C++ compiler flags to prepend | +| LDFLAGS | Linker flags to prepend | + +Example: + +```bash +cd +mkdir build +cd build +export CXX=clang++ +export CXXFLAGS="-stdlib=libc++ -march=native" +export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -Wl,--gdb-index" +cmake .. [-GNinja] -DCMAKE_INSTALL_PREFIX=$HOME/.local -DBUILD_SHARED_LIBS=ON +ninja # or make -jN +ninja test # Run the tests +ninja install +``` + +The CMake build scripts will also install a `cppcoroConfig.cmake` file for consumers to use. +It will check at the consumer site that coroutines are indeed supported by the system and enable the appropriate compiler flag for Clang or MSVC, respectively. +Assuming cppcoro has been installed to `$HOME/.local` like in the example above it can be consumed like this: + +```cmake +find_package(cppcoro REQUIRED) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE cppcoro::cppcoro) +``` + +```bash +$ cmake . -Dcppcoro_ROOT=$HOME/.local +# ... +-- Performing Test _CXX_COROUTINES_SUPPORTS_MS_FLAG +-- Performing Test _CXX_COROUTINES_SUPPORTS_MS_FLAG - Failed +-- Performing Test _CXX_COROUTINES_SUPPORTS_CORO_FLAG +-- Performing Test _CXX_COROUTINES_SUPPORTS_CORO_FLAG - Success +-- Looking for C++ include coroutine +-- Looking for C++ include coroutine - not found +-- Looking for C++ include experimental/coroutine +-- Looking for C++ include experimental/coroutine - found +-- Configuring done +-- Generating done +# ... +``` + +#### With Cake + To build from the command-line just run 'cake.bat' in the workspace root. eg. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt new file mode 100644 index 00000000..e69de29b diff --git a/cmake/FindCoroutines.cmake b/cmake/FindCoroutines.cmake new file mode 100644 index 00000000..e2592759 --- /dev/null +++ b/cmake/FindCoroutines.cmake @@ -0,0 +1,273 @@ +# Copyright (c) 2019-present, Facebook, Inc. +# +# This source code is licensed under the Apache License found in the +# LICENSE.txt file in the root directory of this source tree. + +#[=======================================================================[.rst: + +FindCoroutines +############## + +This module supports the C++ standard support for coroutines. Use +the :imp-target:`std::coroutines` imported target to + +Options +******* + +The ``COMPONENTS`` argument to this module supports the following values: + +.. find-component:: Experimental + :name: coro.Experimental + + Allows the module to find the "experimental" Coroutines TS + version of the coroutines library. This is the library that should be + used with the ``std::experimental`` namespace. + +.. find-component:: Final + :name: coro.Final + + Finds the final C++20 standard version of coroutines. + +If no components are provided, behaves as if the +:find-component:`coro.Final` component was specified. + +If both :find-component:`coro.Experimental` and :find-component:`coro.Final` are +provided, first looks for ``Final``, and falls back to ``Experimental`` in case +of failure. If ``Final`` is found, :imp-target:`std::coroutines` and all +:ref:`variables ` will refer to the ``Final`` version. + + +Imported Targets +**************** + +.. imp-target:: std::coroutines + + The ``std::coroutines`` imported target is defined when any requested + version of the C++ coroutines library has been found, whether it is + *Experimental* or *Final*. + + If no version of the coroutines library is available, this target will not + be defined. + + .. note:: + This target has ``cxx_std_17`` as an ``INTERFACE`` + :ref:`compile language standard feature `. Linking + to this target will automatically enable C++17 if no later standard + version is already required on the linking target. + + +.. _coro.variables: + +Variables +********* + +.. variable:: CXX_COROUTINES_HAVE_COROUTINES + + Set to ``TRUE`` when coroutines are supported in both the language and the + library. + +.. variable:: CXX_COROUTINES_HEADER + + Set to either ``coroutine`` or ``experimental/coroutine`` depending on + whether :find-component:`coro.Final` or :find-component:`coro.Experimental` was + found. + +.. variable:: CXX_COROUTINES_NAMESPACE + + Set to either ``std`` or ``std::experimental`` + depending on whether :find-component:`coro.Final` or + :find-component:`coro.Experimental` was found. + + +Examples +******** + +Using `find_package(Coroutines)` with no component arguments: + +.. code-block:: cmake + + find_package(Coroutines REQUIRED) + + add_executable(my-program main.cpp) + target_link_libraries(my-program PRIVATE std::coroutines) + + +#]=======================================================================] + + +if(TARGET std::coroutines) + # This module has already been processed. Don't do it again. + return() +endif() + +include(CheckCXXCompilerFlag) +include(CMakePushCheckState) +include(CheckIncludeFileCXX) +include(CheckCXXSourceCompiles) + +cmake_push_check_state() + +set(CMAKE_REQUIRED_QUIET ${Coroutines_FIND_QUIETLY}) + +check_cxx_compiler_flag(/await _CXX_COROUTINES_SUPPORTS_MS_FLAG) +check_cxx_compiler_flag(/await:heapelide _CXX_COROUTINES_SUPPORTS_MS_HEAPELIDE_FLAG) +check_cxx_compiler_flag(-fcoroutines-ts _CXX_COROUTINES_SUPPORTS_TS_FLAG) +check_cxx_compiler_flag(-fcoroutines _CXX_COROUTINES_SUPPORTS_CORO_FLAG) + +if(_CXX_COROUTINES_SUPPORTS_MS_FLAG) + set(_CXX_COROUTINES_EXTRA_FLAGS "/await") + if(_CXX_COROUTINES_SUPPORTS_MS_HEAPELIDE_FLAG AND CMAKE_SIZEOF_VOID_P GREATER_EQUAL 8) + list(APPEND _CXX_COROUTINES_EXTRA_FLAGS "/await:heapelide") + endif() +elseif(_CXX_COROUTINES_SUPPORTS_TS_FLAG) + set(_CXX_COROUTINES_EXTRA_FLAGS "-fcoroutines-ts") +elseif(_CXX_COROUTINES_SUPPORTS_CORO_FLAG) + set(_CXX_COROUTINES_EXTRA_FLAGS "-fcoroutines") +endif() + +# Normalize and check the component list we were given +set(want_components ${Coroutines_FIND_COMPONENTS}) +if(Coroutines_FIND_COMPONENTS STREQUAL "") + set(want_components Final) +endif() + +# Warn on any unrecognized components +set(extra_components ${want_components}) +list(REMOVE_ITEM extra_components Final Experimental) +foreach(component IN LISTS extra_components) + message(WARNING "Extraneous find_package component for Coroutines: ${component}") +endforeach() + +# Detect which of Experimental and Final we should look for +set(find_experimental TRUE) +set(find_final TRUE) +if(NOT "Final" IN_LIST want_components) + set(find_final FALSE) +endif() +if(NOT "Experimental" IN_LIST want_components) + set(find_experimental FALSE) +endif() + +if(find_final) + check_include_file_cxx("coroutine" _CXX_COROUTINES_HAVE_HEADER) + if(NOT _CXX_COROUTINES_HAVE_HEADER) + cmake_push_check_state() + set(CMAKE_REQUIRED_FLAGS "${_CXX_COROUTINES_EXTRA_FLAGS}") + check_include_file_cxx("coroutine" _CXX_COROUTINES_HAVE_HEADER_WITH_FLAG) + set(_CXX_COROUTINES_HAVE_HEADER ${_CXX_COROUTINES_HAVE_HEADER_WITH_FLAG}) + cmake_pop_check_state() + endif() + mark_as_advanced(_CXX_COROUTINES_HAVE_HEADER) + if(_CXX_COROUTINES_HAVE_HEADER) + # We found the non-experimental header. Don't bother looking for the + # experimental one. + set(find_experimental FALSE) + endif() +else() + set(_CXX_COROUTINES_HAVE_HEADER FALSE) +endif() + +if(find_experimental) + check_include_file_cxx("experimental/coroutine" _CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER) + if(NOT _CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER) + cmake_push_check_state() + set(CMAKE_REQUIRED_FLAGS "${_CXX_COROUTINES_EXTRA_FLAGS}") + check_include_file_cxx("experimental/coroutine" _CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER_WITH_FLAG) + set(_CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER ${_CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER_WITH_FLAG}) + cmake_pop_check_state() + endif() + mark_as_advanced(_CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER) +else() + set(_CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER FALSE) +endif() + +if(_CXX_COROUTINES_HAVE_HEADER) + set(_have_coro TRUE) + set(_coro_header coroutine) + set(_coro_namespace std) +elseif(_CXX_COROUTINES_HAVE_EXPERIMENTAL_HEADER) + set(_have_coro TRUE) + set(_coro_header experimental/coroutine) + set(_coro_namespace std::experimental) +else() + set(_have_coro FALSE) +endif() + +set(CXX_COROUTINES_HAVE_COROUTINES ${_have_coro} CACHE BOOL "TRUE if we have the C++ coroutines feature") +set(CXX_COROUTINES_HEADER ${_coro_header} CACHE STRING "The header that should be included to obtain the coroutines APIs") +set(CXX_COROUTINES_NAMESPACE ${_coro_namespace} CACHE STRING "The C++ namespace that contains the coroutines APIs") + +set(_found FALSE) + +if(CXX_COROUTINES_HAVE_COROUTINES) + # We have some coroutines library available. Do link checks + string(CONFIGURE [[ + #include + #include <@CXX_COROUTINES_HEADER@> + + struct present { + struct promise_type { + int result; + present get_return_object() { return present{*this}; } + @CXX_COROUTINES_NAMESPACE@::suspend_never initial_suspend() { return {}; } + @CXX_COROUTINES_NAMESPACE@::suspend_always final_suspend() noexcept { return {}; } + void return_value(int i) { result = i; } + void unhandled_exception() {} + }; + friend struct promise_type; + present(present&& that) : coro_(std::exchange(that.coro_, {})) {} + ~present() { if(coro_) coro_.destroy(); } + bool await_ready() const { return true; } + void await_suspend(@CXX_COROUTINES_NAMESPACE@::coroutine_handle<>) const {} + int await_resume() const { return coro_.promise().result; } + private: + present(promise_type& promise) + : coro_(@CXX_COROUTINES_NAMESPACE@::coroutine_handle::from_promise(promise)) {} + @CXX_COROUTINES_NAMESPACE@::coroutine_handle coro_; + }; + + present f(int n) { + if (n < 2) + co_return 1; + else + co_return n * co_await f(n - 1); + } + + int main() { + return f(5).await_resume() != 120; + } + ]] code @ONLY) + + # Try to compile a simple coroutines program without any compiler flags + check_cxx_source_compiles("${code}" CXX_COROUTINES_NO_AWAIT_NEEDED) + + set(can_link ${CXX_COROUTINES_NO_AWAIT_NEEDED}) + + if(NOT CXX_COROUTINES_NO_AWAIT_NEEDED) + # Add the -fcoroutines-ts (or /await) flag + set(CMAKE_REQUIRED_FLAGS "${_CXX_COROUTINES_EXTRA_FLAGS}") + check_cxx_source_compiles("${code}" CXX_COROUTINES_AWAIT_NEEDED) + set(can_link ${CXX_COROUTINES_AWAIT_NEEDED}) + endif() + + if(can_link) + add_library(std::coroutines INTERFACE IMPORTED) + set(_found TRUE) + + if(CXX_COROUTINES_NO_AWAIT_NEEDED) + # Nothing to add... + elseif(CXX_COROUTINES_AWAIT_NEEDED) + target_compile_options(std::coroutines INTERFACE ${_CXX_COROUTINES_EXTRA_FLAGS}) + endif() + else() + set(CXX_COROUTINES_HAVE_COROUTINES FALSE) + endif() +endif() + +cmake_pop_check_state() + +set(Coroutines_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::coroutines" FORCE) + +if(Coroutines_FIND_REQUIRED AND NOT Coroutines_FOUND) + message(FATAL_ERROR "Cannot compile simple program using std::coroutines. Is C++17 or later activated?") +endif() diff --git a/cmake/cppcoroConfig.cmake b/cmake/cppcoroConfig.cmake new file mode 100644 index 00000000..0b9f9c0b --- /dev/null +++ b/cmake/cppcoroConfig.cmake @@ -0,0 +1,6 @@ +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) + +include(CMakeFindDependencyMacro) +find_dependency(Coroutines QUIET REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/cppcoroTargets.cmake") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 00000000..6f2a4d5d --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,178 @@ +set(includes + awaitable_traits.hpp + is_awaitable.hpp + async_auto_reset_event.hpp + async_manual_reset_event.hpp + async_generator.hpp + async_mutex.hpp + async_latch.hpp + async_scope.hpp + broken_promise.hpp + cancellation_registration.hpp + cancellation_source.hpp + cancellation_token.hpp + task.hpp + sequence_barrier.hpp + sequence_traits.hpp + single_producer_sequencer.hpp + multi_producer_sequencer.hpp + shared_task.hpp + shared_task.hpp + single_consumer_event.hpp + single_consumer_async_auto_reset_event.hpp + sync_wait.hpp + task.hpp + io_service.hpp + config.hpp + on_scope_exit.hpp + file_share_mode.hpp + file_open_mode.hpp + file_buffering_mode.hpp + file.hpp + fmap.hpp + when_all.hpp + when_all_ready.hpp + resume_on.hpp + schedule_on.hpp + generator.hpp + readable_file.hpp + recursive_generator.hpp + writable_file.hpp + read_only_file.hpp + write_only_file.hpp + read_write_file.hpp + file_read_operation.hpp + file_write_operation.hpp + static_thread_pool.hpp +) +list(TRANSFORM includes PREPEND "${PROJECT_SOURCE_DIR}/include/cppcoro/") + +set(netIncludes + ip_address.hpp + ip_endpoint.hpp + ipv4_address.hpp + ipv4_endpoint.hpp + ipv6_address.hpp + ipv6_endpoint.hpp + socket.hpp +) +list(TRANSFORM netIncludes PREPEND "${PROJECT_SOURCE_DIR}/include/cppcoro/net/") + +set(detailIncludes + void_value.hpp + when_all_ready_awaitable.hpp + when_all_counter.hpp + when_all_task.hpp + get_awaiter.hpp + is_awaiter.hpp + any.hpp + sync_wait_task.hpp + unwrap_reference.hpp + lightweight_manual_reset_event.hpp +) +list(TRANSFORM detailIncludes PREPEND "${PROJECT_SOURCE_DIR}/include/cppcoro/detail/") + +set(privateHeaders + cancellation_state.hpp + socket_helpers.hpp + auto_reset_event.hpp + spin_wait.hpp + spin_mutex.hpp +) + +set(sources + async_auto_reset_event.cpp + async_manual_reset_event.cpp + async_mutex.cpp + cancellation_state.cpp + cancellation_token.cpp + cancellation_source.cpp + cancellation_registration.cpp + lightweight_manual_reset_event.cpp + ip_address.cpp + ip_endpoint.cpp + ipv4_address.cpp + ipv4_endpoint.cpp + ipv6_address.cpp + ipv6_endpoint.cpp + static_thread_pool.cpp + auto_reset_event.cpp + spin_wait.cpp + spin_mutex.cpp +) + +if(WIN32) + set(win32DetailIncludes + win32.hpp + win32_overlapped_operation.hpp + ) + list(TRANSFORM win32DetailIncludes PREPEND "${PROJECT_SOURCE_DIR}/include/cppcoro/detail/") + list(APPEND detailIncludes ${win32DetailIncludes}) + + set(win32NetIncludes + socket.hpp + socket_accept_operation.hpp + socket_connect_operation.hpp + socket_disconnect_operation.hpp + socket_recv_operation.hpp + socket_recv_from_operation.hpp + socket_send_operation.hpp + socket_send_to_operation.hpp + ) + list(TRANSFORM win32NetIncludes PREPEND "${PROJECT_SOURCE_DIR}/include/cppcoro/net/") + list(APPEND netIncludes ${win32NetIncludes}) + + set(win32Sources + win32.cpp + io_service.cpp + file.cpp + readable_file.cpp + writable_file.cpp + read_only_file.cpp + write_only_file.cpp + read_write_file.cpp + file_read_operation.cpp + file_write_operation.cpp + socket_helpers.cpp + socket.cpp + socket_accept_operation.cpp + socket_connect_operation.cpp + socket_disconnect_operation.cpp + socket_send_operation.cpp + socket_send_to_operation.cpp + socket_recv_operation.cpp + socket_recv_from_operation.cpp + ) + list(APPEND sources ${win32Sources}) + + list(APPEND libraries Ws2_32 Mswsock Synchronization) + list(APPEND compile_options /EHsc) + + if("${MSVC_VERSION}" VERSION_GREATER_EQUAL 1900) + # TODO remove this when experimental/non-experimental include are fixed + list(APPEND compile_definition _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING=1) + endif() +endif() + +add_library(cppcoro + ${includes} + ${netIncludes} + ${detailIncludes} + ${privateHeaders} + ${sources} +) + +target_include_directories(cppcoro PUBLIC + $ + $) + +target_compile_definitions(cppcoro PUBLIC ${compile_definition}) +target_compile_options(cppcoro PUBLIC ${compile_options}) + +find_package(Coroutines COMPONENTS Experimental Final REQUIRED) +target_link_libraries(cppcoro PUBLIC std::coroutines ${libraries}) + +install(TARGETS cppcoro EXPORT cppcoroTargets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..f5afecd1 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,62 @@ +add_library(doctest::doctest INTERFACE IMPORTED) +target_include_directories(doctest::doctest INTERFACE doctest) + +include(${CMAKE_CURRENT_LIST_DIR}/doctest/doctest.cmake) + +find_package(Threads REQUIRED) + +add_library(tests-main STATIC + main.cpp + counted.cpp +) +target_link_libraries(tests-main PUBLIC cppcoro doctest::doctest Threads::Threads) + +set(tests + generator_tests.cpp + recursive_generator_tests.cpp + async_generator_tests.cpp + async_auto_reset_event_tests.cpp + async_manual_reset_event_tests.cpp + async_mutex_tests.cpp + async_latch_tests.cpp + cancellation_token_tests.cpp + task_tests.cpp + sequence_barrier_tests.cpp + shared_task_tests.cpp + sync_wait_tests.cpp + single_consumer_async_auto_reset_event_tests.cpp + single_producer_sequencer_tests.cpp + multi_producer_sequencer_tests.cpp + when_all_tests.cpp + when_all_ready_tests.cpp + ip_address_tests.cpp + ip_endpoint_tests.cpp + ipv4_address_tests.cpp + ipv4_endpoint_tests.cpp + ipv6_address_tests.cpp + ipv6_endpoint_tests.cpp + static_thread_pool_tests.cpp +) + +if(WIN32) + list(APPEND tests + scheduling_operator_tests.cpp + io_service_tests.cpp + file_tests.cpp + socket_tests.cpp + ) +else() + # let more time for some tests + set(async_auto_reset_event_tests_TIMEOUT 60) +endif() + +foreach(test ${tests}) + get_filename_component(test_name ${test} NAME_WE) + add_executable(${test_name} ${test}) + target_link_libraries(${test_name} PRIVATE tests-main) + string(REPLACE "_" " " test_prefix ${test_name}) + if (NOT DEFINED ${test_name}_TIMEOUT) + set(${test_name}_TIMEOUT 30) + endif() + doctest_discover_tests(${test_name} TEST_PREFIX ${test_prefix}- PROPERTIES TIMEOUT ${${test_name}_TIMEOUT}) +endforeach() diff --git a/test/doctest/doctest.cmake b/test/doctest/doctest.cmake new file mode 100644 index 00000000..13768012 --- /dev/null +++ b/test/doctest/doctest.cmake @@ -0,0 +1,175 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +doctest +----- + +This module defines a function to help use the doctest test framework. + +The :command:`doctest_discover_tests` discovers tests by asking the compiled test +executable to enumerate its tests. This does not require CMake to be re-run +when tests change. However, it may not work in a cross-compiling environment, +and setting test properties is less convenient. + +This command is intended to replace use of :command:`add_test` to register +tests, and will create a separate CTest test for each doctest test case. Note +that this is in some cases less efficient, as common set-up and tear-down logic +cannot be shared by multiple test cases executing in the same instance. +However, it provides more fine-grained pass/fail information to CTest, which is +usually considered as more beneficial. By default, the CTest test name is the +same as the doctest name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. + +.. command:: doctest_discover_tests + + Automatically add tests with CTest by querying the compiled test executable + for available tests:: + + doctest_discover_tests(target + [TEST_SPEC arg1...] + [EXTRA_ARGS arg1...] + [WORKING_DIRECTORY dir] + [TEST_PREFIX prefix] + [TEST_SUFFIX suffix] + [PROPERTIES name1 value1...] + [TEST_LIST var] + ) + + ``doctest_discover_tests`` sets up a post-build command on the test executable + that generates the list of tests by parsing the output from running the test + with the ``--list-test-cases`` argument. This ensures that the full + list of tests is obtained. Since test discovery occurs at build time, it is + not necessary to re-run CMake when the list of tests changes. + However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set + in order to function in a cross-compiling environment. + + Additionally, setting properties on tests is somewhat less convenient, since + the tests are not available at CMake time. Additional test properties may be + assigned to the set of tests as a whole using the ``PROPERTIES`` option. If + more fine-grained test control is needed, custom content may be provided + through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` + directory property. The set of discovered tests is made accessible to such a + script via the ``_TESTS`` variable. + + The options are: + + ``target`` + Specifies the doctest executable, which must be a known CMake executable + target. CMake will substitute the location of the built executable when + running the test. + + ``TEST_SPEC arg1...`` + Specifies test cases, wildcarded test cases, tags and tag expressions to + pass to the doctest executable with the ``--list-test-cases`` argument. + + ``EXTRA_ARGS arg1...`` + Any extra arguments to pass on the command line to each test case. + + ``WORKING_DIRECTORY dir`` + Specifies the directory in which to run the discovered test cases. If this + option is not provided, the current binary directory is used. + + ``TEST_PREFIX prefix`` + Specifies a ``prefix`` to be prepended to the name of each discovered test + case. This can be useful when the same test executable is being used in + multiple calls to ``doctest_discover_tests()`` but with different + ``TEST_SPEC`` or ``EXTRA_ARGS``. + + ``TEST_SUFFIX suffix`` + Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of + every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may + be specified. + + ``PROPERTIES name1 value1...`` + Specifies additional properties to be set on all tests discovered by this + invocation of ``doctest_discover_tests``. + + ``TEST_LIST var`` + Make the list of tests available in the variable ``var``, rather than the + default ``_TESTS``. This can be useful when the same test + executable is being used in multiple calls to ``doctest_discover_tests()``. + Note that this variable is only available in CTest. + +#]=======================================================================] + +#------------------------------------------------------------------------------ +function(doctest_discover_tests TARGET) + cmake_parse_arguments( + "" + "" + "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST" + "TEST_SPEC;EXTRA_ARGS;PROPERTIES" + ${ARGN} + ) + + if(NOT _WORKING_DIRECTORY) + set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endif() + if(NOT _TEST_LIST) + set(_TEST_LIST ${TARGET}_TESTS) + endif() + + ## Generate a unique name based on the extra arguments + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") + string(SUBSTRING ${args_hash} 0 7 args_hash) + + # Define rule to generate test list for aforementioned test executable + set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") + set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") + get_property(crosscompiling_emulator + TARGET ${TARGET} + PROPERTY CROSSCOMPILING_EMULATOR + ) + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TEST_TARGET=${TARGET}" + -D "TEST_EXECUTABLE=$" + -D "TEST_EXECUTOR=${crosscompiling_emulator}" + -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" + -D "TEST_SPEC=${_TEST_SPEC}" + -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" + -D "TEST_PROPERTIES=${_PROPERTIES}" + -D "TEST_PREFIX=${_TEST_PREFIX}" + -D "TEST_SUFFIX=${_TEST_SUFFIX}" + -D "TEST_LIST=${_TEST_LIST}" + -D "CTEST_FILE=${ctest_tests_file}" + -P "${_DOCTEST_DISCOVER_TESTS_SCRIPT}" + VERBATIM + ) + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + if(NOT CMAKE_VERSION VERSION_LESS 3.10) + # Add discovered tests to directory TEST_INCLUDE_FILES + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + else() + # Add discovered tests as directory TEST_INCLUDE_FILE if possible + get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) + if(NOT ${test_include_file_set}) + set_property(DIRECTORY + PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" + ) + else() + message(FATAL_ERROR + "Cannot set more than one TEST_INCLUDE_FILE" + ) + endif() + endif() + +endfunction() + +############################################################################### + +set(_DOCTEST_DISCOVER_TESTS_SCRIPT + ${CMAKE_CURRENT_LIST_DIR}/doctestAddTests.cmake +) diff --git a/test/doctest/doctestAddTests.cmake b/test/doctest/doctestAddTests.cmake new file mode 100644 index 00000000..98ee4a2c --- /dev/null +++ b/test/doctest/doctestAddTests.cmake @@ -0,0 +1,81 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +set(prefix "${TEST_PREFIX}") +set(suffix "${TEST_SUFFIX}") +set(spec ${TEST_SPEC}) +set(extra_args ${TEST_EXTRA_ARGS}) +set(properties ${TEST_PROPERTIES}) +set(script) +set(suite) +set(tests) + +function(add_command NAME) + set(_args "") + foreach(_arg ${ARGN}) + if(_arg MATCHES "[^-./:a-zA-Z0-9_]") + set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument + else() + set(_args "${_args} ${_arg}") + endif() + endforeach() + set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) +endfunction() + +# Run test executable to get list of available tests +if(NOT EXISTS "${TEST_EXECUTABLE}") + message(FATAL_ERROR + "Specified test executable '${TEST_EXECUTABLE}' does not exist" + ) +endif() + +if("${spec}" MATCHES .) + set(spec "--test-case=${spec}") +endif() + +execute_process( + COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-cases + OUTPUT_VARIABLE output + RESULT_VARIABLE result +) +if(NOT ${result} EQUAL 0) + message(FATAL_ERROR + "Error running test executable '${TEST_EXECUTABLE}':\n" + " Result: ${result}\n" + " Output: ${output}\n" + ) +endif() + +string(REPLACE "\n" ";" output "${output}") + +# Parse output +foreach(line ${output}) + if("${line}" STREQUAL "===============================================================================" OR "${line}" MATCHES [==[^\[doctest\] ]==]) + continue() + endif() + set(test ${line}) + # use escape commas to handle properly test cases with commas inside the name + string(REPLACE "," "\\," test_name ${test}) + # ...and add to script + add_command(add_test + "${prefix}${test}${suffix}" + ${TEST_EXECUTOR} + "${TEST_EXECUTABLE}" + "--test-case=${test_name}" + ${extra_args} + ) + add_command(set_tests_properties + "${prefix}${test}${suffix}" + PROPERTIES + WORKING_DIRECTORY "${TEST_WORKING_DIR}" + ${properties} + ) + list(APPEND tests "${prefix}${test}${suffix}") +endforeach() + +# Create a list of all discovered tests, which users may use to e.g. set +# properties on the tests +add_command(set ${TEST_LIST} ${tests}) + +# Write CTest script +file(WRITE "${CTEST_FILE}" "${script}")