Add Catch framework for testing embedding support and C++-side features
At this point, there is only a single test for interpreter basics.
Apart from embedding itself, having a C++ test framework will also
benefit the C++-side features by allowing them to be tested directly.
diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_embed/CMakeLists.txt
new file mode 100644
index 0000000..a651031
--- /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 $<TARGET_FILE:test_embed>
+ 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 0000000..f79fe17
--- /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 <catch.hpp>
diff --git a/tests/test_embed/test_interpreter.cpp b/tests/test_embed/test_interpreter.cpp
new file mode 100644
index 0000000..97af7eb
--- /dev/null
+++ b/tests/test_embed/test_interpreter.cpp
@@ -0,0 +1,64 @@
+#include <pybind11/pybind11.h>
+#include <pybind11/eval.h>
+
+#include <catch.hpp>
+
+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_<Widget, PyWidget>(m, "Widget")
+ .def(py::init<std::string>())
+ .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<py::eval_statements>(
+ "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<std::string>() == "Hello, World!");
+
+ const auto &cpp_widget = py_widget.cast<const Widget &>();
+ 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 0000000..26a0479
--- /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