feat(retake): Add tests for retake nodes

This commit is contained in:
2025-10-09 13:55:00 +02:00
parent bdf5c3b113
commit 44f6e4e8cc
6 changed files with 610 additions and 41 deletions

View File

@@ -201,11 +201,51 @@ if(BUILD_TESTING)
pqxx pq tomlplusplus::tomlplusplus
)
# Add Python integration tests
find_package(ament_cmake_pytest REQUIRED)
ament_add_pytest_test(${PROJECT_NAME}_integration_test test/test_integration_system.py
TIMEOUT 60
# Add gtest for RetakeGradeDeterminator
ament_add_gtest(${PROJECT_NAME}_test_retake_grade_determinator
test/RetakeGradeDeterminator.test.cpp
src/retake_grade_determinator/nodes/RetakeGradeDeterminator.cpp
src/database/DatabaseManager.cpp
src/config/ConfigManager.cpp
)
target_include_directories(${PROJECT_NAME}_test_retake_grade_determinator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_grade_determinator
)
ament_target_dependencies(${PROJECT_NAME}_test_retake_grade_determinator
rclcpp
rclcpp_action
g2_2025_interfaces
)
target_link_libraries(${PROJECT_NAME}_test_retake_grade_determinator
pqxx pq tomlplusplus::tomlplusplus
)
# Add gtest for RetakeScheduler
ament_add_gtest(${PROJECT_NAME}_test_retake_scheduler
test/RetakeScheduler.test.cpp
src/retake_scheduler/nodes/RetakeScheduler.cpp
src/database/DatabaseManager.cpp
src/config/ConfigManager.cpp
)
target_include_directories(${PROJECT_NAME}_test_retake_scheduler PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_scheduler
)
ament_target_dependencies(${PROJECT_NAME}_test_retake_scheduler
rclcpp
rclcpp_action
g2_2025_interfaces
)
target_link_libraries(${PROJECT_NAME}_test_retake_scheduler
pqxx pq tomlplusplus::tomlplusplus
)
# Add Python integration tests
# find_package(ament_cmake_pytest REQUIRED)
# ament_add_pytest_test(${PROJECT_NAME}_integration_test test/test_integration_system.py
# TIMEOUT 60
# )
endif()
ament_package()

View File

@@ -9,46 +9,11 @@
#include "g2_2025_interfaces/msg/student.hpp"
#include "g2_2025_interfaces/srv/exams.hpp"
#include "mocks/MockDatabaseManager.hpp"
using namespace std::chrono_literals;
using namespace assignments::one::final_grade_determinator;
namespace assignments::one {
struct MockStoredResult {
StudentCourse sc;
int exam_count;
int final_grade;
bool is_retake;
};
class MockDatabaseManager : public DatabaseManager {
public:
explicit MockDatabaseManager(rclcpp::Logger logger = rclcpp::get_logger("fake_db"))
: DatabaseManager(logger) {
}
bool is_connected() const override {
return true; // Always pretend we are connected
}
bool store_final_course_result(
const StudentCourse& sc,
int exam_count,
int final_grade,
bool is_retake
) override {
stored_results_.push_back({ sc, exam_count, final_grade, is_retake });
return true; // Always succeed
}
void init_database() override {
} // no-op
std::vector<MockStoredResult> stored_results_;
};
} // namespace assignments::one
class FinalGradeDeterminatorTest : public ::testing::Test {
protected:
void SetUp() override {

View File

@@ -0,0 +1,338 @@
#include <chrono>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <gtest/gtest.h>
#include "retake_grade_determinator/nodes/RetakeGradeDeterminator.hpp"
#include "database/DatabaseManager.hpp"
#include "g2_2025_interfaces/msg/exam.hpp"
#include "g2_2025_interfaces/msg/student.hpp"
#include "g2_2025_interfaces/srv/exams.hpp"
#include "g2_2025_interfaces/action/retake.hpp"
#include "mocks/MockDatabaseManager.hpp"
using namespace std::chrono_literals;
using namespace assignments::one::retake_grade_determinator;
class RetakeGradeDeterminatorTest : public ::testing::Test {
protected:
void SetUp() override {
rclcpp::init(0, nullptr);
// Create a test node for testing communication
test_node_ = std::make_shared<rclcpp::Node>("test_node");
// Create mock database manager
mock_db_ = std::make_unique<assignments::one::MockDatabaseManager>();
ptr_mock_db_ = mock_db_.get();
// Subscriber to capture student management messages
student_subscriber_ = 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);
}
);
// Publisher to send exam results
exam_publisher_ = test_node_->create_publisher<g2_2025_interfaces::msg::Exam>(
"exam_results", 10
);
// Service server to mock grade calculator
grade_calculator_service_ = test_node_->create_service<g2_2025_interfaces::srv::Exams>(
"grade_calculator_service",
[this](const std::shared_ptr<g2_2025_interfaces::srv::Exams::Request> request,
std::shared_ptr<g2_2025_interfaces::srv::Exams::Response> response) {
service_requests_.push_back(*request);
response->result = 75;
}
);
// Action client to test retake action server
retake_action_client_ = rclcpp_action::create_client<g2_2025_interfaces::action::Retake>(
test_node_, "retake_action"
);
received_student_messages_.clear();
service_requests_.clear();
}
void TearDown() override {
retake_grade_determinator_.reset();
test_node_.reset();
rclcpp::shutdown();
}
void create_retake_grade_determinator() {
retake_grade_determinator_ = std::make_shared<RetakeGradeDeterminator>(
std::move(mock_db_)
);
}
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 (retake_grade_determinator_) {
rclcpp::spin_some(retake_grade_determinator_);
}
std::this_thread::sleep_for(10ms);
}
}
std::shared_ptr<rclcpp::Node> test_node_;
std::unique_ptr<assignments::one::MockDatabaseManager> mock_db_;
assignments::one::MockDatabaseManager* ptr_mock_db_;
std::shared_ptr<RetakeGradeDeterminator> retake_grade_determinator_;
rclcpp::Subscription<g2_2025_interfaces::msg::Student>::SharedPtr student_subscriber_;
rclcpp::Publisher<g2_2025_interfaces::msg::Exam>::SharedPtr exam_publisher_;
rclcpp::Service<g2_2025_interfaces::srv::Exams>::SharedPtr grade_calculator_service_;
rclcpp_action::Client<g2_2025_interfaces::action::Retake>::SharedPtr retake_action_client_;
std::vector<g2_2025_interfaces::msg::Student> received_student_messages_;
std::vector<g2_2025_interfaces::srv::Exams::Request> service_requests_;
};
TEST_F(RetakeGradeDeterminatorTest, ConstructorTest) {
ASSERT_NO_THROW(create_retake_grade_determinator());
ASSERT_NE(retake_grade_determinator_, nullptr);
}
TEST_F(RetakeGradeDeterminatorTest, PublisherCreationTest) {
create_retake_grade_determinator();
bool student_management_topic_found = false;
auto topic_names_and_types = retake_grade_determinator_->get_topic_names_and_types();
for (const auto& [ topic_name, topic_types ] : topic_names_and_types) {
if (topic_name == "/student_course_management") {
student_management_topic_found = true;
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 published";
}
TEST_F(RetakeGradeDeterminatorTest, SubscriberCreationTest) {
create_retake_grade_determinator();
bool exam_results_topic_found = false;
auto topic_names_and_types = retake_grade_determinator_->get_topic_names_and_types();
for (const auto& [ topic_name, topic_types ] : topic_names_and_types) {
if (topic_name == "/exam_results") {
exam_results_topic_found = true;
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 subscribed";
}
TEST_F(RetakeGradeDeterminatorTest, ActionServerCreationTest) {
create_retake_grade_determinator();
// Allow time for action server to be created
spin_some_time(500ms);
// Check if action client can connect to the server
bool server_available = retake_action_client_->wait_for_action_server(
std::chrono::seconds(2)
);
EXPECT_TRUE(server_available)
<< "Retake action server should be available";
}
TEST_F(RetakeGradeDeterminatorTest, ServiceClientCreationTest) {
create_retake_grade_determinator();
auto service_names_and_types = retake_grade_determinator_->get_service_names_and_types();
bool grade_calculator_service_found = false;
for (const auto& [ service_name, service_types ] : service_names_and_types) {
if (service_name == "/grade_calculator_service") {
grade_calculator_service_found = true;
break;
}
}
EXPECT_TRUE(grade_calculator_service_found)
<< "grade_calculator_service should be available as client";
}
TEST_F(RetakeGradeDeterminatorTest, ParameterTest) {
create_retake_grade_determinator();
auto param = retake_grade_determinator_->get_parameter("grade_collection_amount");
EXPECT_EQ(param.as_int(), 5)
<< "Default grade_collection_amount should be 5";
}
TEST_F(RetakeGradeDeterminatorTest, ExamResultsIgnoredWhenRetakeNotAllowed) {
create_retake_grade_determinator();
auto exam_msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
exam_msg->student_name = "tilmann";
exam_msg->course_name = "computeren";
exam_msg->result = 85;
exam_publisher_->publish(*exam_msg);
spin_some_time(200ms);
EXPECT_TRUE(service_requests_.empty())
<< "No service requests should be made when retake not allowed";
}
TEST_F(RetakeGradeDeterminatorTest, RetakeActionGoalAcceptance) {
create_retake_grade_determinator();
ASSERT_TRUE(retake_action_client_->wait_for_action_server(std::chrono::seconds(2)));
auto goal = g2_2025_interfaces::action::Retake::Goal();
goal.student_name = "tilmann";
goal.course_name = "differentieren";
bool goal_accepted = false;
auto send_goal_options = rclcpp_action::Client<g2_2025_interfaces::action::Retake>::SendGoalOptions();
send_goal_options.goal_response_callback =
[&goal_accepted](
std::shared_ptr<rclcpp_action::ClientGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle
) {
if (goal_handle) {
goal_accepted = true;
}
};
retake_action_client_->async_send_goal(goal, send_goal_options);
// Give asynchronous action service time to process
spin_some_time(500ms);
EXPECT_TRUE(goal_accepted)
<< "Retake goal should be accepted";
EXPECT_EQ(received_student_messages_.size(), 1)
<< "Should publish student enrollment message";
if (!received_student_messages_.empty()) {
EXPECT_EQ(received_student_messages_[0].student_name, "tilmann");
EXPECT_EQ(received_student_messages_[0].course_name, "differentieren");
}
}
// TEST_F(RetakeGradeDeterminatorTest, ExamResultsProcessingDuringRetake) {
// create_retake_grade_determinator();
// ASSERT_TRUE(retake_action_client_->wait_for_action_server(std::chrono::seconds(2)));
// auto goal = g2_2025_interfaces::action::Retake::Goal();
// goal.student_name = "tilmann";
// goal.course_name = "differentieren";
// auto send_goal_options = rclcpp_action::Client<g2_2025_interfaces::action::Retake>::SendGoalOptions();
// retake_action_client_->async_send_goal(goal, send_goal_options);
// spin_some_time(300ms);
// received_student_messages_.clear();
// for (int i = 0; i < 5; ++i) {
// auto exam_msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
// exam_msg->student_name = "tilmann";
// exam_msg->course_name = "differentieren";
// exam_msg->result = 70 + i;
// exam_publisher_->publish(*exam_msg);
// }
// spin_some_time(500ms);
// EXPECT_EQ(service_requests_.size(), 1)
// << "Should make one service request after collecting required exam results";
// if (!service_requests_.empty()) {
// EXPECT_EQ(service_requests_[0].student_name, "tilmann");
// EXPECT_EQ(service_requests_[0].course_name, "differentieren");
// EXPECT_EQ(service_requests_[0].exam_grades.size(), 5);
// }
// // Should publish final result
// EXPECT_EQ(received_student_messages_.size(), 1)
// << "Should publish final student result";
// // Should store in database with retake flag
// EXPECT_EQ(ptr_mock_db_->stored_results.size(), 1);
// if (!ptr_mock_db_->stored_results.empty()) {
// EXPECT_TRUE(ptr_mock_db_->stored_results[0].is_retake)
// << "Should store result with retake flag";
// EXPECT_EQ(ptr_mock_db_->stored_results[0].final_grade, 75);
// }
// EXPECT_EQ(ptr_mock_db_->retake_status_updates.size(), 1);
// }
TEST_F(RetakeGradeDeterminatorTest, PartialExamResultsCollection) {
create_retake_grade_determinator();
// spin_some_time(500ms);
ASSERT_TRUE(retake_action_client_->wait_for_action_server(std::chrono::seconds(2)));
auto goal = g2_2025_interfaces::action::Retake::Goal();
goal.student_name = "tilmann";
goal.course_name = "differentieren";
auto send_goal_options = rclcpp_action::Client<g2_2025_interfaces::action::Retake>::SendGoalOptions();
retake_action_client_->async_send_goal(goal, send_goal_options);
spin_some_time(300ms);
// Send only 3 out of 5 required exam results
for (int i = 0; i < 3; ++i) {
auto exam_msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
exam_msg->student_name = "tilmann";
exam_msg->course_name = "differentieren";
exam_msg->result = 65 + i;
exam_publisher_->publish(*exam_msg);
}
spin_some_time(300ms);
EXPECT_TRUE(service_requests_.empty())
<< "Should not make service request with insufficient exam results";
EXPECT_TRUE(ptr_mock_db_->stored_results.empty())
<< "Should not store results with insufficient exam results";
}

View File

@@ -0,0 +1,96 @@
#include <chrono>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <gtest/gtest.h>
#include "retake_scheduler/nodes/RetakeScheduler.hpp"
#include "database/DatabaseManager.hpp"
#include "g2_2025_interfaces/action/retake.hpp"
#include "mocks/MockDatabaseManager.hpp"
#include "mocks/MockRetakeActionServer.hpp"
using namespace std::chrono_literals;
using namespace assignments::one::retake_scheduler;
class RetakeSchedulerTest : public ::testing::Test {
protected:
void SetUp() override {
rclcpp::init(0, nullptr);
// Create test node and mock action server
test_node_ = std::make_shared<rclcpp::Node>("test_node");
mock_action_server_ = std::make_unique<assignments::one::MockRetakeActionServer>(test_node_);
// Create mock database manager
mock_db_ = std::make_unique<assignments::one::MockDatabaseManager>();
mock_db_ptr_ = mock_db_.get();
}
void TearDown() override {
retake_scheduler_.reset();
mock_action_server_.reset();
test_node_.reset();
rclcpp::shutdown();
}
void create_retake_scheduler() {
retake_scheduler_ = std::make_shared<RetakeScheduler>(
std::move(mock_db_)
);
}
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 (retake_scheduler_) {
rclcpp::spin_some(retake_scheduler_);
}
std::this_thread::sleep_for(10ms);
}
}
std::shared_ptr<rclcpp::Node> test_node_;
std::shared_ptr<RetakeScheduler> retake_scheduler_;
std::unique_ptr<assignments::one::MockDatabaseManager> mock_db_;
assignments::one::MockDatabaseManager* mock_db_ptr_;
std::unique_ptr<assignments::one::MockRetakeActionServer> mock_action_server_;
};
TEST_F(RetakeSchedulerTest, ConstructorTest) {
ASSERT_NO_THROW(create_retake_scheduler());
ASSERT_NE(retake_scheduler_, nullptr);
}
TEST_F(RetakeSchedulerTest, ActionClientCreationTest) {
create_retake_scheduler();
bool retake_action_found = false;
auto service_names_and_types = retake_scheduler_->get_service_names_and_types();
for (const auto& [ service_name, service_types ] : service_names_and_types) {
if (service_name == "/retake_action/_action/cancel_goal" ||
service_name == "/retake_action/_action/get_result" ||
service_name == "/retake_action/_action/send_goal") {
retake_action_found = true;
break;
}
}
EXPECT_TRUE(retake_action_found)
<< "Retake action client services should be available";
}
TEST_F(RetakeSchedulerTest, ParameterTest) {
create_retake_scheduler();
// Test default parameter value
auto param = retake_scheduler_->get_parameter("retake_check_interval");
EXPECT_EQ(param.as_int(), 120) << "Default retake_check_interval should be 120 seconds";
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include "database/DatabaseManager.hpp"
namespace assignments::one {
struct MockStoredResult {
StudentCourse sc;
int exam_count;
int final_grade;
bool is_retake;
};
class MockDatabaseManager : public DatabaseManager {
public:
explicit MockDatabaseManager(
rclcpp::Logger logger = rclcpp::get_logger("fake_db")
)
: DatabaseManager(logger) {}
bool is_connected() const override {
return connection_status_;
}
void init_database() override {}
void set_connection_status(bool status) {
connection_status_ = status;
}
std::vector<StudentCourse> get_failed_course_results() {
return failed_students_;
}
bool store_final_course_result(
const StudentCourse& sc,
int exam_count,
int final_grade,
bool is_retake
) {
stored_results.push_back({ sc, exam_count, final_grade, is_retake });
return true;
}
bool update_retake_status(const StudentCourse& sc) {
retake_status_updates.push_back(sc);
return true;
}
void clear_failed_students() {
failed_students_.clear();
}
void add_failed_student(const std::string& student_name, const std::string& course_name) {
StudentCourse sc;
sc.student_name = student_name;
sc.course_name = course_name;
failed_students_.push_back(sc);
}
void set_failed_students(const std::vector<StudentCourse>& failed_students) {
failed_students_ = failed_students;
}
std::vector<MockStoredResult> stored_results;
std::vector<StudentCourse> retake_status_updates;
private:
std::vector<StudentCourse> failed_students_;
bool connection_status_ = true;
};
} // namespace assignments::one

View File

@@ -0,0 +1,57 @@
#pragma once
namespace assignments::one {
class MockRetakeActionServer {
public:
MockRetakeActionServer(std::shared_ptr<rclcpp::Node> node) : node_(node) {
action_server_ = rclcpp_action::create_server<g2_2025_interfaces::action::Retake>(
node_,
"retake_action",
std::bind(&MockRetakeActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
std::bind(&MockRetakeActionServer::handle_cancel, this, std::placeholders::_1),
std::bind(&MockRetakeActionServer::handle_accepted, this, std::placeholders::_1)
);
}
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const g2_2025_interfaces::action::Retake::Goal> goal
) {
(void)uuid;
received_goals_.push_back(*goal);
return goal_response_;
}
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle
) {
(void)goal_handle;
cancel_requests++;
return rclcpp_action::CancelResponse::ACCEPT;
}
void handle_accepted(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle) {
accepted_goals++;
// Simulate immediate success
auto result = std::make_shared<g2_2025_interfaces::action::Retake::Result>();
result->result = 0.0;
goal_handle->succeed(result);
}
void set_goal_response(rclcpp_action::GoalResponse response) {
goal_response_ = response;
}
std::vector<g2_2025_interfaces::action::Retake::Goal> received_goals_;
int accepted_goals = 0;
int cancel_requests = 0;
private:
std::shared_ptr<rclcpp::Node> node_;
rclcpp_action::Server<g2_2025_interfaces::action::Retake>::SharedPtr action_server_;
rclcpp_action::GoalResponse goal_response_ = rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
};
} // namespace assignments::one