generated from wessel/boilerplate
Compare commits
41 Commits
1-grade-ge
...
1-grade-ge
| Author | SHA1 | Date | |
|---|---|---|---|
| 05f56159fd | |||
| 56eeac9b5c | |||
|
2e6b180eb2
|
|||
|
e68f083439
|
|||
|
e38ee6c4c9
|
|||
|
e195881834
|
|||
|
671fa84b73
|
|||
|
44f6e4e8cc
|
|||
|
bdf5c3b113
|
|||
|
ced37955ee
|
|||
|
d40ab6745d
|
|||
|
160ca14e85
|
|||
|
689223f58b
|
|||
|
|
3f2a5f4eca
|
||
| 964795d770 | |||
|
01c2788a99
|
|||
|
1ceb691fae
|
|||
|
644697326d
|
|||
|
493e69acd1
|
|||
|
6ccbc95b15
|
|||
|
161b5084fc
|
|||
|
e42856ae4e
|
|||
|
5e1df5367c
|
|||
|
130b495030
|
|||
|
62995c13c2
|
|||
|
517d4f5cb0
|
|||
|
f1878270dc
|
|||
|
437c5bc16e
|
|||
|
6e1c0346b0
|
|||
|
2e02ccddc5
|
|||
|
fd07992eee
|
|||
| e955280865 | |||
|
2ab1c1c31f
|
|||
|
b69dbda1a5
|
|||
|
447834dda7
|
|||
|
d89f47833e
|
|||
|
a325e19a41
|
|||
|
1e7c7cefe5
|
|||
|
25e21a15fc
|
|||
|
887e99c909
|
|||
| f147a6e287 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,5 +33,7 @@ qtcreator-*
|
||||
COLCON_IGNORE
|
||||
AMENT_IGNORE
|
||||
|
||||
.vscode
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/ros2
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -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
|
||||
|
||||
151
doc/architecture/architecture.md
Normal file
151
doc/architecture/architecture.md
Normal 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
|
||||
169
doc/architecture/interfaces/interfaces.md
Normal file
169
doc/architecture/interfaces/interfaces.md
Normal 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
|
||||
@@ -130,5 +130,4 @@ ssl = false
|
||||
[database.pool]
|
||||
min_connections = 1
|
||||
max_connections = 10
|
||||
|
||||
```
|
||||
@@ -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()
|
||||
39
doc/architecture/nodes/FinalGradeDeterminator.md
Normal file
39
doc/architecture/nodes/FinalGradeDeterminator.md
Normal 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
|
||||
25
doc/architecture/nodes/GradeCalculator.md
Normal file
25
doc/architecture/nodes/GradeCalculator.md
Normal 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
|
||||
60
doc/architecture/nodes/RetakeGradeDeterminator.md
Normal file
60
doc/architecture/nodes/RetakeGradeDeterminator.md
Normal 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
|
||||
47
doc/architecture/nodes/RetakeScheduler.md
Normal file
47
doc/architecture/nodes/RetakeScheduler.md
Normal 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)`**
|
||||
43
doc/installation/installation.md
Normal file
43
doc/installation/installation.md
Normal 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
|
||||
@@ -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
|
||||
79
doc/tests/ConfigManager.md
Normal file
79
doc/tests/ConfigManager.md
Normal 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`
|
||||
63
doc/tests/DatabaseManager.md
Normal file
63
doc/tests/DatabaseManager.md
Normal 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
|
||||
62
doc/tests/ExamResultGenerator.md
Normal file
62
doc/tests/ExamResultGenerator.md
Normal 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
|
||||
52
doc/tests/FinalGradeDeterminator.md
Normal file
52
doc/tests/FinalGradeDeterminator.md
Normal 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.
|
||||
56
doc/tests/GradeCalculator.md
Normal file
56
doc/tests/GradeCalculator.md
Normal 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)
|
||||
|
||||
151
doc/tests/IntegrationTest.md
Normal file
151
doc/tests/IntegrationTest.md
Normal 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
|
||||
70
doc/tests/RetakeGradeDeterminator.md
Normal file
70
doc/tests/RetakeGradeDeterminator.md
Normal 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.
|
||||
50
doc/tests/RetakeScheduler.md
Normal file
50
doc/tests/RetakeScheduler.md
Normal 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`.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
123
src/g2_2025_grade_calculator_pkg/test/RetakeScheduler.test.cpp
Normal file
123
src/g2_2025_grade_calculator_pkg/test/RetakeScheduler.test.cpp
Normal 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
194
src/g2_2025_grade_calculator_pkg/test/test_integration_system.py
Normal file
194
src/g2_2025_grade_calculator_pkg/test/test_integration_system.py
Normal 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()
|
||||
@@ -1,5 +1,6 @@
|
||||
string student_name
|
||||
string course_name
|
||||
---
|
||||
---
|
||||
float32 result
|
||||
---
|
||||
string status
|
||||
|
||||
Reference in New Issue
Block a user