From 057968d5acf9da04c911b4122063b95163885bf5 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 2 Oct 2025 13:08:46 +0200 Subject: [PATCH 1/3] feat(StudentCourse): Add != operator for use in testing --- .../src/database/StudentCourse.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp index b372f81..f1bad4f 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp @@ -24,6 +24,10 @@ struct StudentCourse { || (student_name == other.student_name && course_name < other.course_name); } + + bool operator!=(const StudentCourse& other) const { + return !(*this == other); + } }; typedef std::map> StudentCourseResultMap; -- 2.39.5 From fe8dc6ceba3ed1192afb2e4e70ae51c38aab4f73 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 2 Oct 2025 13:17:41 +0200 Subject: [PATCH 2/3] feat(Config,Database): Add tests --- .../CMakeLists.txt | 48 +++++-- src/g2_2025_grade_calculator_pkg/package.xml | 3 - .../test/ConfigManager.test.cpp | 135 ++++++++++++++++++ .../test/DatabaseManager.test.cpp | 79 ++++++++++ 4 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/test/DatabaseManager.test.cpp diff --git a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt index 796be24..bb0c855 100644 --- a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt +++ b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt @@ -8,13 +8,13 @@ endif() # external packages include(FetchContent) -FetchContent_Declare( +fetchcontent_declare( tomlplusplus GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git GIT_TAG v3.4.0 ) -FetchContent_MakeAvailable(tomlplusplus) +fetchcontent_makeavailable(tomlplusplus) # find dependencies find_package(ament_cmake REQUIRED) @@ -64,7 +64,7 @@ add_executable(retake_scheduler ) ament_target_dependencies(retake_scheduler rclcpp g2_2025_interfaces) -install ( +install( TARGETS exam_result_generator final_grade_determinator @@ -75,15 +75,39 @@ install ( ) if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() + find_package(ament_cmake_gtest REQUIRED) + + # Add gtest for ConfigManager + ament_add_gtest(${PROJECT_NAME}_test_config_manager + test/ConfigManager.test.cpp + src/config/ConfigManager.cpp + ) + target_include_directories(${PROJECT_NAME}_test_config_manager PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + ament_target_dependencies(${PROJECT_NAME}_test_config_manager + rclcpp + ) + target_link_libraries(${PROJECT_NAME}_test_config_manager + tomlplusplus::tomlplusplus + ) + + # Add gtest for DatabaseManager + ament_add_gtest(${PROJECT_NAME}_test_database_manager + test/DatabaseManager.test.cpp + src/database/DatabaseManager.cpp + src/config/ConfigManager.cpp + ) + target_include_directories(${PROJECT_NAME}_test_database_manager PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + ament_target_dependencies(${PROJECT_NAME}_test_database_manager + rclcpp + g2_2025_interfaces + ) + target_link_libraries(${PROJECT_NAME}_test_database_manager + pqxx pq tomlplusplus::tomlplusplus + ) endif() ament_package() diff --git a/src/g2_2025_grade_calculator_pkg/package.xml b/src/g2_2025_grade_calculator_pkg/package.xml index 31cb72c..fb3cd5e 100644 --- a/src/g2_2025_grade_calculator_pkg/package.xml +++ b/src/g2_2025_grade_calculator_pkg/package.xml @@ -12,9 +12,6 @@ rclcpp g2_2025_interfaces - ament_lint_auto - ament_lint_common - ament_cmake diff --git a/src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp b/src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp new file mode 100644 index 0000000..69b0068 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include + +#include "config/ConfigManager.hpp" + +using namespace assignments::one; + +static const std::string TEST_CONFIG_CONTENT = R"( +[database] +host = "test_host" +port = 1234 +dbname = "test_db" +user = "test_user" +password = "test_password" +timeout = 60 +ssl = true + +[database.pool] +min_connections = 2 +max_connections = 20 +)"; + +static const std::string TEST_CONFIG_NO_POOL_CONTENT = R"( +[database] +host = "localhost" +port = 5432 +dbname = "grades" +user = "postgres" +password = "postgres" +)"; + + +class ConfigManagerTest : public ::testing::Test { +protected: + void SetUp() override { + rclcpp::init(0, nullptr); + node_ = std::make_shared("test_config_node"); + } + + void TearDown() override { + // Clean up temporary files + if (std::filesystem::exists(temp_config_file_)) { + std::filesystem::remove(temp_config_file_); + } + + node_.reset(); + rclcpp::shutdown(); + } + + void create_temp_config_file() { + temp_config_file_ = "test_config.toml"; + std::ofstream file(temp_config_file_); + file << TEST_CONFIG_CONTENT; + file.close(); + } + + std::shared_ptr node_; + std::string temp_config_file_; +}; + +TEST_F(ConfigManagerTest, ConstructorTest) { + // Test ConfigManager can be constructed with logger + ConfigManager config_manager(node_->get_logger()); + // Constructor should not crash + SUCCEED(); +} + +TEST_F(ConfigManagerTest, LoadValidConfigTest) { + create_temp_config_file(); + + ConfigManager config_manager(node_->get_logger()); + bool result = config_manager.load_config(temp_config_file_); + + EXPECT_TRUE(result); + EXPECT_TRUE(config_manager.is_loaded()); +} + +TEST_F(ConfigManagerTest, LoadInvalidFileTest) { + ConfigManager config_manager(node_->get_logger()); + bool result = config_manager.load_config("non_existent_file.toml"); + + EXPECT_FALSE(result); + EXPECT_FALSE(config_manager.is_loaded()); +} + +TEST_F(ConfigManagerTest, DatabaseConfigParsingTest) { + create_temp_config_file(); + + ConfigManager config_manager(node_->get_logger()); + config_manager.load_config(temp_config_file_); + + auto db_config = config_manager.get_database_config(); + + ASSERT_TRUE(db_config.has_value()); + EXPECT_EQ(db_config->host, "test_host"); + EXPECT_EQ(db_config->port, 1234); + EXPECT_EQ(db_config->dbname, "test_db"); + EXPECT_EQ(db_config->user, "test_user"); + EXPECT_EQ(db_config->password, "test_password"); + EXPECT_EQ(db_config->timeout, 60); + EXPECT_TRUE(db_config->ssl); + EXPECT_EQ(db_config->min_connections, 2); + EXPECT_EQ(db_config->max_connections, 20); +} + +TEST_F(ConfigManagerTest, DatabaseConfigWithoutPoolTest) { + // Create config without pool section + std::ofstream file("test_config_no_pool.toml"); + file << TEST_CONFIG_NO_POOL_CONTENT; + file.close(); + + ConfigManager config_manager(node_->get_logger()); + config_manager.load_config("test_config_no_pool.toml"); + + auto db_config = config_manager.get_database_config(); + + ASSERT_TRUE(db_config.has_value()); + EXPECT_EQ(db_config->host, "localhost"); + EXPECT_EQ(db_config->port, 5432); + EXPECT_EQ(db_config->min_connections, 1); // default + EXPECT_EQ(db_config->max_connections, 10); // default + + std::filesystem::remove("test_config_no_pool.toml"); +} + +TEST_F(ConfigManagerTest, GetConfigWithoutLoadingTest) { + ConfigManager config_manager(node_->get_logger()); + + auto db_config = config_manager.get_database_config(); + + EXPECT_FALSE(db_config.has_value()); + EXPECT_FALSE(config_manager.is_loaded()); +} diff --git a/src/g2_2025_grade_calculator_pkg/test/DatabaseManager.test.cpp b/src/g2_2025_grade_calculator_pkg/test/DatabaseManager.test.cpp new file mode 100644 index 0000000..417dd8e --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/test/DatabaseManager.test.cpp @@ -0,0 +1,79 @@ +#include +#include + +#include "database/DatabaseManager.hpp" +#include "database/StudentCourse.hpp" + +using namespace assignments::one; + +class DatabaseManagerTest : public ::testing::Test { +protected: + void SetUp() override { + rclcpp::init(0, nullptr); + + node_ = std::make_shared("test_db_node"); + db_manager_ = std::make_unique(node_->get_logger()); + } + + void TearDown() override { + db_manager_.reset(); + node_.reset(); + rclcpp::shutdown(); + } + + std::shared_ptr node_; + std::unique_ptr db_manager_; +}; + +TEST_F(DatabaseManagerTest, ConstructorTest) { + // Test DatabaseManager can be constructed + ASSERT_NE(db_manager_, nullptr); +} + +TEST_F(DatabaseManagerTest, ConnectionStatusTest) { + bool status = db_manager_->is_connected(); + + EXPECT_TRUE(status == true || status == false); +} + +TEST_F(DatabaseManagerTest, QueuePendingCombinationsTest) { + auto combinations = db_manager_->queue_pending_combinations(); + + EXPECT_GE(combinations.size(), 0); +} + +TEST_F(DatabaseManagerTest, StoreExamResultTest) { + bool result = db_manager_->store_exam_result("TestStudent", "TestCourse", 85); + + EXPECT_TRUE(result == true || result == false); +} + +TEST_F(DatabaseManagerTest, EnrollStudentTest) { + StudentCourse sc; + sc.student_name = "TestStudent"; + sc.course_name = "TestCourse"; + + bool result = db_manager_->enroll_student_into_course(sc); + + EXPECT_TRUE(result == true || result == false); +} + +TEST_F(DatabaseManagerTest, GetFinalGradeTest) { + StudentCourse sc; + sc.student_name = "NonExistentStudent"; + sc.course_name = "NonExistentCourse"; + + int grade = db_manager_->get_final_course_grade(sc); + + EXPECT_EQ(grade, -1); +} + +TEST_F(DatabaseManagerTest, StoreFinalResultTest) { + StudentCourse sc; + sc.student_name = "TestStudent"; + sc.course_name = "TestCourse"; + + bool result = db_manager_->store_final_course_result(sc, 3, 75); + + EXPECT_TRUE(result == true || result == false); +} -- 2.39.5 From 14a50c0f035fa4bbbbf863e9cf102045768733f0 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 2 Oct 2025 13:29:57 +0200 Subject: [PATCH 3/3] feat(exam_result_generator): Add tests --- .../CMakeLists.txt | 19 ++ .../test/ExamResultGenerator.test.cpp | 225 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/g2_2025_grade_calculator_pkg/test/ExamResultGenerator.test.cpp diff --git a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt index bb0c855..7eef918 100644 --- a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt +++ b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt @@ -108,6 +108,25 @@ if(BUILD_TESTING) target_link_libraries(${PROJECT_NAME}_test_database_manager pqxx pq tomlplusplus::tomlplusplus ) + + # Add gtest for ExamResultGenerator + ament_add_gtest(${PROJECT_NAME}_test_exam_result_generator + test/ExamResultGenerator.test.cpp + src/exam_result_generator/nodes/ExamResultGenerator.cpp + src/database/DatabaseManager.cpp + src/config/ConfigManager.cpp + ) + target_include_directories(${PROJECT_NAME}_test_exam_result_generator PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src/exam_result_generator + ) + ament_target_dependencies(${PROJECT_NAME}_test_exam_result_generator + rclcpp + g2_2025_interfaces + ) + target_link_libraries(${PROJECT_NAME}_test_exam_result_generator + pqxx pq tomlplusplus::tomlplusplus + ) endif() ament_package() diff --git a/src/g2_2025_grade_calculator_pkg/test/ExamResultGenerator.test.cpp b/src/g2_2025_grade_calculator_pkg/test/ExamResultGenerator.test.cpp new file mode 100644 index 0000000..5bd3c8e --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/test/ExamResultGenerator.test.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include + +#include "exam_result_generator/nodes/ExamResultGenerator.hpp" +#include "g2_2025_interfaces/msg/exam.hpp" +#include "g2_2025_interfaces/msg/student.hpp" + +using namespace std::chrono_literals; +using namespace assignments::one::exam_result_generator; + +class ExamResultGeneratorTest : public ::testing::Test { +protected: + void SetUp() override { + rclcpp::init(0, nullptr); + + // Create a test subscriber to capture published messages + test_node_ = std::make_shared("test_subscriber_node"); + + // Subscriber to capture exam results + exam_subscriber_ = test_node_->create_subscription( + "exam_results", 10, + [this](const g2_2025_interfaces::msg::Exam::SharedPtr msg) { + received_exam_messages_.push_back(*msg); + } + ); + + // Publisher to send student management messages + student_publisher_ = test_node_->create_subscription( + "student_course_management", 10, + [this](const g2_2025_interfaces::msg::Student::SharedPtr msg) { + received_student_messages_.push_back(*msg); + } + ); + + received_exam_messages_.clear(); + received_student_messages_.clear(); + } + + void TearDown() override { + exam_result_generator_.reset(); + test_node_.reset(); + + rclcpp::shutdown(); + } + + void create_exam_result_generator() { + exam_result_generator_ = std::make_shared(); + } + + void spin_some_time(std::chrono::milliseconds duration = 100ms) { + auto start_time = std::chrono::steady_clock::now(); + + while (std::chrono::steady_clock::now() - start_time < duration) { + rclcpp::spin_some(test_node_); + + if (exam_result_generator_) { + rclcpp::spin_some(exam_result_generator_); + } + + std::this_thread::sleep_for(10ms); + } + } + + std::shared_ptr test_node_; + std::shared_ptr exam_result_generator_; + rclcpp::Subscription::SharedPtr exam_subscriber_; + rclcpp::Subscription::SharedPtr student_publisher_; + + std::vector received_exam_messages_; + std::vector received_student_messages_; +}; + +TEST_F(ExamResultGeneratorTest, ConstructorTest) { + ASSERT_NO_THROW({ + create_exam_result_generator(); + }); + + ASSERT_NE(exam_result_generator_, nullptr); + EXPECT_TRUE(exam_result_generator_->get_node_base_interface() != nullptr); + EXPECT_TRUE(exam_result_generator_->get_node_clock_interface() != nullptr); + EXPECT_TRUE(exam_result_generator_->get_node_logging_interface() != nullptr); +} + +TEST_F(ExamResultGeneratorTest, PublisherCreationTest) { + create_exam_result_generator(); + + // Get topic names and types to verify publisher exists + auto topic_names_and_types = exam_result_generator_->get_topic_names_and_types(); + + bool exam_results_topic_found = false; + for (const auto& [topic_name, topic_types] : topic_names_and_types) { + if (topic_name == "/exam_results") { + exam_results_topic_found = true; + // Check if the topic type includes our Exam message type + bool correct_type = false; + for (const auto& type : topic_types) { + if (type == "g2_2025_interfaces/msg/Exam") { + correct_type = true; + break; + } + } + + EXPECT_TRUE(correct_type) << "exam_results topic should have Exam message type"; + break; + } + } + + EXPECT_TRUE(exam_results_topic_found) << "exam_results topic should be published"; +} + +TEST_F(ExamResultGeneratorTest, SubscriberCreationTest) { + create_exam_result_generator(); + + // Get subscription names and types to verify subscriber exists + auto topic_names_and_types = exam_result_generator_->get_topic_names_and_types(); + + bool student_management_topic_found = false; + for (const auto& [topic_name, topic_types] : topic_names_and_types) { + if (topic_name == "/student_course_management") { + student_management_topic_found = true; + // Check if the topic type includes our Student message type + bool correct_type = false; + for (const auto& type : topic_types) { + if (type == "g2_2025_interfaces/msg/Student") { + correct_type = true; + break; + } + } + EXPECT_TRUE(correct_type) << "student_course_management topic should have Student message type"; + break; + } + } + + EXPECT_TRUE(student_management_topic_found) << "student_course_management topic should be subscribed"; +} + +TEST_F(ExamResultGeneratorTest, StudentManagementMessageHandlingTest) { + create_exam_result_generator(); + + // Create a publisher to send student management messages + auto student_mgmt_publisher = test_node_->create_publisher( + "student_course_management", 10 + ); + + // Allow some time for publisher-subscriber connection + spin_some_time(500ms); + + // Create and send a student management message + auto student_msg = std::make_shared(); + student_msg->student_name = "Test Student"; + student_msg->course_name = "Test Course"; + student_msg->timestamp = test_node_->now(); + + student_mgmt_publisher->publish(*student_msg); + + spin_some_time(500ms); + + // Test passes if no crash occurred during message handling + SUCCEED(); +} + +TEST_F(ExamResultGeneratorTest, MultipleStudentMessagesTest) { + create_exam_result_generator(); + + auto student_mgmt_publisher = test_node_->create_publisher( + "student_course_management", 10 + ); + + spin_some_time(500ms); + + // Send multiple student management messages + std::vector students = {"Alice", "Bob", "Charlie"}; + std::vector courses = {"Math", "Physics", "Chemistry"}; + + for (const auto& student : students) { + for (const auto& course : courses) { + auto msg = std::make_shared(); + msg->student_name = student; + msg->course_name = course; + msg->timestamp = test_node_->now(); + + student_mgmt_publisher->publish(*msg); + spin_some_time(50ms); + } + } + + spin_some_time(1s); + + // Test passes if no crash occurred + SUCCEED(); +} + +// Test for message content validation (when messages are published) +TEST_F(ExamResultGeneratorTest, ExamMessageValidationTest) { + create_exam_result_generator(); + + // Create exam result subscriber to capture messages + auto exam_subscriber = test_node_->create_subscription( + "exam_results", 10, + [this](const g2_2025_interfaces::msg::Exam::SharedPtr msg) { + received_exam_messages_.push_back(*msg); + } + ); + + spin_some_time(500ms); + + // Spin for several timer cycles to potentially catch exam messages + auto start_time = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start_time < 6s) { + spin_some_time(100ms); + + for (const auto& exam_msg : received_exam_messages_) { + // Validate grade range (should be 10-100) + EXPECT_GE(exam_msg.result, 10) << "Grade should be >= 10"; + EXPECT_LE(exam_msg.result, 100) << "Grade should be <= 100"; + + EXPECT_FALSE(exam_msg.course_name.empty()) << "Course name should not be empty"; + } + } + + // Test passes regardless of whether messages were received (depends on database connectivity) + SUCCEED(); +} -- 2.39.5