generated from wessel/boilerplate
Merge pull request '[PR] Add unit tests to documentation, add README, add launchfile' (#7) from 1-grade-generator/documentation-updates into 1-grade-generator/master
Reviewed-on: http://git.wessel.gg/inholland/ros2-assignments/pulls/7
This commit was merged in pull request #7.
This commit is contained in:
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()
|
||||
@@ -5,13 +5,17 @@ The `FinalGradeDeterminator` node collects exam results for student-course combi
|
||||
|
||||
#### 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` <!--(uses provided or creates new instance) std::unique_ptr<DatabaseManager> db_manager -->
|
||||
- Sets up `DatabaseManager`
|
||||
- Creates publisher for student course management
|
||||
- Subscribes to exam results topic
|
||||
- Initializes service client for grade calculation
|
||||
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 @@
|
||||
## 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 @@
|
||||
## 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 @@
|
||||
## 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 @@
|
||||
## 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 @@
|
||||
## 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)
|
||||
|
||||
@@ -74,6 +74,11 @@ install(
|
||||
DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
install(
|
||||
DIRECTORY launch
|
||||
DESTINATION share/${PROJECT_NAME}/
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_cmake_gtest REQUIRED)
|
||||
|
||||
@@ -160,6 +165,12 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(${PROJECT_NAME}_test_final_grade_determinator
|
||||
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,11 @@
|
||||
<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"/>
|
||||
</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,35 @@ 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;
|
||||
}
|
||||
|
||||
} // namespace assignments::one
|
||||
|
||||
@@ -35,9 +35,16 @@ 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);
|
||||
virtual 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();
|
||||
|
||||
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,15 @@ 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,
|
||||
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 +54,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.is_retake = 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 +80,18 @@ 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_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()
|
||||
);
|
||||
|
||||
@@ -92,7 +92,8 @@ void FinalGradeDeterminator::grade_calculator_response(
|
||||
db_manager_->store_final_course_result(
|
||||
studentCourseCombo,
|
||||
grade_collection_amount_,
|
||||
response->result
|
||||
response->result,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ struct MockStoredResult {
|
||||
StudentCourse sc;
|
||||
int exam_count;
|
||||
int final_grade;
|
||||
bool is_retake;
|
||||
};
|
||||
|
||||
class MockDatabaseManager : public DatabaseManager {
|
||||
@@ -30,8 +31,13 @@ public:
|
||||
return true; // Always pretend we are connected
|
||||
}
|
||||
|
||||
bool store_final_course_result(const StudentCourse& sc, int exam_count, int final_grade) override {
|
||||
stored_results_.push_back({ sc, exam_count, final_grade });
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user