diff --git a/.appveyor.yml b/.appveyor.yml index 026e761b..8d30cf31 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -39,6 +39,7 @@ install: if ($env:CONDA -eq "27") { $env:CONDA = "" } if ($env:PLATFORM -eq "x64") { $env:CONDA = "$env:CONDA-x64" } $env:PATH = "C:\Miniconda$env:CONDA\;C:\Miniconda$env:CONDA\Scripts\;$env:PATH" + $env:PYTHONHOME = "C:\Miniconda$env:CONDA" conda install -y -q pytest numpy scipy } - ps: | @@ -46,8 +47,13 @@ install: 7z x 3.3.3.zip -y > $null $env:CMAKE_INCLUDE_PATH = "eigen-eigen-67e894c6cd8f" build_script: -- cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" -DPYBIND11_CPP_STANDARD=/std:c++%CPP% -DPYBIND11_WERROR=ON -DCMAKE_SUPPRESS_REGENERATION=1 +- cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" + -DPYBIND11_CPP_STANDARD=/std:c++%CPP% + -DPYBIND11_WERROR=ON + -DDOWNLOAD_CATCH=ON + -DCMAKE_SUPPRESS_REGENERATION=1 - set MSBuildLogger="C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - cmake --build . --config Release --target pytest -- /v:m /logger:%MSBuildLogger% +- cmake --build . --config Release --target cpptest -- /v:m /logger:%MSBuildLogger% - cmake --build . --config Release --target test_cmake_build -- /v:m /logger:%MSBuildLogger% -on_failure: if exist "tests\test_cmake_build" type tests\test_cmake_build\*.log +on_failure: if exist "tests\test_cmake_build" type tests\test_cmake_build\*.log* diff --git a/.travis.yml b/.travis.yml index 73da10ca..aeb3c866 100644 --- a/.travis.yml +++ b/.travis.yml @@ -192,8 +192,10 @@ script: -DPYBIND11_PYTHON_VERSION=$PYTHON -DPYBIND11_CPP_STANDARD=$CPP -DPYBIND11_WERROR=${WERROR:-ON} + -DDOWNLOAD_CATCH=ON - $SCRIPT_RUN_PREFIX make pytest -j 2 +- $SCRIPT_RUN_PREFIX make cpptest -j 2 - $SCRIPT_RUN_PREFIX make test_cmake_build -after_failure: cat tests/test_cmake_build/*.log +after_failure: cat tests/test_cmake_build/*.log* after_script: - if [ -n "$DOCKER" ]; then docker stop "$containerid"; docker rm "$containerid"; fi diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 580b7915..d40c25ac 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -195,6 +195,9 @@ add_custom_command(TARGET pybind11_tests POST_BUILD COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/tools/libsize.py $ ${CMAKE_CURRENT_BINARY_DIR}/sosize-$.txt) +# Test embedding the interpreter. Provides the `cpptest` target. +add_subdirectory(test_embed) + # Test CMake build using functions and targets from subdirectory or installed location add_custom_target(test_cmake_build) if(NOT CMAKE_VERSION VERSION_LESS 3.1) diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_embed/CMakeLists.txt new file mode 100644 index 00000000..a651031f --- /dev/null +++ b/tests/test_embed/CMakeLists.txt @@ -0,0 +1,31 @@ +if(${PYTHON_MODULE_EXTENSION} MATCHES "pypy") + add_custom_target(cpptest) # Dummy target on PyPy. Embedding is not supported. + set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}") + return() +endif() + +find_package(Catch 1.9.3) +if(NOT CATCH_FOUND) + message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers" + " manually or use `cmake -DDOWNLOAD_CATCH=1` to fetch them automatically.") + return() +endif() + +add_executable(test_embed + catch.cpp + test_interpreter.cpp +) +target_include_directories(test_embed PRIVATE ${CATCH_INCLUDE_DIR}) +pybind11_enable_warnings(test_embed) + +if(NOT CMAKE_VERSION VERSION_LESS 3.0) + target_link_libraries(test_embed PRIVATE pybind11::embed) +else() + target_include_directories(test_embed PRIVATE ${PYBIND11_INCLUDE_DIR} ${PYTHON_INCLUDE_DIRS}) + target_compile_options(test_embed PRIVATE ${PYBIND11_CPP_STANDARD}) + target_link_libraries(test_embed PRIVATE ${PYTHON_LIBRARIES}) +endif() + +add_custom_target(cpptest COMMAND $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +add_dependencies(check cpptest) diff --git a/tests/test_embed/catch.cpp b/tests/test_embed/catch.cpp new file mode 100644 index 00000000..f79fe17a --- /dev/null +++ b/tests/test_embed/catch.cpp @@ -0,0 +1,5 @@ +// Catch provides the `int main()` function here. This is a standalone +// translation unit to avoid recompiling it for every test change. + +#define CATCH_CONFIG_MAIN +#include diff --git a/tests/test_embed/test_interpreter.cpp b/tests/test_embed/test_interpreter.cpp new file mode 100644 index 00000000..97af7eb6 --- /dev/null +++ b/tests/test_embed/test_interpreter.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include + +namespace py = pybind11; +using namespace py::literals; + +class Widget { +public: + Widget(std::string message) : message(message) { } + virtual ~Widget() = default; + + std::string the_message() const { return message; } + virtual int the_answer() const = 0; + +private: + std::string message; +}; + +class PyWidget final : public Widget { + using Widget::Widget; + + int the_answer() const override { PYBIND11_OVERLOAD_PURE(int, Widget, the_answer); } +}; + +PyObject *make_embedded_module() { + py::module m("widget_module"); + + py::class_(m, "Widget") + .def(py::init()) + .def_property_readonly("the_message", &Widget::the_message); + + return m.ptr(); +} + +py::object import_file(const std::string &module, const std::string &path, py::object globals) { + auto locals = py::dict("module_name"_a=module, "path"_a=path); + py::eval( + "import imp\n" + "with open(path) as file:\n" + " new_module = imp.load_module(module_name, file, path, ('py', 'U', imp.PY_SOURCE))", + globals, locals + ); + return locals["new_module"]; +} + +TEST_CASE("Pass classes and data between modules defined in C++ and Python") { + PyImport_AppendInittab("widget_module", &make_embedded_module); + Py_Initialize(); + { + auto globals = py::module::import("__main__").attr("__dict__"); + auto module = import_file("widget", "test_interpreter.py", globals); + REQUIRE(py::hasattr(module, "DerivedWidget")); + + auto py_widget = module.attr("DerivedWidget")("Hello, World!"); + auto message = py_widget.attr("the_message"); + REQUIRE(message.cast() == "Hello, World!"); + + const auto &cpp_widget = py_widget.cast(); + REQUIRE(cpp_widget.the_answer() == 42); + } + Py_Finalize(); +} diff --git a/tests/test_embed/test_interpreter.py b/tests/test_embed/test_interpreter.py new file mode 100644 index 00000000..26a04792 --- /dev/null +++ b/tests/test_embed/test_interpreter.py @@ -0,0 +1,9 @@ +from widget_module import Widget + + +class DerivedWidget(Widget): + def __init__(self, message): + super(DerivedWidget, self).__init__(message) + + def the_answer(self): + return 42 diff --git a/tools/FindCatch.cmake b/tools/FindCatch.cmake new file mode 100644 index 00000000..9d490c5a --- /dev/null +++ b/tools/FindCatch.cmake @@ -0,0 +1,57 @@ +# - Find the Catch test framework or download it (single header) +# +# This is a quick module for internal use. It assumes that Catch is +# REQUIRED and that a minimum version is provided (not EXACT). If +# a suitable version isn't found locally, the single header file +# will be downloaded and placed in the build dir: PROJECT_BINARY_DIR. +# +# This code sets the following variables: +# CATCH_INCLUDE_DIR - path to catch.hpp +# CATCH_VERSION - version number + +if(NOT Catch_FIND_VERSION) + message(FATAL_ERROR "A version number must be specified.") +elseif(Catch_FIND_REQUIRED) + message(FATAL_ERROR "This module assumes Catch is not required.") +elseif(Catch_FIND_VERSION_EXACT) + message(FATAL_ERROR "Exact version numbers are not supported, only minimum.") +endif() + +# Extract the version number from catch.hpp +function(_get_catch_version) + file(STRINGS "${CATCH_INCLUDE_DIR}/catch.hpp" version_line REGEX "Catch v.*" LIMIT_COUNT 1) + if(version_line MATCHES "Catch v([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(CATCH_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE) + endif() +endfunction() + +# Download the single-header version of Catch +function(_download_catch version destination_dir) + message(STATUS "Downloading catch v${version}...") + set(url https://github.com/philsquared/Catch/releases/download/v${version}/catch.hpp) + file(DOWNLOAD ${url} "${destination_dir}/catch.hpp" STATUS status) + list(GET status 0 error) + if(error) + message(FATAL_ERROR "Could not download ${url}") + endif() + set(CATCH_INCLUDE_DIR "${destination_dir}" CACHE INTERNAL "") +endfunction() + +# Look for catch locally +find_path(CATCH_INCLUDE_DIR NAMES catch.hpp PATH_SUFFIXES catch) +if(CATCH_INCLUDE_DIR) + _get_catch_version() +endif() + +# Download the header if it wasn't found or if it's outdated +if(NOT CATCH_VERSION OR CATCH_VERSION VERSION_LESS ${Catch_FIND_VERSION}) + if(DOWNLOAD_CATCH) + _download_catch(${Catch_FIND_VERSION} "${PROJECT_BINARY_DIR}/catch/") + _get_catch_version() + else() + set(CATCH_FOUND FALSE) + return() + endif() +endif() + +set(CATCH_FOUND TRUE)