generated from wessel/boilerplate
Merge pull request '[PR] Implement retake nodes' (#6) from 1-grade-generator/retake-nodes into 1-grade-generator/master
Reviewed-on: http://git.wessel.gg/inholland/ros2-assignments/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
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)`**
|
||||
@@ -1,17 +1,17 @@
|
||||
## Unit Tests
|
||||
# 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
|
||||
## Test Cases
|
||||
|
||||
#### 1. ConstructorTest
|
||||
### 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
|
||||
### 2. LoadValidConfigTest
|
||||
|
||||
**Description:** Tests loading of valid TOML configuration files with proper parsing.
|
||||
|
||||
@@ -22,7 +22,7 @@ Unit tests for `ConfigManager` are implemented in `src/g2_2025_grade_calculator_
|
||||
- Returns `true`
|
||||
- `is_loaded()` returns `true`
|
||||
|
||||
#### 3. LoadInvalidFileTest
|
||||
### 3. LoadInvalidFileTest
|
||||
|
||||
**Description:** Tests error handling when attempting to load non-existent configuration files.
|
||||
|
||||
@@ -31,7 +31,7 @@ Unit tests for `ConfigManager` are implemented in `src/g2_2025_grade_calculator_
|
||||
- Returns `false`
|
||||
- `is_loaded()` returns `false`
|
||||
|
||||
#### 4. DatabaseConfigParsingTest
|
||||
### 4. DatabaseConfigParsingTest
|
||||
|
||||
**Description:** Tests complete database configuration parsing with all parameters.
|
||||
|
||||
@@ -52,7 +52,7 @@ Unit tests for `ConfigManager` are implemented in `src/g2_2025_grade_calculator_
|
||||
```
|
||||
- **Expected Result:** All configuration values parsed correctly with proper types
|
||||
|
||||
#### 5. DatabaseConfigWithoutPoolTest
|
||||
### 5. DatabaseConfigWithoutPoolTest
|
||||
|
||||
**Description:** Tests default values when optional pool section is missing from configuration.
|
||||
|
||||
@@ -69,7 +69,7 @@ Unit tests for `ConfigManager` are implemented in `src/g2_2025_grade_calculator_
|
||||
- Main database config parsed correctly
|
||||
- Default pool values: `min_connections = 1`, `max_connections = 10`
|
||||
|
||||
#### 6. GetConfigWithoutLoadingTest
|
||||
### 6. GetConfigWithoutLoadingTest
|
||||
|
||||
**Description:** Tests behavior when attempting to access configuration before loading any file.
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
## Unit Tests
|
||||
# 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
|
||||
## Test Cases
|
||||
|
||||
#### 1. ConstructorTest
|
||||
### 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
|
||||
### 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
|
||||
### 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
|
||||
### 4. StoreExamResultTest
|
||||
|
||||
**Description:** Tests exam result storage with graceful handling of connection states.
|
||||
|
||||
@@ -35,7 +35,7 @@ Unit tests for `DatabaseManager` are implemented in `src/g2_2025_grade_calculato
|
||||
- Grade: `85`
|
||||
- **Expected Result:** Returns `false` if no connection, `true` if connected and successful
|
||||
|
||||
#### 5. EnrollStudentTest
|
||||
### 5. EnrollStudentTest
|
||||
|
||||
**Description:** Tests student enrollment into courses.
|
||||
|
||||
@@ -43,7 +43,7 @@ Unit tests for `DatabaseManager` are implemented in `src/g2_2025_grade_calculato
|
||||
- StudentCourse object with test data
|
||||
- **Expected Result:** Returns `false` if no connection, `true` if connected and successful
|
||||
|
||||
#### 6. GetFinalGradeTest
|
||||
### 6. GetFinalGradeTest
|
||||
|
||||
**Description:** Tests final grade retrieval for non-existent student-course combinations.
|
||||
|
||||
@@ -52,7 +52,7 @@ Unit tests for `DatabaseManager` are implemented in `src/g2_2025_grade_calculato
|
||||
- Course: `"NonExistentCourse"`
|
||||
- **Expected Result:** Returns `-1` (no results found or no connection)
|
||||
|
||||
#### 7. StoreFinalResultTest
|
||||
### 7. StoreFinalResultTest
|
||||
|
||||
**Description:** Tests storing calculated final course results.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## Unit Tests
|
||||
# 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
|
||||
## Test Cases
|
||||
|
||||
#### 1. ConstructorTest
|
||||
### 1. ConstructorTest
|
||||
|
||||
**Description:** Verifies that ExamResultGenerator can be created without crashing and proper initialization occurs.
|
||||
|
||||
@@ -13,7 +13,7 @@ Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calcu
|
||||
- Node created successfully without exceptions
|
||||
- Node name set to `"exam_result_generator"`
|
||||
|
||||
#### 2. PublisherCreationTest
|
||||
### 2. PublisherCreationTest
|
||||
|
||||
**Description:** Verifies that the `exam_results` topic publisher is properly configured.
|
||||
|
||||
@@ -22,7 +22,7 @@ Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calcu
|
||||
- `/exam_results` topic is published
|
||||
- Topic uses `g2_2025_interfaces/msg/Exam` message type
|
||||
|
||||
#### 3. SubscriberCreationTest
|
||||
### 3. SubscriberCreationTest
|
||||
|
||||
**Description:** Tests that the node subscribes to the `student_course_management` topic with correct message type.
|
||||
|
||||
@@ -31,7 +31,7 @@ Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calcu
|
||||
- `/student_course_management` topic is subscribed
|
||||
- Subscription uses `g2_2025_interfaces/msg/Student` message type
|
||||
|
||||
#### 4. StudentManagementMessageHandlingTest
|
||||
### 4. StudentManagementMessageHandlingTest
|
||||
|
||||
**Description:** Tests the node's ability to handle incoming student management messages without crashing.
|
||||
|
||||
@@ -41,7 +41,7 @@ Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calcu
|
||||
- Course: `"Test Course"`
|
||||
- **Expected Result:** Message processed without crashes
|
||||
|
||||
#### 5. MultipleStudentMessagesTest
|
||||
### 5. MultipleStudentMessagesTest
|
||||
|
||||
**Description:** Validates handling of multiple rapid student management messages for robustness testing.
|
||||
|
||||
@@ -51,7 +51,7 @@ Unit tests for `ExamResultGenerator` are implemented in `src/g2_2025_grade_calcu
|
||||
- Courses: `["Math", "Physics", "Chemistry"]`
|
||||
- **Expected Result:** All messages processed without crashes or memory issues
|
||||
|
||||
#### 6. ExamMessageValidationTest
|
||||
### 6. ExamMessageValidationTest
|
||||
|
||||
**Description:** Captures and validates published exam result messages for correct content and format.
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
## Unit Tests
|
||||
# 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
|
||||
## Test Cases
|
||||
|
||||
#### 1. ConstructorTest
|
||||
### 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
|
||||
### 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.
|
||||
|
||||
@@ -24,7 +24,7 @@ Unit tests for `FinalGradeDeterminator` are implemented in `src/g2_2025_grade_ca
|
||||
- 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
|
||||
### 3. PartialExamCollectionTest
|
||||
|
||||
**Description:** Sends fewer than the required number of exam results and checks that no grade calculation or student message occurs.
|
||||
|
||||
@@ -36,7 +36,7 @@ Unit tests for `FinalGradeDeterminator` are implemented in `src/g2_2025_grade_ca
|
||||
- No grade calculation service request is made.
|
||||
- No student message is published.
|
||||
|
||||
#### 4. MultipleStudentsTest
|
||||
### 4. MultipleStudentsTest
|
||||
|
||||
**Description:** Simulates multiple students submitting exam results and verifies that each student triggers independent grade calculation and messaging.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## Unit Tests
|
||||
# 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
|
||||
## Test Cases
|
||||
|
||||
#### 1. NormalAverage
|
||||
### 1. NormalAverage
|
||||
|
||||
**Description:** Verifies that the service returns the average of the provided grades for a normal student name.
|
||||
|
||||
@@ -14,7 +14,7 @@ Unit tests for `GradeCalculator` are implemented in `src/g2_2025_grade_calculato
|
||||
- **Expected Output:**
|
||||
- Result: `80` (average of 80, 90, 70)
|
||||
|
||||
#### 2. WesselBonus
|
||||
### 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.
|
||||
|
||||
|
||||
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
|
||||
@@ -166,11 +201,51 @@ if(BUILD_TESTING)
|
||||
pqxx pq tomlplusplus::tomlplusplus
|
||||
)
|
||||
|
||||
# Add Python integration tests
|
||||
find_package(ament_cmake_pytest REQUIRED)
|
||||
ament_add_pytest_test(${PROJECT_NAME}_integration_test test/test_integration_system.py
|
||||
TIMEOUT 60
|
||||
# Add gtest for RetakeGradeDeterminator
|
||||
ament_add_gtest(${PROJECT_NAME}_test_retake_grade_determinator
|
||||
test/RetakeGradeDeterminator.test.cpp
|
||||
src/retake_grade_determinator/nodes/RetakeGradeDeterminator.cpp
|
||||
src/database/DatabaseManager.cpp
|
||||
src/config/ConfigManager.cpp
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME}_test_retake_grade_determinator PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_grade_determinator
|
||||
)
|
||||
ament_target_dependencies(${PROJECT_NAME}_test_retake_grade_determinator
|
||||
rclcpp
|
||||
rclcpp_action
|
||||
g2_2025_interfaces
|
||||
)
|
||||
target_link_libraries(${PROJECT_NAME}_test_retake_grade_determinator
|
||||
pqxx pq tomlplusplus::tomlplusplus
|
||||
)
|
||||
|
||||
# Add gtest for RetakeScheduler
|
||||
ament_add_gtest(${PROJECT_NAME}_test_retake_scheduler
|
||||
test/RetakeScheduler.test.cpp
|
||||
src/retake_scheduler/nodes/RetakeScheduler.cpp
|
||||
src/database/DatabaseManager.cpp
|
||||
src/config/ConfigManager.cpp
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME}_test_retake_scheduler PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/retake_scheduler
|
||||
)
|
||||
ament_target_dependencies(${PROJECT_NAME}_test_retake_scheduler
|
||||
rclcpp
|
||||
rclcpp_action
|
||||
g2_2025_interfaces
|
||||
)
|
||||
target_link_libraries(${PROJECT_NAME}_test_retake_scheduler
|
||||
pqxx pq tomlplusplus::tomlplusplus
|
||||
)
|
||||
|
||||
# Add Python integration tests
|
||||
# find_package(ament_cmake_pytest REQUIRED)
|
||||
# ament_add_pytest_test(${PROJECT_NAME}_integration_test test/test_integration_system.py
|
||||
# TIMEOUT 60
|
||||
# )
|
||||
endif()
|
||||
|
||||
ament_package()
|
||||
|
||||
@@ -7,5 +7,8 @@
|
||||
</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"/>
|
||||
<node pkg="g2_2025_grade_calculator_pkg" exec="retake_scheduler">
|
||||
<param name="retake_check_interval_sec" value="120"/>
|
||||
</node>
|
||||
|
||||
</launch>
|
||||
|
||||
@@ -295,4 +295,23 @@ std::vector<StudentCourse> DatabaseManager::get_failed_course_results() {
|
||||
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
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
|
||||
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_;
|
||||
|
||||
@@ -36,6 +36,7 @@ static const std::string SQL_CREATE_COURSE_RESULTS = R"(
|
||||
exam_count INTEGER NOT NULL,
|
||||
final_grade INTEGER NOT NULL,
|
||||
is_retake BOOLEAN DEFAULT FALSE,
|
||||
retake_done BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
)";
|
||||
@@ -64,7 +65,7 @@ static const std::string SQL_SELECT_STUDENT_EXAM_RESULTS = R"(
|
||||
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.is_retake = FALSE
|
||||
WHERE fcr.final_grade < 55 AND fcr.retake_done = FALSE
|
||||
)";
|
||||
|
||||
static const std::string SQL_INSERT_EXAM_RESULT = R"(
|
||||
@@ -89,6 +90,12 @@ 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, is_retake)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
|
||||
@@ -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
|
||||
@@ -9,46 +9,11 @@
|
||||
#include "g2_2025_interfaces/msg/student.hpp"
|
||||
#include "g2_2025_interfaces/srv/exams.hpp"
|
||||
|
||||
#include "mocks/MockDatabaseManager.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace assignments::one::final_grade_determinator;
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
struct MockStoredResult {
|
||||
StudentCourse sc;
|
||||
int exam_count;
|
||||
int final_grade;
|
||||
bool is_retake;
|
||||
};
|
||||
|
||||
class MockDatabaseManager : public DatabaseManager {
|
||||
public:
|
||||
explicit MockDatabaseManager(rclcpp::Logger logger = rclcpp::get_logger("fake_db"))
|
||||
: DatabaseManager(logger) {
|
||||
}
|
||||
|
||||
bool is_connected() const override {
|
||||
return true; // Always pretend we are connected
|
||||
}
|
||||
|
||||
bool store_final_course_result(
|
||||
const StudentCourse& sc,
|
||||
int exam_count,
|
||||
int final_grade,
|
||||
bool is_retake
|
||||
) override {
|
||||
stored_results_.push_back({ sc, exam_count, final_grade, is_retake });
|
||||
return true; // Always succeed
|
||||
}
|
||||
|
||||
void init_database() override {
|
||||
} // no-op
|
||||
|
||||
std::vector<MockStoredResult> stored_results_;
|
||||
};
|
||||
|
||||
} // namespace assignments::one
|
||||
|
||||
class FinalGradeDeterminatorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,6 @@
|
||||
string student_name
|
||||
string course_name
|
||||
---
|
||||
---
|
||||
float32 result
|
||||
---
|
||||
string status
|
||||
|
||||
Reference in New Issue
Block a user