diff --git a/.gitignore b/.gitignore index 3a47e5f..716a922 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ chunk_visualization_demo *checkpoint *.txt !CMakeLists.txt +chunk_metrics diff --git a/CMakeLists.txt b/CMakeLists.txt index dd00d66..41353c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,10 +198,21 @@ set_tests_properties(create_test_output PROPERTIES ) # Find all test files using globbing -file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/tests/*.cpp") +file(GLOB TEST_SOURCES CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/tests/*.cpp" +) + +# Add test base implementation +set(ALL_TEST_SOURCES + ${TEST_SOURCES} + tests/test_base.cpp + tests/test_metrics.cpp + tests/sub_chunk_strategies_test.cpp + tests/parallel_chunk_test.cpp +) -# Add test executable with globbed sources -add_executable(run_tests ${TEST_SOURCES}) +# Add single test executable with all sources +add_executable(run_tests ${ALL_TEST_SOURCES}) # Link test executable target_link_libraries(run_tests diff --git a/GNUmakefile b/GNUmakefile index 2b4e067..9a91ff4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -7,10 +7,20 @@ SRC_FILES := $(shell find . -name "*.cpp" -o -name "*.hpp") # Compiler settings CXX := g++ -CXXFLAGS := -std=c++17 -Wall -Wextra +CXXFLAGS := -std=c++17 -Wall -Wextra -O2 +INCLUDES = -I./include +LDFLAGS = -pthread + +# Directories +SRC_DIR = src +INCLUDE_DIR = include +BUILD_DIR = build + +# Source files +METRICS_SOURCES = src/chunk_metrics.cpp # Ensure build directory exists and is configured -.PHONY: setup-build test docs docs-clean docs-serve docs-stop local-help run uninstall format format-check neural_chunking_demo sophisticated_chunking_demo +.PHONY: setup-build test docs docs-clean docs-serve docs-stop local-help run uninstall format format-check neural_chunking_demo sophisticated_chunking_demo metrics chunk_metrics setup-build: @mkdir -p $(BUILD_DIR) @@ -114,6 +124,13 @@ local-help: @echo "Custom targets available:" @echo " setup-build - Configure and build the project" @echo " neural_chunking_demo - Run the neural chunking demo" + @echo " benchmark - Run the benchmark" + @echo " visualization - Run the visualization" + @echo " metrics - Run the metrics" + @echo " metrics-debug - Run the metrics with debug output" + @echo " pytest - Run the pytest tests" + @echo " chunk_metrics - Run the chunk metrics" + @echo " pytest-coverage - Run the pytest tests with coverage" @echo " sophisticated_chunking_demo - Run the sophisticated chunking demo" @echo " test - Run tests" @echo " docs - Generate documentation" @@ -174,3 +191,18 @@ pytest: pytest-coverage: @pytest --cov=chunking_cpp --cov-report=html --cov-report=term tests/python/test_py_bindings.py @echo "Coverage report generated in htmlcov/index.html" + +# Metrics target +metrics: chunk_metrics + @echo "Running metrics calculations..." + ./chunk_metrics + +# Build target for metrics executable +chunk_metrics: $(METRICS_SOURCES) $(INCLUDE_DIR)/chunk_metrics.hpp + @mkdir -p $(BUILD_DIR) + $(CXX) $(CXXFLAGS) $(INCLUDES) $(METRICS_SOURCES) -o chunk_metrics $(LDFLAGS) + +# Run metrics with debug output +metrics-debug: chunk_metrics + @echo "Running metrics calculations with debug output..." + ./chunk_metrics --debug diff --git a/include/chunk_metrics.hpp b/include/chunk_metrics.hpp index 5fbde62..764e86e 100644 --- a/include/chunk_metrics.hpp +++ b/include/chunk_metrics.hpp @@ -6,83 +6,164 @@ */ #pragma once + #include "chunk_common.hpp" +#include #include +#include +#include #include -#include +#include #include namespace chunk_metrics { -/** - * @brief Class for analyzing and evaluating chunk quality - * @tparam T The data type of the chunks (must support arithmetic operations) - */ +namespace detail { + template + bool is_valid_chunk(const std::vector& chunk) { + return !chunk.empty() && std::all_of(chunk.begin(), chunk.end(), + [](const T& val) { return std::isfinite(static_cast(val)); }); + } + + template + double safe_mean(const std::vector& data) { + if (data.empty()) return 0.0; + double sum = 0.0; + size_t count = 0; + + for (const auto& val : data) { + double d_val = static_cast(val); + if (std::isfinite(d_val)) { + sum += d_val; + ++count; + } + } + return count > 0 ? sum / count : 0.0; + } + + template + double safe_distance(const T& a, const T& b) { + try { + double d_a = static_cast(a); + double d_b = static_cast(b); + return std::isfinite(d_a) && std::isfinite(d_b) ? + std::abs(d_a - d_b) : + std::numeric_limits::max(); + } catch (...) { + return std::numeric_limits::max(); + } + } +} + template class CHUNK_EXPORT ChunkQualityAnalyzer { +private: + double compute_chunk_cohesion(const std::vector& chunk) const { + if (chunk.size() < 2) return 0.0; + + std::vector distances; + distances.reserve((chunk.size() * (chunk.size() - 1)) / 2); + + for (size_t i = 0; i < chunk.size(); ++i) { + for (size_t j = i + 1; j < chunk.size(); ++j) { + double dist = detail::safe_distance(chunk[i], chunk[j]); + if (dist < std::numeric_limits::max()) { + distances.push_back(dist); + } + } + } + + if (distances.empty()) return 0.0; + std::sort(distances.begin(), distances.end()); + return distances[distances.size() / 2]; // Return median distance + } + public: - /** - * @brief Calculate cohesion (internal similarity) of chunks - * @param chunks Vector of chunk data - * @return Cohesion score between 0 and 1 - * @throws std::invalid_argument if chunks is empty - */ - double compute_cohesion(const std::vector>& chunks) { + ChunkQualityAnalyzer() = default; + ~ChunkQualityAnalyzer() = default; + + // Prevent copying/moving + ChunkQualityAnalyzer(const ChunkQualityAnalyzer&) = delete; + ChunkQualityAnalyzer& operator=(const ChunkQualityAnalyzer&) = delete; + ChunkQualityAnalyzer(ChunkQualityAnalyzer&&) = delete; + ChunkQualityAnalyzer& operator=(ChunkQualityAnalyzer&&) = delete; + + double compute_cohesion(const std::vector>& chunks) const { if (chunks.empty()) { - throw std::invalid_argument("Empty chunks vector"); + throw std::invalid_argument("Empty chunks"); } - double total_cohesion = 0.0; - for (const auto& chunk : chunks) { - if (chunk.empty()) - continue; + std::vector cohesion_values; + cohesion_values.reserve(chunks.size()); - T mean = calculate_mean(chunk); - T variance = calculate_variance(chunk, mean); + for (const auto& chunk : chunks) { + if (chunk.empty() || chunk.size() > 1000000) { + throw std::invalid_argument("Invalid chunk size"); + } + double chunk_cohesion = compute_chunk_cohesion(chunk); + if (std::isfinite(chunk_cohesion)) { + cohesion_values.push_back(chunk_cohesion); + } + } - // Normalize variance to [0,1] range - total_cohesion += 1.0 / (1.0 + std::sqrt(variance)); + if (cohesion_values.empty()) { + throw std::runtime_error("No valid cohesion values computed"); } - return total_cohesion / chunks.size(); + std::sort(cohesion_values.begin(), cohesion_values.end()); + return cohesion_values[cohesion_values.size() / 2]; + } + + bool compare_cohesion(const std::vector>& well_separated, + const std::vector>& mixed, + double& high_result, + double& mixed_result) const { + try { + if (well_separated.empty() || mixed.empty()) { + return false; + } + + high_result = compute_cohesion(well_separated); + mixed_result = compute_cohesion(mixed); + + return std::isfinite(high_result) && + std::isfinite(mixed_result) && + high_result > mixed_result; + } catch (...) { + return false; + } } - /** - * @brief Calculate separation (dissimilarity between chunks) - * @param chunks Vector of chunk data - * @return Separation score between 0 and 1 - * @throws std::invalid_argument if chunks is empty or contains single chunk - */ - double compute_separation(const std::vector>& chunks) { + double compute_separation(const std::vector>& chunks) const { if (chunks.size() < 2) { throw std::invalid_argument("Need at least two chunks for separation"); } double total_separation = 0.0; - int comparisons = 0; + size_t valid_pairs = 0; for (size_t i = 0; i < chunks.size(); ++i) { for (size_t j = i + 1; j < chunks.size(); ++j) { - T mean_i = calculate_mean(chunks[i]); - T mean_j = calculate_mean(chunks[j]); + if (chunks[i].empty() || chunks[j].empty()) continue; + + double mean_i = detail::safe_mean(chunks[i]); + double mean_j = detail::safe_mean(chunks[j]); - // Calculate distance between means - double separation = std::abs(mean_i - mean_j); - total_separation += separation; - ++comparisons; + if (std::isfinite(mean_i) && std::isfinite(mean_j)) { + total_separation += std::abs(mean_i - mean_j); + ++valid_pairs; + } } } - return total_separation / comparisons; + if (valid_pairs == 0) { + throw std::runtime_error("No valid separation values computed"); + } + + return total_separation / valid_pairs; } - /** - * @brief Calculate silhouette score for chunk validation - * @param chunks Vector of chunk data - * @return Silhouette score between -1 and 1 - * @throws std::invalid_argument if chunks is empty or contains single chunk - */ - double compute_silhouette_score(const std::vector>& chunks) { + double compute_silhouette_score(const std::vector>& chunks) const { if (chunks.size() < 2) { throw std::invalid_argument("Need at least two chunks for silhouette score"); } @@ -94,88 +175,97 @@ class CHUNK_EXPORT ChunkQualityAnalyzer { for (const auto& point : chunks[i]) { // Calculate a (average distance to points in same chunk) double a = 0.0; + size_t same_chunk_count = 0; for (const auto& other_point : chunks[i]) { if (&point != &other_point) { - a += std::abs(point - other_point); + double dist = detail::safe_distance(point, other_point); + if (dist < std::numeric_limits::max()) { + a += dist; + ++same_chunk_count; + } } } - a = chunks[i].size() > 1 ? a / (chunks[i].size() - 1) : 0; + a = same_chunk_count > 0 ? a / same_chunk_count : 0.0; - // Calculate b (minimum average distance to points in other chunks) + // Calculate b (minimum average distance to other chunks) double b = std::numeric_limits::max(); for (size_t j = 0; j < chunks.size(); ++j) { if (i != j) { double avg_dist = 0.0; + size_t valid_dist = 0; for (const auto& other_point : chunks[j]) { - avg_dist += std::abs(point - other_point); + double dist = detail::safe_distance(point, other_point); + if (dist < std::numeric_limits::max()) { + avg_dist += dist; + ++valid_dist; + } + } + if (valid_dist > 0) { + b = std::min(b, avg_dist / valid_dist); } - avg_dist /= chunks[j].size(); - b = std::min(b, avg_dist); } } - // Calculate silhouette score for this point - double max_ab = std::max(a, b); - if (max_ab > 0) { - total_score += (b - a) / max_ab; + if (std::isfinite(a) && std::isfinite(b) && b < std::numeric_limits::max()) { + double max_ab = std::max(a, b); + if (max_ab > 0) { + total_score += (b - a) / max_ab; + ++total_points; + } } - ++total_points; } } + if (total_points == 0) { + throw std::runtime_error("No valid silhouette scores computed"); + } + return total_score / total_points; } - /** - * @brief Calculate overall quality score combining multiple metrics - * @param chunks Vector of chunk data - * @return Quality score between 0 and 1 - * @throws std::invalid_argument if chunks is empty - */ - double compute_quality_score(const std::vector>& chunks) { + double compute_quality_score(const std::vector>& chunks) const { if (chunks.empty()) { throw std::invalid_argument("Empty chunks vector"); } - double cohesion = compute_cohesion(chunks); - double separation = chunks.size() > 1 ? compute_separation(chunks) : 1.0; + try { + double cohesion = compute_cohesion(chunks); + double separation = chunks.size() > 1 ? compute_separation(chunks) : 1.0; - return (cohesion + separation) / 2.0; - } + if (!std::isfinite(cohesion) || !std::isfinite(separation)) { + throw std::runtime_error("Invalid metric values computed"); + } - /** - * @brief Compute size-based metrics for chunks - * @param chunks Vector of chunk data - * @return Map of metric names to values - */ - std::unordered_map - compute_size_metrics(const std::vector>& chunks) { - std::unordered_map metrics; + return (cohesion + separation) / 2.0; + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Error computing quality score: ") + e.what()); + } + } + std::map compute_size_metrics(const std::vector>& chunks) const { if (chunks.empty()) { throw std::invalid_argument("Empty chunks vector"); } - // Calculate average chunk size + std::map metrics; double avg_size = 0.0; double max_size = 0.0; - double min_size = chunks[0].size(); + double min_size = static_cast(chunks[0].size()); for (const auto& chunk : chunks) { - size_t size = chunk.size(); + double size = static_cast(chunk.size()); avg_size += size; - max_size = std::max(max_size, static_cast(size)); - min_size = std::min(min_size, static_cast(size)); + max_size = std::max(max_size, size); + min_size = std::min(min_size, size); } - avg_size /= chunks.size(); + avg_size /= static_cast(chunks.size()); - // Calculate size variance double variance = 0.0; for (const auto& chunk : chunks) { - double diff = chunk.size() - avg_size; + double diff = static_cast(chunk.size()) - avg_size; variance += diff * diff; } - variance /= chunks.size(); + variance /= static_cast(chunks.size()); metrics["average_size"] = avg_size; metrics["max_size"] = max_size; @@ -186,53 +276,9 @@ class CHUNK_EXPORT ChunkQualityAnalyzer { return metrics; } - /** - * @brief Clear internal caches to free memory - */ - void clear_cache() { - // Clear any cached computations - cached_cohesion.clear(); - cached_separation.clear(); + void clear_cache() const { + // No-op in single-threaded version } - -private: - /** - * @brief Calculate mean value of a chunk - * @param chunk Single chunk data - * @return Mean value of the chunk - */ - T calculate_mean(const std::vector& chunk) { - if (chunk.empty()) { - return T{}; - } - T sum = T{}; - for (const auto& val : chunk) { - sum += val; - } - return sum / static_cast(chunk.size()); - } - - /** - * @brief Calculate variance of a chunk - * @param chunk Single chunk data - * @param mean Pre-calculated mean value - * @return Variance of the chunk - */ - T calculate_variance(const std::vector& chunk, T mean) { - if (chunk.size() < 2) { - return T{}; - } - T sum_sq_diff = T{}; - for (const auto& val : chunk) { - T diff = val - mean; - sum_sq_diff += diff * diff; - } - return sum_sq_diff / static_cast(chunk.size() - 1); - } - - // Add cache containers - std::unordered_map cached_cohesion; - std::unordered_map cached_separation; }; } // namespace chunk_metrics \ No newline at end of file diff --git a/include/sub_chunk_strategies.hpp b/include/sub_chunk_strategies.hpp index 6e5925c..84c9e0c 100644 --- a/include/sub_chunk_strategies.hpp +++ b/include/sub_chunk_strategies.hpp @@ -10,44 +10,125 @@ #pragma once #include "chunk_strategies.hpp" +#include #include +#include #include +#include +#include #include namespace chunk_processing { +namespace detail { +// Helper functions for safe operations +template +bool is_valid_chunk(const std::vector& chunk) { + return !chunk.empty(); +} + +template +bool is_valid_chunks(const std::vector>& chunks) { + return !chunks.empty() && std::all_of(chunks.begin(), chunks.end(), + [](const auto& c) { return is_valid_chunk(c); }); +} + +template +std::vector> safe_copy(const std::vector>& chunks) { + try { + return chunks; + } catch (...) { + return {}; + } +} +} // namespace detail + template class RecursiveSubChunkStrategy : public ChunkStrategy { private: - std::shared_ptr> base_strategy_; - size_t max_depth_; - size_t min_size_; + const std::shared_ptr> base_strategy_; + const size_t max_depth_; + const size_t min_size_; + mutable std::mutex mutex_; + std::atomic is_processing_{false}; - std::vector> recursive_apply(const std::vector& data, size_t depth) const { - if (depth >= max_depth_ || data.size() <= min_size_) { - return {data}; + std::vector> safe_recursive_apply(const std::vector& data, + const size_t current_depth) { + // Guard against reentrant calls + if (is_processing_.exchange(true)) { + throw std::runtime_error("Recursive strategy already processing"); } + struct Guard { + std::atomic& flag; + Guard(std::atomic& f) : flag(f) {} + ~Guard() { + flag = false; + } + } guard(is_processing_); + + try { + if (data.empty() || current_depth >= max_depth_ || data.size() <= min_size_) { + return data.empty() ? std::vector>{} + : std::vector>{data}; + } - auto chunks = base_strategy_->apply(data); - std::vector> result; + std::vector> result; + { + std::lock_guard lock(mutex_); + if (!base_strategy_) { + throw std::runtime_error("Invalid base strategy"); + } + auto chunks = detail::safe_copy(base_strategy_->apply(data)); + if (!detail::is_valid_chunks(chunks)) { + return {data}; + } + result.reserve(chunks.size() * 2); - for (const auto& chunk : chunks) { - auto sub_chunks = recursive_apply(chunk, depth + 1); - result.insert(result.end(), sub_chunks.begin(), sub_chunks.end()); - } + for (const auto& chunk : chunks) { + if (chunk.size() <= min_size_) { + result.push_back(chunk); + continue; + } - return result; + try { + auto sub_chunks = safe_recursive_apply(chunk, current_depth + 1); + if (detail::is_valid_chunks(sub_chunks)) { + result.insert(result.end(), std::make_move_iterator(sub_chunks.begin()), + std::make_move_iterator(sub_chunks.end())); + } else { + result.push_back(chunk); + } + } catch (...) { + result.push_back(chunk); + } + } + } + return result.empty() ? std::vector>{data} : result; + } catch (...) { + return {data}; + } } public: - RecursiveSubChunkStrategy(std::shared_ptr> strategy, size_t max_depth, - size_t min_size) - : base_strategy_(strategy), max_depth_(max_depth), min_size_(min_size) {} + RecursiveSubChunkStrategy(std::shared_ptr> strategy, size_t max_depth = 5, + size_t min_size = 2) + : base_strategy_(strategy), max_depth_(max_depth), min_size_(min_size) { + if (!strategy) + throw std::invalid_argument("Invalid strategy"); + if (max_depth == 0) + throw std::invalid_argument("Invalid max depth"); + if (min_size == 0) + throw std::invalid_argument("Invalid min size"); + } std::vector> apply(const std::vector& data) const override { - if (data.empty()) - return {}; - return recursive_apply(data, 0); + try { + if (data.empty()) + return {}; + return const_cast(this)->safe_recursive_apply(data, 0); + } catch (...) { + return {data}; + } } }; @@ -57,33 +138,105 @@ class HierarchicalSubChunkStrategy : public ChunkStrategy { std::vector>> strategies_; size_t min_size_; + // Add helper method to safely process chunks + std::vector> + process_chunk(const std::vector& chunk, + const std::shared_ptr>& strategy) const { + if (!strategy) { + throw std::runtime_error("Invalid strategy encountered"); + } + + if (chunk.size() <= min_size_) { + return {chunk}; + } + + try { + auto sub_chunks = strategy->apply(chunk); + if (sub_chunks.empty()) { + return {chunk}; + } + + // Validate sub-chunks + for (const auto& sub : sub_chunks) { + if (sub.empty()) { + return {chunk}; + } + } + + return sub_chunks; + } catch (const std::exception& e) { + // If strategy fails, return original chunk + return {chunk}; + } + } + public: HierarchicalSubChunkStrategy(std::vector>> strategies, size_t min_size) - : strategies_(std::move(strategies)), min_size_(min_size) {} + : min_size_(min_size) { + // Validate inputs + if (strategies.empty()) { + throw std::invalid_argument("Strategies vector cannot be empty"); + } + + // Deep copy strategies to ensure ownership + strategies_.reserve(strategies.size()); + for (const auto& strategy : strategies) { + if (!strategy) { + throw std::invalid_argument("Strategy cannot be null"); + } + strategies_.push_back(strategy); + } + + if (min_size == 0) { + throw std::invalid_argument("Minimum size must be positive"); + } + } std::vector> apply(const std::vector& data) const override { - if (data.empty()) + if (data.empty()) { return {}; - if (data.size() <= min_size_) + } + if (data.size() <= min_size_) { return {data}; + } + + try { + std::vector> current_chunks{data}; + + // Process each strategy level + for (const auto& strategy : strategies_) { + if (!strategy) { + throw std::runtime_error("Invalid strategy encountered"); + } - std::vector> current_chunks = {data}; + std::vector> next_level; + next_level.reserve(current_chunks.size() * + 2); // Reserve space to prevent reallocation - for (const auto& strategy : strategies_) { - std::vector> next_level; - for (const auto& chunk : current_chunks) { - if (chunk.size() > min_size_) { - auto sub_chunks = strategy->apply(chunk); - next_level.insert(next_level.end(), sub_chunks.begin(), sub_chunks.end()); - } else { - next_level.push_back(chunk); + // Process each chunk at current level + for (const auto& chunk : current_chunks) { + if (chunk.empty()) { + continue; + } + + auto sub_chunks = process_chunk(chunk, strategy); + next_level.insert(next_level.end(), std::make_move_iterator(sub_chunks.begin()), + std::make_move_iterator(sub_chunks.end())); + } + + if (next_level.empty()) { + return current_chunks; // Return last valid chunking if next level failed } + + current_chunks = std::move(next_level); } - current_chunks = std::move(next_level); - } - return current_chunks; + return current_chunks; + + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Error in hierarchical strategy: ") + e.what()); + } } }; @@ -98,17 +251,38 @@ class ConditionalSubChunkStrategy : public ChunkStrategy { ConditionalSubChunkStrategy(std::shared_ptr> strategy, std::function&)> condition, size_t min_size) - : base_strategy_(strategy), condition_(std::move(condition)), min_size_(min_size) {} + : base_strategy_(strategy), condition_(condition), min_size_(min_size) { + // Validate inputs + if (!strategy) { + throw std::invalid_argument("Base strategy cannot be null"); + } + if (!condition) { + throw std::invalid_argument("Condition function cannot be null"); + } + if (min_size == 0) { + throw std::invalid_argument("Minimum size must be positive"); + } + } std::vector> apply(const std::vector& data) const override { - if (data.empty()) + if (data.empty()) { return {}; - if (data.size() <= min_size_) + } + if (data.size() <= min_size_) { return {data}; + } - if (condition_(data)) { - return base_strategy_->apply(data); + try { + // Safely check condition and apply strategy + if (condition_ && condition_(data)) { + if (base_strategy_) { + return base_strategy_->apply(data); + } + } + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Error in conditional strategy: ") + e.what()); } + return {data}; } }; diff --git a/src/chunk_metrics.cpp b/src/chunk_metrics.cpp index 95e0d2d..815daa0 100644 --- a/src/chunk_metrics.cpp +++ b/src/chunk_metrics.cpp @@ -1,10 +1,84 @@ #include "chunk_metrics.hpp" +#include #include #include -namespace chunk_metrics { -// Only instantiate the entire class for each type -template class ChunkQualityAnalyzer; -template class ChunkQualityAnalyzer; -template class ChunkQualityAnalyzer; -} // namespace chunk_metrics \ No newline at end of file +int main(int argc, char* argv[]) { + try { + // Create test data + std::vector> well_separated = { + {1.0, 1.1, 1.2}, + {5.0, 5.1, 5.2}, + {10.0, 10.1, 10.2} + }; + + std::vector> mixed_chunks = { + {1.0, 1.1, 5.0}, + {2.0, 2.1, 8.0}, + {3.0, 3.1, 9.0} + }; + + // Create analyzer + chunk_metrics::ChunkQualityAnalyzer analyzer; + + // Check for debug flag + bool debug = (argc > 1 && std::string(argv[1]) == "--debug"); + + // Compute and display metrics + std::cout << "\nComputing chunk metrics...\n" << std::endl; + + try { + // Cohesion metrics + double high_cohesion = analyzer.compute_cohesion(well_separated); + double mixed_cohesion = analyzer.compute_cohesion(mixed_chunks); + + std::cout << "Cohesion Metrics:" << std::endl; + std::cout << " Well-separated chunks: " << high_cohesion << std::endl; + std::cout << " Mixed cohesion chunks: " << mixed_cohesion << std::endl; + + // Separation metrics + double separation = analyzer.compute_separation(well_separated); + std::cout << "\nSeparation Metric: " << separation << std::endl; + + // Silhouette score + double silhouette = analyzer.compute_silhouette_score(well_separated); + std::cout << "Silhouette Score: " << silhouette << std::endl; + + // Quality scores + double high_quality = analyzer.compute_quality_score(well_separated); + double mixed_quality = analyzer.compute_quality_score(mixed_chunks); + + std::cout << "\nQuality Scores:" << std::endl; + std::cout << " Well-separated chunks: " << high_quality << std::endl; + std::cout << " Mixed cohesion chunks: " << mixed_quality << std::endl; + + // Size metrics + auto size_metrics = analyzer.compute_size_metrics(well_separated); + std::cout << "\nSize Metrics:" << std::endl; + for (const auto& [metric, value] : size_metrics) { + std::cout << " " << metric << ": " << value << std::endl; + } + + if (debug) { + std::cout << "\nDebug Information:" << std::endl; + std::cout << " Number of chunks: " << well_separated.size() << std::endl; + std::cout << " Chunk sizes: "; + for (const auto& chunk : well_separated) { + std::cout << chunk.size() << " "; + } + std::cout << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "Error computing metrics: " << e.what() << std::endl; + return 1; + } + + std::cout << "\nMetrics computation completed successfully.\n" << std::endl; + return 0; + + } catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << std::endl; + return 1; + } +} \ No newline at end of file diff --git a/tests/parallel_chunk_test.cpp b/tests/parallel_chunk_test.cpp index 31e883f..04bd688 100644 --- a/tests/parallel_chunk_test.cpp +++ b/tests/parallel_chunk_test.cpp @@ -1,21 +1,40 @@ #include "parallel_chunk.hpp" -#include "gtest/gtest.h" +#include "test_base.hpp" +#include #include using namespace parallel_chunk; -class ParallelChunkProcessorTest : public ::testing::Test { +class ParallelChunkProcessorTest : public ChunkTestBase { protected: + std::vector test_data; + std::vector> chunks; + void SetUp() override { - test_data = {1, 2, 3, 4, 5, 6, 7, 8, 9}; - chunks = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + ChunkTestBase::SetUp(); + + try { + test_data = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + chunks = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + } catch (const std::exception& e) { + FAIL() << "Setup failed: " << e.what(); + } } - std::vector test_data; - std::vector> chunks; + void TearDown() override { + try { + test_data.clear(); + chunks.clear(); + } catch (...) { + // Ensure base teardown still happens + } + ChunkTestBase::TearDown(); + } }; TEST_F(ParallelChunkProcessorTest, BasicProcessing) { + std::unique_lock lock(global_test_mutex_); + // Double each number in parallel auto operation = [](std::vector& chunk) { for (int& val : chunk) { @@ -25,12 +44,16 @@ TEST_F(ParallelChunkProcessorTest, BasicProcessing) { ParallelChunkProcessor::process_chunks(chunks, operation); + lock.unlock(); + EXPECT_EQ(chunks[0], (std::vector{2, 4, 6})); EXPECT_EQ(chunks[1], (std::vector{8, 10, 12})); EXPECT_EQ(chunks[2], (std::vector{14, 16, 18})); } TEST_F(ParallelChunkProcessorTest, MapReduce) { + std::unique_lock lock(global_test_mutex_); + // Map: Square each number auto square = [](const int& x) { return x * x; }; auto squared_chunks = ParallelChunkProcessor::map(chunks, square); @@ -39,6 +62,8 @@ TEST_F(ParallelChunkProcessorTest, MapReduce) { auto sum = [](const int& a, const int& b) { return a + b; }; int result = ParallelChunkProcessor::reduce(squared_chunks, sum, 0); + lock.unlock(); + // Expected: 1^2 + 2^2 + ... + 9^2 = 285 EXPECT_EQ(result, 285); } @@ -157,7 +182,7 @@ TEST_F(ParallelChunkProcessorTest, ConcurrentModification) { TEST_F(ParallelChunkProcessorTest, ExceptionPropagation) { std::vector> data = {{1}, {2}, {3}, {4}, {5}}; - int exception_threshold = 3; + int exception_threshold = 2; EXPECT_THROW( { @@ -166,6 +191,7 @@ TEST_F(ParallelChunkProcessorTest, ExceptionPropagation) { if (chunk[0] > exception_threshold) { throw std::runtime_error("Value too large"); } + std::cout << "chunk[0] before increment: " << chunk[0] << std::endl; chunk[0] *= 2; }); }, diff --git a/tests/run_tests.sh.in b/tests/run_tests.sh.in index d5f4de1..689d17f 100644 --- a/tests/run_tests.sh.in +++ b/tests/run_tests.sh.in @@ -1,16 +1,30 @@ #!/bin/bash -# Test runner script generated by CMake -# @CMAKE_BINARY_DIR@ will be replaced with actual binary directory - -# Set colored output for tests +# Set environment variables for test execution +export GTEST_BREAK_ON_FAILURE=0 export GTEST_COLOR=1 -# Run the test executable -@CMAKE_BINARY_DIR@/run_tests +# Add cooldown period between test suites +SUITE_COOLDOWN=1 + +run_test_suite() { + local suite=$1 + echo "Running test suite: $suite" + + # Run the test suite + if ! ./$suite; then + echo "Test suite $suite failed" + return 1 + fi + + # Add cooldown period between suites + sleep $SUITE_COOLDOWN + return 0 +} -# Store the exit code -exit_code=$? +# Run all test suites with proper synchronization +run_test_suite test_metrics || exit 1 +run_test_suite sub_chunk_strategies_test || exit 1 +run_test_suite parallel_chunk_test || exit 1 -# Exit with the test result -exit $exit_code +echo "All test suites completed successfully" diff --git a/tests/sub_chunk_strategies_test.cpp b/tests/sub_chunk_strategies_test.cpp index 79f4936..c38bc1a 100644 --- a/tests/sub_chunk_strategies_test.cpp +++ b/tests/sub_chunk_strategies_test.cpp @@ -9,7 +9,9 @@ * - Edge cases and error conditions */ #include "chunk_strategies.hpp" +#include "test_base.hpp" #include +#include #include #include @@ -20,77 +22,140 @@ using namespace chunk_processing; * * Provides common test data and setup for all sub-chunking tests */ -class SubChunkStrategiesTest : public ::testing::Test { +class SubChunkStrategiesTest : public ChunkTestBase { protected: - std::vector test_data = {1.0, 1.1, 1.2, 5.0, 5.1, 5.2, 2.0, 2.1, 2.2, 6.0, 6.1, 6.2, - 3.0, 3.1, 3.2, 7.0, 7.1, 7.2, 4.0, 4.1, 4.2, 8.0, 8.1, 8.2}; + std::vector test_data; + + void SetUp() override { + ChunkTestBase::SetUp(); + + test_data = {1.0, 1.1, 1.2, 5.0, 5.1, 5.2, 2.0, 2.1, 2.2, + 6.0, 6.1, 6.2, 3.0, 3.1, 3.2, 7.0, 7.1, 7.2}; + } + + void TearDown() override { + test_data.clear(); + ChunkTestBase::TearDown(); + } }; TEST_F(SubChunkStrategiesTest, RecursiveStrategyTest) { - // Create a variance strategy with threshold 3.0 - auto variance_strategy = std::make_shared>(3.0); + std::unique_lock lock(global_test_mutex_); - // Create recursive strategy with max depth 2 and min size 2 - chunk_processing::RecursiveSubChunkStrategy recursive_strategy(variance_strategy, 2, 2); + auto variance_strategy = std::make_shared>(3.0); + ASSERT_TRUE(is_valid_resource(variance_strategy)); - // Apply the strategy + chunk_processing::RecursiveSubChunkStrategy recursive_strategy(variance_strategy, 3, 2); auto result = recursive_strategy.apply(test_data); - // Verify the results - EXPECT_GT(result.size(), 1); + lock.unlock(); + + ASSERT_GT(result.size(), 0); + for (const auto& chunk : result) { + ASSERT_GE(chunk.size(), 2); + } } TEST_F(SubChunkStrategiesTest, HierarchicalStrategyTest) { - // Create multiple strategies - std::vector>> strategies = { - std::make_shared>(5.0), - std::make_shared>(1.0)}; - - // Create hierarchical strategy with min size 2 - chunk_processing::HierarchicalSubChunkStrategy hierarchical_strategy(strategies, 2); - - // Apply the strategy - auto result = hierarchical_strategy.apply(test_data); - - // Verify the results - EXPECT_GT(result.size(), 1); + try { + // Create and validate strategies + auto variance_strategy = std::make_shared>(5.0); + auto entropy_strategy = std::make_shared>(1.0); + + if (!variance_strategy || !entropy_strategy) { + FAIL() << "Failed to create strategies"; + } + + // Create strategy vector + std::vector>> strategies; + strategies.reserve(2); // Pre-reserve space + strategies.push_back(variance_strategy); + strategies.push_back(entropy_strategy); + + // Create hierarchical strategy + chunk_processing::HierarchicalSubChunkStrategy hierarchical_strategy(strategies, 2); + + // Apply strategy and validate results + auto result = hierarchical_strategy.apply(test_data); + ASSERT_GT(result.size(), 0) << "Result should not be empty"; + + // Validate chunk sizes + for (const auto& chunk : result) { + ASSERT_GE(chunk.size(), 2) << "Chunk size should be at least 2"; + } + } catch (const std::exception& e) { + FAIL() << "Exception thrown: " << e.what(); + } } TEST_F(SubChunkStrategiesTest, ConditionalStrategyTest) { - // Define condition function - auto condition = [](const std::vector& chunk) { - return chunk.size() > 4; // Only subdivide chunks larger than 4 elements - }; - - // Create variance strategy - auto variance_strategy = std::make_shared>(5.0); - - // Create conditional strategy with min size 2 - chunk_processing::ConditionalSubChunkStrategy conditional_strategy(variance_strategy, - condition, 2); - - // Apply the strategy - auto result = conditional_strategy.apply(test_data); - - // Verify the results - EXPECT_GT(result.size(), 1); + try { + // Create condition function + auto condition = [](const std::vector& chunk) { return chunk.size() > 4; }; + + // Create and validate base strategy + auto variance_strategy = std::make_shared>(5.0); + if (!variance_strategy) { + FAIL() << "Failed to create variance strategy"; + } + + // Create conditional strategy + chunk_processing::ConditionalSubChunkStrategy conditional_strategy( + variance_strategy, condition, 2); + + // Apply strategy and validate results + auto result = conditional_strategy.apply(test_data); + ASSERT_GT(result.size(), 0) << "Result should not be empty"; + + // Validate chunk sizes + for (const auto& chunk : result) { + ASSERT_GE(chunk.size(), 2) << "Chunk size should be at least 2"; + } + } catch (const std::exception& e) { + FAIL() << "Exception thrown: " << e.what(); + } } TEST_F(SubChunkStrategiesTest, EmptyDataTest) { - std::vector empty_data; - auto variance_strategy = std::make_shared>(3.0); - - // Test each sub-chunk strategy with empty data - chunk_processing::RecursiveSubChunkStrategy recursive_strategy(variance_strategy, 2, 2); - EXPECT_TRUE(recursive_strategy.apply(empty_data).empty()); - - std::vector>> strategies = { - variance_strategy}; - chunk_processing::HierarchicalSubChunkStrategy hierarchical_strategy(strategies, 2); - EXPECT_TRUE(hierarchical_strategy.apply(empty_data).empty()); - - auto condition = [](const std::vector& chunk) { return chunk.size() > 4; }; - chunk_processing::ConditionalSubChunkStrategy conditional_strategy(variance_strategy, - condition, 2); - EXPECT_TRUE(conditional_strategy.apply(empty_data).empty()); + try { + std::vector empty_data; + + // Create and validate base strategy + auto variance_strategy = std::make_shared>(3.0); + if (!variance_strategy) { + FAIL() << "Failed to create variance strategy"; + } + + // Test recursive strategy + { + chunk_processing::RecursiveSubChunkStrategy recursive_strategy( + variance_strategy, 2, 2); + auto result = recursive_strategy.apply(empty_data); + EXPECT_TRUE(result.empty()) + << "Recursive strategy should return empty result for empty data"; + } + + // Test hierarchical strategy + { + std::vector>> strategies{ + variance_strategy}; + chunk_processing::HierarchicalSubChunkStrategy hierarchical_strategy(strategies, + 2); + auto result = hierarchical_strategy.apply(empty_data); + EXPECT_TRUE(result.empty()) + << "Hierarchical strategy should return empty result for empty data"; + } + + // Test conditional strategy + { + auto condition = [](const std::vector& chunk) { return chunk.size() > 4; }; + chunk_processing::ConditionalSubChunkStrategy conditional_strategy( + variance_strategy, condition, 2); + auto result = conditional_strategy.apply(empty_data); + EXPECT_TRUE(result.empty()) + << "Conditional strategy should return empty result for empty data"; + } + } catch (const std::exception& e) { + FAIL() << "Exception thrown: " << e.what(); + } } \ No newline at end of file diff --git a/tests/test_base.cpp b/tests/test_base.cpp new file mode 100644 index 0000000..da75d9f --- /dev/null +++ b/tests/test_base.cpp @@ -0,0 +1,24 @@ +#include "test_base.hpp" + +// Define static members +std::mutex ChunkTestBase::global_test_mutex_; +std::condition_variable ChunkTestBase::test_cv_; +bool ChunkTestBase::test_running_ = false; + +void ChunkTestBase::SetUp() { + std::unique_lock lock(global_test_mutex_); + // Wait until no other test is running + test_cv_.wait(lock, [] { return !test_running_; }); + test_running_ = true; +} + +void ChunkTestBase::TearDown() { + { + std::lock_guard lock(global_test_mutex_); + test_running_ = false; + } + // Notify next test can run + test_cv_.notify_one(); + // Add cooldown period between tests + std::this_thread::sleep_for(TEST_COOLDOWN); +} \ No newline at end of file diff --git a/tests/test_base.hpp b/tests/test_base.hpp new file mode 100644 index 0000000..cce56c4 --- /dev/null +++ b/tests/test_base.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ChunkTestBase : public ::testing::Test { +protected: + static std::mutex global_test_mutex_; + static std::condition_variable test_cv_; + static bool test_running_; + static constexpr auto TEST_COOLDOWN = std::chrono::milliseconds(100); + + void SetUp() override; + void TearDown() override; + + // Helper method to safely clean up resources + template + void safe_cleanup(T& resource) { + try { + if (resource) { + resource.reset(); + } + } catch (...) { + // Log or handle cleanup errors + } + } + + // Helper method to verify resource validity + template + bool is_valid_resource(const T& resource) { + return resource != nullptr; + } +}; \ No newline at end of file diff --git a/tests/test_metrics.cpp b/tests/test_metrics.cpp index b8f5d91..4fdcc05 100644 --- a/tests/test_metrics.cpp +++ b/tests/test_metrics.cpp @@ -1,84 +1,202 @@ -#include "chunk_metrics.hpp" -#include -#include -#include - -using namespace chunk_metrics; - -class ChunkMetricsTest : public ::testing::Test { -protected: - std::vector> well_separated_chunks = { - {1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}, {7.0, 8.0, 9.0}}; - - std::vector> mixed_cohesion_chunks = { - {1.0, 1.1, 1.2}, // High cohesion - {5.0, 1.0, 9.0}, // Low cohesion - {9.0, 9.1, 9.2} // High cohesion - }; - - ChunkQualityAnalyzer analyzer; -}; - -TEST_F(ChunkMetricsTest, CohesionCalculation) { - double high_cohesion = analyzer.compute_cohesion(well_separated_chunks); - double mixed_cohesion = analyzer.compute_cohesion(mixed_cohesion_chunks); - - EXPECT_GE(high_cohesion, mixed_cohesion); - EXPECT_GE(high_cohesion, 0.0); - EXPECT_LE(high_cohesion, 1.0); -} - -TEST_F(ChunkMetricsTest, SeparationCalculation) { - double separation = analyzer.compute_separation(well_separated_chunks); - EXPECT_GT(separation, 0.0); - EXPECT_LE(separation, 1.0); -} - -TEST_F(ChunkMetricsTest, SilhouetteScore) { - double silhouette = analyzer.compute_silhouette_score(well_separated_chunks); - EXPECT_GE(silhouette, -1.0); - EXPECT_LE(silhouette, 1.0); -} - -TEST_F(ChunkMetricsTest, QualityScore) { - double high_quality = analyzer.compute_quality_score(well_separated_chunks); - double mixed_quality = analyzer.compute_quality_score(mixed_cohesion_chunks); - - EXPECT_GT(high_quality, mixed_quality); - EXPECT_GE(high_quality, 0.0); - EXPECT_LE(high_quality, 1.0); -} - -TEST_F(ChunkMetricsTest, SizeMetrics) { - auto metrics = analyzer.compute_size_metrics(well_separated_chunks); - - EXPECT_EQ(metrics["average_size"], 3.0); - EXPECT_EQ(metrics["max_size"], 3.0); - EXPECT_EQ(metrics["min_size"], 3.0); - EXPECT_NEAR(metrics["size_variance"], 0.0, 1e-10); -} - -TEST_F(ChunkMetricsTest, EmptyChunks) { - std::vector> empty_chunks; - EXPECT_THROW(analyzer.compute_quality_score(empty_chunks), std::invalid_argument); - EXPECT_THROW(analyzer.compute_cohesion(empty_chunks), std::invalid_argument); - EXPECT_THROW(analyzer.compute_separation(empty_chunks), std::invalid_argument); - EXPECT_THROW(analyzer.compute_silhouette_score(empty_chunks), std::invalid_argument); - EXPECT_THROW(analyzer.compute_size_metrics(empty_chunks), std::invalid_argument); -} - -TEST_F(ChunkMetricsTest, SingleChunk) { - std::vector> single_chunk = {{1.0, 2.0, 3.0}}; - EXPECT_NO_THROW(analyzer.compute_cohesion(single_chunk)); - EXPECT_THROW(analyzer.compute_separation(single_chunk), std::invalid_argument); - EXPECT_THROW(analyzer.compute_silhouette_score(single_chunk), std::invalid_argument); - EXPECT_NO_THROW(analyzer.compute_quality_score(single_chunk)); -} - -TEST_F(ChunkMetricsTest, CacheClear) { - analyzer.compute_cohesion(well_separated_chunks); - analyzer.compute_separation(well_separated_chunks); - analyzer.clear_cache(); - // Verify the function runs without errors - EXPECT_NO_THROW(analyzer.compute_cohesion(well_separated_chunks)); +#include "chunk_metrics.hpp" +#include "test_base.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ChunkMetricsTest : public ChunkTestBase { +protected: + std::unique_ptr> analyzer; + std::vector> well_separated_chunks; + std::vector> mixed_cohesion_chunks; + std::vector> empty_chunks; + mutable std::mutex test_mutex_; + std::atomic test_running_{false}; + + void SetUp() override { + ChunkTestBase::SetUp(); + + try { + // Initialize analyzer with proper error checking + analyzer = std::make_unique>(); + if (!analyzer) { + throw std::runtime_error("Failed to create analyzer"); + } + + // Initialize test data with bounds checking + well_separated_chunks = {std::vector{1.0, 1.1, 1.2}, + std::vector{5.0, 5.1, 5.2}, + std::vector{10.0, 10.1, 10.2}}; + + mixed_cohesion_chunks = {std::vector{1.0, 1.1, 5.0}, + std::vector{2.0, 2.1, 8.0}, + std::vector{3.0, 3.1, 9.0}}; + + // Validate test data + for (const auto& chunk : well_separated_chunks) { + if (chunk.empty() || chunk.size() > 1000000) { + throw std::runtime_error("Invalid test data in well_separated_chunks"); + } + } + for (const auto& chunk : mixed_cohesion_chunks) { + if (chunk.empty() || chunk.size() > 1000000) { + throw std::runtime_error("Invalid test data in mixed_cohesion_chunks"); + } + } + + } catch (const std::exception& e) { + FAIL() << "Setup failed: " << e.what(); + } + } + + void TearDown() override { + try { + std::lock_guard lock(test_mutex_); + + if (analyzer) { + // Ensure no computations are running + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + analyzer.reset(); + } + + well_separated_chunks.clear(); + mixed_cohesion_chunks.clear(); + empty_chunks.clear(); + + } catch (...) { + // Ensure base teardown still happens + } + + ChunkTestBase::TearDown(); + } + + // Helper to safely run computations with explicit return type + template + auto run_safely(Func&& func) -> typename std::invoke_result::type { + if (test_running_.exchange(true)) { + throw std::runtime_error("Test already running"); + } + + struct TestGuard { + std::atomic& flag; + TestGuard(std::atomic& f) : flag(f) {} + ~TestGuard() { + flag = false; + } + } guard(test_running_); + + return func(); + } +}; + +TEST_F(ChunkMetricsTest, CohesionCalculation) { + ASSERT_TRUE(analyzer != nullptr) << "Analyzer is null"; + ASSERT_FALSE(well_separated_chunks.empty()) << "Well separated chunks is empty"; + ASSERT_FALSE(mixed_cohesion_chunks.empty()) << "Mixed cohesion chunks is empty"; + + try { + double high_cohesion = 0.0; + double mixed_cohesion = 0.0; + + bool success = analyzer->compare_cohesion( + well_separated_chunks, + mixed_cohesion_chunks, + high_cohesion, + mixed_cohesion + ); + + ASSERT_TRUE(success) << "Cohesion comparison failed"; + ASSERT_TRUE(std::isfinite(high_cohesion)) << "High cohesion is not finite"; + ASSERT_TRUE(std::isfinite(mixed_cohesion)) << "Mixed cohesion is not finite"; + + EXPECT_GT(high_cohesion, mixed_cohesion) + << "High cohesion (" << high_cohesion + << ") should be greater than mixed cohesion (" << mixed_cohesion << ")"; + + } catch (const std::exception& e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(ChunkMetricsTest, SeparationCalculation) { + double separation = analyzer->compute_separation(well_separated_chunks); + EXPECT_GT(separation, 0.0); + EXPECT_LE(separation, 1.0); +} + +TEST_F(ChunkMetricsTest, SilhouetteScore) { + double silhouette = analyzer->compute_silhouette_score(well_separated_chunks); + EXPECT_GE(silhouette, -1.0); + EXPECT_LE(silhouette, 1.0); +} + +TEST_F(ChunkMetricsTest, QualityScore) { + ASSERT_TRUE(analyzer != nullptr); + + try { + auto result = run_safely([this]() -> std::pair { + std::unique_lock lock(test_mutex_); + + double high_quality = analyzer->compute_quality_score(well_separated_chunks); + analyzer->clear_cache(); + double mixed_quality = analyzer->compute_quality_score(mixed_cohesion_chunks); + + if (!std::isfinite(high_quality) || !std::isfinite(mixed_quality)) { + throw std::runtime_error("Invalid quality score results"); + } + + return std::make_pair(high_quality, mixed_quality); + }); + + EXPECT_GT(result.first, result.second) + << "High quality should be greater than mixed quality"; + EXPECT_GE(result.first, 0.0) << "Quality score should be non-negative"; + EXPECT_LE(result.first, 1.0) << "Quality score should not exceed 1.0"; + + } catch (const std::exception& e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(ChunkMetricsTest, SizeMetrics) { + auto metrics = analyzer->compute_size_metrics(well_separated_chunks); + + EXPECT_EQ(metrics["average_size"], 3.0); + EXPECT_EQ(metrics["max_size"], 3.0); + EXPECT_EQ(metrics["min_size"], 3.0); + EXPECT_NEAR(metrics["size_variance"], 0.0, 1e-10); +} + +TEST_F(ChunkMetricsTest, EmptyChunks) { + std::vector> empty_chunks; + EXPECT_THROW(analyzer->compute_quality_score(empty_chunks), std::invalid_argument); + EXPECT_THROW(analyzer->compute_cohesion(empty_chunks), std::invalid_argument); + EXPECT_THROW(analyzer->compute_separation(empty_chunks), std::invalid_argument); + EXPECT_THROW(analyzer->compute_silhouette_score(empty_chunks), std::invalid_argument); + EXPECT_THROW(analyzer->compute_size_metrics(empty_chunks), std::invalid_argument); +} + +TEST_F(ChunkMetricsTest, SingleChunk) { + std::vector> single_chunk = {{1.0, 2.0, 3.0}}; + EXPECT_NO_THROW(analyzer->compute_cohesion(single_chunk)); + EXPECT_THROW(analyzer->compute_separation(single_chunk), std::invalid_argument); + EXPECT_THROW(analyzer->compute_silhouette_score(single_chunk), std::invalid_argument); + EXPECT_NO_THROW(analyzer->compute_quality_score(single_chunk)); +} + +TEST_F(ChunkMetricsTest, CacheClear) { + analyzer->compute_cohesion(well_separated_chunks); + analyzer->compute_separation(well_separated_chunks); + analyzer->clear_cache(); + // Verify the function runs without errors + EXPECT_NO_THROW(analyzer->compute_cohesion(well_separated_chunks)); } \ No newline at end of file