generated from wessel/boilerplate
[PR] Implement tests for Config, Database and ExamResultGenerator #3
@@ -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()
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
<depend>rclcpp</depend>
|
||||
<depend>g2_2025_interfaces</depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
|
||||
@@ -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<StudentCourse, std::vector<int>> StudentCourseResultMap;
|
||||
|
||||
135
src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp
Normal file
135
src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
|
||||
#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<rclcpp::Node>("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<rclcpp::Node> 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());
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
|
||||
#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<rclcpp::Node>("test_db_node");
|
||||
db_manager_ = std::make_unique<DatabaseManager>(node_->get_logger());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
db_manager_.reset();
|
||||
node_.reset();
|
||||
rclcpp::shutdown();
|
||||
}
|
||||
|
||||
std::shared_ptr<rclcpp::Node> node_;
|
||||
std::unique_ptr<DatabaseManager> 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);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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<rclcpp::Node>("test_subscriber_node");
|
||||
|
||||
// Subscriber to capture exam results
|
||||
exam_subscriber_ = test_node_->create_subscription<g2_2025_interfaces::msg::Exam>(
|
||||
"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<g2_2025_interfaces::msg::Student>(
|
||||
"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<ExamResultGenerator>();
|
||||
}
|
||||
|
||||
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<rclcpp::Node> test_node_;
|
||||
std::shared_ptr<ExamResultGenerator> exam_result_generator_;
|
||||
rclcpp::Subscription<g2_2025_interfaces::msg::Exam>::SharedPtr exam_subscriber_;
|
||||
rclcpp::Subscription<g2_2025_interfaces::msg::Student>::SharedPtr student_publisher_;
|
||||
|
||||
std::vector<g2_2025_interfaces::msg::Exam> received_exam_messages_;
|
||||
std::vector<g2_2025_interfaces::msg::Student> 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<g2_2025_interfaces::msg::Student>(
|
||||
"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<g2_2025_interfaces::msg::Student>();
|
||||
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<g2_2025_interfaces::msg::Student>(
|
||||
"student_course_management", 10
|
||||
);
|
||||
|
||||
spin_some_time(500ms);
|
||||
|
||||
// Send multiple student management messages
|
||||
std::vector<std::string> students = {"Alice", "Bob", "Charlie"};
|
||||
std::vector<std::string> courses = {"Math", "Physics", "Chemistry"};
|
||||
|
||||
for (const auto& student : students) {
|
||||
for (const auto& course : courses) {
|
||||
auto msg = std::make_shared<g2_2025_interfaces::msg::Student>();
|
||||
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<g2_2025_interfaces::msg::Exam>(
|
||||
"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();
|
||||
}
|
||||
Reference in New Issue
Block a user