diff --git a/Makefile b/Makefile index e3c9b89..a5c3f14 100644 --- a/Makefile +++ b/Makefile @@ -28,11 +28,7 @@ install: ## Install the project test: ## Build Debug version and run tests @echo ">>> Building Debug version and running tests..." - @if [ ! -d $(f_debug) ]; then \ - $(MAKE) debug; \ - else \ - echo ">>> Debug build already exists, skipping build."; \ - fi + @$(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; \ diff --git a/README.md b/README.md index 94d5880..d371819 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build](https://github.com/rmontanana/mdlp/actions/workflows/build.yml/badge.svg)](https://github.com/rmontanana/mdlp/actions/workflows/build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_mdlp&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=rmontanana_mdlp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_mdlp&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=rmontanana_mdlp) -[![Coverage Badge](https://img.shields.io/badge/Coverage-91,5%25-green)](html/index.html) +[![Coverage Badge](https://img.shields.io/badge/Coverage-96,1%25-green)](html/index.html) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rmontanana/mdlp) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14245443.svg)](https://doi.org/10.5281/zenodo.14245443) 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/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..6712d64 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;