diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 997a19d2..bc4f6683 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest,macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml new file mode 100644 index 00000000..5078cf5e --- /dev/null +++ b/.github/workflows/build_windows.yml @@ -0,0 +1,37 @@ +name: Build and test on Windows + +on: + workflow_dispatch: + push: + pull_request: + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python dependencies + run: python -m pip install ninja numpy matplotlib pytest pytest-check boost-histogram + + # Puts cl.exe on the path so that CMake can use the Ninja generator + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAARE_TESTS=ON -DAARE_PYTHON_BINDINGS=ON -DAARE_WARNINGS_AS_ERRORS=ON + + - name: Build + run: cmake --build build + + - name: C++ unit tests + run: ctest --test-dir build --output-on-failure -j4 + + - name: Python unit tests + env: + PYTHONPATH: ${{ github.workspace }}\build + run: python -m pytest python/tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 40f268b0..a9972be3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,6 +208,11 @@ if(AARE_FETCH_FMT) CACHE BOOL "") FetchContent_MakeAvailable(fmt) set_property(TARGET fmt PROPERTY POSITION_INDEPENDENT_CODE ON) + if(MSVC) + # fmt 12.1.0 calls std::isalpha only to check that ADL finds it and drops + # the result, which MSVC's standard library marks [[nodiscard]] + target_compile_options(fmt PRIVATE /wd4834) + endif() else() find_package(fmt 8 REQUIRED) endif() @@ -254,18 +259,30 @@ target_compile_features(aare_compiler_flags INTERFACE cxx_std_17) # MSVC specific # # ############################################################################## if(MSVC) - add_compile_definitions(AARE_MSVC) - if(CMAKE_BUILD_TYPE STREQUAL "Release") - message(STATUS "Release build") - target_compile_options(aare_compiler_flags INTERFACE /O2) - else() - message(STATUS "Debug build") - target_compile_options(aare_compiler_flags - INTERFACE /Od /Zi /MDd /D_ITERATOR_DEBUG_LEVEL=2) - target_link_options(aare_compiler_flags INTERFACE /DEBUG:FULL) - endif() - target_compile_options(aare_compiler_flags INTERFACE /w # disable warnings + # CMake sets the per-configuration optimisation, debug-info and + # runtime-library flags itself, so only warnings and conformance here. + target_compile_options( + aare_compiler_flags + INTERFACE /W4 # -Wall -Wextra + /permissive- # standard conformance, like CMAKE_CXX_EXTENSIONS OFF + /utf-8 # sources are UTF-8 + /we4715 # not all control paths return a value, like + # -Werror=return-type + /wd4244 # narrowing conversions: -Wconversion is not enabled + /wd4267 # on gcc/clang either + /wd4068 # unknown pragma: the GCC diagnostic pragmas + /wd4324 # structure padded due to alignas: intended ) + target_compile_definitions(aare_compiler_flags + INTERFACE _CRT_SECURE_NO_WARNINGS) + + if(AARE_WARNINGS_AS_ERRORS) + target_compile_options(aare_compiler_flags INTERFACE /WX) + endif() + + if(AARE_ASAN) + message(WARNING "AARE_ASAN is not supported with MSVC. Ignoring.") + endif() else() # ############################################################################ @@ -318,22 +335,22 @@ else() endif() endif() + if(AARE_ASAN) + message(STATUS "AddressSanitizer enabled") + target_compile_options( + aare_compiler_flags INTERFACE -fsanitize=address,undefined,pointer-compare + -fno-omit-frame-pointer) + target_link_libraries( + aare_compiler_flags INTERFACE -fsanitize=address,undefined,pointer-compare + -fno-omit-frame-pointer) + endif() + endif() # GCC/Clang specific if(AARE_PYTHON_BINDINGS) add_subdirectory(python) endif() -if(AARE_ASAN) - message(STATUS "AddressSanitizer enabled") - target_compile_options( - aare_compiler_flags INTERFACE -fsanitize=address,undefined,pointer-compare - -fno-omit-frame-pointer) - target_link_libraries( - aare_compiler_flags INTERFACE -fsanitize=address,undefined,pointer-compare - -fno-omit-frame-pointer) -endif() - if(AARE_TESTS) enable_testing() add_subdirectory(tests) @@ -485,6 +502,7 @@ if(AARE_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/hist/PixelHistogram.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/hist/PedestalTrackingPixelHistogram.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/JungfrauDataFile.test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/logger.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MultiThreadedFileReader.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/NumpyFile.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/NumpyHelpers.test.cpp diff --git a/RELEASE.md b/RELEASE.md index 34e82535..39b46b89 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,15 @@ # Release notes -### New Features: +## Next: +### New Features: + +- Added the Python ``xy`` type with ``row`` and ``col`` fields, enabling access + to ``RawFile.geometry``, ``RawMasterFile.detector_layout``, and + ``RawMasterFile.udp_interfaces_per_module``. It unpacks as ``row, col``. +- Added Windows wheels (``win_amd64``, Python 3.12 to 3.14) to the wheel + build, so releases publish them to PyPI alongside the Linux and macOS + wheels. - Added ``read_columns`` to JungfrauDataFile to only read selected columns ## 2026.9.30 diff --git a/benchmarks/fit_benchmark.cpp b/benchmarks/fit_benchmark.cpp index 8e522f80..2a307002 100644 --- a/benchmarks/fit_benchmark.cpp +++ b/benchmarks/fit_benchmark.cpp @@ -10,6 +10,8 @@ #include #include +using aare::ssize_t; + struct TestCase { std::string name; double true_A; diff --git a/benchmarks/ndarray_benchmark.cpp b/benchmarks/ndarray_benchmark.cpp index b3a3f610..46e7138e 100644 --- a/benchmarks/ndarray_benchmark.cpp +++ b/benchmarks/ndarray_benchmark.cpp @@ -3,6 +3,7 @@ #include using aare::NDArray; +using aare::ssize_t; constexpr ssize_t size = 1024; class TwoArrays : public benchmark::Fixture { diff --git a/include/aare/Cluster.hpp b/include/aare/Cluster.hpp index b08a2878..df67884c 100755 --- a/include/aare/Cluster.hpp +++ b/include/aare/Cluster.hpp @@ -83,7 +83,7 @@ struct Cluster { data[cluster_center_index - ClusterSizeX] + data[cluster_center_index - 1 - ClusterSizeX]; // subcluster top right from center - if (ClusterSizeX > 2) { + if constexpr (ClusterSizeX > 2) { sum_2x2_subcluster[1] = data[cluster_center_index] + data[cluster_center_index + 1] + @@ -91,7 +91,7 @@ struct Cluster { data[cluster_center_index - ClusterSizeX + 1]; } // subcluster bottom left from center - if (ClusterSizeY > 2) { + if constexpr (ClusterSizeY > 2) { sum_2x2_subcluster[2] = data[cluster_center_index] + data[cluster_center_index - 1] + @@ -99,7 +99,7 @@ struct Cluster { data[cluster_center_index + ClusterSizeX - 1]; } // subcluster bottom right from center - if (ClusterSizeX > 2 && ClusterSizeY > 2) { + if constexpr (ClusterSizeX > 2 && ClusterSizeY > 2) { sum_2x2_subcluster[3] = data[cluster_center_index] + data[cluster_center_index + 1] + diff --git a/include/aare/FilePtr.hpp b/include/aare/FilePtr.hpp index 2e84b87c..16ea198d 100644 --- a/include/aare/FilePtr.hpp +++ b/include/aare/FilePtr.hpp @@ -1,10 +1,29 @@ // SPDX-License-Identifier: MPL-2.0 #pragma once +#include "aare/defs.hpp" +#include #include #include namespace aare { +// MSVC's long is 32 bits, so fseek/ftell stop working past 2 GB there +inline int fseek64(FILE *fp, int64_t offset, int whence) { +#ifdef _WIN32 + return _fseeki64(fp, offset, whence); +#else + return fseek(fp, static_cast(offset), whence); +#endif +} + +inline int64_t ftell64(FILE *fp) { +#ifdef _WIN32 + return _ftelli64(fp); +#else + return ftell(fp); +#endif +} + /** * \brief RAII wrapper for FILE pointer */ @@ -22,7 +41,7 @@ class FilePtr { FILE *get(); ssize_t tell(); void seek(ssize_t offset, int whence = SEEK_SET) { - if (fseek(fp_, offset, whence) != 0) + if (fseek64(fp_, offset, whence) != 0) throw std::runtime_error("Error seeking in file"); } std::string error_msg(); diff --git a/include/aare/NumpyFile.hpp b/include/aare/NumpyFile.hpp index caa68531..0d7a523d 100644 --- a/include/aare/NumpyFile.hpp +++ b/include/aare/NumpyFile.hpp @@ -2,6 +2,7 @@ #pragma once #include "aare/Dtype.hpp" #include "aare/FileInterface.hpp" +#include "aare/FilePtr.hpp" #include "aare/NumpyHelpers.hpp" #include "aare/defs.hpp" @@ -77,7 +78,7 @@ class NumpyFile : public FileInterface { */ template NDArray load() { NDArray arr(make_shape(m_header.shape)); - if (fseek(fp, static_cast(header_size), SEEK_SET)) { + if (fseek64(fp, header_size, SEEK_SET)) { throw std::runtime_error(LOCATION + "Error seeking to the start of the data"); } diff --git a/include/aare/defs.hpp b/include/aare/defs.hpp index 39c3fee9..9c94e5f6 100644 --- a/include/aare/defs.hpp +++ b/include/aare/defs.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,10 @@ #include #include +#if !defined(_MSC_VER) +#include // ::ssize_t +#endif + /** * @brief LOCATION macro to get the current location in the code */ @@ -54,6 +59,18 @@ namespace aare { +/** + * Signed size and index type used throughout the library (shapes, strides, + * frame indices, ROI coordinates). On POSIX this is the platform's ssize_t, + * so nothing changes for existing code. MSVC has no ssize_t, so there it is + * std::ptrdiff_t, which is the same type as Py_ssize_t and NumPy's npy_intp. + */ +#if defined(_MSC_VER) +using ssize_t = std::ptrdiff_t; +#else +using ssize_t = ::ssize_t; +#endif + inline constexpr size_t bits_per_byte = 8; void assert_failed(const std::string &msg); diff --git a/include/aare/logger.hpp b/include/aare/logger.hpp index 278d4d7d..b5504d73 100644 --- a/include/aare/logger.hpp +++ b/include/aare/logger.hpp @@ -2,9 +2,12 @@ #pragma once /*Utility to log to console*/ +#include +#include +#include #include #include -#include +#include namespace aare { @@ -55,12 +58,53 @@ enum TLogLevel { #define __AT__ \ std::string(__FILE__) + std::string("::") + std::string(__func__) + \ std::string("(): ") -#define __SHORT_FORM_OF_FILE__ \ - (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#define __SHORT_FORM_OF_FILE__ aare::short_file_name(__FILE__) #define __SHORT_AT__ \ std::string(__SHORT_FORM_OF_FILE__) + std::string("::") + \ std::string(__func__) + std::string("(): ") +/** + * @brief Strip the directory part of a path, accepting both '/' and '\\' as + * separators so that __FILE__ is shortened the same way on every platform. + */ +inline const char *short_file_name(const char *path) noexcept { + const char *base = path; + for (const char *p = path; *p != '\0'; ++p) { + if (*p == '/' || *p == '\\') + base = p + 1; + } + return base; +} + +/** + * @brief Local wall-clock time as HH:MM:SS.mmm (strftime "%X" plus + * milliseconds), built from std::chrono so it works on every platform. + */ +inline std::string log_timestamp() { + using namespace std::chrono; + const auto now = system_clock::now(); + const time_t t = system_clock::to_time_t(now); + const auto ms = duration_cast(now.time_since_epoch()) % 1000; + + tm local{}; +#if defined(_MSC_VER) + localtime_s(&local, &t); +#else + localtime_r(&t, &local); +#endif + + constexpr size_t buffer_len = 12; + char buffer[buffer_len]; + if (strftime(buffer, buffer_len, "%X", &local) == 0) + buffer[0] = '\0'; + + constexpr size_t result_len = 100; + char result[result_len]; + snprintf(result, result_len, "%s.%03d", buffer, + static_cast(ms.count())); + return result; +} + class Logger { std::ostringstream os; TLogLevel m_level = AARE_LOG_LEVEL; @@ -111,28 +155,12 @@ class Logger { return os; } - static std::string Timestamp() { - constexpr size_t buffer_len = 12; - char buffer[buffer_len]; - time_t t; - ::time(&t); - tm r; - strftime(buffer, buffer_len, "%X", localtime_r(&t, &r)); - buffer[buffer_len - 1] = '\0'; - struct timeval tv; - gettimeofday(&tv, nullptr); - constexpr size_t result_len = 100; - char result[result_len]; - snprintf(result, result_len, "%s.%03ld", buffer, - static_cast(tv.tv_usec) / 1000); - result[result_len - 1] = '\0'; - return result; - } + static std::string Timestamp() { return log_timestamp(); } }; // TODO! Do we need to keep the runtime option? #define LOG(level) \ - if (level > AARE_LOG_LEVEL) \ + if constexpr (level > AARE_LOG_LEVEL) \ ; \ else if (level > aare::Logger::ReportingLevel()) \ ; \ diff --git a/include/aare/to_string.hpp b/include/aare/to_string.hpp index dbb0c09f..547ebc09 100644 --- a/include/aare/to_string.hpp +++ b/include/aare/to_string.hpp @@ -19,7 +19,7 @@ T string_to(const std::string &t, const std::string &unit) { double tval{0}; try { tval = std::stod(t); - } catch (const std::invalid_argument &e) { + } catch (const std::invalid_argument &) { throw std::runtime_error("Could not convert string to time"); } diff --git a/pyproject.toml b/pyproject.toml index 6054356c..16636a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ build = "cp{312,313,314}-manylinux_x86_64" build = "cp{312,313,314}-macosx_*" archs = ["arm64"] +[tool.cibuildwheel.windows] +build = "cp{312,313,314}-win_amd64" + [tool.scikit-build] build.verbose = true diff --git a/python/aare/__init__.py b/python/aare/__init__.py index 892bde17..c5665269 100644 --- a/python/aare/__init__.py +++ b/python/aare/__init__.py @@ -25,6 +25,7 @@ from ._aare import Pedestal_d, Pedestal_f, ClusterFinder_Cluster3x3i, VarCluster from ._aare import DetectorType, FrameDiscardPolicy, ReadoutMode from ._aare import hitmap from ._aare import ROI +from ._aare import xy from ._aare import corner from ._aare import UDPPortPosition diff --git a/python/src/bind_Cluster.hpp b/python/src/bind_Cluster.hpp index afa9bfd1..49618bf2 100644 --- a/python/src/bind_Cluster.hpp +++ b/python/src/bind_Cluster.hpp @@ -13,9 +13,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_Cluster(py::module &m, const std::string &typestr) { @@ -140,5 +137,3 @@ void reduce_to_2x2(py::module &m) { row-major order. )doc"); } - -#pragma GCC diagnostic pop diff --git a/python/src/bind_ClusterCollector.hpp b/python/src/bind_ClusterCollector.hpp index 01dd03c2..fab91522 100644 --- a/python/src/bind_ClusterCollector.hpp +++ b/python/src/bind_ClusterCollector.hpp @@ -20,9 +20,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_ClusterCollector(py::module &m, const std::string &typestr) { @@ -42,5 +39,3 @@ void define_ClusterCollector(py::module &m, const std::string &typestr) { }, py::return_value_policy::take_ownership); } - -#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/src/bind_ClusterFile.hpp b/python/src/bind_ClusterFile.hpp index c8544e9d..75c5458d 100644 --- a/python/src/bind_ClusterFile.hpp +++ b/python/src/bind_ClusterFile.hpp @@ -13,11 +13,6 @@ #include #include -// Disable warnings for unused parameters, as we ignore some -// in the __exit__ method -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - namespace py = pybind11; using namespace ::aare; @@ -171,14 +166,12 @@ void define_ClusterFile(py::module &m, const std::string &typestr) { .def("__enter__", [](ClusterFile &self) { return &self; }) .def("__exit__", [](ClusterFile &self, - const std::optional &exc_type, - const std::optional &exc_value, - const std::optional &traceback) { - self.close(); - }) + [[maybe_unused]] const std::optional &exc_type, + [[maybe_unused]] const std::optional + &exc_value, + [[maybe_unused]] const std::optional + &traceback) { self.close(); }) .def("__iter__", [](ClusterFile &self) { return &self; }) .def("__next__", [](File &self) { return ChunkIterator(self.chunks()).next(); }); } - -#pragma GCC diagnostic pop diff --git a/python/src/bind_ClusterFileSink.hpp b/python/src/bind_ClusterFileSink.hpp index b366886a..50600fcb 100644 --- a/python/src/bind_ClusterFileSink.hpp +++ b/python/src/bind_ClusterFileSink.hpp @@ -17,9 +17,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_ClusterFileSink(py::module &m, const std::string &typestr) { @@ -34,5 +31,3 @@ void define_ClusterFileSink(py::module &m, const std::string &typestr) { const std::filesystem::path &>()) .def("stop", &ClusterFileSink::stop); } - -#pragma GCC diagnostic pop diff --git a/python/src/bind_ClusterFinder.hpp b/python/src/bind_ClusterFinder.hpp index 034b1b88..8872db29 100644 --- a/python/src/bind_ClusterFinder.hpp +++ b/python/src/bind_ClusterFinder.hpp @@ -20,9 +20,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_ClusterFinder(py::module &m, const std::string &typestr) { @@ -92,5 +89,3 @@ void define_ClusterFinder(py::module &m, const std::string &typestr) { }, py::arg("frame").noconvert(), py::arg("frame_number") = 0); } - -#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/src/bind_ClusterFinderMT.hpp b/python/src/bind_ClusterFinderMT.hpp index fdd4b580..7e662841 100644 --- a/python/src/bind_ClusterFinderMT.hpp +++ b/python/src/bind_ClusterFinderMT.hpp @@ -20,9 +20,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_ClusterFinderMT(py::module &m, const std::string &typestr) { @@ -55,7 +52,8 @@ void define_ClusterFinderMT(py::module &m, const std::string &typestr) { py::arg("frame").noconvert(), py::arg("frame_number") = 0) .def_property_readonly( "cluster_size", - [](ClusterFinderMT &self) { + []([[maybe_unused]] ClusterFinderMT + &self) { return py::make_tuple(ClusterSizeX, ClusterSizeY); }) .def("clear_pedestal", @@ -93,5 +91,3 @@ void define_ClusterFinderMT(py::module &m, const std::string &typestr) { py::arg("nSigma"), R"(sets the number of sigma for all cluster finders.)"); } - -#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/src/bind_ClusterVector.hpp b/python/src/bind_ClusterVector.hpp index 9b031e5d..13ad5a6d 100644 --- a/python/src/bind_ClusterVector.hpp +++ b/python/src/bind_ClusterVector.hpp @@ -18,9 +18,6 @@ namespace py = pybind11; using namespace aare; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - template void define_ClusterVector(py::module &m, const std::string &typestr) { @@ -123,8 +120,8 @@ void define_ClusterVector(py::module &m, const std::string &typestr) { "padding.") .def_property_readonly( "fmt", - [typestr](ClusterVector &self) { - return fmt_format; + [typestr]([[maybe_unused]] ClusterVector &self) { + return fmt_format(); }, "PEP 3118 format string for one stored cluster.") @@ -145,11 +142,11 @@ void define_ClusterVector(py::module &m, const std::string &typestr) { .def_buffer( [typestr](ClusterVector &self) -> py::buffer_info { return py::buffer_info( - self.data(), /* Pointer to buffer */ - self.item_size(), /* Size of one scalar */ - fmt_format, /* Format descriptor */ - 1, /* Number of dimensions */ - {self.size()}, /* Buffer dimensions */ + self.data(), /* Pointer to buffer */ + self.item_size(), /* Size of one scalar */ + fmt_format(), /* Format descriptor */ + 1, /* Number of dimensions */ + {self.size()}, /* Buffer dimensions */ {self.item_size()} /* Strides (in bytes) for each index */ ); }); @@ -228,5 +225,3 @@ void define_3x3_reduction(py::module &m) { )doc", py::arg("clustervector")); } - -#pragma GCC diagnostic pop diff --git a/python/src/bind_Defs.hpp b/python/src/bind_Defs.hpp index 9850f407..e75fc8cb 100644 --- a/python/src/bind_Defs.hpp +++ b/python/src/bind_Defs.hpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MPL-2.0 +#include #include #include @@ -77,6 +78,21 @@ void define_defs_bindings(py::module &m) { self.ymin == other.ymin && self.ymax == other.ymax; }); + py::class_(m, "xy") + .def(py::init(), py::arg("row"), py::arg("col")) + .def_readwrite("row", &xy::row) + .def_readwrite("col", &xy::col) + .def("__repr__", + [](const xy &self) { + return fmt::format("xy(row={}, col={})", self.row, self.col); + }) + .def("__iter__", + [](const xy &self) { + return py::iter(py::make_tuple(self.row, self.col)); + }) + .def(py::self == py::self) + .def(py::self != py::self); + py::enum_(m, "DetectorType") .value("Jungfrau", DetectorType::Jungfrau) .value("Eiger", DetectorType::Eiger) diff --git a/python/src/bind_MultiThreadedFileReader.hpp b/python/src/bind_MultiThreadedFileReader.hpp index f63f79c8..0f39cb6c 100644 --- a/python/src/bind_MultiThreadedFileReader.hpp +++ b/python/src/bind_MultiThreadedFileReader.hpp @@ -16,6 +16,7 @@ #include namespace py = pybind11; +using aare::ssize_t; inline py::dtype multi_threaded_reader_numpy_dtype(const aare::Dtype &dtype) { using aare::Dtype; diff --git a/python/src/bind_RawFile.hpp b/python/src/bind_RawFile.hpp index 2c4e20c8..11d1d31a 100644 --- a/python/src/bind_RawFile.hpp +++ b/python/src/bind_RawFile.hpp @@ -318,7 +318,14 @@ void define_raw_file_io_bindings(py::module &m) { Cols for the given ROI. )") .def_property_readonly("bitdepth", &RawFile::bitdepth) - .def_property_readonly("geometry", &RawFile::geometry) + .def_property_readonly("geometry", &RawFile::geometry, R"( + Number of modules along each axis of the detector. + + Returns + ------- + xy + Module rows and columns. + )") .def_property_readonly("detector_type", &RawFile::detector_type) .def_property_readonly("master", &RawFile::master) .def_property_readonly("n_modules", &RawFile::n_modules) diff --git a/python/src/bind_calibration.hpp b/python/src/bind_calibration.hpp index 6efa87bc..cdb16b86 100644 --- a/python/src/bind_calibration.hpp +++ b/python/src/bind_calibration.hpp @@ -9,6 +9,7 @@ #include namespace py = pybind11; +using aare::ssize_t; template py::array_t diff --git a/python/src/file.hpp b/python/src/file.hpp index a493c910..030ba181 100644 --- a/python/src/file.hpp +++ b/python/src/file.hpp @@ -21,11 +21,6 @@ namespace py = pybind11; using namespace ::aare; -// Disable warnings for unused parameters, as we ignore some -// in the __exit__ method -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - void define_file_io_bindings(py::module &m) { PYBIND11_NUMPY_DTYPE(DetectorHeader, frameNumber, expLength, packetNumber, @@ -122,9 +117,12 @@ void define_file_io_bindings(py::module &m) { }) .def("__enter__", [](File &self) { return &self; }) .def("__exit__", - [](File &self, const std::optional &exc_type, - const std::optional &exc_value, - const std::optional &traceback) { + []([[maybe_unused]] File &self, + [[maybe_unused]] const std::optional &exc_type, + [[maybe_unused]] const std::optional + &exc_value, + [[maybe_unused]] const std::optional + &traceback) { // self.close(); }) .def("__iter__", [](File &self) { return &self; }) @@ -146,7 +144,7 @@ void define_file_io_bindings(py::module &m) { self.read_into( reinterpret_cast(image.mutable_data())); return image; - } catch (std::runtime_error &e) { + } catch (std::runtime_error &) { throw py::stop_iteration(); } }); @@ -160,6 +158,4 @@ void define_file_io_bindings(py::module &m) { .def_property_readonly("start", &ScanParameters::start) .def_property_readonly("stop", &ScanParameters::stop) .def_property_readonly("step", &ScanParameters::step); - -#pragma GCC diagnostic pop } \ No newline at end of file diff --git a/python/src/fit.hpp b/python/src/fit.hpp index c72e875b..2cc6ee79 100644 --- a/python/src/fit.hpp +++ b/python/src/fit.hpp @@ -11,6 +11,7 @@ namespace py = pybind11; using namespace pybind11::literals; +using aare::ssize_t; template py::object diff --git a/python/src/jungfrau_data_file.hpp b/python/src/jungfrau_data_file.hpp index a215b7f6..50f6b07d 100644 --- a/python/src/jungfrau_data_file.hpp +++ b/python/src/jungfrau_data_file.hpp @@ -15,11 +15,6 @@ namespace py = pybind11; using namespace ::aare; -// Disable warnings for unused parameters, as we ignore some -// in the __exit__ method -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - auto read_dat_frame(JungfrauDataFile &self) { py::array_t header(1); py::array_t image({self.rows(), self.cols()}); @@ -119,20 +114,20 @@ void define_jungfrau_data_file_io_bindings(py::module &m) { )") .def("__enter__", [](JungfrauDataFile &self) { return &self; }) .def("__exit__", - [](JungfrauDataFile &self, - const std::optional &exc_type, - const std::optional &exc_value, - const std::optional &traceback) { + []([[maybe_unused]] JungfrauDataFile &self, + [[maybe_unused]] const std::optional &exc_type, + [[maybe_unused]] const std::optional + &exc_value, + [[maybe_unused]] const std::optional + &traceback) { // self.close(); }) .def("__iter__", [](JungfrauDataFile &self) { return &self; }) .def("__next__", [](JungfrauDataFile &self) { try { return read_dat_frame(self); - } catch (std::runtime_error &e) { + } catch (std::runtime_error &) { throw py::stop_iteration(); } }); } - -#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/src/np_helper.hpp b/python/src/np_helper.hpp index d21aceff..83880470 100644 --- a/python/src/np_helper.hpp +++ b/python/src/np_helper.hpp @@ -112,8 +112,13 @@ struct fmt_format_trait> { } }; -template -auto fmt_format = fmt_format_trait::value(); +// Function-local static rather than a variable template: MSVC does not run +// the dynamic initializer of a variable template instantiated from inside +// another template, which left the format string empty. +template const std::string &fmt_format() { + static const std::string value = fmt_format_trait::value(); + return value; +} /** * Helper function to allocate image data given item size and shape diff --git a/python/src/raw_master_file.hpp b/python/src/raw_master_file.hpp index 40e942a9..d30762a4 100644 --- a/python/src/raw_master_file.hpp +++ b/python/src/raw_master_file.hpp @@ -67,9 +67,23 @@ void define_raw_master_file_bindings(py::module &m) { .def_property_readonly("total_frames_expected", &RawMasterFile::total_frames_expected) .def_property_readonly("detector_layout", - &RawMasterFile::detector_layout) + &RawMasterFile::detector_layout, R"( + Number of modules along each axis of the detector + + Returns + ---------- + xy + Module rows and columns + )") .def_property_readonly("udp_interfaces_per_module", - &RawMasterFile::udp_interfaces_per_module) + &RawMasterFile::udp_interfaces_per_module, R"( + Number of UDP interfaces along each axis of a module + + Returns + ---------- + xy + UDP interface rows and columns + )") .def_property_readonly("analog_samples", &RawMasterFile::analog_samples, R"( Number of analog samples diff --git a/python/src/raw_sub_file.hpp b/python/src/raw_sub_file.hpp index 5359c054..da858058 100644 --- a/python/src/raw_sub_file.hpp +++ b/python/src/raw_sub_file.hpp @@ -61,11 +61,6 @@ auto read_n_frames_from_RawSubFile(RawSubFile &self, size_t n_frames) { return py::make_tuple(header, image); } -// Disable warnings for unused parameters, as we ignore some -// in the __exit__ method -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - void define_raw_sub_file_io_bindings(py::module &m) { py::class_(m, "RawSubFile") .def(py::init &exc_type, - const std::optional &exc_value, - const std::optional &traceback) {}) + []([[maybe_unused]] RawSubFile &self, + [[maybe_unused]] const std::optional &exc_type, + [[maybe_unused]] const std::optional + &exc_value, + [[maybe_unused]] const std::optional + &traceback) {}) .def("__iter__", [](RawSubFile &self) { return &self; }) .def("__next__", [](RawSubFile &self) { try { return read_frame_from_RawSubFile(self); - } catch (std::runtime_error &e) { + } catch (std::runtime_error &) { throw py::stop_iteration(); } }); } - -#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/tests/test_RawFile.py b/python/tests/test_RawFile.py index 1fb6c3f7..76988db9 100644 --- a/python/tests/test_RawFile.py +++ b/python/tests/test_RawFile.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MPL-2.0 import pytest import json -from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition +from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition, xy import numpy as np @@ -237,6 +237,20 @@ def test_raw_frame_count_across_rois(small_raw_file, short_module): reader.read_roi(roi_index) +def test_raw_file_geometry(small_raw_file): + metadata = json.loads(small_raw_file.read_text()) + metadata["Geometry"] = {"x": 1, "y": 2} + small_raw_file.write_text(json.dumps(metadata)) + for index in range(2): + data = (small_raw_file.parent / f"run_d0_f{index}_0.raw").read_bytes() + (small_raw_file.parent / f"run_d1_f{index}_0.raw").write_bytes(data) + + reader = RawFile(small_raw_file) + assert reader.geometry == xy(row=2, col=1) + assert reader.master.detector_layout == xy(row=2, col=1) + assert reader.master.udp_interfaces_per_module == xy(row=1, col=1) + + @pytest.mark.parametrize("disabled_port", [0, 1]) def test_raw_frame_count_with_disabled_udp_port(small_raw_file, disabled_port): metadata = json.loads(small_raw_file.read_text()) diff --git a/python/tests/test_xy.py b/python/tests/test_xy.py new file mode 100644 index 00000000..efc9c7d2 --- /dev/null +++ b/python/tests/test_xy.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MPL-2.0 +from aare import xy + + +def test_xy_fields(): + layout = xy(row=2, col=3) + assert layout.row == 2 + assert layout.col == 3 + + layout.row = 4 + assert layout.row == 4 + + +def test_xy_unpacks_as_row_col(): + row, col = xy(2, 3) + assert (row, col) == (2, 3) + + +def test_xy_comparison(): + assert xy(2, 3) == xy(2, 3) + assert xy(2, 3) != xy(3, 2) + assert xy(2, 3) != (2, 3) + + +def test_xy_repr(): + assert repr(xy(2, 3)) == "xy(row=2, col=3)" diff --git a/src/ClusterVector.test.cpp b/src/ClusterVector.test.cpp index 135719c1..088f0059 100644 --- a/src/ClusterVector.test.cpp +++ b/src/ClusterVector.test.cpp @@ -12,6 +12,7 @@ using aare::Cluster; using aare::ClusterVector; +using aare::ssize_t; using C1 = Cluster; TEST_CASE("A newly created ClusterVector is empty") { @@ -180,7 +181,8 @@ TEST_CASE("Summing 3x1 clusters of int64") { Cluster c3 = {11, 12, {13, 14, 15}}; cv.push_back(c3); - REQUIRE(cv.capacity() == 4); + // growth factor is implementation defined (2x libstdc++, 1.5x MSVC) + REQUIRE(cv.capacity() >= 3); REQUIRE(cv.size() == 3); /* @@ -236,7 +238,8 @@ TEST_CASE("Push back more than initial capacity") { Cluster c3 = {11, 12, {13, 14, 15, 16}}; cv.push_back(c3); REQUIRE(cv.size() == 3); - REQUIRE(cv.capacity() == 4); + // growth factor is implementation defined (2x libstdc++, 1.5x MSVC) + REQUIRE(cv.capacity() >= 3); Cluster *ptr = reinterpret_cast *>(cv.data()); diff --git a/src/Dtype.cpp b/src/Dtype.cpp index 08098874..9ad6d673 100644 --- a/src/Dtype.cpp +++ b/src/Dtype.cpp @@ -159,7 +159,7 @@ Dtype Dtype::from_bitdepth(uint8_t bitdepth) { std::string Dtype::to_string() const { char ec{}; - if (endian::native == endian::little) + if constexpr (endian::native == endian::little) ec = '<'; else ec = '>'; diff --git a/src/FilePtr.cpp b/src/FilePtr.cpp index 45e65d46..c6edfe62 100644 --- a/src/FilePtr.cpp +++ b/src/FilePtr.cpp @@ -9,10 +9,10 @@ namespace aare { FilePtr::FilePtr(const std::filesystem::path &fname, const std::string &mode = "rb") { - fp_ = fopen(fname.c_str(), mode.c_str()); + fp_ = fopen(fname.string().c_str(), mode.c_str()); if (!fp_) throw std::runtime_error( - fmt::format("Could not open: {}", fname.c_str())); + fmt::format("Could not open: {}", fname.string())); } FilePtr::FilePtr(FilePtr &&other) { std::swap(fp_, other.fp_); } @@ -27,7 +27,7 @@ FilePtr::operator bool() const noexcept { return fp_ != nullptr; } FILE *FilePtr::get() { return fp_; } ssize_t FilePtr::tell() { - auto pos = ftell(fp_); + auto pos = ftell64(fp_); if (pos == -1) throw std::runtime_error( fmt::format("Error getting file position: {}", error_msg())); diff --git a/src/JungfrauDataFile.cpp b/src/JungfrauDataFile.cpp index 3fe5c3d3..84032683 100644 --- a/src/JungfrauDataFile.cpp +++ b/src/JungfrauDataFile.cpp @@ -174,7 +174,7 @@ void JungfrauDataFile::find_frame_size(const std::filesystem::path &fname) { void JungfrauDataFile::parse_fname(const std::filesystem::path &fname) { m_path = fname.parent_path(); - m_base_name = fname.stem(); + m_base_name = fname.stem().string(); // find file index, then remove if from the base name if (auto pos = m_base_name.find_last_of('_'); pos != std::string::npos) { @@ -259,7 +259,7 @@ JungfrauDataHeader JungfrauDataFile::read_header() { throw std::runtime_error(LOCATION + "Could not read header from file" + m_fp.error_msg()); } - m_fp.seek(-header_size, SEEK_CUR); + m_fp.seek(-static_cast(header_size), SEEK_CUR); return header; } diff --git a/src/MultiThreadedFileReader.test.cpp b/src/MultiThreadedFileReader.test.cpp index 4896d788..c6fdcd90 100644 --- a/src/MultiThreadedFileReader.test.cpp +++ b/src/MultiThreadedFileReader.test.cpp @@ -22,6 +22,7 @@ using aare::File; using aare::FileConfig; using aare::Frame; using aare::NumpyFile; +using aare::ssize_t; using aare::experimental::MultiThreadedFileReader; namespace { diff --git a/src/NDArray.test.cpp b/src/NDArray.test.cpp index 872f22cd..56514102 100644 --- a/src/NDArray.test.cpp +++ b/src/NDArray.test.cpp @@ -9,6 +9,7 @@ using aare::NDArray; using aare::NDView; using aare::Shape; +using aare::ssize_t; TEST_CASE("Initial size is zero if no size is specified") { NDArray a; diff --git a/src/NDView.test.cpp b/src/NDView.test.cpp index 1a8c6a2b..59d231b3 100644 --- a/src/NDView.test.cpp +++ b/src/NDView.test.cpp @@ -11,6 +11,7 @@ using aare::NDView; using aare::num_elements; using aare::Shape; +using aare::ssize_t; static_assert(std::is_convertible_v, NDView>); static_assert(!std::is_convertible_v, NDView>); diff --git a/src/NumpyFile.cpp b/src/NumpyFile.cpp index 699fa08c..ad9cdf6f 100644 --- a/src/NumpyFile.cpp +++ b/src/NumpyFile.cpp @@ -28,8 +28,7 @@ NumpyFile::NumpyFile(const std::filesystem::path &fname, throw std::runtime_error( fmt::format("Could not open: {} for reading", fname.string())); } - initial_header_len = aare::NumpyHelpers::write_header( - std::filesystem::path(fname.c_str()), m_header); + initial_header_len = aare::NumpyHelpers::write_header(fname, m_header); } m_pixels_per_frame = std::accumulate(m_header.shape.begin() + 1, m_header.shape.end(), 1, @@ -46,7 +45,7 @@ void NumpyFile::write_impl(void *data, uint64_t size) { if (!(m_mode == "w" || m_mode == "a")) { throw std::invalid_argument("File not open for writing"); } - if (fseek(fp, 0, SEEK_END)) + if (fseek64(fp, 0, SEEK_END)) throw std::runtime_error("Could not seek to end of file"); size_t const rc = fwrite(data, size, 1, fp); if (rc != 1) { @@ -68,8 +67,8 @@ void NumpyFile::get_frame_into(size_t frame_number, std::byte *image_buf) { if (frame_number > m_header.shape[0]) { throw std::invalid_argument("Frame number out of range"); } - if (fseek(fp, header_size + frame_number * m_bytes_per_frame, - SEEK_SET)) // NOLINT + if (fseek64(fp, header_size + frame_number * m_bytes_per_frame, + SEEK_SET)) // NOLINT throw std::runtime_error("Could not seek to frame"); size_t const rc = fread(image_buf, m_bytes_per_frame, 1, fp); @@ -101,15 +100,15 @@ void NumpyFile::read_into(std::byte *image_buf, size_t n_frames) { NumpyFile::~NumpyFile() noexcept { if (m_mode == "w" || m_mode == "a") { // determine number of frames - if (fseek(fp, 0, SEEK_END)) { + if (fseek64(fp, 0, SEEK_END)) { std::cout << "Could not seek to end of file" << std::endl; } - size_t const file_size = ftell(fp); + size_t const file_size = ftell64(fp); size_t const data_size = file_size - initial_header_len; size_t const n_frames = data_size / m_bytes_per_frame; // update number of frames in header (first element of shape) m_header.shape[0] = n_frames; - if (fseek(fp, 0, SEEK_SET)) { + if (fseek64(fp, 0, SEEK_SET)) { std::cout << "Could not seek to beginning of file" << std::endl; } // create string stream to contain header diff --git a/src/ROI.cpp b/src/ROI.cpp index 7e91c49d..56882015 100644 --- a/src/ROI.cpp +++ b/src/ROI.cpp @@ -20,7 +20,7 @@ bool complete_ROI(const ROI &roi, const DetectorGeometry &geometry) { bool complete_ROI(const std::vector &rois, const DetectorGeometry &geometry) { - if (rois.empty() or rois.size() > 1) { + if (rois.empty() || rois.size() > 1) { return false; } else { return complete_ROI(rois[0], geometry); @@ -34,7 +34,7 @@ bool complete_ROI(const ROIGeometry &roi, const DetectorGeometry &geometry) { bool complete_ROI(const std::vector &rois, const DetectorGeometry &geometry) { - if (rois.empty() or rois.size() > 1) { + if (rois.empty() || rois.size() > 1) { return false; } else { return complete_ROI(rois[0], geometry); diff --git a/src/RawFile.cpp b/src/RawFile.cpp index 1e33bf83..e416a27e 100644 --- a/src/RawFile.cpp +++ b/src/RawFile.cpp @@ -302,7 +302,7 @@ void RawFile::open_subfiles(const size_t roi_index) { DetectorHeader RawFile::read_header(const std::filesystem::path &fname) { DetectorHeader h{}; - FILE *fp = fopen(fname.string().c_str(), "r"); + FILE *fp = fopen(fname.string().c_str(), "rb"); if (!fp) throw std::runtime_error(fmt::format( "Could not open file '{}' for frame index 0", fname.string())); diff --git a/src/RawMasterFile.cpp b/src/RawMasterFile.cpp index e0880ba5..6e86de47 100644 --- a/src/RawMasterFile.cpp +++ b/src/RawMasterFile.cpp @@ -11,8 +11,8 @@ namespace aare { RawFileNameComponents::RawFileNameComponents( const std::filesystem::path &fname) { m_base_path = fname.parent_path(); - m_base_name = fname.stem(); - m_ext = fname.extension(); + m_base_name = fname.stem().string(); + m_ext = fname.extension().string(); if (m_ext != ".json" && m_ext != ".raw") { throw std::runtime_error(LOCATION + @@ -23,7 +23,7 @@ RawFileNameComponents::RawFileNameComponents( try { auto pos = m_base_name.rfind('_'); m_file_index = std::stoi(m_base_name.substr(pos + 1)); - } catch (const std::invalid_argument &e) { + } catch (const std::invalid_argument &) { throw std::runtime_error(LOCATION + "Could not parse file index"); } @@ -370,7 +370,7 @@ void RawMasterFile::parse_json(std::istream &is) { try { m_quad = j.at("Quad"); - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // keep the optional empty } // try{ @@ -384,7 +384,7 @@ void RawMasterFile::parse_json(std::istream &is) { if (digital_flag) { m_digital_samples = j.at("Digital Samples"); } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // keep the optional empty } try { @@ -393,7 +393,7 @@ void RawMasterFile::parse_json(std::istream &is) { if (m_transceiver_flag) { m_transceiver_samples = j.at("Transceiver Samples"); } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // keep the optional empty } try { @@ -414,7 +414,7 @@ void RawMasterFile::parse_json(std::istream &is) { m_scan_parameters .increment_stop(); // adjust for endpoint being included } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // not a scan } try { @@ -424,7 +424,7 @@ void RawMasterFile::parse_json(std::istream &is) { m_udp_port_types.value().push_back( string_to(elem)); } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // leave the optional empty } try { @@ -433,13 +433,13 @@ void RawMasterFile::parse_json(std::istream &is) { for (auto &elem : json_list_obj) { m_disabled_udp_ports.push_back(static_cast(elem)); } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { m_disabled_udp_ports .clear(); // empty list if not present in master file } try { m_udp_interfaces_per_module = {j.at("Number of UDP Interfaces"), 1}; - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { if (m_type == DetectorType::Eiger && m_quad == 1) m_udp_interfaces_per_module = {2, 1}; else if (m_type == DetectorType::Eiger) { @@ -485,7 +485,7 @@ void RawMasterFile::parse_json(std::istream &is) { } } - } catch (const json::out_of_range &e) { + } catch (const json::out_of_range &) { // fill ROI with full detector size if not present in master file m_rois.push_back( {0, m_detector_layout.col * static_cast(m_pixels_x), 0, diff --git a/src/RawSubFile.cpp b/src/RawSubFile.cpp index 7a42bbba..cd1a6fba 100644 --- a/src/RawSubFile.cpp +++ b/src/RawSubFile.cpp @@ -163,7 +163,7 @@ void RawSubFile::parse_fname(const std::filesystem::path &fname) { // f1 is the file index - thi is the one we need // 0 is the measurement index, will not change m_path = fname.parent_path(); - m_base_name = fname.filename(); + m_base_name = fname.filename().string(); // Regex to extract numbers after 'd' and 'f' std::regex pattern(R"(^(.*_d)(\d+)(_f)(\d+)(_\d+\.raw)$)"); diff --git a/src/algorithm.test.cpp b/src/algorithm.test.cpp index e498c3b2..026fcacb 100644 --- a/src/algorithm.test.cpp +++ b/src/algorithm.test.cpp @@ -3,6 +3,8 @@ #include #include +using aare::ssize_t; + TEST_CASE("Find the closed index in a 1D array", "[algorithm]") { aare::NDArray arr({5}); for (ssize_t i = 0; i < arr.size(); i++) { diff --git a/src/decode.test.cpp b/src/decode.test.cpp index 09d6c61c..3fc3637f 100644 --- a/src/decode.test.cpp +++ b/src/decode.test.cpp @@ -10,6 +10,7 @@ using Catch::Matchers::WithinAbs; #include using aare::BitOffset; +using aare::ssize_t; TEST_CASE("test_adc_sar_05_decode64to16") { uint64_t input = 0; @@ -24,7 +25,7 @@ TEST_CASE("test_adc_sar_05_decode64to16") { // test all bits by iteratting through the bitlist std::vector bitlist = {29, 19, 28, 18, 31, 21, 27, 20, 24, 23, 25, 22}; for (size_t i = 0; i < bitlist.size(); i++) { - input = 1UL << bitlist[i]; + input = 1ULL << bitlist[i]; output = aare::adc_sar_05_decode64to16(input); CHECK(output == (1 << i)); } @@ -94,7 +95,7 @@ TEST_CASE("Each byte buffer decoder maps to its own bit order") { }; for (const auto &c : cases) { - uint64_t word = (1UL << c.bit0) | (1UL << c.bit1); + uint64_t word = (1ULL << c.bit0) | (1ULL << c.bit1); std::vector buffer(8); std::memcpy(buffer.data(), &word, sizeof(word)); aare::NDView bytes(buffer.data(), {1, 8}); diff --git a/src/fit/Fit.cpp b/src/fit/Fit.cpp index f8d0b2dc..b6d7a1db 100644 --- a/src/fit/Fit.cpp +++ b/src/fit/Fit.cpp @@ -269,7 +269,10 @@ void fit_3d(const FitModel &model, NDView x, NDView par_out, NDView err_out, NDView chi2_out, int n_threads, std::optional> fit_range) { - constexpr std::size_t npar = Model::npar; + + // static so the lambda below can use it as an array size without + // capturing it, which MSVC otherwise rejects + static constexpr std::size_t npar = Model::npar; detail::check_fit_3d_shapes(x, y, y_err, par_out, err_out, chi2_out); const bool has_errors = (y_err.size() > 0); diff --git a/src/fit/Fit.test.cpp b/src/fit/Fit.test.cpp index 992500e8..1edadce6 100644 --- a/src/fit/Fit.test.cpp +++ b/src/fit/Fit.test.cpp @@ -17,6 +17,7 @@ using aare::Minimizer; using aare::NDArray; using aare::NDView; +using aare::ssize_t; using Catch::Approx; namespace { @@ -1099,7 +1100,7 @@ TEST_CASE("Samples with a zero error are ignored even when NaN", "[fit]") { INFO("minimizer " << name(minimizer)); const double nan = std::numeric_limits::quiet_NaN(); - constexpr ssize_t n = 10; + static constexpr ssize_t n = 10; // static: used inside GENERATE's lambda NDArray x({n}); NDArray y({n}); NDArray y_err({n}, 1.0); diff --git a/src/hist/PedestalTrackingPixelHistogram.test.cpp b/src/hist/PedestalTrackingPixelHistogram.test.cpp index 2c9f1590..b8d81df4 100644 --- a/src/hist/PedestalTrackingPixelHistogram.test.cpp +++ b/src/hist/PedestalTrackingPixelHistogram.test.cpp @@ -20,6 +20,7 @@ using aare::FileConfig; using aare::Frame; using aare::NumpyFile; using aare::PedestalTrackingPixelHistogram; +using aare::ssize_t; namespace { diff --git a/src/hist/PixelHistogram.test.cpp b/src/hist/PixelHistogram.test.cpp index 50b1fcec..25767257 100644 --- a/src/hist/PixelHistogram.test.cpp +++ b/src/hist/PixelHistogram.test.cpp @@ -18,6 +18,7 @@ using aare::NDArray; using aare::NDView; using aare::PixelHistogram; +using aare::ssize_t; namespace { // The synchronous fill() has been removed; fill_async() is the only entry diff --git a/src/logger.test.cpp b/src/logger.test.cpp new file mode 100644 index 00000000..17218d96 --- /dev/null +++ b/src/logger.test.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MPL-2.0 +#include "aare/logger.hpp" + +#include +#include + +#include +#include +#include + +using Catch::Matchers::EndsWith; +using Catch::Matchers::Matches; +using Catch::Matchers::StartsWith; + +TEST_CASE("Log timestamp is HH:MM:SS.mmm", "[logger]") { + const std::string ts = aare::Logger::Timestamp(); + REQUIRE_THAT(ts, Matches(R"(\d{2}:\d{2}:\d{2}\.\d{3})")); +} + +TEST_CASE("short_file_name strips both kinds of directory separators", + "[logger]") { + REQUIRE(std::string(aare::short_file_name("/a/b/File.cpp")) == "File.cpp"); + REQUIRE(std::string(aare::short_file_name("C:\\a\\b\\File.cpp")) == + "File.cpp"); + REQUIRE(std::string(aare::short_file_name("File.cpp")) == "File.cpp"); +} + +TEST_CASE("LOG writes one coloured line with timestamp and level", "[logger]") { + std::ostringstream captured; + auto *previous = std::clog.rdbuf(captured.rdbuf()); + LOG(aare::logWARNING) << "hello " << 42; + std::clog.rdbuf(previous); + + const std::string line = captured.str(); + // - HH:MM:SS.mmm WARNING: hello 42\n + REQUIRE_THAT(line, StartsWith(std::string(YELLOW BOLD) + "- ")); + REQUIRE_THAT(line, Matches(std::string("\x1b\\[33m\x1b\\[1m- ") + + R"(\d{2}:\d{2}:\d{2}\.\d{3} WARNING: hello 42)" + + "\x1b\\[0m\n")); + REQUIRE_THAT(line, EndsWith(std::string(RESET) + "\n")); +}