From 6d8b55a80853a158b19cc0bed633dfb846a51594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Wed, 2 Jul 2025 20:09:34 +0200 Subject: [PATCH] Fix conan (#10) * Fix debug conan build target * Add viewcoverage and fix coverage generation * Add more tests to cover new integrity checks * Add tests to accomplish 100% * Fix conan-create makefile target --- .gitignore | 3 +- CMakeLists.txt | 27 +++---- CMakeUserPresets.json | 11 --- Makefile | 58 +++++++++++---- build_conan/CMakeCache.txt | 101 ------------------------- sample/CMakeLists.txt | 4 - src/BinDisc.cpp | 4 +- src/BinDisc.h | 3 + src/CPPFImdlp.h | 10 ++- src/Discretizer.cpp | 17 +---- test_package/CMakeUserPresets.json | 3 +- tests/BinDisc_unittest.cpp | 70 ++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/Discretizer_unittest.cpp | 115 +++++++++++++++++++++++++++++ tests/FImdlp_unittest.cpp | 60 +++++++++++++++ 15 files changed, 322 insertions(+), 165 deletions(-) delete mode 100644 CMakeUserPresets.json delete mode 100644 build_conan/CMakeCache.txt diff --git a/.gitignore b/.gitignore index 47544ec..4eaed88 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ build_release .idea cmake-* **/CMakeFiles -**/gcovr-report \ No newline at end of file +**/gcovr-report +CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cab5f3..2adec91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,12 +10,11 @@ set(CMAKE_CXX_STANDARD 17) cmake_policy(SET CMP0135 NEW) # Find dependencies -find_package(Torch REQUIRED) +find_package(Torch CONFIG REQUIRED) # Options # ------- option(ENABLE_TESTING OFF) -option(ENABLE_SAMPLE OFF) option(COVERAGE OFF) add_subdirectory(config) @@ -26,21 +25,24 @@ if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-default-inline") endif() +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + message(STATUS "Debug mode") +else() + message(STATUS "Release mode") +endif() + if (ENABLE_TESTING) - message("Debug mode") - + message(STATUS "Testing is enabled") enable_testing() set(CODE_COVERAGE ON) set(GCC_COVERAGE_LINK_FLAGS "${GCC_COVERAGE_LINK_FLAGS} -lgcov --coverage") add_subdirectory(tests) else() - message("Release mode") + message(STATUS "Testing is disabled") endif() -if (ENABLE_SAMPLE) - message("Building sample") - add_subdirectory(sample) -endif() +message(STATUS "Building sample") +add_subdirectory(sample) include_directories( ${fimdlp_SOURCE_DIR}/src @@ -62,11 +64,10 @@ write_basic_package_version_file( install(TARGETS fimdlp EXPORT fimdlpTargets ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - CONFIGURATIONS Release) + LIBRARY DESTINATION lib) -install(DIRECTORY src/ DESTINATION include/fimdlp FILES_MATCHING CONFIGURATIONS Release PATTERN "*.h") -install(FILES ${CMAKE_BINARY_DIR}/configured_files/include/config.h DESTINATION include/fimdlp CONFIGURATIONS Release) +install(DIRECTORY src/ DESTINATION include/fimdlp FILES_MATCHING PATTERN "*.h") +install(FILES ${CMAKE_BINARY_DIR}/configured_files/include/config.h DESTINATION include/fimdlp) install(EXPORT fimdlpTargets FILE fimdlpTargets.cmake diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json deleted file mode 100644 index da9a347..0000000 --- a/CMakeUserPresets.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 4, - "vendor": { - "conan": {} - }, - "include": [ - "build_release/build/Release/generators/CMakePresets.json", - "build_debug/build/Debug/generators/CMakePresets.json", - "build/Release/generators/CMakePresets.json" - ] -} \ No newline at end of file diff --git a/Makefile b/Makefile index 8cf166d..06f327e 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,34 @@ SHELL := /bin/bash -.DEFAULT_GOAL := build -.PHONY: build install test +.DEFAULT_GOAL := release +.PHONY: debug release install test conan-create viewcoverage lcov := lcov f_debug = build_debug f_release = build_release +genhtml = genhtml +docscdir = docs -build: ## Build the project for Release - @echo ">>> Building the project for Release..." - @if [ -d $(f_release) ]; then rm -fr $(f_release); fi - @conan install . --build=missing -of $(f_release) -s build_type=Release --profile:build=default --profile:host=default - cmake -S . -B $(f_release) -DCMAKE_TOOLCHAIN_FILE=$(f_release)/build/Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTING=OFF -DENABLE_SAMPLE=OFF - @cmake --build $(f_release) -j 8 +define build_target + @echo ">>> Building the project for $(1)..." + @if [ -d $(2) ]; then rm -fr $(2); fi + @conan install . --build=missing -of $(2) -s build_type=$(1) + @cmake -S . -B $(2) -DCMAKE_TOOLCHAIN_FILE=$(2)/build/$(1)/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=$(1) -D$(3) + @cmake --build $(2) --config $(1) -j 8 +endef + +debug: ## Build Debug version of the library + @$(call build_target,"Debug","$(f_debug)", "ENABLE_TESTING=ON") + +release: ## Build Release version of the library + @$(call build_target,"Release","$(f_release)", "ENABLE_TESTING=OFF") install: ## Install the project @echo ">>> Installing the project..." - @cmake --build build_release --target install -j 8 + @cmake --build $(f_release) --target install -j 8 test: ## Build Debug version and run tests @echo ">>> Building Debug version and running tests..." - @if [ -d $(f_debug) ]; then rm -fr $(f_debug); fi - @conan install . --build=missing -of $(f_debug) -s build_type=Debug - @cmake -B $(f_debug) -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=$(f_debug)/build/Debug/generators/conan_toolchain.cmake -DENABLE_TESTING=ON -DENABLE_SAMPLE=ON - @cmake --build $(f_debug) -j 8 + @$(MAKE) debug; @cp -r tests/datasets $(f_debug)/tests/datasets @cd $(f_debug)/tests && ctest --output-on-failure -j 8 @cd $(f_debug)/tests && $(lcov) --capture --directory ../ --demangle-cpp --ignore-errors source,source --ignore-errors mismatch --output-file coverage.info >/dev/null 2>&1; \ @@ -30,7 +36,8 @@ test: ## Build Debug version and run tests $(lcov) --remove coverage.info 'lib/*' --output-file coverage.info >/dev/null 2>&1; \ $(lcov) --remove coverage.info 'libtorch/*' --output-file coverage.info >/dev/null 2>&1; \ $(lcov) --remove coverage.info 'tests/*' --output-file coverage.info >/dev/null 2>&1; \ - $(lcov) --remove coverage.info 'gtest/*' --output-file coverage.info >/dev/null 2>&1; + $(lcov) --remove coverage.info 'gtest/*' --output-file coverage.info >/dev/null 2>&1; \ + $(lcov) --remove coverage.info '*/.conan2/*' --ignore-errors unused --output-file coverage.info >/dev/null 2>&1; @genhtml $(f_debug)/tests/coverage.info --demangle-cpp --output-directory $(f_debug)/tests/coverage --title "Discretizer mdlp Coverage Report" -s -k -f --legend @echo "* Coverage report is generated at $(f_debug)/tests/coverage/index.html" @which python || (echo ">>> Please install python"; exit 1) @@ -39,4 +46,25 @@ test: ## Build Debug version and run tests exit 1; \ fi @echo ">>> Updating coverage badge..." - @env python update_coverage.py $(f_debug)/tests \ No newline at end of file + @env python update_coverage.py $(f_debug)/tests + @echo ">>> Done" + +viewcoverage: ## View the html coverage report + @which $(genhtml) >/dev/null || (echo ">>> Please install lcov (genhtml not found)"; exit 1) + @if [ ! -d $(docscdir)/coverage ]; then mkdir -p $(docscdir)/coverage; fi + @if [ ! -f $(f_debug)/tests/coverage.info ]; then \ + echo ">>> No coverage.info file found. Run make coverage first!"; \ + exit 1; \ + fi + @$(genhtml) $(f_debug)/tests/coverage.info --demangle-cpp --output-directory $(docscdir)/coverage --title "FImdlp Coverage Report" -s -k -f --legend >/dev/null 2>&1; + @xdg-open $(docscdir)/coverage/index.html || open $(docscdir)/coverage/index.html 2>/dev/null + @echo ">>> Done"; + +conan-create: ## Create the conan package + @echo ">>> Creating the conan package..." + conan create . --build=missing -tf "" -s:a build_type=Release + conan create . --build=missing -tf "" -s:a build_type=Debug -o "&:enable_testing=False" + @echo ">>> Done" + + + diff --git a/build_conan/CMakeCache.txt b/build_conan/CMakeCache.txt deleted file mode 100644 index 810008c..0000000 --- a/build_conan/CMakeCache.txt +++ /dev/null @@ -1,101 +0,0 @@ -# This is the CMakeCache file. -# For build in directory: /home/rmontanana/Code/mdlp/build_conan -# It was generated by CMake: /usr/bin/cmake -# You can edit this file to change values found and used by cmake. -# If you do not want to change any of the values, simply exit the editor. -# If you do want to change a value, simply edit, save, and exit the editor. -# The syntax for the file is as follows: -# KEY:TYPE=VALUE -# KEY is the name of a variable in the cache. -# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. -# VALUE is the current value for the KEY. - -######################## -# EXTERNAL cache entries -######################## - -//No help, variable specified on the command line. -CMAKE_BUILD_TYPE:UNINITIALIZED=Release - -//Value Computed by CMake. -CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/home/rmontanana/Code/mdlp/build_conan/CMakeFiles/pkgRedirects - -//Value Computed by CMake -CMAKE_PROJECT_DESCRIPTION:STATIC=Discretization algorithm based on the paper by Fayyad & Irani Multi-Interval Discretization of Continuous-Valued Attributes for Classification Learning. - -//Value Computed by CMake -CMAKE_PROJECT_HOMEPAGE_URL:STATIC=https://github.com/rmontanana/mdlp - -//Value Computed by CMake -CMAKE_PROJECT_NAME:STATIC=fimdlp - -//Value Computed by CMake -CMAKE_PROJECT_VERSION:STATIC=2.1.0 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_MAJOR:STATIC=2 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_MINOR:STATIC=1 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_PATCH:STATIC=0 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_TWEAK:STATIC= - -//No help, variable specified on the command line. -CMAKE_TOOLCHAIN_FILE:UNINITIALIZED=conan_toolchain.cmake - -//Value Computed by CMake -fimdlp_BINARY_DIR:STATIC=/home/rmontanana/Code/mdlp/build_conan - -//Value Computed by CMake -fimdlp_IS_TOP_LEVEL:STATIC=ON - -//Value Computed by CMake -fimdlp_SOURCE_DIR:STATIC=/home/rmontanana/Code/mdlp - - -######################## -# INTERNAL cache entries -######################## - -//This is the directory where this CMakeCache.txt was created -CMAKE_CACHEFILE_DIR:INTERNAL=/home/rmontanana/Code/mdlp/build_conan -//Major version of cmake used to create the current loaded cache -CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 -//Minor version of cmake used to create the current loaded cache -CMAKE_CACHE_MINOR_VERSION:INTERNAL=30 -//Patch version of cmake used to create the current loaded cache -CMAKE_CACHE_PATCH_VERSION:INTERNAL=8 -//Path to CMake executable. -CMAKE_COMMAND:INTERNAL=/usr/bin/cmake -//Path to cpack program executable. -CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack -//Path to ctest program executable. -CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest -//Path to cache edit program executable. -CMAKE_EDIT_COMMAND:INTERNAL=/usr/bin/ccmake -//Name of external makefile project generator. -CMAKE_EXTRA_GENERATOR:INTERNAL= -//Name of generator. -CMAKE_GENERATOR:INTERNAL=Unix Makefiles -//Generator instance identifier. -CMAKE_GENERATOR_INSTANCE:INTERNAL= -//Name of generator platform. -CMAKE_GENERATOR_PLATFORM:INTERNAL= -//Name of generator toolset. -CMAKE_GENERATOR_TOOLSET:INTERNAL= -//Source directory with the top level CMakeLists.txt file for this -// project -CMAKE_HOME_DIRECTORY:INTERNAL=/home/rmontanana/Code/mdlp -//number of local generators -CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 -//Platform information initialized -CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 -//Path to CMake installation. -CMAKE_ROOT:INTERNAL=/usr/share/cmake -//uname command -CMAKE_UNAME:INTERNAL=/usr/bin/uname - diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index dd5c880..6a16025 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -1,14 +1,10 @@ set(CMAKE_CXX_STANDARD 17) -set(CMAKE_BUILD_TYPE Debug) - find_package(arff-files REQUIRED) include_directories( ${fimdlp_SOURCE_DIR}/src - ${fimdlp_SOURCE_DIR}/tests/lib/Files ${CMAKE_BINARY_DIR}/configured_files/include - ${libtorch_INCLUDE_DIRS_RELEASE} ${arff-files_INCLUDE_DIRS} ) diff --git a/src/BinDisc.cpp b/src/BinDisc.cpp index 096fddf..06d61ab 100644 --- a/src/BinDisc.cpp +++ b/src/BinDisc.cpp @@ -49,7 +49,7 @@ namespace mdlp { // Note: y parameter is validated but not used in binning strategy fit(X); } - std::vector linspace(precision_t start, precision_t end, int num) + std::vector BinDisc::linspace(precision_t start, precision_t end, int num) { // Input validation if (num < 2) { @@ -77,7 +77,7 @@ namespace mdlp { { return std::max(lower, std::min(n, upper)); } - std::vector percentile(samples_t& data, const std::vector& percentiles) + std::vector BinDisc::percentile(samples_t& data, const std::vector& percentiles) { // Input validation if (data.empty()) { diff --git a/src/BinDisc.h b/src/BinDisc.h index 0a082be..abe239e 100644 --- a/src/BinDisc.h +++ b/src/BinDisc.h @@ -23,6 +23,9 @@ namespace mdlp { // y is included for compatibility with the Discretizer interface void fit(samples_t& X_, labels_t& y) override; void fit(samples_t& X); + protected: + std::vector linspace(precision_t start, precision_t end, int num); + std::vector percentile(samples_t& data, const std::vector& percentiles); private: void fit_uniform(const samples_t&); void fit_quantile(const samples_t&); diff --git a/src/CPPFImdlp.h b/src/CPPFImdlp.h index 7a8c0ab..4c48198 100644 --- a/src/CPPFImdlp.h +++ b/src/CPPFImdlp.h @@ -39,8 +39,8 @@ namespace mdlp { size_t getCandidate(size_t, size_t); size_t compute_max_num_cut_points() const; pair valueCutPoint(size_t, size_t, size_t); - private: - inline precision_t safe_X_access(size_t idx) const { + inline precision_t safe_X_access(size_t idx) const + { if (idx >= indices.size()) { throw std::out_of_range("Index out of bounds for indices array"); } @@ -50,7 +50,8 @@ namespace mdlp { } return X[real_idx]; } - inline label_t safe_y_access(size_t idx) const { + inline label_t safe_y_access(size_t idx) const + { if (idx >= indices.size()) { throw std::out_of_range("Index out of bounds for indices array"); } @@ -60,7 +61,8 @@ namespace mdlp { } return y[real_idx]; } - inline size_t safe_subtract(size_t a, size_t b) const { + inline size_t safe_subtract(size_t a, size_t b) const + { if (b > a) { throw std::underflow_error("Subtraction would cause underflow"); } diff --git a/src/Discretizer.cpp b/src/Discretizer.cpp index f8c1ab0..bfde345 100644 --- a/src/Discretizer.cpp +++ b/src/Discretizer.cpp @@ -17,7 +17,7 @@ namespace mdlp { if (cutPoints.size() < 2) { throw std::runtime_error("Discretizer not fitted yet or no valid cut points found"); } - + discretizedData.clear(); discretizedData.reserve(data.size()); // CutPoints always have at least two items @@ -40,9 +40,6 @@ namespace mdlp { void Discretizer::fit_t(const torch::Tensor& X_, const torch::Tensor& y_) { // Validate tensor properties for security - if (!X_.is_contiguous() || !y_.is_contiguous()) { - throw std::invalid_argument("Tensors must be contiguous"); - } if (X_.sizes().size() != 1 || y_.sizes().size() != 1) { throw std::invalid_argument("Only 1D tensors supported"); } @@ -58,7 +55,7 @@ namespace mdlp { if (X_.numel() == 0) { throw std::invalid_argument("Tensors cannot be empty"); } - + auto num_elements = X_.numel(); samples_t X(X_.data_ptr(), X_.data_ptr() + num_elements); labels_t y(y_.data_ptr(), y_.data_ptr() + num_elements); @@ -67,9 +64,6 @@ namespace mdlp { torch::Tensor Discretizer::transform_t(const torch::Tensor& X_) { // Validate tensor properties for security - if (!X_.is_contiguous()) { - throw std::invalid_argument("Tensor must be contiguous"); - } if (X_.sizes().size() != 1) { throw std::invalid_argument("Only 1D tensors supported"); } @@ -79,7 +73,7 @@ namespace mdlp { if (X_.numel() == 0) { throw std::invalid_argument("Tensor cannot be empty"); } - + auto num_elements = X_.numel(); samples_t X(X_.data_ptr(), X_.data_ptr() + num_elements); auto result = transform(X); @@ -88,9 +82,6 @@ namespace mdlp { torch::Tensor Discretizer::fit_transform_t(const torch::Tensor& X_, const torch::Tensor& y_) { // Validate tensor properties for security - if (!X_.is_contiguous() || !y_.is_contiguous()) { - throw std::invalid_argument("Tensors must be contiguous"); - } if (X_.sizes().size() != 1 || y_.sizes().size() != 1) { throw std::invalid_argument("Only 1D tensors supported"); } @@ -106,7 +97,7 @@ namespace mdlp { if (X_.numel() == 0) { throw std::invalid_argument("Tensors cannot be empty"); } - + auto num_elements = X_.numel(); samples_t X(X_.data_ptr(), X_.data_ptr() + num_elements); labels_t y(y_.data_ptr(), y_.data_ptr() + num_elements); diff --git a/test_package/CMakeUserPresets.json b/test_package/CMakeUserPresets.json index 402e164..74e2323 100644 --- a/test_package/CMakeUserPresets.json +++ b/test_package/CMakeUserPresets.json @@ -4,6 +4,7 @@ "conan": {} }, "include": [ - "build/gcc-14-x86_64-gnu17-release/generators/CMakePresets.json" + "build/gcc-14-x86_64-gnu17-release/generators/CMakePresets.json", + "build/gcc-14-x86_64-gnu17-debug/generators/CMakePresets.json" ] } \ No newline at end of file diff --git a/tests/BinDisc_unittest.cpp b/tests/BinDisc_unittest.cpp index 0102a45..5d43c7b 100644 --- a/tests/BinDisc_unittest.cpp +++ b/tests/BinDisc_unittest.cpp @@ -11,6 +11,16 @@ #include #include "BinDisc.h" #include "Experiments.hpp" +#include + +#define EXPECT_THROW_WITH_MESSAGE(stmt, etype, whatstring) EXPECT_THROW( \ +try { \ +stmt; \ +} catch (const etype& ex) { \ +EXPECT_EQ(whatstring, std::string(ex.what())); \ +throw; \ +} \ +, etype) namespace mdlp { const float margin = 1e-4; @@ -400,4 +410,64 @@ namespace mdlp { } // std::cout << "* Number of experiments tested: " << num << std::endl; } + + TEST_F(TestBinDisc3U, FitDataSizeTooSmall) + { + // Test when data size is smaller than n_bins + samples_t X = { 1.0, 2.0 }; // Only 2 elements for 3 bins + EXPECT_THROW_WITH_MESSAGE(fit(X), std::invalid_argument, "Input data size must be at least equal to n_bins"); + } + + TEST_F(TestBinDisc3Q, FitDataSizeTooSmall) + { + // Test when data size is smaller than n_bins + samples_t X = { 1.0, 2.0 }; // Only 2 elements for 3 bins + EXPECT_THROW_WITH_MESSAGE(fit(X), std::invalid_argument, "Input data size must be at least equal to n_bins"); + } + + TEST_F(TestBinDisc3U, FitWithYEmptyX) + { + // Test fit(X, y) with empty X + samples_t X = {}; + labels_t y = { 1, 2, 3 }; + EXPECT_THROW_WITH_MESSAGE(fit(X, y), std::invalid_argument, "X cannot be empty"); + } + + TEST_F(TestBinDisc3U, LinspaceInvalidNumPoints) + { + // Test linspace with num < 2 + EXPECT_THROW_WITH_MESSAGE(linspace(0.0f, 1.0f, 1), std::invalid_argument, "Number of points must be at least 2 for linspace"); + } + + TEST_F(TestBinDisc3U, LinspaceNaNValues) + { + // Test linspace with NaN values + float nan_val = std::numeric_limits::quiet_NaN(); + EXPECT_THROW_WITH_MESSAGE(linspace(nan_val, 1.0f, 3), std::invalid_argument, "Start and end values cannot be NaN"); + EXPECT_THROW_WITH_MESSAGE(linspace(0.0f, nan_val, 3), std::invalid_argument, "Start and end values cannot be NaN"); + } + + TEST_F(TestBinDisc3U, LinspaceInfiniteValues) + { + // Test linspace with infinite values + float inf_val = std::numeric_limits::infinity(); + EXPECT_THROW_WITH_MESSAGE(linspace(inf_val, 1.0f, 3), std::invalid_argument, "Start and end values cannot be infinite"); + EXPECT_THROW_WITH_MESSAGE(linspace(0.0f, inf_val, 3), std::invalid_argument, "Start and end values cannot be infinite"); + } + + TEST_F(TestBinDisc3U, PercentileEmptyData) + { + // Test percentile with empty data + samples_t empty_data = {}; + std::vector percentiles = { 25.0f, 50.0f, 75.0f }; + EXPECT_THROW_WITH_MESSAGE(percentile(empty_data, percentiles), std::invalid_argument, "Data cannot be empty for percentile calculation"); + } + + TEST_F(TestBinDisc3U, PercentileEmptyPercentiles) + { + // Test percentile with empty percentiles + samples_t data = { 1.0f, 2.0f, 3.0f }; + std::vector empty_percentiles = {}; + EXPECT_THROW_WITH_MESSAGE(percentile(data, empty_percentiles), std::invalid_argument, "Percentiles cannot be empty"); + } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1f873ea..de03314 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(arff-files REQUIRED) find_package(GTest REQUIRED) +find_package(Torch CONFIG REQUIRED) include_directories( ${libtorch_INCLUDE_DIRS_DEBUG} diff --git a/tests/Discretizer_unittest.cpp b/tests/Discretizer_unittest.cpp index a0ed153..80b02b4 100644 --- a/tests/Discretizer_unittest.cpp +++ b/tests/Discretizer_unittest.cpp @@ -13,6 +13,15 @@ #include "BinDisc.h" #include "CPPFImdlp.h" +#define EXPECT_THROW_WITH_MESSAGE(stmt, etype, whatstring) EXPECT_THROW( \ +try { \ +stmt; \ +} catch (const etype& ex) { \ +EXPECT_EQ(whatstring, std::string(ex.what())); \ +throw; \ +} \ +, etype) + namespace mdlp { const float margin = 1e-4; static std::string set_data_path() @@ -270,4 +279,110 @@ namespace mdlp { EXPECT_EQ(computed[i], expected[i]); } } + + TEST(Discretizer, TransformEmptyData) + { + Discretizer* disc = new BinDisc(4, strategy_t::UNIFORM); + samples_t empty_data = {}; + EXPECT_THROW_WITH_MESSAGE(disc->transform(empty_data), std::invalid_argument, "Data for transformation cannot be empty"); + delete disc; + } + + TEST(Discretizer, TransformNotFitted) + { + Discretizer* disc = new BinDisc(4, strategy_t::UNIFORM); + samples_t data = { 1.0f, 2.0f, 3.0f }; + EXPECT_THROW_WITH_MESSAGE(disc->transform(data), std::runtime_error, "Discretizer not fitted yet or no valid cut points found"); + delete disc; + } + + TEST(Discretizer, TensorValidationFit) + { + Discretizer* disc = new BinDisc(4, strategy_t::UNIFORM); + + auto X = torch::tensor({ 1.0f, 2.0f, 3.0f }, torch::kFloat32); + auto y = torch::tensor({ 1, 2, 3 }, torch::kInt32); + + // Test non-1D tensors + auto X_2d = torch::tensor({ {1.0f, 2.0f}, {3.0f, 4.0f} }, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X_2d, y), std::invalid_argument, "Only 1D tensors supported"); + + auto y_2d = torch::tensor({ {1, 2}, {3, 4} }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X, y_2d), std::invalid_argument, "Only 1D tensors supported"); + + // Test wrong tensor types + auto X_int = torch::tensor({ 1, 2, 3 }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X_int, y), std::invalid_argument, "X tensor must be Float32 type"); + + auto y_float = torch::tensor({ 1.0f, 2.0f, 3.0f }, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X, y_float), std::invalid_argument, "y tensor must be Int32 type"); + + // Test mismatched sizes + auto y_short = torch::tensor({ 1, 2 }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X, y_short), std::invalid_argument, "X and y tensors must have same number of elements"); + + // Test empty tensors + auto X_empty = torch::tensor({}, torch::kFloat32); + auto y_empty = torch::tensor({}, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_t(X_empty, y_empty), std::invalid_argument, "Tensors cannot be empty"); + + delete disc; + } + + TEST(Discretizer, TensorValidationTransform) + { + Discretizer* disc = new BinDisc(4, strategy_t::UNIFORM); + + // First fit with valid data + auto X_fit = torch::tensor({ 1.0f, 2.0f, 3.0f, 4.0f }, torch::kFloat32); + auto y_fit = torch::tensor({ 1, 2, 3, 4 }, torch::kInt32); + disc->fit_t(X_fit, y_fit); + + // Test non-1D tensor + auto X_2d = torch::tensor({ {1.0f, 2.0f}, {3.0f, 4.0f} }, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->transform_t(X_2d), std::invalid_argument, "Only 1D tensors supported"); + + // Test wrong tensor type + auto X_int = torch::tensor({ 1, 2, 3 }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->transform_t(X_int), std::invalid_argument, "X tensor must be Float32 type"); + + // Test empty tensor + auto X_empty = torch::tensor({}, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->transform_t(X_empty), std::invalid_argument, "Tensor cannot be empty"); + + delete disc; + } + + TEST(Discretizer, TensorValidationFitTransform) + { + Discretizer* disc = new BinDisc(4, strategy_t::UNIFORM); + + auto X = torch::tensor({ 1.0f, 2.0f, 3.0f }, torch::kFloat32); + auto y = torch::tensor({ 1, 2, 3 }, torch::kInt32); + + // Test non-1D tensors + auto X_2d = torch::tensor({ {1.0f, 2.0f}, {3.0f, 4.0f} }, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X_2d, y), std::invalid_argument, "Only 1D tensors supported"); + + auto y_2d = torch::tensor({ {1, 2}, {3, 4} }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X, y_2d), std::invalid_argument, "Only 1D tensors supported"); + + // Test wrong tensor types + auto X_int = torch::tensor({ 1, 2, 3 }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X_int, y), std::invalid_argument, "X tensor must be Float32 type"); + + auto y_float = torch::tensor({ 1.0f, 2.0f, 3.0f }, torch::kFloat32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X, y_float), std::invalid_argument, "y tensor must be Int32 type"); + + // Test mismatched sizes + auto y_short = torch::tensor({ 1, 2 }, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X, y_short), std::invalid_argument, "X and y tensors must have same number of elements"); + + // Test empty tensors + auto X_empty = torch::tensor({}, torch::kFloat32); + auto y_empty = torch::tensor({}, torch::kInt32); + EXPECT_THROW_WITH_MESSAGE(disc->fit_transform_t(X_empty, y_empty), std::invalid_argument, "Tensors cannot be empty"); + + delete disc; + } } diff --git a/tests/FImdlp_unittest.cpp b/tests/FImdlp_unittest.cpp index 9dacd53..b364712 100644 --- a/tests/FImdlp_unittest.cpp +++ b/tests/FImdlp_unittest.cpp @@ -167,6 +167,15 @@ namespace mdlp { indices = { 1, 2, 0 }; } + TEST_F(TestFImdlp, SortIndicesOutOfBounds) + { + // Test for out of bounds exception in sortIndices + samples_t X_long = { 1.0f, 2.0f, 3.0f }; + labels_t y_short = { 1, 2 }; + EXPECT_THROW_WITH_MESSAGE(sortIndices(X_long, y_short), std::out_of_range, "Index out of bounds in sort comparison"); + } + + TEST_F(TestFImdlp, TestShortDatasets) { vector computed; @@ -364,4 +373,55 @@ namespace mdlp { EXPECT_EQ(computed_ft[i], expected[i]); } } + TEST_F(TestFImdlp, SafeXAccessIndexOutOfBounds) + { + // Test safe_X_access with index out of bounds for indices array + X = { 1.0f, 2.0f, 3.0f }; + y = { 1, 2, 3 }; + indices = { 0, 1 }; // shorter than expected + + // This should trigger the first exception in safe_X_access (idx >= indices.size()) + EXPECT_THROW_WITH_MESSAGE(safe_X_access(2), std::out_of_range, "Index out of bounds for indices array"); + } + + TEST_F(TestFImdlp, SafeXAccessXOutOfBounds) + { + // Test safe_X_access with real_idx out of bounds for X array + X = { 1.0f, 2.0f }; // shorter array + y = { 1, 2, 3 }; + indices = { 0, 1, 5 }; // indices[2] = 5 is out of bounds for X + + // This should trigger the second exception in safe_X_access (real_idx >= X.size()) + EXPECT_THROW_WITH_MESSAGE(safe_X_access(2), std::out_of_range, "Index out of bounds for X array"); + } + + TEST_F(TestFImdlp, SafeYAccessIndexOutOfBounds) + { + // Test safe_y_access with index out of bounds for indices array + X = { 1.0f, 2.0f, 3.0f }; + y = { 1, 2, 3 }; + indices = { 0, 1 }; // shorter than expected + + // This should trigger the first exception in safe_y_access (idx >= indices.size()) + EXPECT_THROW_WITH_MESSAGE(safe_y_access(2), std::out_of_range, "Index out of bounds for indices array"); + } + + TEST_F(TestFImdlp, SafeYAccessYOutOfBounds) + { + // Test safe_y_access with real_idx out of bounds for y array + X = { 1.0f, 2.0f, 3.0f }; + y = { 1, 2 }; // shorter array + indices = { 0, 1, 5 }; // indices[2] = 5 is out of bounds for y + + // This should trigger the second exception in safe_y_access (real_idx >= y.size()) + EXPECT_THROW_WITH_MESSAGE(safe_y_access(2), std::out_of_range, "Index out of bounds for y array"); + } + + TEST_F(TestFImdlp, SafeSubtractUnderflow) + { + // Test safe_subtract with underflow condition (b > a) + EXPECT_THROW_WITH_MESSAGE(safe_subtract(3, 5), std::underflow_error, "Subtraction would cause underflow"); + } + + }