diff --git a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt
index 796be24..7eef918 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,58 @@ 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
+ )
+
+ # 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/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/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;
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);
+}
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();
+}