aquarium_control/database/sql_interface_schedule.rs
1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10//! Manages time-based scheduling for various application modules.
11//!
12//! This module provides a dedicated interface, `SqlInterfaceSchedule`, for reading and
13//! interpreting time-based restrictions from the `schedule` table in the database.
14//! Its primary purpose is to allow users to inhibit certain activities that may cause
15//! noise or other disturbances (e.g., pumps for Balling or Refill) during specific
16//! times, such as at night.
17//!
18//! ## Key Components
19//!
20//! - **`SqlInterfaceSchedule`**: The main struct that holds a database connection,
21//! performs validation, and provides methods to read and query schedule data.
22//!
23//! - **`ScheduleEntry`**: A processed, type-safe representation of a single schedule
24//! rule. It converts raw string data from the database into strongly typed
25//! `ScheduleType` and `chrono::NaiveTime` fields.
26//!
27//! - **`ScheduleType`**: An enum that identifies the specific application module
28//! (e.g., `Balling`, `Refill`, `Heating`) that a schedule rule applies to.
29//!
30//! ## Design and Purpose
31//!
32//! The module is designed to be a robust and centralized point for all scheduling logic.
33//!
34//! - **Fail-Fast Validation**: The `new()` constructor performs several "fail-fast"
35//! checks at startup. It verifies that the `schedule` table contains no `NULL`
36//! values and that the number of entries does not exceed a configured limit. This
37//! prevents the application from starting in an invalid state.
38//!
39//! - **Data Processing and Caching**: The `read_schedule()` method fetches all rules
40//! from the database, parses them into `ScheduleEntry` objects, and stores them in
41//! an internal cache (`Vec`). This is efficient as the database is only queried once.
42//!
43//! - **Robust Error Handling**: During processing, `read_schedule()` does not fail on
44//! the first malformed entry. Instead, it collects all parsing errors (e.g., for
45//! an invalid time string) and returns them in a `Vec`, allowing for comprehensive
46//! diagnostics of database integrity issues.
47//!
48//! - **Time Window Logic**: The `ScheduleEntry::check_if_allowed()` method provides a
49//! simple way to determine if the current system time falls within the active
50//! window of a given schedule.
51
52use chrono::*;
53use mysql::prelude::*;
54use mysql::*;
55use std::fmt;
56use std::str::FromStr;
57
58use crate::database::sql_interface::SqlInterface;
59use crate::database::sql_query_strings::{
60 SQL_QUERY_CHECK_SCHEDULE_COUNT, SQL_QUERY_CHECK_SCHEDULE_NULL,
61};
62use crate::database::{sql_interface_error::SqlInterfaceError, sql_query_strings};
63
64/// List of all modules that are subject to limitations by schedule.
65/// Background: These activities cause noise - the user shall be able to inhibit these noises during nighttime.
66#[derive(PartialEq, Debug)]
67pub enum ScheduleType {
68 Balling,
69 Refill,
70 Ventilation,
71 Heating,
72}
73
74impl FromStr for ScheduleType {
75 type Err = ();
76
77 /// Converts a string slice into a `ScheduleType` enum variant.
78 ///
79 /// This function attempts to match the input string to one of the predefined
80 /// `ScheduleType` variants (e.g., "Balling", "Refill"). The comparison is case-sensitive.
81 ///
82 /// # Arguments
83 /// * `input` - The string slice to be converted.
84 ///
85 /// # Returns
86 /// A `Result` containing the corresponding `ScheduleType` variant on a successful match.
87 ///
88 /// # Errors
89 /// Returns an empty `Err(())` if the input string does not match any of the known
90 /// `ScheduleType` variants.
91 fn from_str(input: &str) -> Result<ScheduleType, Self::Err> {
92 match input {
93 "Balling" => Ok(ScheduleType::Balling),
94 "Refill" => Ok(ScheduleType::Refill),
95 "Ventilation" => Ok(ScheduleType::Ventilation),
96 "Heating" => Ok(ScheduleType::Heating),
97 _ => Err(()),
98 }
99 }
100}
101
102impl fmt::Display for ScheduleType {
103 /// Formats the `ScheduleType` enum into a human-readable string representation.
104 ///
105 /// This implementation allows `ScheduleType` variants to be displayed directly
106 /// using `println!` or `format!`, returning the string name of the variant.
107 ///
108 /// # Arguments
109 /// * `f` - A mutable reference to the formatter.
110 ///
111 /// # Returns
112 /// A `fmt::Result` indicating whether the formatting was successful.
113 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 match self {
115 ScheduleType::Balling => write!(f, "Balling"),
116 ScheduleType::Refill => write!(f, "Refill"),
117 ScheduleType::Ventilation => write!(f, "Ventilation"),
118 ScheduleType::Heating => write!(f, "Heating"),
119 }
120 }
121}
122
123/// Contains one schedule data set as read from the database
124pub struct SqlScheduleEntry {
125 /// Schedule type (either Balling, Refill, Ventilation or Heating)
126 schedule_type: String,
127
128 /// Time of day from which on the control shall be active
129 start_time: NaiveTime,
130
131 /// Time of day from which on the control shall not be active
132 stop_time: NaiveTime,
133
134 /// Flag describing if the schedule entry is active
135 is_active: u32,
136}
137
138/// Contains one schedule data set after post-processing
139#[derive(Debug)]
140pub struct ScheduleEntry {
141 /// Schedule type (either Balling, Refill, Ventilation or Heating)
142 schedule_type: ScheduleType,
143
144 /// Time of day from which on the control shall be active
145 start_time: NaiveTime,
146
147 /// Time of day from which on the control shall not be active
148 stop_time: NaiveTime,
149
150 /// Flag describing if the schedule entry is active
151 #[allow(unused)]
152 is_active: bool,
153}
154
155impl ScheduleEntry {
156 /// Creates a new `ScheduleEntry` by converting raw data from an `SqlScheduleEntry`.
157 ///
158 /// This constructor takes a `SqlScheduleEntry` (typically read from the database)
159 /// and performs necessary type conversions, such as transforming string representations
160 /// of the schedule type and times into their respective Rust types.
161 ///
162 /// # Arguments
163 /// * `sql_schedule_entry` - A struct containing the raw schedule data from the SQL database.
164 ///
165 /// # Returns
166 /// A `Result` containing a new, fully typed `ScheduleEntry` on success.
167 ///
168 /// # Errors
169 /// This function will return an `Err` variant of `SqlInterfaceError` if:
170 /// - The `schedule_type` string cannot be parsed into a valid `ScheduleType` enum
171 /// (`ScheduleTypeConversionFailure`).
172 pub fn new(sql_schedule_entry: SqlScheduleEntry) -> Result<ScheduleEntry, SqlInterfaceError> {
173 let schedule_type =
174 ScheduleType::from_str(&sql_schedule_entry.schedule_type).map_err(|_| {
175 SqlInterfaceError::ScheduleTypeConversionFailure(
176 module_path!().to_string(),
177 sql_schedule_entry.schedule_type,
178 )
179 })?;
180
181 let is_active = sql_schedule_entry.is_active > 0;
182
183 Ok(Self {
184 schedule_type,
185 start_time: sql_schedule_entry.start_time,
186 stop_time: sql_schedule_entry.stop_time,
187 is_active,
188 })
189 }
190
191 /// Determines if the current local time falls within the allowed time window of this schedule entry.
192 ///
193 /// This function compares the system's current local time (`Local::now().time()`)
194 /// against the `start_time` and `stop_time` defined in this `ScheduleEntry`.
195 ///
196 /// # Returns
197 /// - `true`: If the current local time is on or after `start_time` AND on or before `stop_time`.
198 /// - `false`: Given an active schedule limitation: If the current local time is outside this inclusive window.
199 pub fn check_if_allowed(&self) -> bool {
200 let current_time = Local::now().time();
201 if self.is_active {
202 // allow operation only if time is within the permissible interval
203 (current_time >= self.start_time) && (current_time <= self.stop_time)
204 } else {
205 // allow operation if schedule limitation is not active
206 true
207 }
208 }
209
210 #[cfg(test)]
211 pub fn new_for_test(
212 schedule_type: ScheduleType,
213 start_time: NaiveTime,
214 stop_time: NaiveTime,
215 is_active: bool,
216 ) -> ScheduleEntry {
217 Self {
218 schedule_type,
219 start_time,
220 stop_time,
221 is_active,
222 }
223 }
224
225 #[cfg(test)]
226 pub fn deactivate(&mut self) {
227 self.is_active = false;
228 }
229}
230
231/// Contains the configuration and the implementation of the SQL interface for Schedule.
232#[derive(Debug)]
233pub struct SqlInterfaceSchedule {
234 /// Connection to the database
235 pub conn: PooledConn,
236
237 /// Result of the post-processed SQL query
238 schedules: Vec<ScheduleEntry>,
239}
240
241impl SqlInterfaceSchedule {
242 /// Creates a new `SqlInterfaceSchedule` instance.
243 ///
244 /// This constructor initializes the interface with a database connection. It performs
245 /// several pre-flight checks to ensure data integrity, such as verifying that the
246 /// `schedule` table contains no NULL values and that its row count is within the
247 /// configured limit.
248 ///
249 /// # Arguments
250 /// * `conn` - An active, pooled database connection.
251 /// * `max_rows_schedule` - The maximum allowed number of rows in the `schedule` table.
252 ///
253 /// # Returns
254 /// A `Result` containing a new `SqlInterfaceSchedule` instance on success.
255 ///
256 /// # Errors
257 /// This function will return an error if:
258 /// - Any of the initial database queries to get table counts fail (`DatabaseCheckScheduleFailure`).
259 /// - Any of the retrieved counts are negative, indicating a database issue (`DatabaseScheduleTableNegativeValue`).
260 /// - The `schedule` table contains entries with `NULL` values (`DatabaseScheduleTableContainsNull`).
261 /// - The number of rows in the `schedule` table exceeds `max_rows_schedule` (`DatabaseScheduleTableContainsTooManyRows`).
262 pub fn new(
263 mut conn: PooledConn,
264 max_rows_schedule: u64,
265 ) -> Result<SqlInterfaceSchedule, SqlInterfaceError> {
266 let count_null_values = SqlInterface::get_single_integer_from_database(
267 &mut conn,
268 SQL_QUERY_CHECK_SCHEDULE_NULL,
269 )
270 .map_err(|e| SqlInterfaceError::DatabaseCheckScheduleFailure {
271 location: module_path!().to_string(),
272 source: Box::new(e),
273 })?;
274 let count_rows = SqlInterface::get_single_integer_from_database(
275 &mut conn,
276 SQL_QUERY_CHECK_SCHEDULE_COUNT,
277 )
278 .map_err(|e| SqlInterfaceError::DatabaseCheckScheduleFailure {
279 location: module_path!().to_string(),
280 source: Box::new(e),
281 })?;
282
283 // check the query results
284 if count_null_values < 0 || count_rows < 0 {
285 return Err(SqlInterfaceError::DatabaseScheduleTableNegativeValue(
286 module_path!().to_string(),
287 count_null_values,
288 count_rows,
289 ));
290 }
291
292 if count_null_values > 0 {
293 return Err(SqlInterfaceError::DatabaseScheduleTableContainsNull(
294 module_path!().to_string(),
295 count_null_values,
296 ));
297 }
298
299 if max_rows_schedule > 0 {
300 // execute the check only when the limit is greater than zero
301 if count_rows > max_rows_schedule.cast_signed() {
302 return Err(SqlInterfaceError::DatabaseScheduleTableContainsTooManyRows(
303 module_path!().to_string(),
304 count_rows.cast_unsigned(),
305 max_rows_schedule,
306 ));
307 }
308 }
309
310 Ok(Self {
311 conn,
312 schedules: Vec::new(),
313 })
314 }
315
316 /// Reads all schedule entries from the SQL database, processing and storing them internally.
317 ///
318 /// This function queries the database for all available schedule configurations.
319 /// It then attempts to convert each raw database entry into a `ScheduleEntry` struct.
320 /// The internal `schedules` vector is cleared and then populated with only the
321 /// successfully processed entries.
322 ///
323 /// # Returns
324 /// - `Ok(())`: If the database query was successful and all retrieved entries were processed without errors.
325 ///
326 /// # Errors
327 /// Returns a `Vec<SqlInterfaceError>` which will contain one or more errors if:
328 /// - The initial database query fails (`ScheduleRequestFailure`).
329 /// - One or more raw schedule entries fail to be converted into a `ScheduleEntry`
330 /// (`ScheduleProcessingFailure`). This can happen due to invalid data formats
331 /// (e.g., a malformed time string) in the database.
332 pub fn read_schedule(&mut self) -> Result<(), Vec<SqlInterfaceError>> {
333 // Get all schedule entries from the database
334 let schedule_entry_array = match self.conn.query_map(
335 sql_query_strings::SQL_QUERY_READ_SCHEDULE,
336 |(schedule_type, start_time, stop_time, is_active)| SqlScheduleEntry {
337 schedule_type,
338 start_time,
339 stop_time,
340 is_active,
341 },
342 ) {
343 Ok(c) => c,
344 Err(e) => {
345 return Err(vec![SqlInterfaceError::ScheduleRequestFailure {
346 location: module_path!().to_string(),
347 query: sql_query_strings::SQL_QUERY_READ_SCHEDULE.to_string(),
348 source: e,
349 }]);
350 }
351 };
352
353 self.schedules.clear();
354
355 let mut errors = vec![]; // for collecting the errors
356 for schedule_string in schedule_entry_array {
357 match ScheduleEntry::new(schedule_string) {
358 Ok(schedule_entry) => {
359 self.schedules.push(schedule_entry);
360 }
361 Err(e) => {
362 errors.push(
363 // collect the error
364 SqlInterfaceError::ScheduleProcessingFailure {
365 location: module_path!().to_string(),
366 source: Box::new(e),
367 },
368 );
369 }
370 }
371 }
372 if errors.is_empty() {
373 Ok(())
374 } else {
375 Err(errors)
376 }
377 }
378
379 /// Finds a specific schedule entry by its type from the internal cache.
380 ///
381 /// This function searches the `schedules` vector, which must be populated by
382 /// a prior call to `read_schedule`, for an entry matching the given `schedule_type`.
383 ///
384 /// # Arguments
385 /// * `schedule_type` - The `ScheduleType` to search for.
386 ///
387 /// # Returns
388 /// A `Result` containing a reference to the found `ScheduleEntry` on success.
389 ///
390 /// # Errors
391 /// Returns `SqlInterfaceError::ScheduleTypeNotFound` if no schedule entry
392 /// matching the provided `schedule_type` exists in the internal cache.
393 pub fn find_schedule(
394 &self,
395 schedule_type: ScheduleType,
396 ) -> Result<&ScheduleEntry, SqlInterfaceError> {
397 self.schedules
398 .iter()
399 .find(|&schedule| schedule.schedule_type == schedule_type)
400 .ok_or_else(|| {
401 // Generate a comma-separated string of all available schedule types.
402 let available_types = self
403 .schedules
404 .iter()
405 .map(|s| s.schedule_type.to_string())
406 .collect::<Vec<_>>()
407 .join(", ");
408
409 SqlInterfaceError::ScheduleTypeNotFound(
410 module_path!().to_string(),
411 schedule_type.to_string(),
412 available_types,
413 )
414 })
415 }
416}
417
418#[cfg(test)]
419pub mod tests {
420 use crate::database::sql_interface::SqlInterface;
421 use crate::database::sql_interface_error::SqlInterfaceError;
422 use crate::database::sql_interface_schedule::{
423 ScheduleEntry, ScheduleType, SqlInterfaceSchedule, SqlScheduleEntry,
424 };
425 use crate::database::sql_query_strings;
426 use crate::database::sql_query_strings::SQL_TABLE_SCHEDULE;
427 use crate::utilities::config::{read_config_file_with_test_database, ConfigData};
428 use mysql::params;
429 use mysql::prelude::Queryable;
430 use std::collections::HashMap;
431
432 #[test]
433 // This test case verifies the validation and data processing logic for SqlInterfaceSchedule.
434 // It covers the following scenarios:
435 // 1. `new()`: Happy path, failure on NULL, and failure on row limits.
436 // 2. `read_schedule()`: Failure on invalid data (bad schedule type) and partial success.
437 // Test case uses test database #63.
438 fn test_sql_interface_schedule_new_and_read() {
439 // --- Common Setup ---
440 let config: ConfigData = read_config_file_with_test_database(
441 "/config/aquarium_control_test_generic.toml".to_string(),
442 63, // Using database #63 as requested
443 );
444 println!("Testing with database {}", config.sql_interface.db_name);
445 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface.clone())
446 .expect("Initialization of SQL interface for test failed.");
447
448 // --- Test `new()`: Happy Path (empty table) ---
449 println!("* Testing new() with an empty table (Happy Path)...");
450 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string())
451 .expect("Test setup failed: Could not truncate table.");
452
453 let result = SqlInterfaceSchedule::new(sql_interface.get_connection().unwrap(), 10);
454 assert!(
455 result.is_ok(),
456 "Expected new() to succeed with an empty table, but it failed: {:?}",
457 result.err()
458 );
459 println!("* Succeeded: Happy path initialization is successful on an empty table.");
460
461 // --- Test `new()`: Failure on too many rows ---
462 println!("* Testing new() with more rows than the limit...");
463 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string())
464 .expect("Test setup failed: Could not truncate table.");
465 let mut conn = sql_interface.get_connection().unwrap();
466 conn.exec_drop(
467 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
468 params! { "schedule_name" => "Balling", "start_time" => "01:00:00", "stop_time" => "02:00:00" },
469 )
470 .unwrap();
471 conn.exec_drop(
472 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
473 params! { "schedule_name" => "Refill", "start_time" => "03:00:00", "stop_time" => "04:00:00" },
474 )
475 .unwrap();
476
477 // Set the limit to 1
478 let result = SqlInterfaceSchedule::new(conn, 1);
479 assert!(
480 matches!(
481 result,
482 Err(SqlInterfaceError::DatabaseScheduleTableContainsTooManyRows(
483 _,
484 _,
485 _
486 ))
487 ),
488 "Expected row limit error, but got {:?}",
489 result
490 );
491 println!("* Succeeded: Initialization fails if table exceeds row limit.");
492
493 // --- Test `read_schedule()`: Failure on invalid data ---
494 println!("* Testing read_schedule() with an invalid schedule type...");
495 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string())
496 .expect("Test setup failed: Could not truncate table.");
497 let mut conn = sql_interface.get_connection().unwrap();
498 conn.exec_drop(
499 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
500 params! { "schedule_name" => "InvalidType", "start_time" => "01:00:00", "stop_time" => "02:00:00" },
501 )
502 .unwrap();
503
504 let mut schedule_db = SqlInterfaceSchedule::new(conn, 10).unwrap();
505 let read_result = schedule_db.read_schedule();
506
507 assert!(
508 matches!(read_result, Err(ref errors) if !errors.is_empty()),
509 "Expected read_schedule to return a vector of errors"
510 );
511
512 if let Err(errors) = read_result {
513 assert!(matches!(
514 errors[0],
515 SqlInterfaceError::ScheduleProcessingFailure { .. }
516 ));
517 println!("* Succeeded: read_schedule correctly fails on invalid data.");
518 }
519
520 // --- Test `read_schedule()`: Partial success ---
521 println!("* Testing read_schedule() with mixed valid and invalid data...");
522 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string())
523 .expect("Test setup failed: Could not truncate table.");
524 let mut conn = sql_interface.get_connection().unwrap();
525 // Insert one valid row
526 conn.exec_drop(
527 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
528 params! { "schedule_name" => "Balling", "start_time" => "01:00:00", "stop_time" => "02:00:00" },
529 )
530 .unwrap();
531 // Insert one invalid row
532 conn.exec_drop(
533 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
534 params! { "schedule_name" => "AnotherInvalidType", "start_time" => "03:00:00", "stop_time" => "04:00:00" },
535 )
536 .unwrap();
537
538 let mut schedule_db = SqlInterfaceSchedule::new(conn, 10).unwrap();
539 let read_result = schedule_db.read_schedule();
540
541 // Expect an error, but also check that the valid data was processed.
542 assert!(
543 read_result.is_err(),
544 "Expected an error due to the invalid entry"
545 );
546 assert_eq!(
547 schedule_db.schedules.len(),
548 1,
549 "Expected exactly one valid schedule to be processed"
550 );
551 assert_eq!(
552 schedule_db.schedules[0].schedule_type,
553 ScheduleType::Balling
554 );
555 println!("* Succeeded: read_schedule correctly processes valid data while reporting errors for invalid data.");
556 }
557
558 #[test]
559 // Test case reads entries from the SQL database.
560 // Since the application does not modify schedule data in the SQL database,
561 // results are compared to fixed references.
562 // Test case uses test database #38.
563 pub fn test_sql_interface_schedule() {
564 let config: ConfigData = read_config_file_with_test_database(
565 "/config/aquarium_control_test_generic.toml".to_string(),
566 38,
567 );
568 println!("Testing with database {}", config.sql_interface.db_name);
569 let max_rows_schedule = config.sql_interface.max_rows_schedule;
570 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
571 .expect("Initialization of SQL interface for test failed.");
572 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string()).unwrap();
573
574 let test_data = [
575 ("Balling", "01:00:00", "23:00:00"),
576 ("Heating", "00:00:00", "23:59:00"),
577 ("Ventilation", "03:00:00", "21:00:00"),
578 ("Refill", "02:00:00", "22:00:00"),
579 ];
580
581 for (name, start, stop) in &test_data {
582 sql_interface
583 .conn
584 .exec_drop(
585 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
586 params! {
587 "schedule_name" => *name,
588 "start_time" => *start,
589 "stop_time" => *stop,
590 },
591 )
592 .unwrap();
593 }
594
595 let mut sql_interface_schedule =
596 SqlInterfaceSchedule::new(sql_interface.get_connection().unwrap(), max_rows_schedule)
597 .unwrap();
598
599 match sql_interface_schedule.read_schedule() {
600 Ok(_) => {
601 println!("Reading schedule succeeded checking content now.");
602 }
603 Err(e) => {
604 panic!("Reading schedule from SQL database failed: {e:?}");
605 }
606 }
607
608 let references: HashMap<String, _> = test_data
609 .iter()
610 .map(|(name, start, stop)| {
611 let entry = ScheduleEntry::new(SqlScheduleEntry {
612 schedule_type: name.to_string(),
613 start_time: start.to_string().parse().unwrap(),
614 stop_time: stop.to_string().parse().unwrap(),
615 is_active: 1,
616 })
617 .unwrap();
618 (name.to_string(), entry)
619 })
620 .collect();
621
622 assert_eq!(sql_interface_schedule.schedules.len(), 4);
623
624 for schedule_entry in sql_interface_schedule.schedules {
625 let reference = references
626 .get(&schedule_entry.schedule_type.to_string())
627 .unwrap();
628 assert_eq!(reference.start_time, schedule_entry.start_time);
629 assert_eq!(reference.stop_time, schedule_entry.stop_time);
630 assert_eq!(reference.is_active, schedule_entry.is_active);
631 }
632 // *** check if row limitation is active ***
633 let test_result = SqlInterfaceSchedule::new(sql_interface.conn, 1);
634 assert!(matches!(
635 test_result,
636 Err(SqlInterfaceError::DatabaseScheduleTableContainsTooManyRows(
637 _,
638 _,
639 _
640 ))
641 ));
642 }
643
644 #[test]
645 // Test case verifies that the correct error is returned when a schedule is not found.
646 // Test case uses test database #53.
647 pub fn test_sql_interface_schedule_not_found() {
648 let config: ConfigData = read_config_file_with_test_database(
649 "/config/aquarium_control_test_generic.toml".to_string(),
650 53,
651 );
652 println!(
653 "Testing ScheduleTypeNotFound with database {}",
654 config.sql_interface.db_name
655 );
656 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface.clone())
657 .expect("Initialization of SQL interface for test failed.");
658 SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_SCHEDULE.to_string()).unwrap();
659
660 // Insert a SUBSET of schedules to test the "not found" case
661 let test_data = [
662 ("Heating", "00:00:00", "23:59:00"),
663 ("Ventilation", "03:00:00", "21:00:00"),
664 ];
665
666 for (name, start, stop) in &test_data {
667 sql_interface
668 .conn
669 .exec_drop(
670 sql_query_strings::SQL_QUERY_WRITE_SCHEDULE_TEST_DATA,
671 params! {
672 "schedule_name" => *name,
673 "start_time" => *start,
674 "stop_time" => *stop,
675 },
676 )
677 .unwrap();
678 }
679
680 let mut sql_interface_schedule = SqlInterfaceSchedule::new(
681 sql_interface.get_connection().unwrap(),
682 config.sql_interface.max_rows_schedule,
683 )
684 .unwrap();
685
686 sql_interface_schedule
687 .read_schedule()
688 .expect("Reading schedule from SQL database failed");
689
690 // Now, try to find a schedule that was NOT inserted (e.g., Balling)
691 println!("Checking for non-existent schedule entry (Balling).");
692 let find_result = sql_interface_schedule.find_schedule(ScheduleType::Balling);
693
694 // Assert that we get the correct error variant
695 assert!(matches!(
696 find_result,
697 Err(SqlInterfaceError::ScheduleTypeNotFound(_, _, _))
698 ));
699
700 // For more robust testing, also verify the content of the error message.
701 if let Err(SqlInterfaceError::ScheduleTypeNotFound(_, requested, available)) = find_result {
702 assert_eq!(requested, "Balling");
703 assert!(available.contains("Heating") && available.contains("Ventilation"));
704 println!(
705 "Correctly received ScheduleTypeNotFound for '{}'. Available types: '{}'",
706 requested, available
707 );
708 }
709 }
710}