From 5ab2c1693ec46df6d98579ebf53c100b3dadb94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Fr=C3=B6jdh?= Date: Thu, 3 Apr 2025 12:00:57 +0200 Subject: [PATCH] Fixed broken import in typecaster.h (#1181) - Fixed the broken import _slsdet --> slsdet._slsdet caused by a previous upgrade - Added tests that exercises the conversion from python to C++ and from C++ to python - Python unit tests now run in CI (!) --- .github/workflows/cmake.yaml | 19 ++++++--- python/slsdet/__init__.py | 3 ++ python/src/duration.cpp | 22 +++++++++++ python/src/typecaster.h | 9 ++++- python/tests/test_DurationWrapper.py | 58 ++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 python/tests/test_DurationWrapper.py diff --git a/.github/workflows/cmake.yaml b/.github/workflows/cmake.yaml index 5e99d1f35..00d6a4b86 100644 --- a/.github/workflows/cmake.yaml +++ b/.github/workflows/cmake.yaml @@ -14,7 +14,13 @@ jobs: runs-on: ubuntu-latest name: Configure and build using cmake steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + cache: 'pip' + - run: pip install pytest numpy + - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: libhdf5-dev qtbase5-dev qt5-qmake libqt5svg5-dev libpng-dev libtiff-dev @@ -27,12 +33,15 @@ jobs: - name: Build # Build your program with the given configuration - run: cmake --build ${{github.workspace}}/build -j2 --config ${{env.BUILD_TYPE}} + run: cmake --build ${{github.workspace}}/build -j4 --config ${{env.BUILD_TYPE}} - - name: Test + - name: C++ unit tests working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} -j1 + - name: Python unit tests + working-directory: ${{github.workspace}}/build/bin + run: | + python -m pytest ${{github.workspace}}/python/tests + diff --git a/python/slsdet/__init__.py b/python/slsdet/__init__.py index 4b216ee62..a3097eda4 100755 --- a/python/slsdet/__init__.py +++ b/python/slsdet/__init__.py @@ -41,3 +41,6 @@ def read_version(): __version__ = read_version() + + + diff --git a/python/src/duration.cpp b/python/src/duration.cpp index db6da3d06..ed8927c90 100644 --- a/python/src/duration.cpp +++ b/python/src/duration.cpp @@ -1,3 +1,4 @@ +#include #include "py_headers.h" #include "DurationWrapper.h" @@ -19,4 +20,25 @@ void init_duration(py::module &m) { << " count: " << self.count() << ")"; return ss.str(); }); + + m.def( + "test_return_DurationWrapper", + []() { + DurationWrapper t(1.3); + return t; + }, + R"( + Test function to return a DurationWrapper object. Ensures that the automatic conversion in typecaster.h works. + )"); + + m.def( + "test_duration_to_ns", + [](const std::chrono::nanoseconds t) { + //Duration wrapper is used to be able to convert from time in python to chrono::nanoseconds + //return count to have something to test + return t.count(); + }, + R"( + Test function convert DurationWrapper or number to chrono::ns. Ensures that the automatic conversion in typecaster.h works. + )"); // default value to test the default constructor } diff --git a/python/src/typecaster.h b/python/src/typecaster.h index 5eba7b436..6412afa72 100644 --- a/python/src/typecaster.h +++ b/python/src/typecaster.h @@ -54,11 +54,16 @@ template <> struct type_caster { value = duration_cast(duration(PyFloat_AsDouble(src.ptr()))); return true; } + // If invoked with an int we assume it is nanoseconds and convert, same as in chrono.h + if (PyLong_Check(src.ptr())) { + value = duration_cast(duration(PyLong_AsLongLong(src.ptr()))); + return true; + } // Lastly if we were actually called with a DurationWrapper object we get // the number of nanoseconds and create a std::chrono::nanoseconds from it - py::object py_cls = py::module::import("_slsdet").attr("DurationWrapper"); + py::object py_cls = py::module::import("slsdet._slsdet").attr("DurationWrapper"); if (py::isinstance(src, py_cls)){ sls::DurationWrapper *cls = src.cast(); value = nanoseconds(cls->count()); @@ -77,7 +82,7 @@ template <> struct type_caster { * set the count from chrono::nanoseconds and return */ static handle cast(std::chrono::nanoseconds src, return_value_policy /* policy */, handle /* parent */) { - py::object py_cls = py::module::import("_slsdet").attr("DurationWrapper"); + py::object py_cls = py::module::import("slsdet._slsdet").attr("DurationWrapper"); py::object* obj = new py::object; *obj = py_cls(); sls::DurationWrapper *dur = obj->cast(); diff --git a/python/tests/test_DurationWrapper.py b/python/tests/test_DurationWrapper.py new file mode 100644 index 000000000..f6902da26 --- /dev/null +++ b/python/tests/test_DurationWrapper.py @@ -0,0 +1,58 @@ +import pytest + +from slsdet import DurationWrapper + +#import the compiled extension to use test functions for the automatic conversion +from slsdet import _slsdet + + +def test_default_construction_of_DurationWrapper(): + """Test default construction of DurationWrapper""" + t = DurationWrapper() + assert t.count() == 0 + assert t.total_seconds() == 0 + +def test_construction_of_DurationWrapper(): + """Test construction of DurationWrapper with total_seconds""" + t = DurationWrapper(5) + assert t.count() == 5e9 + assert t.total_seconds() == 5 + +def test_set_count_on_DurationWrapper(): + """Test set_count on DurationWrapper""" + t = DurationWrapper() + t.set_count(10) + assert t.count() == 10 + assert t.total_seconds() == 10e-9 + t.set_count(0) + assert t.count() == 0 + assert t.total_seconds() == 0 + + +def test_return_a_DurationWrapper_from_cpp(): + """Test returning a DurationWrapper from C++""" + t = _slsdet.test_return_DurationWrapper() + assert t.count() == 1.3e9 + assert t.total_seconds() == 1.3 + +def test_call_a_cpp_function_with_a_duration_wrapper(): + """C++ functions can accept a DurationWrapper""" + t = DurationWrapper(5) + assert _slsdet.test_duration_to_ns(t) == 5e9 + +def test_call_a_cpp_function_converting_number_to_DurationWrapper(): + """int and float can be converted to std::chrono::nanoseconds""" + assert _slsdet.test_duration_to_ns(0) == 0 + assert _slsdet.test_duration_to_ns(3) == 3e9 + assert _slsdet.test_duration_to_ns(1.3) == 1.3e9 + assert _slsdet.test_duration_to_ns(10e-9) == 10 + +def test_call_a_cpp_function_with_datetime_timedelta(): + """datetime.timedelta can be converted to std::chrono::nanoseconds""" + import datetime + t = datetime.timedelta(seconds=5) + assert _slsdet.test_duration_to_ns(t) == 5e9 + t = datetime.timedelta(seconds=0) + assert _slsdet.test_duration_to_ns(t) == 0 + t = datetime.timedelta(seconds=1.3) + assert _slsdet.test_duration_to_ns(t) == 1.3e9 \ No newline at end of file