41 Commits

Author SHA1 Message Date
05f56159fd fix(documentation): Fixed spelling in IntegrationTest.md 2025-10-09 19:53:35 +02:00
56eeac9b5c feat(documentation): Added IntegrationTest.md 2025-10-09 19:16:21 +02:00
2e6b180eb2 fix(RetakeGradeDeterminator): Remove abundant comment 2025-10-09 18:53:45 +02:00
e68f083439 fix(documentation + launch): Add RetakeGradeDeterminator test documentation, Fix wrong parameter value in launch xml 2025-10-09 17:32:14 +02:00
e38ee6c4c9 feat(documentation): Add documentation for RetakeScheduler tests 2025-10-09 16:56:10 +02:00
e195881834 fix(documentation): Fix formatting 2025-10-09 16:55:37 +02:00
671fa84b73 feat(retake_scheduler): Add 2 additional unit tests 2025-10-09 16:34:36 +02:00
44f6e4e8cc feat(retake): Add tests for retake nodes 2025-10-09 13:55:50 +02:00
bdf5c3b113 fix(retake_nodes): Fix parameter and add comment 2025-10-09 13:55:50 +02:00
ced37955ee docs(retake): Add documentation for retake nodes 2025-10-09 13:55:50 +02:00
d40ab6745d fix(retakenodes): Fix retake functionality, add Database functions 2025-10-09 13:55:50 +02:00
160ca14e85 fix(retake_nodes): Retake nodes semi-functional 2025-10-09 13:55:50 +02:00
689223f58b fixed retakegradedeterminator 2025-10-09 13:55:50 +02:00
Evanovesky
3f2a5f4eca added retakeGradeDiterminator and Retakeschduler 2025-10-09 13:55:50 +02:00
964795d770 Merge pull request '[PR] Add unit tests to documentation, add README, add launchfile' (#7) from 1-grade-generator/documentation-updates into 1-grade-generator/master
Reviewed-on: http://git.wessel.gg/inholland/ros2-assignments/pulls/7
2025-10-09 13:27:20 +02:00
01c2788a99 Merge branch '1-grade-generator/documentation-updates' of https://git.wessel.gg/inholland/ros2-assignments into 1-grade-generator/documentation-updates 2025-10-09 13:25:35 +02:00
1ceb691fae fix(documentation): Fix formatting, move texst to correct location 2025-10-09 13:25:26 +02:00
644697326d feat(database): Add method to fetch failed courses 2025-10-08 20:20:17 +02:00
493e69acd1 fix(database): Fix database structure, remove from table if requested
- Fixes the `is_retake` field on all tables
- Makes the `ExamResultGenerator` remove the enrollment from the table
if it is popped from its queue
- Added the option to submit a grade as retake
2025-10-08 20:04:13 +02:00
6ccbc95b15 fix(launch): Added parameter 2025-10-08 18:44:01 +02:00
161b5084fc fix(documentation): Update installation instructions 2025-10-08 18:39:16 +02:00
e42856ae4e feat(tests): Add integration tests 2025-10-08 17:52:21 +02:00
5e1df5367c Major feat(Documentation): Complete documentation rework
Split documentation into three folders: Architecture, Testing and
Installation.

Rework and expand architecture.md alongside interfaces.md

Split all parts to do with testing from Architecture md's into Test md's

Add parameter to ExamResultGenerator node documentation
2025-10-08 16:30:44 +02:00
130b495030 docs: Add unit tests
- DatabaseManager: Added unit test documentation
- ConfigManager: Added unit test documentation
- ExamResultGenerator: Added unit test documentation
- FinalGradeDeterminator: Fix spacing
2025-10-07 19:49:31 +02:00
62995c13c2 fix(exam_result_generator): Fix timestamp being 0 2025-10-07 18:59:13 +02:00
517d4f5cb0 Merge branch '1-grade-generator/documentation-updates' of https://git.wessel.gg/inholland/ros2-assignments into 1-grade-generator/documentation-updates 2025-10-07 15:51:16 +02:00
f1878270dc feat(readme): Add aditional content to the main readme 2025-10-07 15:50:59 +02:00
437c5bc16e feat(readme): Add aditional content to the main readme 2025-10-07 15:47:15 +02:00
6e1c0346b0 feat(launchfile): Add launchfile to project 2025-10-07 15:17:06 +02:00
2e02ccddc5 fix(documentation): Add parameter to documentation 2025-10-07 15:16:42 +02:00
fd07992eee fix(documentation): Add test documentation to GradeCalculator and FinalGradeDeterminator 2025-10-07 14:42:44 +02:00
e955280865 Merge pull request '[PR] Implement tests for GradeCalculator and FinalGradeDeterminator, add documentation for aformentioned nodes' (#4) from 1-grade-generator/cijfer-determinator-calculator into 1-grade-generator/master
Reviewed-on: http://git.wessel.gg/inholland/ros2-assignments/pulls/4
2025-10-07 10:07:10 +02:00
2ab1c1c31f fix(tests): fix formatting, syntax and struct positioning 2025-10-07 10:04:19 +02:00
b69dbda1a5 fix(FinalGradeDeterminator): Add mock class for DatabaseManger
The mock class is used to bypass the blocking error of the
FinalGradeDeterminator when no Database is present for testing purposes
2025-10-07 10:04:18 +02:00
447834dda7 fix(DatabaseManager): Make functions virtual for testing 2025-10-07 10:04:18 +02:00
d89f47833e fix(FinalGradeDeterminator): Make db_manager optional for testing 2025-10-07 10:04:17 +02:00
a325e19a41 feat(GradeCalc,GradeDeterm): Add documention 2025-10-07 10:04:17 +02:00
1e7c7cefe5 fix(gitignore): add .vscode folder 2025-10-07 10:04:16 +02:00
25e21a15fc feat(FinalGradeDeterminator): Add tests 2025-10-07 10:04:16 +02:00
887e99c909 feat(GradeCalculator): Add tests 2025-10-07 10:04:15 +02:00
f147a6e287 Merge pull request '[PR] Make the delay between random grades a parameter' (#5) from 1-grade-generator/exam_result_generator into 1-grade-generator/master
Reviewed-on: http://git.wessel.gg/inholland/ros2-assignments/pulls/5
Reviewed-by: Vincent Kompjoeteraar Winter <v.winter.03@gmail.com>
2025-10-05 18:34:05 +02:00
44 changed files with 2875 additions and 76 deletions

2
.gitignore vendored
View File

@@ -33,5 +33,7 @@ qtcreator-*
COLCON_IGNORE
AMENT_IGNORE
.vscode
# End of https://www.toptal.com/developers/gitignore/api/ros2

View File

@@ -10,3 +10,21 @@ Assignments made for the first semester of ROS2
<br><br>
## Table of contents
- [System Architecture](#system-architecture)
- [Components](#components)
- [Installation](#installation)
---
## System Architecture
For the complete system architecture see [architecture.md](doc/architecture/architecture.md) located in the `doc/architecture` folder
### Testing
The testing documentation can be found in the [doc/tests](doc/tests/) folder for each node
## Installation
For installation instructions see [Installation.md](doc/installation/installation.md) located in the `doc/installation` folder

View File

@@ -0,0 +1,151 @@
# TI Minor Grade Generator Design Document
## Project Overview
Giving marks to students cost a lot of time and pain. For this a automatic solution is needed.
Therefore the overall Software architect (Tilmann K.) has designed a solution for the ROS2 architecture.
## System Architecture
### High-Level Architecture
The system consists of multiple ROS2 nodes that communicate through standardized topics and services to process exam data, calculate grades, and persist results. The architecture ensures scalability, maintainability, and fault tolerance.
### Key Design Principles
- **Microservices Architecture**: Each component has a single responsibility
- **Asynchronous Communication**: Uses ROS2 topics and services for loose coupling
- **Data Persistence**: Centralized database management for datastorage
- **Comprehensive Testing**: Unit tests ensure code reliability
## System Components
### Core Nodes
#### 1. FinalGradeDeterminator Node
**Namespace**: `assignments::one::final_grade_determinator`
**Brief Description**: Collects exam results, triggers grade calculation when thresholds are met, and stores final grades.
**Key Features**: Configurable collection thresholds, automatic grade calculation triggering, database persistence
*For detailed documentation, see: [FinalGradeDeterminator.md](nodes/FinalGradeDeterminator.md)*
#### 2. GradeCalculator Node
**Namespace**: `assignments::one::grade_calculator`
**Brief Description**: Provides grade calculation service with business logic including bonus points and grade validation.
**Key Features**: Average calculation, special student rules, grade bounds validation (10-100)
*For detailed documentation, see: [GradeCalculator.md](nodes/GradeCalculator.md)*
#### 3. ExamResultGenerator Node
**Namespace**: `assignments::one::exam_result_generator`
**Brief Description**: Simulates exam result generation by maintaining a queue of student-course combinations and publishing random grades.
**Key Features**: Database-driven queue management, random grade generation, student enrollment handling
*For detailed documentation, see: [ExamResultGenerator.md](nodes/ExamResultGenerator.md)*
#### 4. RetakeScheduler Node
**Namespace**: `assignments::one::retake_scheduler`
**Brief Description**: Manages retake exam scheduling and coordination for students who need to retake exams.
**Key Features**: Retake request processing, schedule management, action server implementation for retake workflows
*For detailed documentation, see: [RetakeScheduler.md](nodes/RetakeScheduler.md)*
#### 5. RetakeGradeDeterminator Node
**Namespace**: `assignments::one::retake_grade_determinator`
**Brief Description**: Handles grade calculation and processing specifically for retake exams.
**Key Features**: Retake-specific grade processing, integration with main grade system, retake result validation
*For detailed documentation, see: [RetakeGradeDeterminator.md](nodes/RetakeGradeDeterminator.md)*
### Data Management
#### DatabaseManager
**Brief Description**: PostgreSQL database interface handling connections, table management, and data persistence.
**Key Features**: Connection management, automatic table creation, student enrollment tracking, exam result storage
*For detailed documentation, see: [DatabaseManager.md](../DatabaseManager.md)*
#### ConfigManager
**Brief Description**: TOML-based configuration management system allowing runtime configuration without recompilation.
**Key Features**: Automatic config file discovery, type-safe TOML parsing, database connection configuration
*For detailed documentation, see: [ConfigManager.md](../ConfigManager.md)*
### Communication Interfaces
#### ROS2 Message and Service Interfaces
**Brief Description**: Custom message types and service definitions for inter-node communication.
**Key Components**: Exam and Student message types, Grade calculation service, Retake action interface, standardized timestamps
*For detailed documentation, see: [interfaces.md](interfaces/interfaces.md)*
## System Workflow
### 1. Exam Result Processing
1. **Input**: Exam results are published to the `exam_results` topic
2. **Collection**: FinalGradeDeterminator receives and stores exam results by student-course combination
3. **Monitoring**: System tracks the number of results per student-course pair
4. **Threshold Check**: When `grade_collection_amount` results are collected, proceed to calculation
### 2. Grade Calculation Process
1. **Service Request**: FinalGradeDeterminator calls GradeCalculator service
2. **Calculation**: GradeCalculator computes final grade using business logic
3. **Validation**: Result is validated and clamped to acceptable range
4. **Response**: Calculated grade is returned to FinalGradeDeterminator
### 3. Result Management
1. **Database Storage**: Final grade is persisted in the database
2. **Publication**: Student information is published to management topic
3. **Logging**: Process completion is logged for audit purposes
### 4. Retake Processing Workflow
1. **Retake Request**: RetakeScheduler receives retake requests via action interface
2. **Schedule Management**: System schedules retake exams and manages timelines
3. **Retake Execution**: Student completes retake exam, results are processed
4. **Grade Processing**: RetakeGradeDeterminator calculates retake grades with specialized logic
5. **Integration**: Retake results are integrated with main grade system and database
## Configuration Management
### TOML Configuration Structure
The system uses TOML files for environment-specific configuration:
```toml
[database]
host = "localhost"
port = 5432
name = "grade_db"
user = "grade_user"
password = "secure_password"
[grade_calculation]
collection_amount = 5
min_grade = 10
max_grade = 100
[logging]
level = "INFO"
output_file = "grade_system.log"
```
## Testing
The testing documentation can be found in the [doc/tests](/doc/tests/) folder for each node

View File

@@ -0,0 +1,169 @@
# ROS2 Interface Definitions - g2_2025_interfaces
## Package Overview
This document describes the custom ROS2 interface definitions in the `g2_2025_interfaces` package. These interfaces provide standardized communication protocols for the TI Minor Grade Generator system.
**Package Name**: `g2_2025_interfaces`
**Interface Types**: Messages, Services, Actions
**Location**: `src/g2_2025_interfaces/`
## Message Types (.msg)
### Exam.msg
Represents examination result data exchanged between system components.
```
string student_name
string course_name
int32 result
builtin_interfaces/Time timestamp
```
**Field Descriptions**:
- `student_name`: Name identifier for the student
- `course_name`: Course identifier for the examination
- `result`: Numerical exam result/grade (integer value)
- `timestamp`: Timestamp of the message
**Usage**: Primary message type for exam result communication between ExamResultGenerator and grade processing nodes.
### Student.msg
Represents student information and course enrollment data.
```
string student_name
string course_name
builtin_interfaces/Time timestamp
```
**Field Descriptions**:
- `student_name`: Name identifier for the student
- `course_name`: Course identifier for enrollment/management
- `timestamp`: Timestamp of the message
**Usage**: Used for student-course management operations and enrollment tracking.
## Service Definitions (.srv)
### Exams.srv
Service interface for grade calculation operations using multiple exam results.
#### Request
```
string student_name
string course_name
int32[] exam_grades
```
#### Response
```
int32 result # Final calculated result
```
**Request Fields**:
- `student_name`: Student identifier for grade calculation
- `course_name`: Course identifier for context
- `exam_grades`: Array of exam grades to be processed
**Response Fields**:
- `result`: Final calculated grade result (integer value)
**Usage**: Main service interface used by GradeCalculator node for processing multiple exam grades into final results.
## Action Definitions (.action)
### Retake.action
Action interface for managing retake exam scheduling and processing workflows.
#### Goal
```
string student_name
string course_name
```
#### Result
```
float32 result
```
#### Feedback
```
string status
```
**Goal Fields**:
- `student_name`: Student for which a retake is requested
- `course_name`: Course for which a retake is requested
**Result Field**
- `result`: Progress indicator or intermediate result (float value) (*Not Implemented*)
**Feedback Field**:
- `status`: Status indicator (*Not Implemented*)
**Usage**: Used by RetakeScheduler node to handle long-running retake management operations with progress feedback.
## Node Interface Usage
### ExamResultGenerator Node
**Publishers**:
- **Topic**: `exam_results`
- **Message Type**: `g2_2025_interfaces::msg::Exam`
- **Purpose**: Publishes generated exam results to downstream processing nodes
- **Rate**: Configurable interval (default: 2 seconds)
**Subscribers**:
- **Topic**: `student_course_management`
- **Message Type**: `g2_2025_interfaces::msg::Student`
- **Purpose**: Receives student-course enrollment updates for exam generation queue
### FinalGradeDeterminator Node
**Subscribers**:
- **Topic**: `exam_results`
- **Message Type**: `g2_2025_interfaces::msg::Exam`
- **Purpose**: Collects exam results for grade calculation processing
**Service Clients**:
- **Service**: `calculate_grade`
- **Service Type**: `g2_2025_interfaces::srv::Exams`
- **Purpose**: Requests grade calculation from GradeCalculator when threshold is met
**Publishers**:
- **Topic**: `student_course_management`
- **Message Type**: `g2_2025_interfaces::msg::Student`
- **Purpose**: Communicate end of enrollment when grade has been calculated
### GradeCalculator Node
**Service Servers**:
- **Service**: `calculate_grade`
- **Service Type**: `g2_2025_interfaces::srv::Exams`
- **Purpose**: Provides grade calculation services for multiple exam grades
- **Processing**: Applies business logic (averaging, bonus points, validation)
### RetakeScheduler Node
**Action Servers**:
- **Action**: `retake_request`
- **Action Type**: `g2_2025_interfaces::action::Retake`
- **Purpose**: Handles long-running retake scheduling operations
- **Feedback**: Provides progress updates during scheduling process
### RetakeGradeDeterminator Node
**Service Clients**:
- **Service**: `calculate_grade`
- **Service Type**: `g2_2025_interfaces::srv::Exams`
- **Purpose**: Requests specialized retake grade calculations
**Subscribers**:
- **Topic**: `retake_results`
- **Message Type**: `g2_2025_interfaces::msg::Exam`
- **Purpose**: Processes completed retake exam results

View File

@@ -130,5 +130,4 @@ ssl = false
[database.pool]
min_connections = 1
max_connections = 10
```

View File

@@ -7,6 +7,10 @@ grades, and publishes them to the system.
#### Implementation Details
**Parameters**
- **`delay_between_grades_ms`** (int, default: 2000): Delay (in milliseconds) between generated grades.
**Constructor**
```cpp
ExamResultGenerator()

View File

@@ -0,0 +1,39 @@
# FinalGradeDeterminator (`assignments::one::final_grade_determinator`)
## Overview
The `FinalGradeDeterminator` node collects exam results for student-course combinations, triggers grade calculation when enough results are gathered, and stores final grades in the database. It interacts with ROS2 publishers, subscribers, and service clients to manage the grading workflow.
#### Implementation Details
**Parameters**
- **`grade_collection_amount`** (int, default: 5): Number of exam results required before triggering grade calculation for a student-course combination.
**Constructor**
```cpp
FinalGradeDeterminator()
```
- Initializes ROS2 node with name `final_grade_determinator`
- Declares and retrieves `grade_collection_amount` parameter
- Sets up `DatabaseManager`
- Creates publisher for student course management
- Subscribes to exam results topic
- Initializes service client for grade calculation
**Core Functions**
**`void exam_results_callback(const g2_2025_interfaces::msg::Exam::SharedPtr msg)`**
- Updates internal map with received exam result for student-course combo
- Checks if enough results have been collected
- Triggers grade calculation request when threshold is met
**`void grade_calculator_request(StudentCourse combo)`**
- Waits for grade calculator service to be available
- Sends async request with collected exam grades for the student-course combination
- Uses callback to handle service response
**`void grade_calculator_response(rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedFuture future, StudentCourse studentCourseCombo)`**
- Verifies database connection
- Publishes final student message to ROS2 topic
- Logs final grade information
- Stores final course result in database

View File

@@ -0,0 +1,25 @@
# GradeCalculator (`assignments::one::grade_calculator::GradeCalculator`)
## Overview
The `GradeCalculator` node provides a ROS2 service for calculating student exam grades. It processes exam scores, applies custom logic for specific student names, and ensures grade results are within valid bounds.
#### Implementation Details
**Constructor**
```cpp
GradeCalculator()
```
- Initializes ROS2 node with name `grade_calculator`
- Creates a ROS2 service server for `grade_calculator_service`
- Binds the service callback to handle grade calculation requests
- Logs service startup
**Core Functions**
**`void grade_calculator_callback(const Exams::Request::SharedPtr request, const Exams::Response::SharedPtr response)`**
- Checks if exam grades are provided
- Calculates the total and average of exam grades
- Converts student name to lowercase for comparison
- Adds a bonus of 10 points if the student name is "wessel"
- Ensures the final grade is clamped between 10 and 100
- Sends the calculated grade back through the service response

View File

@@ -0,0 +1,60 @@
# RetakeGradeDeterminator (`assignments::one::retake_grade_determinator`)
## Overview
The `RetakeGradeDeterminator` node handles the processing of retake exams for students who have
failed courses. It provides a ROS2 action server that accepts retake requests, manages exam result
collection for retake scenarios, and coordinates with the grade calculator service to determine
final retake grades.
#### Implementation Details
**Parameters**
- **`grade_collection_amount`** (int, default: 5): Number of exam results required before triggering grade calculation for a retake student-course combination.
**Constructor**
```cpp
RetakeGradeDeterminator(std::unique_ptr<DatabaseManager> db_manager = nullptr)
```
- Initializes ROS2 node with name `retake_grade_determinator`
- Sets up `DatabaseManager` (optional injection for testing)
- Creates publisher for student course management
- Subscribes to exam results topic with retake-specific callback
- Initializes service client for grade calculation
- Creates ROS2 action server for retake requests with reentrant callback group
**Core Functions**
**`void exam_results_callback(const g2_2025_interfaces::msg::Exam::SharedPtr msg)`**
- Only processes exam results when `retake_allowed_` flag is true
- Triggers grade calculation request when threshold is met
**`void grade_calculator_request(StudentCourse combo)`**
- Waits for grade calculator service to be available
- Sends async request with collected retake exam grades
**`void grade_calculator_response(rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedFuture future, StudentCourse studentCourseCombo)`**
- Clears collected exam data from internal map
- Updates retake status in database for the student-course combination
- Publishes final student message to ROS2 topic with timestamp
- Stores final course result in database with retake flag set to true
**Action Server Callbacks**
**`rclcpp_action::GoalResponse goal_callback(const rclcpp_action::GoalUUID& uuid, std::shared_ptr<const g2_2025_interfaces::action::Retake::Goal> goal)`**
- Accepts retake goal requests for specific student-course combinations
- Logs received retake requests with student and course information
- Returns `ACCEPT_AND_EXECUTE` for all valid requests
**`rclcpp_action::CancelResponse cancel_callback(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle)`**
- Handles retake action cancellation requests
- Returns `ACCEPT` for all cancellation requests
**`void spawn_callback_thread(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle)`**
- Creates detached thread for asynchronous retake action execution
- Ensures non-blocking retake processing
**`void async_execute_callback_thread(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle)`**
- Enables retake exam processing by setting `retake_allowed_` flag
- Publishes student enrollment message to trigger exam generation
- Creates and returns successful action result

View File

@@ -0,0 +1,47 @@
# RetakeScheduler (`assignments::one::retake_scheduler`)
## Overview
The `RetakeScheduler` node automatically identifies students who have failed courses and schedules retake exams for them. It periodically queries the database for failing students and sends retake action requests to the `RetakeGradeDeterminator` node to initiate the retake process.
#### Implementation Details
**Parameters**
- **`retake_check_interval_sec`** (int, default: 120): Time interval in seconds between checks for failing students requiring retakes.
**Constructor**
```cpp
RetakeScheduler(std::unique_ptr<DatabaseManager> db_manager = nullptr)
```
- Initializes ROS2 node with name `retake_scheduler`
- Sets up `DatabaseManager` (optional injection for testing)
- Creates ROS2 action client for retake action communication
- Initializes wall timer for periodic failing student checks
**Core Functions**
**`void check_failing_students()`**
- Timer callback function executed periodically based on `retake_check_interval_sec`
- Queries database for all failed course results
- Iterates through failing students and initiates retake requests
**`void send_retake_request(StudentCourse student_course_combo)`**
- Creates retake action goal with student and course information
- Sends asynchronous retake goal to `RetakeGradeDeterminator`
**`void cancel_retake_request()`**
- Cancels all active retake goals
- Provides emergency stop functionality for retake processing
**Action Client Callbacks**
**`void request_response_callback(const rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr &goal_handle)`**
- Handles retake action server responses
- Logs goal acceptance or rejection status
- Provides feedback on retake request processing state
**`void request_result_callback(const rclcpp_action::ClientGoalHandle<RetakeAction>::WrappedResult &result)`**
- Processes final retake action results
- Logs appropriate messages based on retake completion status
**`void request_feedback_callback(rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr goal_handle, const std::shared_ptr<const RetakeAction::Feedback> feedback)`**

View File

@@ -0,0 +1,43 @@
## Installation
### Prerequisites
- ROS2 Jazzy or newer installed ([ROS2 Installation Guide](https://docs.ros.org/en/jazzy/Installation.html))
- CMake (version 3.8+)
- Python 3.8+
- libtomlplusplus-dev
- libpqxx-dev
- Colcon build tool
- Docker compose
### Clone the Repository
```bash
git clone https://git.wessel.gg/inholland/ros2-assignments.git
cd ros2-assignments
```
### Build the Workspace
```bash
colcon build
```
Any parameters can be changed before building by editing the `grade_calculator.launch.xml` in the launch folder
### Source the Workspace
```bash
source install/setup.bash
```
### Start the database
```bash
sudo docker-compose up
```
You can configure specific database settings in the `docker-compose.yaml` in the root folder or the `config.toml` file in the `src/` folder
### Start the Grade calculator program
```bash
ros2 launch g2_2025_grade_calculator_pkg grade_calculator.launch.xml
```
To change parameters when using the launch file it will need to be edited in the `src/g2_2025_grade_calculator_pkg/launch` folder. All parameters are already added to this document and thus only the values will need to be changed

View File

@@ -1,16 +0,0 @@
# Interface Overview
**Publishers**
- Topic: `exam_results`
- Message Type: `g2_2025_interfaces::msg::Exam`
- Publishes generated exam results to downstream nodes
**Subscribers**
- Topic: `student_course_management`
- Message Type: `g2_2025_interfaces::msg::Student`
- Handles requests to add/remove student-course combinations
**Timers**
- Interval: 2 seconds
- Function: `generate_random_result()`
- Generates and publishes exam results at regular intervals

View File

@@ -0,0 +1,79 @@
# Config Manager Unit Tests
Unit tests for `ConfigManager` are implemented in `src/g2_2025_grade_calculator_pkg/test/ConfigManager.test.cpp` using Google Test and ROS2 test utilities. The tests use temporary TOML files to validate configuration loading and parsing functionality.
## Test Cases
### 1. ConstructorTest
**Description:** Verifies that ConfigManager can be created with a ROS2 logger without crashing.
- **Test Action:** Create ConfigManager instance with ROS2 logger
- **Expected Result:** Instance created successfully without exceptions
### 2. LoadValidConfigTest
**Description:** Tests loading of valid TOML configuration files with proper parsing.
- **Test Action:**
- Create temporary TOML file with valid configuration
- Call `load_config()` with file path
- **Expected Result:**
- Returns `true`
- `is_loaded()` returns `true`
### 3. LoadInvalidFileTest
**Description:** Tests error handling when attempting to load non-existent configuration files.
- **Test Action:** Call `load_config()` with non-existent file path
- **Expected Result:**
- Returns `false`
- `is_loaded()` returns `false`
### 4. DatabaseConfigParsingTest
**Description:** Tests complete database configuration parsing with all parameters.
- **Test Configuration:**
```toml
[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
```
- **Expected Result:** All configuration values parsed correctly with proper types
### 5. DatabaseConfigWithoutPoolTest
**Description:** Tests default values when optional pool section is missing from configuration.
- **Test Configuration:**
```toml
[database]
host = "localhost"
port = 5432
dbname = "grades"
user = "postgres"
password = "postgres"
```
- **Expected Result:**
- Main database config parsed correctly
- Default pool values: `min_connections = 1`, `max_connections = 10`
### 6. GetConfigWithoutLoadingTest
**Description:** Tests behavior when attempting to access configuration before loading any file.
- **Test Action:** Call `get_database_config()` without loading configuration
- **Expected Result:**
- Returns `std::nullopt`
- `is_loaded()` returns `false`

View File

@@ -0,0 +1,63 @@
# Database Manager Unit Tests
Unit tests for `DatabaseManager` are implemented in `src/g2_2025_grade_calculator_pkg/test/DatabaseManager.test.cpp` using Google Test and ROS2 test utilities. The tests are designed to work reliably whether a database connection is available or not, focusing on error handling and method behavior validation.
## Test Cases
### 1. ConstructorTest
**Description:** Verifies that DatabaseManager can be created without crashing and proper initialization occurs.
- **Test Action:** Create DatabaseManager instance with ROS2 logger
- **Expected Result:** Instance created successfully without exceptions
### 2. ConnectionStatusTest
**Description:** Tests that the `is_connected()` method returns a valid boolean value.
- **Test Action:** Call `is_connected()` method
- **Expected Result:** Returns either `true` or `false` (no crashes or invalid states)
### 3. QueuePendingCombinationsTest
**Description:** Verifies retrieval of pending student-course combinations that need exam results.
- **Test Action:** Call `queue_pending_combinations()`
- **Expected Result:** Returns valid vector (empty if no database connection, populated if connected)
### 4. StoreExamResultTest
**Description:** Tests exam result storage with graceful handling of connection states.
- **Test Action:**
- Student: `"TestStudent"`
- Course: `"TestCourse"`
- Grade: `85`
- **Expected Result:** Returns `false` if no connection, `true` if connected and successful
### 5. EnrollStudentTest
**Description:** Tests student enrollment into courses.
- **Test Action:**
- StudentCourse object with test data
- **Expected Result:** Returns `false` if no connection, `true` if connected and successful
### 6. GetFinalGradeTest
**Description:** Tests final grade retrieval for non-existent student-course combinations.
- **Test Action:**
- Student: `"NonExistentStudent"`
- Course: `"NonExistentCourse"`
- **Expected Result:** Returns `-1` (no results found or no connection)
### 7. StoreFinalResultTest
**Description:** Tests storing calculated final course results.
- **Test Action:**
- StudentCourse object
- Exam count: `3`
- Final grade: `75`
- **Expected Result:** Returns `false` if no connection, `true` if connected and successful

View File

@@ -0,0 +1,62 @@
# Exam Result Generator Node Unit Tests
Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calculator_pkg/test/ExamResultGenerator.test.cpp` using Google Test and ROS2 test utilities. The tests use a test subscriber node to capture published messages and validate node behavior.
## Test Cases
### 1. ConstructorTest
**Description:** Verifies that ExamResultGenerator can be created without crashing and proper initialization occurs.
- **Test Action:** Create ExamResultGenerator node instance
- **Expected Result:**
- Node created successfully without exceptions
- Node name set to `"exam_result_generator"`
### 2. PublisherCreationTest
**Description:** Verifies that the `exam_results` topic publisher is properly configured.
- **Test Action:** Check topic names and types after node creation
- **Expected Result:**
- `/exam_results` topic is published
- Topic uses `g2_2025_interfaces/msg/Exam` message type
### 3. SubscriberCreationTest
**Description:** Tests that the node subscribes to the `student_course_management` topic with correct message type.
- **Test Action:** Check subscription topics and types
- **Expected Result:**
- `/student_course_management` topic is subscribed
- Subscription uses `g2_2025_interfaces/msg/Student` message type
### 4. StudentManagementMessageHandlingTest
**Description:** Tests the node's ability to handle incoming student management messages without crashing.
- **Test Action:**
- Publish student management message:
- Student: `"Test Student"`
- Course: `"Test Course"`
- **Expected Result:** Message processed without crashes
### 5. MultipleStudentMessagesTest
**Description:** Validates handling of multiple rapid student management messages for robustness testing.
- **Test Action:**
- Send multiple messages with different student-course combinations
- Students: `["Alice", "Bob", "Charlie"]`
- Courses: `["Math", "Physics", "Chemistry"]`
- **Expected Result:** All messages processed without crashes or memory issues
### 6. ExamMessageValidationTest
**Description:** Captures and validates published exam result messages for correct content and format.
- **Test Action:** Listen for published exam result messages over 6 seconds
- **Expected Result:**
- Published grades are within range `[10, 100]`
- Course names are not empty
- Message format is correct

View File

@@ -0,0 +1,52 @@
# Final Grade Determintator Node Unit Tests
Unit tests for `FinalGradeDeterminator` are implemented in `src/g2_2025_grade_calculator_pkg/test/FinalGradeDeterminator.test.cpp` using Google Test and ROS2 test utilities. The tests use a mock database manager and a mock grade calculator service to simulate the node's interactions.
## Test Cases
### 1. ConstructorTest
**Description:** Verifies that the node can be constructed without errors.
- **Input:** Construct a `FinalGradeDeterminator` node.
- **Expected Output:** No exceptions are thrown. The node is created successfully.
### 2. ExamCollectionTest
**Description:** Sends the required number of exam results and checks that a grade calculation request is triggered, a student message is published, and the results are stored.
- **Input:**
- Student: `"Test Student"`
- Course: `"Test Course"`
- Grades published: `[80, 85, 90, 75, 95]` (5 exam results, which is the default required amount)
- **Expected Output:**
- A grade calculation service request is made with the correct student, course, and grades.
- A student message is published with the correct student and course.
- The final grade is calculated as the average: (80 + 85 + 90 + 75 + 95) / 5 = 85.
### 3. PartialExamCollectionTest
**Description:** Sends fewer than the required number of exam results and checks that no grade calculation or student message occurs.
- **Input:**
- Student: `"Test Student"`
- Course: `"Test Course"`
- Grades published: `[80, 85, 90]` (only 3 exam results, less than required)
- **Expected Output:**
- No grade calculation service request is made.
- No student message is published.
### 4. MultipleStudentsTest
**Description:** Simulates multiple students submitting exam results and verifies that each student triggers independent grade calculation and messaging.
- **Input:**
- Students: `"Alice"`, `"Bob"`
- Course: `"Test Course"`
- Each student receives grades: `[80, 85, 90, 75, 95]` (5 exam results per student)
- **Expected Output:**
- Two grade calculation service requests are made, one for each student, with the correct grades.
- Two student messages are published, one for each student.
- The final grade for each student is calculated as the average: (80 + 85 + 90 + 75 + 95) / 5 = 85.
These tests ensure that the node correctly collects exam results, triggers grade calculation at the right time, publishes the appropriate messages, and interacts with the database as expected.

View File

@@ -0,0 +1,56 @@
# Grade Calculator Node Unit Tests
Unit tests for `GradeCalculator` are implemented in `src/g2_2025_grade_calculator_pkg/test/GradeCalculator.test.cpp` using Google Test and ROS2 test utilities. The tests use a client to call the grade calculation service and verify the results.
## Test Cases
### 1. NormalAverage
**Description:** Verifies that the service returns the average of the provided grades for a normal student name.
- **Input:**
- Student: `"Alice"`
- Grades: `[80, 90, 70]`
- **Expected Output:**
- Result: `80` (average of 80, 90, 70)
### 2. WesselBonus
**Description:** Checks that the student name "Wessel" (case-insensitive) receives a 10-point bonus, and the result is clamped to 100 if necessary.
- **Input:**
- Student: `"Wessel"`
- Grades: `[80, 90, 70]`
- **Expected Output:**
- Result: `90` (average 80 + 10 bonus)
#### 3. wesselBonus
**Description:** Checks that the bonus logic is case-insensitive for the student name "wessel".
- **Input:**
- Student: `"wessel"`
- Grades: `[80, 90, 70]`
- **Expected Output:**
- Result: `90` (average 80 + 10 bonus)
#### 4. GradeTooHigh
**Description:** Ensures that the grade is clamped to a maximum of 100, even after applying the bonus.
- **Input:**
- Student: `"Wessel"`
- Grades: `[100, 100, 100]`
- **Expected Output:**
- Result: `100` (average 100 + 10 bonus, clamped to 100)
#### 5. GradeTooLow
**Description:** Ensures that the grade is clamped to a minimum of 10.
- **Input:**
- Student: `"Alice"`
- Grades: `[0, 0, 0]`
- **Expected Output:**
- Result: `10` (average 0, clamped to 10)

View File

@@ -0,0 +1,151 @@
# Integration Test
Tests the whole integrated system `nodes` connected with each other, with supervisory control and visualization of the database. It tests all major components working together: exam result generation, calculation, database usage, and retake handling.
For validation and verification of database data the databse viewer Dbeaver can be used.
## Test Input Data
**Student Enrollments:**
- `"Wessel", "ROS2"`
- `"Vincent", "ROS2"`
- `"Mohammed", "Differentieren"`
- `"Tilmann", "Differentieren"`
## Test workflow
### 1. Complete Grade Processing Pipeline
**Description**: Tests the full workflow from student enrollment through exam generation to final grade calculation.
**Steps:**
1. **Setup Phase**
- Clear the database via `docker container prune` or by deleting the tables in dbeaver.
- Start the databse in a standalone terminal via the command `sudo docker compose up`.
- Verify database connection.
- Initialize all system nodes (ExamResultGenerator, FinalGradeDeterminator, GradeCalculator) after building tha package and starting it via the `grade_calculator.launch.xml`.
2. **Exam Generation Phase**
- ExamResultGenerator publishes Exam messages per student-course pair to table `exam_results` topic.
- Verify exam results have valid grades (10-100 range).
- Confirm proper timestamp and student/course name matching.
3. **Grade Collection Phase**
- FinalGradeDeterminator collects exam results for each student-course combination.
- Monitor collection count reaches threshold (5 exams per student-course by default) for each student.
4. **Grade Calculation Phase**
- FinalGradeDeterminator calls GradeCalculator service when threshold reached.
- Service calculates average grade.
5. **Persistence and Notification Phase**
- Final grades stored in database with proper timestamps.
- verify that final_grade_determinator has sent a de-enroll request via student_course_managment to exam_result_generator.
- exam_result_generator should now prints `removed from queue`.
- Verify final grade is within valid bounds (10-100) in table `final_course_results`.
**Expected Results:**
```text
Student: Wessel, Course: ROS2
- 5 exam results generated (random grades 10-100)
- Avrage final grade calculated
- Grade stored in database
Student: Vincent, Course: ROS2
- 5 exam results generated (random grades 10-100)
- Avrage final grade calculated
- Grade stored in database
Student: Mohammed, Course: Differentieren
- 5 exam results generated (random grades 10-100)
- Avrage final grade calculated
- Grade stored in database
Student: Tilmann, Course: Differentieren
- 5 exam results generated (random grades 10-100)
- Avrage final grade calculated
- Grade stored in database
```
### 2. Retake Process Integration
**Description**: Tests the retake workflow for students who need to retake exams.
**Setup:**
no setup required, this starts immediately after `final_grade_diterminator` is done after getting the first grades for the students.
**Steps:**
1. **Varify Retake Schedualer is started**
- Verify in the terminal the schedualer starts the retake process.
2. **Retake Request Phase**
- RetakeScheduler checks the database for failing students
- Schedules retake exams with `retake_grade_determinator`
3. **Retake Execution Phase**
- Generate retake exam results
- `retake_grade_determinator` processes retake grades and sends them to the calcculator.
- `retake_grade_determinator` finaly stores the calculated result in the database.
4. **Final Grade Update**
- Verify grade history preservation
- Confirm final grade reflects retake performance
**Failing student Expected Retake Results:**
```text
Student: student, Course: course
- 5 exam results generated (random grades 10-100)
- Avrage final grade calculated
- Grade stored in database
- Retake_done checkmark
- Is_retake checkmark
```
## Success Criteria
### Functional Requirements
All student enrollments processed correctly
Exam results generated within valid range (10-100)
Grade collection reaches configured thresholds
Final grades calculated using proper business logic
Database persistence works reliably
Retake process functions correctly when needed
All ROS2 communication protocols working
## Test Execution Commands
```bash
# 1. enter the ros2 assignment folder
cd /YOUR/PATH/HERE/ros2-assignment
# 2. Build the packages and source the enviroment
colcon build && source install/setup.bash
# 3. clear and Start database (if not running) in stand alone terminal
sudo docker container prune && sudo docker compose up
# 4. Launch all system nodes
ros2 launch g2_2025_grade_calculator_pkg grade_calculator.launch.xml
# 6. Monitor database state
By opening dbeaver and connecting to the database "grades".
Password: postgres
```
## Verification
### Database Validation
- **Student Records**: Verify all students are enrolled correctly
- **Exam Results**: Confirm all exam data is good
- **Final Grades**: Validate calculated grades and timestamps

View File

@@ -0,0 +1,70 @@
# RetakeGradeDeterminator Node Unit Tests
This document describes the unit tests for the `RetakeGradeDeterminator` ROS 2 node, as implemented in [`RetakeGradeDeterminator.test.cpp`](../../src/g2_2025_grade_calculator_pkg/test/RetakeGradeDeterminator.test.cpp).
## Test Cases
### 1. ConstructorTest
- **Purpose:**
Verifies that the `RetakeGradeDeterminator` can be constructed without throwing exceptions.
- **Checks:**
- The node is created successfully and is not `nullptr`.
### 2. PublisherCreationTest
- **Purpose:**
Ensures that the node creates a publisher for the `student_course_management` topic.
- **Checks:**
- The `/student_course_management` topic is present and uses the `Student` message type.
### 3. SubscriberCreationTest
- **Purpose:**
Ensures that the node subscribes to the `exam_results` topic.
- **Checks:**
- The `/exam_results` topic is present and uses the `Exam` message type.
### 4. ActionServerCreationTest
- **Purpose:**
Verifies that the node creates an action server for the `retake_action`.
- **Checks:**
- The action server is available and can be connected to by an action client.
### 5. ServiceClientCreationTest
- **Purpose:**
Ensures that the node creates a client for the `grade_calculator_service`.
- **Checks:**
- The `/grade_calculator_service` is present in the node's service graph.
### 6. ParameterTest
- **Purpose:**
Confirms that the node's parameters are set to their expected default values.
- **Checks:**
- The `grade_collection_amount` parameter defaults to `5`.
### 7. ExamResultsIgnoredWhenRetakeNotAllowed
- **Purpose:**
Ensures that exam results are ignored if a retake is not currently allowed.
- **Checks:**
- No service requests are made when a retake is not in progress.
### 8. RetakeActionGoalAcceptance
- **Purpose:**
Verifies that the node accepts retake action goals and publishes the appropriate student enrollment message.
- **Checks:**
- The retake goal is accepted.
- A `Student` message is published with the correct student and course names.
### 9. PartialExamResultsCollection
- **Purpose:**
Ensures that the node does not process or store results if not enough exam results have been collected during a retake.
- **Checks:**
- No service requests are made.
- No results are stored in the database if the required number of exam results has not been reached.

View File

@@ -0,0 +1,50 @@
# RetakeScheduler Node Unit Tests
Unit tests for `RetakeScheduler` are implemented in `src/g2_2025_grade_calculator_pkg/test/RetakeScheduler.test.cpp`
The unit tests are written using Google Test and ROS 2 test utilities. They utilize clients to interact
with the RetakeScheduler node's services and actions, verifying correct behavior and expected outcomes.
## Test Cases
### 1. ConstructorTest
- **Purpose:**
Verifies that the `RetakeScheduler` can be constructed without throwing exceptions.
- **Checks:**
- The node is created successfully and is not `nullptr`.
### 2. ActionClientCreationTest
- **Purpose:**
Ensures that the `RetakeScheduler` node creates the expected action client services for the retake action.
- **Checks:**
- At least one of the retake action client services (`/retake_action/_action/cancel_goal`, `/retake_action/_action/get_result`, `/retake_action/_action/send_goal`) is present in the node's service graph.
### 3. ParameterTest
- **Purpose:**
Confirms that the node's parameters are set to their expected default values.
- **Checks:**
- The `retake_check_interval_sec` parameter defaults to `120`.
### 4. ActionClientAllServicesPresentTest
- **Purpose:**
Verifies that all required action client services for the retake action are available.
- **Checks:**
- The following services are present:
- `/retake_action/_action/cancel_goal`
- `/retake_action/_action/get_result`
- `/retake_action/_action/send_goal`
### 5. ConstructorWithNullDatabaseManager
- **Purpose:**
Tests the node's behavior when constructed with a `nullptr` database manager.
- **Checks:**
- The node can be constructed without throwing, even if the database manager is `nullptr`.

View File

@@ -19,6 +19,8 @@ fetchcontent_makeavailable(tomlplusplus)
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(std_msgs REQUIRED)
find_package(g2_2025_interfaces REQUIRED)
add_executable(exam_result_generator
@@ -56,13 +58,46 @@ ament_target_dependencies(grade_calculator rclcpp g2_2025_interfaces)
add_executable(retake_grade_determinator
src/retake_grade_determinator/main.cpp
src/database/DatabaseManager.cpp
src/config/ConfigManager.cpp
src/retake_grade_determinator/nodes/RetakeGradeDeterminator.cpp
)
ament_target_dependencies(retake_grade_determinator rclcpp g2_2025_interfaces)
ament_target_dependencies(retake_grade_determinator rclcpp_action rclcpp g2_2025_interfaces)
target_link_libraries(retake_grade_determinator pqxx pq tomlplusplus::tomlplusplus)
add_executable(retake_scheduler
src/retake_scheduler/main.cpp
src/database/DatabaseManager.cpp
src/config/ConfigManager.cpp
src/retake_scheduler/nodes/RetakeScheduler.cpp
)
ament_target_dependencies(retake_scheduler rclcpp_action rclcpp g2_2025_interfaces)
target_link_libraries(retake_scheduler pqxx pq tomlplusplus::tomlplusplus)
target_include_directories(retake_scheduler PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_scheduler
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_grade_calculator_pkg/database
)
target_include_directories(retake_scheduler PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_scheduler
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_grade_calculator_pkg/config
)
target_include_directories(retake_grade_determinator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_grade_determinator
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_grade_calculator_pkg/database
)
target_include_directories(retake_grade_determinator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_grade_determinator
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_grade_calculator_pkg/config
)
ament_target_dependencies(retake_scheduler rclcpp g2_2025_interfaces)
install(
TARGETS
@@ -74,6 +109,11 @@ install(
DESTINATION lib/${PROJECT_NAME}
)
install(
DIRECTORY launch
DESTINATION share/${PROJECT_NAME}/
)
if(BUILD_TESTING)
find_package(ament_cmake_gtest REQUIRED)
@@ -127,6 +167,85 @@ if(BUILD_TESTING)
target_link_libraries(${PROJECT_NAME}_test_exam_result_generator
pqxx pq tomlplusplus::tomlplusplus
)
# Add gtest for GradeCalculator
ament_add_gtest(${PROJECT_NAME}_test_grade_calculator
test/GradeCalculator.test.cpp
src/grade_calculator/nodes/GradeCalculator.cpp
)
target_include_directories(${PROJECT_NAME}_test_grade_calculator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/grade_calculator
)
ament_target_dependencies(${PROJECT_NAME}_test_grade_calculator
rclcpp
g2_2025_interfaces
)
# Add gtest for FinalGradeDeterminator
ament_add_gtest(${PROJECT_NAME}_test_final_grade_determinator
test/FinalGradeDeterminator.test.cpp
src/final_grade_determinator/nodes/FinalGradeDeterminator.cpp
src/database/DatabaseManager.cpp
src/config/ConfigManager.cpp
)
target_include_directories(${PROJECT_NAME}_test_final_grade_determinator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/final_grade_determinator
)
ament_target_dependencies(${PROJECT_NAME}_test_final_grade_determinator
rclcpp
g2_2025_interfaces
)
target_link_libraries(${PROJECT_NAME}_test_final_grade_determinator
pqxx pq tomlplusplus::tomlplusplus
)
# 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

@@ -0,0 +1,14 @@
<launch>
<node pkg="g2_2025_grade_calculator_pkg" exec="exam_result_generator">
<param name="delay_between_grades_ms" value="2000"/>
</node>
<node pkg="g2_2025_grade_calculator_pkg" exec="final_grade_determinator">
<param name="grade_collection_amount" value="5"/>
</node>
<node pkg="g2_2025_grade_calculator_pkg" exec="grade_calculator"/>
<node pkg="g2_2025_grade_calculator_pkg" exec="retake_grade_determinator"/>
<node pkg="g2_2025_grade_calculator_pkg" exec="retake_scheduler">
<param name="retake_check_interval_sec" value="120"/>
</node>
</launch>

View File

@@ -91,7 +91,9 @@ std::vector<StudentCourse> DatabaseManager::queue_pending_combinations() {
}
bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) {
if (!is_connected()) return false;
if (!is_connected()) {
return false;
}
try {
pqxx::work txn(*conn_);
@@ -110,6 +112,28 @@ bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) {
}
}
bool DatabaseManager::remove_student_from_course(const StudentCourse& sc) {
if (!is_connected()) {
return false;
}
try {
pqxx::work txn(*conn_);
txn.exec_params(SQL_DELETE_STUDENT_ENROLLMENT, sc.student_name, sc.course_name);
txn.commit();
return true;
} catch (const pqxx::sql_error &e) {
RCLCPP_ERROR(logger_, "[DBS] unenroll student from course failed: %s", e.what());
return false;
} catch (const std::exception &e) {
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
return false;
}
}
bool DatabaseManager::store_exam_result(const std::string& student_name, const std::string& course_name, int grade) {
if (!is_connected()) return false;
@@ -136,7 +160,8 @@ bool DatabaseManager::store_exam_result(const std::string& student_name, const s
bool DatabaseManager::store_final_course_result(
const StudentCourse& sc,
int exam_count,
int final_grade
int final_grade,
bool is_retake
) {
if (!is_connected()) return false;
@@ -145,7 +170,7 @@ bool DatabaseManager::store_final_course_result(
txn.exec_params(
SQL_INSERT_FINAL_COURSE_RESULT,
sc.student_name, sc.course_name, exam_count, final_grade
sc.student_name, sc.course_name, exam_count, final_grade, is_retake
);
txn.commit();
@@ -182,16 +207,20 @@ void DatabaseManager::create_tables() {
}
void DatabaseManager::insert_sample_data() {
if (!is_connected()) return;
if (!is_connected()) {
return;
}
try {
pqxx::work txn(*conn_);
// Check if we already have data
auto result = txn.exec(SQL_SELECT_STUDENT_LIST);
int count = result[0][0].as<int>();
auto student_result = txn.exec(SQL_SELECT_STUDENT_LIST);
int student_count = student_result[0][0].as<int>();
if (count == 0) {
auto grades_result = txn.exec(SQL_SELECT_EXAM_RESULTS_COUNT);
int grades_count = grades_result[0][0].as<int>();
if (student_count == 0 && grades_count == 0) {
for (const auto& [student, course] : sample_students_data_) {
txn.exec_params(SQL_INSERT_STUDENT_ENROLLMENT, student, course);
}
@@ -213,7 +242,7 @@ int DatabaseManager::get_final_course_grade(const StudentCourse& sc) {
try {
pqxx::work txn(*conn_);
auto result = txn.exec_params(SQL_SELECT_STUDENT_COURSE_RESULTS, sc.student_name, sc.course_name);
auto result = txn.exec_params(SQL_SELECT_STUDENT_EXAM_RESULTS, sc.student_name, sc.course_name);
if (result.size() == 1) {
int exam_count = result[0][0].as<int>();
@@ -235,4 +264,54 @@ int DatabaseManager::get_final_course_grade(const StudentCourse& sc) {
}
}
std::vector<StudentCourse> DatabaseManager::get_failed_course_results() {
std::vector<StudentCourse> failed_courses;
if (!is_connected()) {
return failed_courses;
}
try {
pqxx::work txn(*conn_);
auto result = txn.exec(SQL_SELECT_FAILED_COURSE_RESULTS);
for (const auto& row : result) {
StudentCourse sc;
sc.student_name = row[0].as<std::string>();
sc.course_name = row[1].as<std::string>();
failed_courses.push_back(sc);
}
RCLCPP_INFO(logger_, "[DBS] found %zu failed course results", failed_courses.size());
} catch (const pqxx::sql_error &e) {
RCLCPP_ERROR(logger_, "[DBS] 'get failed course results' failed: %s", e.what());
} catch (const std::exception &e) {
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
}
return failed_courses;
}
bool DatabaseManager::update_retake_status(const StudentCourse& sc) {
if (!is_connected()) {
return false;
}
try {
pqxx::work txn(*conn_);
txn.exec_params(SQL_UPDATE_RETAKE_STATUS, sc.student_name, sc.course_name);
txn.commit();
return true;
} catch (const pqxx::sql_error &e) {
RCLCPP_ERROR(logger_, "[DBS] 'update retake status' failed: %s", e.what());
return false;
} catch (const std::exception &e) {
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
return false;
}
}
} // namespace assignments::one

View File

@@ -24,10 +24,10 @@ public:
~DatabaseManager() = default;
bool connect(const std::string& connection_string);
bool is_connected() const;
virtual bool is_connected() const;
// Table operations
void init_database();
virtual void init_database();
void create_tables();
void insert_sample_data();
@@ -35,9 +35,17 @@ public:
std::vector<StudentCourse> queue_pending_combinations();
bool store_exam_result(const std::string& student_name, const std::string& course_name, int grade);
bool enroll_student_into_course(const StudentCourse& sc);
bool store_final_course_result(const StudentCourse& sc, int exam_count, int final_grade);
bool remove_student_from_course(const StudentCourse& sc);
virtual bool store_final_course_result(
const StudentCourse& sc,
int exam_count,
int final_grade,
bool is_retake
);
int get_final_course_grade(const StudentCourse& sc);
std::vector<StudentCourse> get_failed_course_results();
bool update_retake_status(const StudentCourse &sc);
private:
rclcpp::Logger logger_;

View File

@@ -12,11 +12,10 @@ static const std::string SQL_CREATE_ENROLLMENTS_TABLE = R"(
id SERIAL PRIMARY KEY,
student_name VARCHAR(100) NOT NULL,
course_name VARCHAR(100) NOT NULL,
is_retake BOOLEAN DEFAULT FALSE,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(student_name, course_name)
);
)";
// UNIQUE(student_name, course_name)
static const std::string SQL_CREATE_EXAM_RESULTS = R"(
CREATE TABLE IF NOT EXISTS exam_results (
@@ -24,7 +23,6 @@ static const std::string SQL_CREATE_EXAM_RESULTS = R"(
student_name VARCHAR(100) NOT NULL,
course_name VARCHAR(100) NOT NULL,
exam_grade INTEGER NOT NULL,
is_retake BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
)";
@@ -37,10 +35,16 @@ static const std::string SQL_CREATE_COURSE_RESULTS = R"(
course_name VARCHAR(100) NOT NULL,
exam_count INTEGER NOT NULL,
final_grade INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(student_name, course_name)
is_retake BOOLEAN DEFAULT FALSE,
retake_done BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
)";
// UNIQUE(student_name, course_name)
static const std::string SQL_SELECT_EXAM_RESULTS_COUNT = R"(
SELECT COUNT(*) FROM exam_results
)";
static const std::string SQL_SELECT_MISSING_RESULTS = R"(
SELECT se.student_name, se.course_name
@@ -51,12 +55,19 @@ static const std::string SQL_SELECT_MISSING_RESULTS = R"(
WHERE fcr.id IS NULL
)";
static const std::string SQL_SELECT_STUDENT_COURSE_RESULTS = R"(
static const std::string SQL_SELECT_STUDENT_EXAM_RESULTS = R"(
SELECT COUNT(*), AVG(exam_grade)
FROM exam_results
WHERE student_name = $1 AND course_name = $2
)";
static const std::string SQL_SELECT_FAILED_COURSE_RESULTS = R"(
SELECT fcr.student_name, fcr.course_name, fcr.final_grade, fcr.exam_count
FROM final_course_results fcr
WHERE fcr.final_grade < 55 AND fcr.retake_done = FALSE
)";
static const std::string SQL_INSERT_EXAM_RESULT = R"(
INSERT INTO exam_results (student_name, course_name, exam_grade)
VALUES ($1, $2, $3)
@@ -70,13 +81,24 @@ static const std::string SQL_INSERT_STUDENT_ENROLLMENT = R"(
ON CONFLICT DO NOTHING
)";
static const std::string SQL_DELETE_STUDENT_ENROLLMENT = R"(
DELETE FROM student_enrollments
WHERE student_name = $1 AND course_name = $2
)";
static const std::string SQL_SELECT_STUDENT_LIST = R"(
SELECT COUNT(*) FROM student_enrollments
)";
static const std::string SQL_UPDATE_RETAKE_STATUS = R"(
UPDATE final_course_results
SET retake_done = TRUE
WHERE student_name = $1 AND course_name = $2 AND retake_done = FALSE
)";
static const std::string SQL_INSERT_FINAL_COURSE_RESULT = R"(
INSERT INTO final_course_results (student_name, course_name, exam_count, final_grade)
VALUES ($1, $2, $3, $4)
INSERT INTO final_course_results (student_name, course_name, exam_count, final_grade, is_retake)
VALUES ($1, $2, $3, $4, $5)
)";
// ON CONFLICT (student_name, course_name)
// DO UPDATE SET exam_count = EXCLUDED.exam_count, final_grade = EXCLUDED.final_grade, created_at = CURRENT_TIMESTAMP

View File

@@ -72,6 +72,7 @@ void ExamResultGenerator::generate_random_result() {
exam_msg.student_name = selected.student_name;
exam_msg.course_name = selected.course_name;
exam_msg.result = grade;
exam_msg.timestamp = this->get_clock()->now();
exam_publisher_->publish(exam_msg);
@@ -90,6 +91,7 @@ void ExamResultGenerator::student_management_callback(const g2_2025_interfaces::
if (it != operations_queue_.end()) {
operations_queue_.erase(it);
db_manager_->remove_student_from_course(sc);
RCLCPP_INFO(this->get_logger(), "removed from queue: %s - %s",
sc.student_name.c_str(), sc.course_name.c_str()
);

View File

@@ -2,12 +2,16 @@
namespace assignments::one::final_grade_determinator {
FinalGradeDeterminator::FinalGradeDeterminator() : Node("final_grade_determinator") {
FinalGradeDeterminator::FinalGradeDeterminator(std::unique_ptr<DatabaseManager> db_manager) : Node("final_grade_determinator") {
this->declare_parameter("grade_collection_amount", 5);
grade_collection_amount_ = this->get_parameter("grade_collection_amount").as_int();
db_manager_ = std::make_unique<DatabaseManager>(this->get_logger());
// Make db_manager optional for testing purposes
if (db_manager) {
db_manager_ = std::move(db_manager);
} else {
db_manager_ = std::make_unique<DatabaseManager>(this->get_logger());
}
// Create publisher for exam results
student_publisher_ = this->create_publisher<g2_2025_interfaces::msg::Student>(
"student_course_management", 10
@@ -88,7 +92,8 @@ void FinalGradeDeterminator::grade_calculator_response(
db_manager_->store_final_course_result(
studentCourseCombo,
grade_collection_amount_,
response->result
response->result,
false
);
}

View File

@@ -18,7 +18,7 @@ namespace assignments::one::final_grade_determinator {
class FinalGradeDeterminator : public rclcpp::Node {
public:
FinalGradeDeterminator();
FinalGradeDeterminator(std::unique_ptr<DatabaseManager> db_manager = nullptr);
private:
rclcpp::Subscription<g2_2025_interfaces::msg::Exam>::SharedPtr exam_subscriber_;
rclcpp::Publisher<g2_2025_interfaces::msg::Student>::SharedPtr student_publisher_;

View File

@@ -10,28 +10,14 @@
* [04-09-2025] Wessel T: Implement template
*/
#include <cstdlib>
#include "rclcpp/rclcpp.hpp"
namespace lessons::zero::tmp {
class NodeTemplate : public rclcpp::Node {
public:
NodeTemplate()
: Node("node_template")
{
}
private:
};
} // namespace lessons::zero::template
#include "nodes/RetakeGradeDeterminator.hpp"
int main(int argc,char *argv[]) {
rclcpp::init(argc,argv);
auto node = std::make_shared<lessons::zero::tmp::NodeTemplate>();
auto node = std::make_shared<assignments::one::retake_grade_determinator::RetakeGradeDeterminator>();
rclcpp::spin(node);
rclcpp::shutdown();

View File

@@ -0,0 +1,161 @@
#include "RetakeGradeDeterminator.hpp"
namespace assignments::one::retake_grade_determinator {
RetakeGradeDeterminator::RetakeGradeDeterminator(std::unique_ptr<DatabaseManager> db_manager) : Node("retake_grade_determinator") {
this->declare_parameter("grade_collection_amount", 5);
grade_collection_amount_ = this->get_parameter("grade_collection_amount").as_int();
// Make db_manager optional for testing purposes
if (db_manager) {
db_manager_ = std::move(db_manager);
} else {
db_manager_ = std::make_unique<DatabaseManager>(this->get_logger());
}
// Create publisher for exam results
student_publisher_ = this->create_publisher<g2_2025_interfaces::msg::Student>(
"student_course_management", 10
);
// Create subscriber for adding/removing student/course combinations
exam_subscriber_ = this->create_subscription<g2_2025_interfaces::msg::Exam>(
"exam_results", 10,
std::bind(
&RetakeGradeDeterminator::exam_results_callback,
this,
std::placeholders::_1
)
);
exam_service_client_ = this->create_client<g2_2025_interfaces::srv::Exams>("grade_calculator_service");
// Create action server for retake requests
callback_group_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);
retake_actionserver_ = rclcpp_action::create_server<g2_2025_interfaces::action::Retake>(
this,
"retake_action",
std::bind(&RetakeGradeDeterminator::goal_callback, this, std::placeholders::_1, std::placeholders::_2),
std::bind(&RetakeGradeDeterminator::cancel_callback, this, std::placeholders::_1),
std::bind(&RetakeGradeDeterminator::spawn_callback_thread, this, std::placeholders::_1),
rcl_action_server_get_default_options(),
callback_group_
);
RCLCPP_INFO(this->get_logger(), "Action server has been started.");
}
void RetakeGradeDeterminator::exam_results_callback(
const g2_2025_interfaces::msg::Exam::SharedPtr msg
) {
if (retake_allowed_) {
student_course_combo_.student_name = msg->student_name;
student_course_combo_.course_name = msg->course_name;
data_map_[student_course_combo_].push_back(msg->result);
auto grade_collection_as_ulong = static_cast<unsigned long>(grade_collection_amount_);
if (data_map_[student_course_combo_].size() == grade_collection_as_ulong) {
RCLCPP_INFO(this->get_logger(),
"%s // %s: results sent to calculator",
msg->student_name.c_str(), msg->course_name.c_str()
);
grade_calculator_request(student_course_combo_);
}
}
}
void RetakeGradeDeterminator::grade_calculator_request(StudentCourse combo) {
if (!exam_service_client_->wait_for_service(std::chrono::seconds(1))) {
RCLCPP_WARN(this->get_logger(), "Service not available");
return;
}
auto request = std::make_shared<g2_2025_interfaces::srv::Exams::Request>();
request->course_name = combo.course_name;
request->student_name = combo.student_name;
request->exam_grades = data_map_[combo];
// Callback is used due to ros2 not liking passing multiple arguments in async calls
auto callback = [this, combo](rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedFuture future) {
this->grade_calculator_response(future, combo);
};
exam_service_client_->async_send_request(request, callback);
}
void RetakeGradeDeterminator::grade_calculator_response(
rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedFuture future,
StudentCourse studentCourseCombo
) {
if (!db_manager_ || !db_manager_->is_connected()) {
RCLCPP_WARN(this->get_logger(), "no database connection");
return;
}
data_map_[studentCourseCombo].clear();
db_manager_->update_retake_status(studentCourseCombo);
auto response = future.get();
auto student_message = g2_2025_interfaces::msg::Student();
student_message.student_name = studentCourseCombo.student_name;
student_message.course_name = studentCourseCombo.course_name;
student_message.timestamp = this->now();
student_publisher_->publish(student_message);
RCLCPP_INFO(this->get_logger(),
"%s // %s is %d",
studentCourseCombo.student_name.c_str(), studentCourseCombo.course_name.c_str(), response->result
);
db_manager_->store_final_course_result(
studentCourseCombo,
grade_collection_amount_,
response->result,
true
);
}
rclcpp_action::GoalResponse RetakeGradeDeterminator::goal_callback(
const rclcpp_action::GoalUUID& uuid,
std::shared_ptr<const g2_2025_interfaces::action::Retake::Goal> goal
) {
(void)uuid;
RCLCPP_INFO(this->get_logger(),
"Received retake goal request for student: %s, course: %s",
goal->student_name.c_str(), goal->course_name.c_str());
RCLCPP_INFO(this->get_logger(), "Retake goal accepted");
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
rclcpp_action::CancelResponse RetakeGradeDeterminator::cancel_callback(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle
) {
(void)goal_handle;
RCLCPP_INFO(this->get_logger(), "Received request to cancel retake goal");
return rclcpp_action::CancelResponse::ACCEPT;
}
void RetakeGradeDeterminator::spawn_callback_thread(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle
) { // Spawn a new thread to prevent blocking the executor
std::thread{std::bind(&RetakeGradeDeterminator::async_execute_callback_thread, this, std::placeholders::_1), goal_handle}.detach();
}
void RetakeGradeDeterminator::async_execute_callback_thread(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle
) {
retake_allowed_ = true;
RCLCPP_INFO(this->get_logger(), "Executing retake action for %s // %s", goal_handle->get_goal()->student_name.c_str(), goal_handle->get_goal()->course_name.c_str());
auto student_message = g2_2025_interfaces::msg::Student();
student_message.student_name = goal_handle->get_goal()->student_name;
student_message.course_name = goal_handle->get_goal()->course_name;
student_message.timestamp = this->now();
student_publisher_->publish(student_message);
auto result = std::make_shared<g2_2025_interfaces::action::Retake::Result>();
result->result = 0.0;
goal_handle->succeed(result);
}
} // namespace assignments::one::retake_grade_determinator

View File

@@ -0,0 +1,61 @@
#pragma once
#include <memory>
#include <string>
#include <random>
#include <vector>
#include <chrono>
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.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 "database/DatabaseManager.hpp"
#include "database/StudentCourse.hpp"
namespace assignments::one::retake_grade_determinator {
class RetakeGradeDeterminator : public rclcpp::Node {
public:
RetakeGradeDeterminator(std::unique_ptr<DatabaseManager> db_manager = nullptr);
private:
rclcpp::Subscription<g2_2025_interfaces::msg::Exam>::SharedPtr exam_subscriber_;
rclcpp::Publisher<g2_2025_interfaces::msg::Student>::SharedPtr student_publisher_;
rclcpp_action::Server<g2_2025_interfaces::action::Retake>::SharedPtr retake_actionserver_;
rclcpp::CallbackGroup::SharedPtr callback_group_;
rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedPtr exam_service_client_;
std::unique_ptr<DatabaseManager> db_manager_;
StudentCourse student_course_combo_;
StudentCourseResultMap data_map_;
// Params
int grade_collection_amount_;
bool retake_allowed_ = false;
void grade_calculator_request(StudentCourse combo);
void exam_results_callback(const g2_2025_interfaces::msg::Exam::SharedPtr msg);
void grade_calculator_response(
rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedFuture future,
StudentCourse studentCourseCombo
);
rclcpp_action::GoalResponse goal_callback(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const g2_2025_interfaces::action::Retake::Goal> goal);
rclcpp_action::CancelResponse cancel_callback(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle);
void spawn_callback_thread(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle);
void async_execute_callback_thread(const std::shared_ptr<rclcpp_action::ServerGoalHandle<g2_2025_interfaces::action::Retake>> goal_handle);
};
} // namespace assignments::one::retake_grade_determinator

View File

@@ -13,25 +13,14 @@
#include <cstdlib>
#include "rclcpp/rclcpp.hpp"
#include "nodes/RetakeScheduler.hpp"
namespace lessons::zero::tmp {
class NodeTemplate : public rclcpp::Node {
public:
NodeTemplate()
: Node("node_template")
{
}
private:
};
} // namespace lessons::zero::template
int main(int argc,char *argv[]) {
rclcpp::init(argc,argv);
auto node = std::make_shared<lessons::zero::tmp::NodeTemplate>();
auto node = std::make_shared<assignments::one::retake_scheduler::RetakeScheduler>();
rclcpp::spin(node);
rclcpp::shutdown();

View File

@@ -0,0 +1,104 @@
#include "RetakeScheduler.hpp"
namespace assignments::one::retake_scheduler {
RetakeScheduler::RetakeScheduler(std::unique_ptr<DatabaseManager> db_manager) : Node("retake_scheduler") {
this->declare_parameter("retake_check_interval_sec", 120); // Default to 120 seconds
retake_check_interval_ = this->get_parameter("retake_check_interval_sec").as_int();
// Make db_manager optional for testing purposes
if (db_manager) {
db_manager_ = std::move(db_manager);
} else {
db_manager_ = std::make_unique<DatabaseManager>(this->get_logger());
}
// Create action client to communicate with RetakeGradeDeterminator
retake_action_client_ = rclcpp_action::create_client<RetakeAction>(this, "retake_action");
// Check for failing students periodically
timer_ = this->create_wall_timer(
std::chrono::seconds(retake_check_interval_),
std::bind(&RetakeScheduler::check_failing_students, this)
);
}
void RetakeScheduler::check_failing_students() {
RCLCPP_INFO(this->get_logger(), "Checking for failing students...");
std::vector<StudentCourse> failing_students = db_manager_->get_failed_course_results();
if (failing_students.empty()) {
RCLCPP_INFO(this->get_logger(), "No failing students found.");
return;
}
for (const auto& student_course_combo : failing_students) {
send_retake_request(student_course_combo);
}
}
void RetakeScheduler::send_retake_request(StudentCourse student_course_combo) {
if (!retake_action_client_->wait_for_action_server(std::chrono::seconds(5))) {
RCLCPP_ERROR(this->get_logger(), "Retake action server not available after waiting");
return;
}
auto goal_msg = RetakeAction::Goal();
goal_msg.student_name = student_course_combo.student_name;
goal_msg.course_name = student_course_combo.course_name;
RCLCPP_INFO(this->get_logger(), "Sending retake request for %s // %s", student_course_combo.student_name.c_str(), student_course_combo.course_name.c_str());
auto send_goal_options = rclcpp_action::Client<RetakeAction>::SendGoalOptions();
send_goal_options.goal_response_callback = std::bind(&RetakeScheduler::request_response_callback, this, std::placeholders::_1);
send_goal_options.result_callback = std::bind(&RetakeScheduler::request_result_callback, this, std::placeholders::_1);
send_goal_options.feedback_callback = std::bind(&RetakeScheduler::request_feedback_callback, this, std::placeholders::_1, std::placeholders::_2);
retake_action_client_->async_send_goal(goal_msg, send_goal_options);
RCLCPP_INFO(this->get_logger(), "Retake request sent.");
}
void RetakeScheduler::cancel_retake_request() {
retake_action_client_->async_cancel_all_goals();
RCLCPP_INFO(this->get_logger(), "Sent cancel request for all retake goals.");
}
void RetakeScheduler::request_response_callback(
const rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr &goal_handle
) {
if (!goal_handle) {
RCLCPP_ERROR(this->get_logger(), "Retake goal was rejected by server");
} else {
RCLCPP_INFO(this->get_logger(), "Retake goal accepted by server, waiting for result");
}
}
void RetakeScheduler::request_result_callback(
const rclcpp_action::ClientGoalHandle<RetakeAction>::WrappedResult &result
) {
switch (result.code) {
case rclcpp_action::ResultCode::SUCCEEDED:
RCLCPP_INFO(this->get_logger(), "Retake goal succeeded");
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Retake goal was aborted");
return;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "Retake goal was canceled");
return;
default:
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
return;
}
RCLCPP_INFO(this->get_logger(), "Retake result received: %.2f", result.result->result);
}
void RetakeScheduler::request_feedback_callback(
rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr goal_handle,
const std::shared_ptr<const RetakeAction::Feedback> feedback
) {
(void)goal_handle;
RCLCPP_INFO(this->get_logger(), "Received feedback: %s", feedback->status.c_str());
}
} // namespace assignments::one::retake_scheduler

View File

@@ -0,0 +1,49 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <chrono>
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "g2_2025_interfaces/action/retake.hpp"
#include "database/DatabaseManager.hpp"
#include "database/StudentCourse.hpp"
namespace assignments::one::retake_scheduler {
class RetakeScheduler : public rclcpp::Node {
public:
RetakeScheduler(std::unique_ptr<DatabaseManager> db_manager = nullptr);
private:
rclcpp::TimerBase::SharedPtr timer_;
std::unique_ptr<DatabaseManager> db_manager_;
using RetakeAction = g2_2025_interfaces::action::Retake;
rclcpp_action::Client<RetakeAction>::SharedPtr retake_action_client_;
int retake_check_interval_;
void send_retake_request(StudentCourse combo);
void cancel_retake_request();
void check_failing_students();
void request_response_callback(
const rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr &goal_handle
);
void request_result_callback(
const rclcpp_action::ClientGoalHandle<RetakeAction>::WrappedResult &result
);
void request_feedback_callback(
rclcpp_action::ClientGoalHandle<RetakeAction>::SharedPtr goal_handle,
const std::shared_ptr<const RetakeAction::Feedback> feedback
);
};
} // namespace assignments::one::retake_scheduler

View File

@@ -73,7 +73,7 @@ TEST_F(DatabaseManagerTest, StoreFinalResultTest) {
sc.student_name = "TestStudent";
sc.course_name = "TestCourse";
bool result = db_manager_->store_final_course_result(sc, 3, 75);
bool result = db_manager_->store_final_course_result(sc, 3, 75, false);
EXPECT_TRUE(result == true || result == false);
}

View File

@@ -0,0 +1,192 @@
#include <chrono>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <gtest/gtest.h>
#include "final_grade_determinator/nodes/FinalGradeDeterminator.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 "mocks/MockDatabaseManager.hpp"
using namespace std::chrono_literals;
using namespace assignments::one::final_grade_determinator;
class FinalGradeDeterminatorTest : public ::testing::Test {
protected:
void SetUp() override {
rclcpp::init(0, nullptr);
test_node_ = std::make_shared<rclcpp::Node>("test_node");
// Subscriber to capture student 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 messages
exam_publisher_ = test_node_->create_publisher<g2_2025_interfaces::msg::Exam>(
"exam_results", 10
);
// Mock service server for grade calculator
grade_calculator_service_ = test_node_->create_service<g2_2025_interfaces::srv::Exams>(
"grade_calculator_service",
[this](const g2_2025_interfaces::srv::Exams::Request::SharedPtr request,
g2_2025_interfaces::srv::Exams::Response::SharedPtr response) {
service_requests_.push_back(*request);
// Mock calculation - average of grades
int sum = 0;
for (const auto& grade : request->exam_grades) {
sum += grade;
}
response->result = sum / request->exam_grades.size();
}
);
received_student_messages_.clear();
service_requests_.clear();
}
void TearDown() override {
final_grade_determinator_.reset();
fake_db_.reset();
test_node_.reset();
rclcpp::shutdown();
}
void create_final_grade_determinator() {
fake_db_ = std::make_unique<assignments::one::MockDatabaseManager>();
final_grade_determinator_ = std::make_shared<FinalGradeDeterminator>(std::move(fake_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 (final_grade_determinator_) {
rclcpp::spin_some(final_grade_determinator_);
}
std::this_thread::sleep_for(10ms);
}
}
std::unique_ptr<assignments::one::MockDatabaseManager> fake_db_;
std::shared_ptr<rclcpp::Node> test_node_;
std::shared_ptr<FinalGradeDeterminator> final_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_;
std::vector<g2_2025_interfaces::msg::Student> received_student_messages_;
std::vector<g2_2025_interfaces::srv::Exams::Request> service_requests_;
};
// ---- TEST CASES ----
TEST_F(FinalGradeDeterminatorTest, ConstructorTest) {
ASSERT_NO_THROW({
create_final_grade_determinator();
});
ASSERT_NE(final_grade_determinator_, nullptr);
}
TEST_F(FinalGradeDeterminatorTest, ExamCollectionTest) {
create_final_grade_determinator();
spin_some_time(500ms);
const std::string student_name = "Test Student";
const std::string course_name = "Test Course";
std::vector<int> grades = { 80, 85, 90, 75, 95 };
// Send 5 exam results (default collection amount)
for (const auto& grade : grades) {
auto msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
msg->student_name = student_name;
msg->course_name = course_name;
msg->result = grade;
msg->timestamp = test_node_->now();
exam_publisher_->publish(*msg);
spin_some_time(50ms);
}
spin_some_time(1s);
// Verify service request was made
ASSERT_FALSE(service_requests_.empty());
EXPECT_EQ(service_requests_.back().student_name, student_name);
EXPECT_EQ(service_requests_.back().course_name, course_name);
EXPECT_EQ(service_requests_.back().exam_grades, grades);
// Verify student message was published
ASSERT_FALSE(received_student_messages_.empty());
EXPECT_EQ(received_student_messages_.back().student_name, student_name);
EXPECT_EQ(received_student_messages_.back().course_name, course_name);
}
TEST_F(FinalGradeDeterminatorTest, PartialExamCollectionTest) {
create_final_grade_determinator();
spin_some_time(500ms);
// Send only 3 exam results (less than required 5)
const std::string student_name = "Test Student";
const std::string course_name = "Test Course";
std::vector<int> grades = { 80, 85, 90 };
for (const auto& grade : grades) {
auto msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
msg->student_name = student_name;
msg->course_name = course_name;
msg->result = grade;
msg->timestamp = test_node_->now();
exam_publisher_->publish(*msg);
spin_some_time(50ms);
}
spin_some_time(1s);
// Verify no service request was made yet
EXPECT_TRUE(service_requests_.empty());
// Verify no student message was published
EXPECT_TRUE(received_student_messages_.empty());
}
TEST_F(FinalGradeDeterminatorTest, MultipleStudentsTest) {
create_final_grade_determinator();
spin_some_time(500ms);
std::vector<std::string> students = { "Alice", "Bob" };
const std::string course_name = "Test Course";
std::vector<int> grades = { 80, 85, 90, 75, 95 };
// Send complete set of grades for each student
for (const auto& student : students) {
for (const auto& grade : grades) {
auto msg = std::make_shared<g2_2025_interfaces::msg::Exam>();
msg->student_name = student;
msg->course_name = course_name;
msg->result = grade;
msg->timestamp = test_node_->now();
exam_publisher_->publish(*msg);
spin_some_time(50ms);
}
}
spin_some_time(1s);
// Verify service requests were made for both students
ASSERT_EQ(service_requests_.size(), 2);
// Verify student messages were published for both students
ASSERT_EQ(received_student_messages_.size(), 2);
}

View File

@@ -0,0 +1,98 @@
#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <thread>
#include <g2_2025_interfaces/srv/exams.hpp>
#include "grade_calculator/nodes/GradeCalculator.hpp"
using namespace std::chrono_literals;
using assignments::one::grade_calculator::GradeCalculator;
class GradeCalculatorTest : public ::testing::Test
{
protected:
static void SetUpTestSuite() {
int argc = 0;
char** argv = nullptr;
rclcpp::init(argc, argv);
}
static void TearDownTestSuite() {
rclcpp::shutdown();
}
void SetUp() override
{
// Start GradeCalculator node
grade_calculator_node_ = std::make_shared<GradeCalculator>();
executor_ = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
executor_->add_node(grade_calculator_node_);
// Spin in background thread
spin_thread_ = std::thread([this]() {
executor_->spin();
});
// Create client node
client_node_ = rclcpp::Node::make_shared("grade_calculator_test_client");
client_ = client_node_->create_client<g2_2025_interfaces::srv::Exams>("grade_calculator_service");
}
void TearDown() override
{
executor_->cancel();
if (spin_thread_.joinable()) {
spin_thread_.join();
}
grade_calculator_node_.reset();
client_.reset();
client_node_.reset();
}
int call_service(const std::string& name, const std::vector<int>& grades)
{
if (!client_->wait_for_service(5s)) {
throw std::runtime_error("Service not available");
}
auto request = std::make_shared<g2_2025_interfaces::srv::Exams::Request>();
request->student_name = name;
request->exam_grades = grades;
auto future = client_->async_send_request(request);
if (rclcpp::spin_until_future_complete(client_node_, future) == rclcpp::FutureReturnCode::SUCCESS) {
return future.get()->result;
} else {
throw std::runtime_error("Service call failed");
}
}
std::shared_ptr<rclcpp::executors::SingleThreadedExecutor> executor_;
std::shared_ptr<GradeCalculator> grade_calculator_node_;
std::thread spin_thread_;
rclcpp::Node::SharedPtr client_node_;
rclcpp::Client<g2_2025_interfaces::srv::Exams>::SharedPtr client_;
};
// ---- TEST CASES ----
TEST_F(GradeCalculatorTest, NormalAverage) {
EXPECT_EQ(call_service("Alice", { 80, 90, 70 }), 80);
}
TEST_F(GradeCalculatorTest, WesselBonus) {
EXPECT_EQ(call_service("Wessel", { 80, 90, 70 }), 90);
}
TEST_F(GradeCalculatorTest, wesselBonus) {
EXPECT_EQ(call_service("wessel", { 80, 90, 70 }), 90);
}
TEST_F(GradeCalculatorTest, GradeTooHigh) {
EXPECT_EQ(call_service("Wessel", { 100, 100, 100 }), 100);
}
TEST_F(GradeCalculatorTest, GradeTooLow) {
EXPECT_EQ(call_service("Alice", { 0, 0, 0 }), 10);
}

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,123 @@
#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_sec");
EXPECT_EQ(param.as_int(), 120) << "Default retake_check_interval should be 120 seconds";
}
TEST_F(RetakeSchedulerTest, ActionClientAllServicesPresentTest) {
create_retake_scheduler();
// Verify all 3 action services are visible on the graph
auto service_names_and_types = retake_scheduler_->get_service_names_and_types();
bool has_cancel = false, has_get_result = false, has_send_goal = false;
for (const auto& [ service_name, _ ] : service_names_and_types) {
if (service_name == "/retake_action/_action/cancel_goal") has_cancel = true;
if (service_name == "/retake_action/_action/get_result") has_get_result = true;
if (service_name == "/retake_action/_action/send_goal") has_send_goal = true;
}
EXPECT_TRUE(has_cancel) << "cancel_goal service should be available";
EXPECT_TRUE(has_get_result) << "get_result service should be available";
EXPECT_TRUE(has_send_goal) << "send_goal service should be available";
}
TEST_F(RetakeSchedulerTest, ConstructorWithNullDatabaseManager) {
// Explicitly construct with nullptr to exercise optional DB manager path
ASSERT_NO_THROW({
retake_scheduler_ = std::make_shared<RetakeScheduler>(nullptr);
});
ASSERT_NE(retake_scheduler_, nullptr);
}

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

View File

@@ -0,0 +1,194 @@
import time
import rclpy
from rclpy.node import Node
from rclpy.executors import SingleThreadedExecutor
from g2_2025_interfaces.msg import Exam, Student
from g2_2025_interfaces.srv import Exams
class IntegrationTestNode(Node):
"""Test node for integration testing of the grade calculator"""
def __init__(self):
super().__init__('integration_test_node')
self.received_exams = []
self.received_students = []
self.student_publisher = self.create_publisher(
Student,
'student_course_management',
10
)
self.exam_subscriber = self.create_subscription(
Exam,
'exam_results',
self.exam_callback,
10
)
self.grade_calculator_client = self.create_client(
Exams,
'grade_calculator_service'
)
def exam_callback(self, msg):
"""Callback for received exam messages"""
self.get_logger().info(f'Received exam: {msg.student_name} - {msg.course_name} - {msg.result}')
self.received_exams.append(msg)
def publish_student_enrollment(self, student_name, course_name):
"""Publish a student enrollment message"""
msg = Student()
msg.student_name = student_name
msg.course_name = course_name
msg.timestamp = self.get_clock().now().to_msg()
self.get_logger().info(f'Publishing student enrollment: {student_name} - {course_name}')
self.student_publisher.publish(msg)
def call_grade_calculator_service(self, student_name, grades):
"""Call the grade calculator service"""
if not self.grade_calculator_client.wait_for_service(timeout_sec=5.0):
self.get_logger().warn('Grade calculator service not available')
return None
request = Exams.Request()
request.student_name = student_name
request.grades = grades
future = self.grade_calculator_client.call_async(request)
return future
def test_message_publishing():
"""Test basic message publishing and receiving"""
rclpy.init()
try:
node = IntegrationTestNode()
executor = SingleThreadedExecutor()
executor.add_node(node)
# Allow some time for setup
time.sleep(1.0)
# Test publishing student enrollment
node.publish_student_enrollment("TestStudent", "TestCourse")
# Spin for a short time to process messages
start_time = time.time()
while time.time() - start_time < 3.0:
executor.spin_once(timeout_sec=0.1)
# Test passes if no exceptions occurred
assert True, "Message publishing test completed"
finally:
node.destroy_node()
rclpy.shutdown()
def test_grade_calculator_service():
"""Test the grade calculator service"""
rclpy.init()
try:
node = IntegrationTestNode()
executor = SingleThreadedExecutor()
executor.add_node(node)
# Test grade calculation
future = node.call_grade_calculator_service("TestStudent", [80, 90, 70])
if future is not None:
# Spin until service response received or timeout
start_time = time.time()
while not future.done() and time.time() - start_time < 10.0:
executor.spin_once(timeout_sec=0.1)
if future.done():
result = future.result()
# Verify the result is reasonable
assert 10 <= result.final_grade <= 100, f"Grade {result.final_grade} is out of valid range"
assert result.final_grade == 80, f"Expected grade 80, got {result.final_grade}"
else:
# Service not available, test passes with warning
print("Warning: Grade calculator service not available during test")
assert True, "Grade calculator service test completed"
finally:
node.destroy_node()
rclpy.shutdown()
def test_system_integration():
"""Test integration between multiple components"""
rclpy.init()
try:
node = IntegrationTestNode()
executor = SingleThreadedExecutor()
executor.add_node(node)
# Clear any existing messages
node.received_exams.clear()
# Publish student enrollment
node.publish_student_enrollment("IntegrationStudent", "IntegrationCourse")
# Spin and wait for potential exam results
start_time = time.time()
while time.time() - start_time < 5.0:
executor.spin_once(timeout_sec=0.1)
assert True, "System integration test completed"
for exam in node.received_exams:
assert 10 <= exam.result <= 100, f"Exam grade {exam.result} is out of valid range"
assert len(exam.student_name) > 0, "Student name should not be empty"
assert len(exam.course_name) > 0, "Course name should not be empty"
finally:
node.destroy_node()
rclpy.shutdown()
def test_multiple_enrollments():
"""Test handling multiple student enrollments"""
rclpy.init()
try:
node = IntegrationTestNode()
executor = SingleThreadedExecutor()
executor.add_node(node)
test_students = [
("Tilmann", "Differentieren"),
("Vincent", "Integreren"),
("Wessel", "Kompjuteren")
]
for student, course in test_students:
node.publish_student_enrollment(student, course)
time.sleep(0.1)
# Spin to process messages
start_time = time.time()
while time.time() - start_time < 3.0:
executor.spin_once(timeout_sec=0.1)
assert True, "Multiple enrollments test completed"
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
test_message_publishing()
test_grade_calculator_service()
test_system_integration()
test_multiple_enrollments()

View File

@@ -1,5 +1,6 @@
string student_name
string course_name
---
---
float32 result
---
string status