aquarium_control/food/feed.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//! Implements the main control logic for automated and manual fish feeding.
11//!
12//! This module contains the `Feed` struct, which runs as a dedicated thread to manage all
13//! aspects of the feeding process. It is responsible for executing scheduled feed profiles,
14//! responding to external commands (e.g., from aquarium_client or the web server), and ensuring
15//! that feeding operations are performed safely and reliably.
16//!
17//! ## Key Components
18//!
19//! - **`Feed` Struct**: The central state machine for the feeding subsystem. It holds the
20//! configuration, communication channels, and internal state flags like `execution_blocked`.
21//!
22//! - **`execute()` Method**: The main thread loop. It continuously performs the following actions:
23//! 1. Checks for external commands (`Start`, `Stop`, `Execute`, `Quit`).
24//! 2. If active, periodically queries the database for overdue feed schedules.
25//! 3. Uses a configurable strategy to select a single profile if multiple are overdue.
26//! 4. Executes the selected profile by calling the `FoodInjectionTrait`.
27//! 5. Logs the feed event to the database.
28//! 6. Pings the database to keep the connection alive.
29//!
30//! - **`execute_feed()` Method**: A private helper that orchestrates a single feeding event. It
31//! retrieves the feed pattern from the database, actuates the feeder via the
32//! `FoodInjectionTrait`, and logs the event.
33//!
34//! ## Design and Architecture
35//!
36//! The `Feed` module is designed to be a robust, testable, and decoupled component.
37//!
38//! - **Dependency Injection**: The module relies on traits for its core dependencies:
39//! - `FoodInjectionTrait`: An abstraction for the physical act of dispensing food. This
40//! allows for mock implementations in tests without needing real hardware.
41//! - `DatabaseInterfaceFeedTrait`: An abstraction for all database operations, enabling
42//! the use of a mock database during testing.
43//!
44//! - **State Management**: The module maintains an internal state to handle various conditions:
45//! - `feed_inhibited`: A flag to temporarily pause scheduled feeding based on `Start`/`Stop` commands.
46//! - `execution_blocked`: A critical safety flag. If any part of a feeding process fails
47//! (e.g., cannot write to the database), this flag is set to `true`, preventing all
48//! further feeding operations until it is reset by an external command. This
49//! prevents unintended behavior due to a faulty system state.
50//!
51//! - **Concurrency Control**: It uses an `Arc<Mutex<i32>>` to coordinate with other device-actuating
52//! modules, ensuring that only one major physical action (like feeding or dosing) occurs at a time.
53//!
54//! - **Scheduling Strategy**: When multiple scheduled feeds are found in the past, a
55//! configurable strategy (`strategy_multiple_schedule_entries_in_past`) is used to
56//! decide which one to execute (e.g., the oldest, the newest, or none).
57//!
58//! ### Example Flow
59//!
60//! 1. The `execute()` loop starts.
61//! 2. It checks the database and finds a feed profile was scheduled for 10 minutes ago.
62//! 3. It calls `execute_feed()` with the profile ID.
63//! 4. `execute_feed()` retrieves the pattern.
64//! 5. It calls `food_injection.inject_food()`, which communicates with the `RelayManager`
65//! to turn the feeder relay on and then off.
66//! 6. After injection, it writes a record of the event to the `feedlog` table.
67//! 7. Finally, it updates the `feedschedule` table to mark the entry as completed or to
68//! reschedule it if it's a repeating event.
69
70#[cfg(not(test))]
71use log::info;
72
73#[cfg(all(feature = "debug_feed", not(test)))]
74use log::debug;
75
76#[cfg(all(feature = "debug_signal_handler", not(test)))]
77use log::debug;
78
79#[cfg(all(target_os = "linux", not(test)))]
80use nix::unistd::gettid;
81
82use crate::check_quit_increment_counter_ping_database;
83use crate::database::database_interface_feed_trait::DatabaseInterfaceFeedTrait;
84use crate::food::feed_channels::FeedChannels;
85use crate::food::feed_config::FeedConfig;
86use crate::food::feed_schedule_entry::FeedScheduleEntry;
87use crate::food::food_injection::FoodInjectionTrait;
88use crate::utilities::acknowledge_signal_handler::AcknowledgeSignalHandlerTrait;
89use crate::utilities::database_ping_trait::DatabasePingTrait;
90use crate::utilities::logger::log_error_chain;
91use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
92use crate::utilities::wait_for_termination::WaitForTerminationTrait;
93use spin_sleep::SpinSleeper;
94use std::sync::{Arc, Mutex};
95use std::time::{Duration, Instant};
96
97#[cfg_attr(doc, aquamarine::aquamarine)]
98/// Contains the configuration and the implementation for the feed control.
99/// Thread communication is as follows:
100/// ```mermaid
101/// graph LR
102/// feed[Feed Control] --> food_injection[Food Injection]
103/// food_injection --> relay_manager[Relay Manager]
104/// relay_manager --> food_injection
105/// feed --> signal_handler[Signal Handler]
106/// signal_handler --> feed
107/// signal_handler --> food_injection
108/// messaging[Messaging] --> feed
109/// ```
110/// Communication channel to relay manager is forwarded to implementation of FoodInjectionTrait.
111/// Signal handler is communicating with both directly: feed as well with implementation of FoodInjectionTrait.
112pub struct Feed {
113 /// Configuration data for Feed control
114 config: FeedConfig,
115
116 /// Communication from trait implementation: request to execute a certain feed profile has been received
117 pub execute_command_received: bool,
118
119 /// Communication from trait implementation: id of feed profile to be executed requested externally
120 pub profile_id_requested: i32,
121
122 /// Inhibition flag for further execution of feed profiles. It is set whenever errors occur during feeding and can be reset by external command.
123 pub execution_blocked: bool,
124
125 /// Inhibition flag to avoid flooding the log file with repeated messages about failure to read feed schedule entry from the database
126 lock_error_get_feedschedule_entry: bool,
127
128 /// recording when the last database ping happened
129 pub last_ping_instant: Instant,
130
131 /// database ping interval
132 pub database_ping_interval: Duration,
133
134 /// Inhibition flag to avoid flooding the log file with repeated messages about having received inapplicable command
135 pub lock_warn_inapplicable_command_signal_handler: bool,
136
137 /// Inhibition flag to avoid flooding the log file with repeated messages about failure to receive termination signal via the channel
138 pub lock_error_channel_receive_termination: bool,
139}
140
141impl Feed {
142 /// Creates a new `Feed` control instance.
143 ///
144 /// This constructor initializes the feed control module with its configuration
145 /// and a database interface for managing feed-related data. It also sets
146 /// initial states for command reception, execution blocking, and internal
147 /// logging inhibition flags.
148 ///
149 /// # Arguments
150 /// * `config` - Configuration data for the feed control, loaded from a TOML file.
151 /// * `database_ping_interval` - A `Duration` instance, providing the interval to ping the database.
152 ///
153 /// # Returns
154 /// A new `Feed` struct, ready for operation within the application loop.
155 pub fn new(config: FeedConfig, database_ping_interval: Duration) -> Feed {
156 Self {
157 config,
158 execute_command_received: false,
159 profile_id_requested: -1,
160 execution_blocked: false,
161 lock_error_get_feedschedule_entry: false,
162 last_ping_instant: Instant::now(),
163 database_ping_interval,
164 lock_warn_inapplicable_command_signal_handler: false,
165 lock_error_channel_receive_termination: false,
166 }
167 }
168
169 #[cfg(all(feature = "debug_feed", not(test)))]
170 // Converts a vector of `FeedScheduleEntry` into a human-readable string for visualization.
171 //
172 // This helper function is active only in non-test builds when the `debug_feed` feature is enabled.
173 // It iterates through the provided vector of feed schedule entries and formats them into a
174 // single string, displaying their index, profile ID, and profile name. This is useful for
175 // logging or debugging the contents of the feed schedule.
176 //
177 // # Arguments
178 // * `feedschedule_entries` - A `Vec<FeedScheduleEntry>` (passed by value to allow internal iteration)
179 // containing the feed schedule entries to be visualized.
180 //
181 // # Returns
182 // A `String` representation of the feed schedule entries.
183 fn feedschedule_entries_to_string(feedschedule_entries: Vec<FeedScheduleEntry>) -> String {
184 let mut output: String = "Vec<FeedscheduleEntry>=".to_string();
185 for (i, s) in feedschedule_entries.iter().enumerate() {
186 output +=
187 &(" #".to_owned() + &format!("{}: ID= {} ", i, s.profile_id) + &s.profile_name);
188 }
189 output
190 }
191
192 /// Loads a specified feed profile from the database, executes it via food injection, and logs the event.
193 ///
194 /// This private helper function orchestrates the process of executing a single feed profile.
195 /// It first attempts to retrieve the full `Feedpattern` from the database. If successful,
196 /// it then calls the `food_injection` trait method to actuate the feeder according to the
197 /// pattern. Finally, it records the completed feed event in the database.
198 ///
199 /// The function also monitors for a `Quit` command from the signal handler during the
200 /// food injection process, indicating an early termination request.
201 ///
202 /// # Arguments
203 /// * `profile_id` - The unique identifier of the feed profile to be loaded and executed.
204 /// * `food_injection` - A mutable reference to an object implementing the `FoodInjectionTrait`,
205 /// responsible for the physical actuation of the feeder.
206 /// * `feed_channels` - A mutable reference to the struct containing the channels.
207 /// * `sql_interface_feed` - A boxed trait object representing the interface
208 /// to the SQL database for feed-specific operations. This allows for
209 /// dependency injection and mock implementations during testing.
210 ///
211 /// # Returns
212 /// A tuple `(Result<(), ()>, bool)` where:
213 /// - The first element is the overall result of the operation. It is `Ok(())` only if
214 /// the food injection and the database logging both complete without error.
215 /// - The second element is a `bool,` which is `true` if a `Quit` command was received
216 /// from the signal handler during the `inject_food` process, `false` otherwise. This
217 /// value is independent of the success or failure of the operation itself.
218 ///
219 /// # Errors
220 /// This function does not return a distinct error type. Instead, it returns `Err(())`
221 /// in the result tuple to signify failure. The function can fail if:
222 /// - It cannot read the specified feed pattern from the database.
223 /// - The `food_injection.inject_food` method reports an error.
224 /// - It cannot retrieve the current timestamp from the database after injection.
225 /// - It fails to insert the feed event log into the database.
226 ///
227 /// In all failure cases, a detailed error message is logged to the console.
228 fn execute_feed(
229 &mut self,
230 profile_id: i32,
231 food_injection: &mut impl FoodInjectionTrait,
232 feed_channels: &mut FeedChannels,
233 sql_interface_feed: &mut Box<dyn DatabaseInterfaceFeedTrait + Sync + Send>,
234 ) -> (Result<(), ()>, bool) {
235 // First, load the feed pattern from the database.
236 let feed_pattern = match sql_interface_feed.get_single_feedpattern_from_database(profile_id)
237 {
238 Ok(pattern) => pattern,
239 Err(e) => {
240 log_error_chain(
241 module_path!(),
242 "Error occurred when trying to read feed pattern {profile_id} from database.",
243 e,
244 );
245 return (Err(()), false); // Return failure, no quit command received yet.
246 }
247 };
248
249 // *** Execute the feed pattern and destructure the returned tuple ***
250 // 1. `quit_during_injection`: A bool indicating if a quit command was received.
251 // 2. `injection_result`: A Result indicating if the injection itself succeeded.
252 let (quit_during_injection, injection_result) =
253 food_injection.inject_food(feed_channels, &feed_pattern);
254 // *** Handle the Result part of the tuple independently ***
255 // If errors occurred during injection, log each one.
256 if let Err(errors) = &injection_result {
257 for error in errors {
258 log_error_chain(module_path!(), "Food injection failed.", error);
259 }
260 }
261
262 // Get the current timestamp to log the event, regardless of success/failure.
263 let current_timestamp = match sql_interface_feed.get_current_timestamp() {
264 Ok(c) => c,
265 Err(e) => {
266 log_error_chain(
267 module_path!(),
268 "Could not get current timestamp from database.",
269 e,
270 );
271 // If we can't get a timestamp, we can't log the event.
272 // Return the original injection failure status and the quit status.
273 let overall_result = if injection_result.is_err() {
274 Err(())
275 } else {
276 Ok(())
277 };
278 return (overall_result, quit_during_injection);
279 }
280 };
281
282 //*** Insert the feed event into the log ***
283 let log_result = match sql_interface_feed.insert_feed_event(
284 current_timestamp,
285 feed_pattern.calc_feeder_runtime(),
286 feed_pattern.profile_name,
287 profile_id,
288 ) {
289 Ok(_) => Ok(()),
290 Err(e) => {
291 log_error_chain(
292 module_path!(),
293 "Could not insert feed event into database.",
294 e,
295 );
296 Err(())
297 }
298 };
299
300 // Determine the final result. If either the injection or the logging failed,
301 // the overall result is a failure.
302 let final_result = if injection_result.is_err() || log_result.is_err() {
303 Err(())
304 } else {
305 Ok(())
306 };
307
308 // Return the final result and the independent quit status.
309 (final_result, quit_during_injection)
310 }
311
312 /// Selects a specific feed schedule entry for execution based on the configured strategy.
313 ///
314 /// This function is used when multiple `FeedScheduleEntry` items are found to be
315 /// in the past (i.e., overdue for execution). It applies the `strategy_multiple_schedule_entries_in_past`
316 /// from the `FeedConfig` to determine which single entry, if any, should be executed.
317 ///
318 /// The strategy is defined as follows:
319 /// - **Negative value**: No entry will be selected for execution.
320 /// - **Positive value (N)**: The N-th entry from the `feedschedule_entries` vector (0-indexed)
321 /// will be selected.
322 /// - If `N` is greater than or equal to the number of available entries, the *last* entry in the vector is selected.
323 ///
324 /// # Arguments
325 /// * `feedschedule_entries` - A slice of `FeedScheduleEntry` structs, assumed to be ordered.
326 ///
327 /// # Returns
328 /// An `Option<FeedScheduleEntry>`:
329 /// - `Some(FeedScheduleEntry)`: The selected feed schedule entry to be executed.
330 /// - `None`: If no entry is selected based on the strategy (e.g., negative strategy value,
331 /// or an empty `feedschedule_entries` vector).
332 fn select_feed_schedule_entry_for_execution(
333 &self,
334 feedschedule_entries: &[FeedScheduleEntry],
335 ) -> Option<FeedScheduleEntry> {
336 // MODIFIED: Refactored for conciseness and idiomatic Rust.
337 let strategy = self.config.strategy_multiple_schedule_entries_in_past;
338
339 if strategy < 0 || feedschedule_entries.is_empty() {
340 return None;
341 }
342
343 // Try to get the entry at the specified index.
344 // If the index is out of bounds (`get` returns None), fall back to the last element.
345 feedschedule_entries
346 .get(strategy as usize)
347 .or_else(|| feedschedule_entries.last())
348 .cloned()
349 }
350
351 /// Executes a specified feed profile and updates the feed schedule in the database.
352 ///
353 /// This function is a core part of the feed control logic. It first calls `execute_feed`
354 /// to perform the actual food injection and log the event. Based on the outcome of
355 /// `execute_feed`, it updates the internal `execution_blocked` flag if an error occurs.
356 /// Finally, it triggers the update or deletion of the corresponding entries in the
357 /// feed schedule database.
358 ///
359 /// # Arguments
360 /// * `profile_id` - The numeric identifier of the feed profile to be executed.
361 /// * `food_injection` - A mutable reference to an object implementing `FoodInjectionTrait`,
362 /// responsible for physical feeder actuation.
363 /// * `feed_channels` - A mutable reference to the struct `FoodInjectionTrait` containing the channels.
364 /// * `rx_feed_from_relay_manager` - The receiver channel for receiving acknowledgments from the relay manager.
365 /// * `rx_feed_from_signal_handler` - The receiver channel for listening to signals (e.g., `Quit`) from the signal handler.
366 /// * `feed_schedule_entries` - A mutable reference to the vector of `FeedScheduleEntry`
367 /// that includes the entry(ies) just executed, which will be updated/deleted in the database.
368 /// * `sql_interface_feed` - A boxed trait object representing the interface
369 /// to the SQL database for feed-specific operations. This allows for
370 /// dependency injection and mock implementations during testing.
371 ///
372 /// # Returns
373 /// A `bool` which is `true` if a `Quit` command was received from the signal handler
374 /// during the feed execution, indicating that the application should shut down; otherwise `false`.
375 fn execute_profile_update_database(
376 &mut self,
377 profile_id: i32,
378 food_injection: &mut impl FoodInjectionTrait,
379 feed_channels: &mut FeedChannels,
380 feed_schedule_entries: &mut Vec<FeedScheduleEntry>,
381 sql_interface_feed: &mut Box<dyn DatabaseInterfaceFeedTrait + Sync + Send>,
382 ) -> bool {
383 #[cfg(all(feature = "debug_feed", not(test)))]
384 debug!(
385 target: module_path!(),
386 "executing feed profile id #{}",
387 profile_id
388 );
389
390 let (result, quit_command_received_during_feed) = self.execute_feed(
391 profile_id,
392 food_injection,
393 feed_channels,
394 sql_interface_feed,
395 );
396
397 match result {
398 Ok(_) => { /* do nothing */ }
399 Err(_) => {
400 // error occurred. Block subsequent executions
401 self.execution_blocked = true;
402 }
403 }
404
405 //*** update schedule ***
406 match sql_interface_feed.update_feedschedule_entries_in_database(feed_schedule_entries) {
407 Ok(()) => {
408 // do nothing
409 }
410 #[cfg(not(test))]
411 Err(e) => {
412 log_error_chain(module_path!(), "Updating feed schedule entry failed.", e);
413 self.execution_blocked = true;
414 }
415 #[cfg(test)]
416 Err(_) => {
417 self.execution_blocked = true;
418 }
419 }
420 quit_command_received_during_feed
421 }
422
423 /// Executes the main control loop for the feed module.
424 ///
425 /// This function runs continuously, managing the automatic execution of feed profiles
426 /// based on a schedule, processing external commands, and ensuring graceful shutdown.
427 /// It periodically checks the database for overdue feed schedule entries
428 /// and executes selected profiles, while also handling `Start`, `Stop`, and `Execute`
429 /// commands received from external channels.
430 ///
431 /// The loop breaks when a `Quit` command is received from the signal handler.
432 /// After breaking, it sends a confirmation back to the signal handler and then
433 /// waits for a `Terminate` command to ensure a complete shutdown.
434 ///
435 /// # Arguments
436 /// * `mutex_device_scheduler_feed` - An `Arc<Mutex<i32>>` used for coordinating
437 /// access to device scheduling, preventing parallel actuation across different
438 /// control modules. It holds a counter of the completed actuation.
439 /// * `feed_channels` - A mutable reference to `FeedChannels` struct containing all necessary `mpsc`
440 /// sender and receiver channels for inter-thread communication (e.g., with
441 /// the signal handler, relay manager, and messaging).
442 /// * `food_injection` - A mutable reference to an object implementing the
443 /// `FoodInjectionTrait`, responsible for the physical dispensing of food.
444 /// * `sql_interface_feed` - A boxed trait object representing the interface
445 /// to the SQL database for feed-specific operations. This allows for
446 /// dependency injection and mock implementations during testing.
447 pub fn execute(
448 &mut self,
449 mutex_device_scheduler_feed: Arc<Mutex<i32>>,
450 feed_channels: &mut FeedChannels,
451 food_injection: &mut impl FoodInjectionTrait,
452 mut sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send>,
453 ) {
454 #[cfg(all(target_os = "linux", not(test)))]
455 info!(target: module_path!(), "Thread started with TID: {}", gettid());
456
457 let sleep_duration_hundred_millis = Duration::from_millis(100);
458 let spin_sleeper = SpinSleeper::default();
459 let mut loop_counter = 0;
460 let mut feed_inhibited: bool = false; // state of feed control determined by if the start/stop command has been received
461 let mut quit_command_received: bool = false; // the request to end the application has been received
462 let mut start_command_received: bool; // the request to (re-)start feed has been received
463 let mut stop_command_received: bool; // the request to (temporarily) stop the feed has been received
464
465 loop {
466 if self.config.active
467 && !feed_inhibited
468 && (loop_counter % (self.config.schedule_check_interval * 10) == 0)
469 {
470 #[cfg(all(feature = "debug_feed", not(test)))]
471 debug!(
472 target: module_path!(),
473 "Starting feed control cycle"
474 );
475
476 match sql_interface_feed.get_past_feedschedule_entries_from_database() {
477 Ok(feed_schedule_entries_opt) => {
478 if let Some(mut feed_schedule_entries) = feed_schedule_entries_opt {
479 #[cfg(all(feature = "debug_feed", not(test)))]
480 debug!(
481 target: module_path!(),
482 "Discovered following feed schedule entries in the past: {}",
483 Self::feedschedule_entries_to_string(feed_schedule_entries.clone())
484 );
485
486 let feed_schedule_entry_opt = self
487 .select_feed_schedule_entry_for_execution(&feed_schedule_entries);
488
489 if let Some(feed_schedule_entry) = feed_schedule_entry_opt {
490 #[cfg(all(feature = "debug_feed", not(test)))]
491 debug!(
492 target: module_path!(),
493 "Selected following feed schedule entry for execution: {}",
494 feed_schedule_entry
495 );
496
497 self.lock_error_get_feedschedule_entry = false;
498
499 if !self.execution_blocked {
500 {
501 // scope to limit the lifetime of unlocked mutex
502 let mut mutex_data =
503 mutex_device_scheduler_feed.lock().unwrap();
504
505 quit_command_received = self
506 .execute_profile_update_database(
507 feed_schedule_entry.profile_id,
508 food_injection,
509 feed_channels,
510 &mut feed_schedule_entries,
511 &mut sql_interface_feed,
512 );
513
514 *mutex_data = mutex_data.saturating_add(1);
515 }
516 }
517 }
518 }
519 }
520 Err(e) => {
521 #[cfg(not(test))]
522 if !self.lock_error_get_feedschedule_entry {
523 log_error_chain(
524 module_path!(),
525 "Could not retrieve feed schedule information from database.",
526 e,
527 );
528 self.lock_error_get_feedschedule_entry = true;
529 }
530 #[cfg(test)]
531 let _e = e; // Explicitly declare and "use" with an underscore
532 self.lock_error_get_feedschedule_entry = true;
533 }
534 };
535 }
536
537 // exit loop right after feed execution if the quit command has been received meanwhile
538 if quit_command_received {
539 break;
540 }
541
542 // check and process external requests
543 (
544 quit_command_received, // the request to end the application has been received
545 start_command_received, // the request to (re-)start feed has been received
546 stop_command_received, // the request to (temporarily) stop the feed has been received
547 ) = self.process_external_request(
548 &mut feed_channels.rx_feed_from_signal_handler,
549 feed_channels.rx_feed_from_messaging_opt.as_mut(),
550 );
551 if quit_command_received {
552 break;
553 }
554 if stop_command_received {
555 #[cfg(not(test))]
556 info!(
557 target: module_path!(),
558 "received Stop command. Inhibiting feed."
559 );
560
561 #[cfg(all(feature = "debug_feed", not(test)))]
562 debug!(
563 target: module_path!(),
564 "received Stop command."
565 );
566
567 feed_inhibited = true;
568 }
569 if start_command_received {
570 #[cfg(not(test))]
571 info!(
572 target: module_path!(),
573 "received Start command. Restarting feed."
574 );
575
576 #[cfg(all(feature = "debug_feed", not(test)))]
577 debug!(
578 target: module_path!(),
579 "received Start command"
580 );
581
582 feed_inhibited = false;
583 }
584 if self.execute_command_received
585 && self.profile_id_requested >= 0
586 && !self.execution_blocked
587 {
588 #[cfg(all(feature = "debug_feed", not(test)))]
589 debug!(
590 target: module_path!(),
591 "received execution command"
592 );
593
594 {
595 // scope to limit the lifetime of unlocked mutex
596 let mut mutex_data = mutex_device_scheduler_feed.lock().unwrap();
597
598 let (result, quit_command_received_during_requested_feed) = self.execute_feed(
599 self.profile_id_requested,
600 food_injection,
601 feed_channels,
602 &mut sql_interface_feed,
603 );
604 self.execution_blocked |= result.is_err(); // block next execution if error occurred
605
606 quit_command_received = quit_command_received_during_requested_feed;
607 *mutex_data = mutex_data.saturating_add(1);
608 }
609 }
610
611 check_quit_increment_counter_ping_database!(
612 quit_command_received,
613 spin_sleeper,
614 sleep_duration_hundred_millis,
615 loop_counter,
616 self,
617 &mut *sql_interface_feed
618 );
619 }
620
621 // The application received request to terminate. That is why the loop was left.
622 // Answer to the thread which sent the request for termination, so that shutdown can proceed further.
623 #[cfg(all(feature = "debug_signal_handler", not(test)))]
624 debug!(
625 target: module_path!(),
626 "Sending Quit confirmation to signal handler."
627 );
628
629 feed_channels.acknowledge_signal_handler();
630
631 // This thread has channel connections to underlying threads.
632 // Those threads have to stop receiving commands from this thread.
633 // The shutdown sequence is handled by the signal_handler module.
634 self.wait_for_termination(
635 &mut feed_channels.rx_feed_from_signal_handler,
636 sleep_duration_hundred_millis,
637 module_path!(),
638 );
639 }
640}
641
642#[cfg(test)]
643pub mod tests {
644 use spin_sleep::SpinSleeper;
645 use std::sync::{Arc, Mutex};
646 use std::thread;
647 use std::thread::scope;
648 use std::time::Duration;
649
650 use crate::database::database_interface_feed_trait::DatabaseInterfaceFeedTrait;
651 use crate::database::sql_interface::SqlInterface;
652 use crate::database::sql_interface_feed::SqlInterfaceFeed;
653 use crate::food::feed::Feed;
654 use crate::food::feed_schedule_entry::FeedScheduleEntry;
655 use crate::utilities::channel_content::InternalCommand;
656 use crate::utilities::config::{
657 read_config_file, read_config_file_with_test_database, ConfigData,
658 };
659 use crate::utilities::logger::setup_logger;
660 use crate::utilities::logger_config::LoggerConfig;
661
662 use crate::database::sql_interface_feed::tests::insert_feed_patterns;
663 use crate::database::sql_query_strings::{SQL_TABLE_FEEDLOG, SQL_TABLE_FEEDSCHEDULE};
664 use crate::launch::channels::{AquaReceiver, AquaSender, Channels};
665 use crate::mocks::mock_food_injection::MockFoodInjection;
666 use crate::mocks::mock_sql_interface_feed::tests::MockSqlInterfaceFeed;
667
668 /// Helper function for test cases to simulate the signal handler.
669 ///
670 /// The function waits for a determined period and then sends the Quit and the Terminate
671 /// commands to the test object.
672 ///
673 /// # Arguments
674 /// * `tx_signal_handler_to_feed` - Sender part of the channel for communication to the test object
675 /// * `rx_signal_handler_from_feed` - Receiver part of the channel for communication from the test object
676 /// * `spin_sleeper` - SpinSleeper instance used for waiting before sending Quit command to the test object
677 /// * `sleep_duration` - sleep duration used for waiting before sending Quit command to the test object
678 fn create_mock_signal_handler(
679 tx_signal_handler_to_feed: &mut AquaSender<InternalCommand>,
680 rx_signal_handler_from_feed: &mut AquaReceiver<bool>,
681 spin_sleeper: SpinSleeper,
682 sleep_duration: Duration,
683 ) {
684 spin_sleeper.sleep(sleep_duration);
685 let _ = tx_signal_handler_to_feed.send(InternalCommand::Quit);
686 rx_signal_handler_from_feed.recv().unwrap();
687 let _ = tx_signal_handler_to_feed.send(InternalCommand::Terminate);
688 }
689
690 #[test]
691 // "Happy" case executing a set of feed schedule entries. Additionally, it checks row limitation.
692 // Test case uses test database #07.
693 pub fn test_feed_schedule_execution() {
694 let sleep_duration_100_millis = Duration::from_millis(100);
695 let sleep_duration_8_seconds = Duration::from_millis(10000);
696 let spin_sleeper = SpinSleeper::default();
697
698 let mut channels = Channels::new_for_test();
699
700 let mut mock_food_injection = MockFoodInjection::new();
701
702 let config: ConfigData = read_config_file_with_test_database(
703 "/config/aquarium_control_test_generic.toml".to_string(),
704 7,
705 );
706
707 let mut sql_interface = match SqlInterface::new(config.sql_interface.clone()) {
708 Ok(c) => c,
709 Err(e) => {
710 panic!("Could not connect to SQL database: {e:?}");
711 }
712 };
713
714 let mut sql_interface_feed_unboxed = SqlInterfaceFeed::new(
715 sql_interface.get_connection().unwrap(),
716 config.sql_interface.max_rows_feed_pattern,
717 config.sql_interface.max_rows_feed_schedule,
718 config.sql_interface.max_rows_feed_log,
719 )
720 .unwrap();
721
722 insert_feed_patterns(&mut sql_interface, &mut sql_interface_feed_unboxed);
723
724 let mut sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send> =
725 Box::new(sql_interface_feed_unboxed);
726
727 // empty the feed schedule table
728 match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
729 Ok(_) => {}
730 Err(e) => {
731 panic!("Could not prepare test case: {e:?}")
732 }
733 }
734 // empty the feed log table
735 match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDLOG.to_string()) {
736 Ok(_) => {}
737 Err(e) => {
738 panic!("Could not prepare test case: {e:?}")
739 }
740 }
741
742 let time_stamp_feed_schedule_entry_one_time =
743 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, 2) {
744 Ok(c) => c,
745 Err(e) => {
746 panic!(
747 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
748 );
749 }
750 };
751
752 let time_stamp_feed_schedule_entry_repeat =
753 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, 4) {
754 Ok(c) => c,
755 Err(e) => {
756 panic!(
757 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
758 );
759 }
760 };
761
762 // manipulation of the database: inserting feed schedule entries with future timestamp
763 sql_interface_feed
764 .insert_feed_schedule_entry(
765 time_stamp_feed_schedule_entry_one_time,
766 1,
767 "testOneTime".to_string(),
768 false,
769 false,
770 )
771 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
772
773 sql_interface_feed
774 .insert_feed_schedule_entry(
775 time_stamp_feed_schedule_entry_repeat,
776 2,
777 "testRepeat".to_string(),
778 false,
779 true,
780 )
781 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
782
783 let mut feed = Feed::new(config.feed, Duration::from_millis(1000));
784
785 scope(|scope| {
786 let mutex_device_scheduler_feed = Arc::new(Mutex::new(0));
787
788 // thread for mock signal handler
789 scope.spawn(move || {
790 create_mock_signal_handler(
791 &mut channels.signal_handler.tx_signal_handler_to_feed,
792 &mut channels.signal_handler.rx_signal_handler_from_feed,
793 spin_sleeper,
794 sleep_duration_8_seconds,
795 );
796 });
797
798 spin_sleeper.sleep(sleep_duration_100_millis);
799
800 // thread for the test object
801 scope.spawn(move || {
802 feed.execute(
803 mutex_device_scheduler_feed.clone(),
804 &mut channels.feed,
805 &mut mock_food_injection,
806 sql_interface_feed,
807 );
808
809 // check if the right profiles have been executed in the right order
810 assert_eq!(mock_food_injection.pop_profile_id_executed(), Some(2));
811 assert_eq!(mock_food_injection.pop_profile_id_executed(), Some(1));
812 assert_eq!(mock_food_injection.pop_profile_id_executed(), None);
813 });
814 });
815 }
816
817 #[test]
818 // Test case runs feed control and triggers inhibition by sending the stop message via the channel.
819 // After verification that feed is stopped,
820 // The test case sends the start message via the channel and verifies if feed control
821 // resumes operation. Afterward, the test case triggers execution of a specific feed profile
822 // by sending the "execute" message via the channel.
823 // Test case uses test database #08.
824 pub fn test_messaging_stops_starts_execute_feed() {
825 let sleep_duration_100_millis = Duration::from_millis(100);
826 let sleep_duration_2_secs = Duration::from_secs(2);
827 let sleep_duration_10_secs = Duration::from_secs(10);
828 let spin_sleeper = SpinSleeper::default();
829
830 setup_logger(LoggerConfig::default()).unwrap();
831
832 let config: ConfigData = read_config_file_with_test_database(
833 "/config/aquarium_control_test_generic.toml".to_string(),
834 8,
835 );
836
837 let mut sql_interface = match SqlInterface::new(config.sql_interface.clone()) {
838 Ok(c) => c,
839 Err(e) => {
840 panic!("Could not connect to SQL database: {e:?}");
841 }
842 };
843
844 let mut sql_interface_feed_unboxed = SqlInterfaceFeed::new(
845 sql_interface.get_connection().unwrap(),
846 config.sql_interface.max_rows_feed_pattern,
847 config.sql_interface.max_rows_feed_schedule,
848 config.sql_interface.max_rows_feed_log,
849 )
850 .unwrap();
851
852 insert_feed_patterns(&mut sql_interface, &mut sql_interface_feed_unboxed);
853
854 let mut sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send> =
855 Box::new(sql_interface_feed_unboxed);
856
857 // empty the feed schedule
858 match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
859 Ok(_) => {}
860 Err(e) => {
861 panic!("Could not prepare test case: {e:?}")
862 }
863 }
864
865 let time_stamp_feed_schedule_entry =
866 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, 4) {
867 Ok(c) => c,
868 Err(e) => {
869 panic!(
870 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
871 );
872 }
873 };
874
875 // manipulation of the database: inserting feed schedule entries with future timestamp
876 sql_interface_feed
877 .insert_feed_schedule_entry(
878 time_stamp_feed_schedule_entry,
879 1,
880 "testOneTime".to_string(),
881 false,
882 false,
883 )
884 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
885
886 // create the test object
887 let mut feed = Feed::new(config.feed, Duration::from_millis(1000));
888
889 let mut channels = Channels::new_for_test();
890
891 let mut mock_food_injection = MockFoodInjection::new();
892
893 let mutex_device_scheduler_feed_test_object = Arc::new(Mutex::new(0));
894 let mutex_device_scheduler_feed_test_environment =
895 mutex_device_scheduler_feed_test_object.clone();
896
897 // thread for test environment
898 let join_handle_test_environment = thread::Builder::new()
899 .name("test_environment".to_string())
900 .spawn(move || {
901 // wait to make sure the test object can receive the message
902 spin_sleeper.sleep(sleep_duration_100_millis);
903
904 // sending the message requesting stop of feed control
905 match channels
906 .messaging
907 .tx_messaging_to_feed
908 .send(InternalCommand::Stop)
909 {
910 Ok(()) => { /* do nothing */ }
911 Err(e) => {
912 panic!(
913 "{}: error when sending stop command to test object ({e:?})",
914 module_path!()
915 );
916 }
917 }
918
919 // Wait for 10 seconds. If the test object does not follow the stop request,
920 // then food injection will happen in this period.
921 spin_sleeper.sleep(sleep_duration_10_secs);
922
923 let actuation_count_1 =
924 *mutex_device_scheduler_feed_test_environment.lock().unwrap();
925
926 // check the amount of actuation (after stop request)
927 assert_eq!(actuation_count_1, 0);
928
929 // sending the message requesting restart of feed control
930 match channels
931 .messaging
932 .tx_messaging_to_feed
933 .send(InternalCommand::Start)
934 {
935 Ok(()) => { /* do nothing */ }
936 Err(e) => {
937 panic!(
938 "{}: error when sending start command to test object ({e:?})",
939 module_path!()
940 );
941 }
942 }
943
944 // Wait for 10 seconds. If the test object follows the start request,
945 // then food injections will happen in this period.
946 spin_sleeper.sleep(sleep_duration_10_secs);
947
948 // get count of feed profiles executed
949 let actuation_count_2 =
950 *mutex_device_scheduler_feed_test_environment.lock().unwrap();
951
952 // check the amount of actuation (after start request)
953 assert_eq!(actuation_count_2, 1);
954
955 // sending the message requesting execution of the feed profile
956 match channels
957 .messaging
958 .tx_messaging_to_feed
959 .send(InternalCommand::Execute(1))
960 {
961 Ok(()) => { /* do nothing */ }
962 Err(e) => {
963 panic!(
964 "{}: error when sending execution command to test object ({e:?})",
965 module_path!()
966 );
967 }
968 }
969
970 // Wait for 2 seconds. If the test object follows the "execute" request,
971 // then food injection will happen in this period.
972 spin_sleeper.sleep(sleep_duration_2_secs);
973
974 // get count of feed profiles executed
975 let actuation_count_3 =
976 *mutex_device_scheduler_feed_test_environment.lock().unwrap();
977
978 // check the amount of actuation (after the "execute" request)
979 assert_eq!(actuation_count_3, 2);
980
981 // requesting feed control to quit
982 let _ = channels.signal_handler.send_to_feed(InternalCommand::Quit);
983 channels.signal_handler.receive_from_feed().unwrap();
984 let _ = channels
985 .signal_handler
986 .send_to_feed(InternalCommand::Terminate);
987 })
988 .unwrap();
989
990 spin_sleeper.sleep(sleep_duration_100_millis);
991
992 // thread for the test object
993 let join_handle_test_object = thread::Builder::new()
994 .name("test_object".to_string())
995 .spawn(move || {
996 feed.execute(
997 mutex_device_scheduler_feed_test_object.clone(),
998 &mut channels.feed,
999 &mut mock_food_injection,
1000 sql_interface_feed,
1001 );
1002 println!("MockFoodInjection:\n{}", mock_food_injection);
1003
1004 // check if the right feed profiles have been executed
1005 assert_eq!(mock_food_injection.profile_ids_executed.len(), 2);
1006 assert_eq!(mock_food_injection.profile_ids_executed[0], 1);
1007 assert_eq!(mock_food_injection.profile_ids_executed[1], 1);
1008 })
1009 .unwrap();
1010
1011 join_handle_test_environment
1012 .join()
1013 .expect("Test environment thread did not finish.");
1014 join_handle_test_object
1015 .join()
1016 .expect("Test object thread did not finish.");
1017 }
1018
1019 #[test]
1020 // Test case runs feed control and triggers inhibition by using a mock SQL interface which returns only errors.
1021 // This test case does not require any communication with the database.
1022 pub fn test_messaging_sql_error_inhibition_feed() {
1023 let sleep_duration_100_millis = Duration::from_millis(100);
1024 let sleep_duration_10_secs = Duration::from_secs(10);
1025 let spin_sleeper = SpinSleeper::default();
1026
1027 setup_logger(LoggerConfig::default()).unwrap();
1028
1029 let config: ConfigData =
1030 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1031
1032 // mock interface will be used in operation of the test object
1033 let mock_sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send> =
1034 Box::new(MockSqlInterfaceFeed::new());
1035
1036 // create the test object
1037 let mut feed = Feed::new(config.feed, Duration::from_millis(1000));
1038
1039 let mut channels = Channels::new_for_test();
1040
1041 let mut mock_food_injection = MockFoodInjection::new();
1042
1043 let mutex_device_scheduler_feed_test_object = Arc::new(Mutex::new(0));
1044 let mutex_device_scheduler_feed_test_environment =
1045 mutex_device_scheduler_feed_test_object.clone();
1046
1047 // thread for test environment
1048 let join_handle_test_environment = thread::Builder::new()
1049 .name("test_environment".to_string())
1050 .spawn(move || {
1051 // Wait for 10 seconds to be able to afterward check
1052 // if the test object still managed to execute a feed operation
1053 spin_sleeper.sleep(sleep_duration_10_secs);
1054
1055 let actuation_count_1 =
1056 *mutex_device_scheduler_feed_test_environment.lock().unwrap();
1057
1058 // check the amount of actuation (after stop request)
1059 assert_eq!(actuation_count_1, 0);
1060
1061 // requesting feed control to quit
1062 let _ = channels.signal_handler.send_to_feed(InternalCommand::Quit);
1063 channels.signal_handler.receive_from_feed().unwrap();
1064 let _ = channels
1065 .signal_handler
1066 .send_to_feed(InternalCommand::Terminate);
1067 })
1068 .unwrap();
1069
1070 spin_sleeper.sleep(sleep_duration_100_millis);
1071
1072 // thread for the test object
1073 let join_handle_test_object = thread::Builder::new()
1074 .name("test_object".to_string())
1075 .spawn(move || {
1076 feed.execute(
1077 mutex_device_scheduler_feed_test_object.clone(),
1078 &mut channels.feed,
1079 &mut mock_food_injection,
1080 mock_sql_interface_feed,
1081 );
1082 println!("MockFoodInjection:\n{}", mock_food_injection);
1083
1084 // check if the right feed profiles have been executed
1085 assert_eq!(mock_food_injection.profile_ids_executed.len(), 0);
1086 })
1087 .unwrap();
1088
1089 join_handle_test_environment
1090 .join()
1091 .expect("Test environment thread did not finish.");
1092 join_handle_test_object
1093 .join()
1094 .expect("Test object thread did not finish.");
1095 }
1096
1097 // Helper function that creates an array of feed schedule entries
1098 // with data from the past.
1099 fn create_mock_feedschedule_entries() -> Vec<FeedScheduleEntry> {
1100 let mut feedschedule_entries: Vec<FeedScheduleEntry> = Vec::new();
1101
1102 feedschedule_entries.push(FeedScheduleEntry::new_for_test(
1103 "2024-10-24 10:00:00",
1104 77,
1105 "dummy1",
1106 true,
1107 ));
1108 feedschedule_entries.push(FeedScheduleEntry::new_for_test(
1109 "2024-10-24 11:00:00",
1110 78,
1111 "dummy2",
1112 true,
1113 ));
1114 feedschedule_entries.push(FeedScheduleEntry::new_for_test(
1115 "2024-10-24 12:00:00",
1116 79,
1117 "dummy3",
1118 true,
1119 ));
1120
1121 feedschedule_entries
1122 }
1123
1124 #[test]
1125 // Test case test selection of feed schedule entry in case there are multiple entries in the past
1126 // This test case tests the strategy to select none of them.
1127 // This test case does not require any communication with the database.
1128 pub fn test_select_feed_schedule_entry_for_execution_none() {
1129 setup_logger(LoggerConfig::default()).unwrap();
1130 let mut config: ConfigData =
1131 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1132 config.feed.strategy_multiple_schedule_entries_in_past = -1;
1133
1134 // create the test object
1135 let feed = Feed::new(config.feed, Duration::from_millis(1000));
1136
1137 // input for the function to be tested
1138 let feedschedule_entries = create_mock_feedschedule_entries();
1139
1140 assert_eq!(
1141 feed.select_feed_schedule_entry_for_execution(&feedschedule_entries),
1142 None
1143 );
1144 }
1145
1146 #[test]
1147 // Test case test selection of feed schedule entry in case there are multiple entries in the past
1148 // This test case tests the strategy to select the first one.
1149 // This test case does not require any communication with the database.
1150 pub fn test_select_feed_schedule_entry_for_execution_first() {
1151 setup_logger(LoggerConfig::default()).unwrap();
1152 let mut config: ConfigData =
1153 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1154 config.feed.strategy_multiple_schedule_entries_in_past = 0;
1155
1156 // create the test object
1157 let feed = Feed::new(config.feed, Duration::from_millis(1000));
1158
1159 // input for the function to be tested
1160 let feedschedule_entries = create_mock_feedschedule_entries();
1161
1162 assert_eq!(
1163 feed.select_feed_schedule_entry_for_execution(&feedschedule_entries)
1164 .unwrap()
1165 .profile_id,
1166 77
1167 );
1168 }
1169
1170 #[test]
1171 // Test case test selection of feed schedule entry in case there are multiple entries in the past
1172 // This test case tests the strategy to select a middle one.
1173 // This test case does not require any communication with the database.
1174 pub fn test_select_feed_schedule_entry_for_execution_middle() {
1175 setup_logger(LoggerConfig::default()).unwrap();
1176 let mut config: ConfigData =
1177 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1178 config.feed.strategy_multiple_schedule_entries_in_past = 1;
1179
1180 // create the test object
1181 let feed = Feed::new(config.feed, Duration::from_millis(1000));
1182
1183 // input for the function to be tested
1184 let feedschedule_entries = create_mock_feedschedule_entries();
1185
1186 assert_eq!(
1187 feed.select_feed_schedule_entry_for_execution(&feedschedule_entries)
1188 .unwrap()
1189 .profile_id,
1190 78
1191 );
1192 }
1193
1194 #[test]
1195 // Test case tests selection of feed schedule entry in case there are multiple entries in the past.
1196 // This test case tests the strategy to select the last one.
1197 // This test case does not require any communication with the database.
1198 pub fn test_select_feed_schedule_entry_for_execution_last() {
1199 let mut config: ConfigData =
1200 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1201 config.feed.strategy_multiple_schedule_entries_in_past = 99;
1202
1203 // create the test object
1204 let feed = Feed::new(config.feed, Duration::from_millis(1000));
1205
1206 // input for the function to be tested
1207 let feedschedule_entries = create_mock_feedschedule_entries();
1208
1209 assert_eq!(
1210 feed.select_feed_schedule_entry_for_execution(&feedschedule_entries)
1211 .unwrap()
1212 .profile_id,
1213 79
1214 );
1215 }
1216
1217 #[test]
1218 // Test case test implementation with multiple feed schedule entries in the past.
1219 // The youngest feed schedule entry shall be selected.
1220 // Test case uses test database #09.
1221 pub fn test_feed_schedule_execution_past_entries_select_youngest() {
1222 let sleep_duration_100_millis = Duration::from_millis(100);
1223 let sleep_duration_8_seconds = Duration::from_millis(10000);
1224 let spin_sleeper = SpinSleeper::default();
1225
1226 let mut channels = Channels::new_for_test();
1227
1228 setup_logger(LoggerConfig::default()).unwrap();
1229
1230 let mut mock_food_injection = MockFoodInjection::new();
1231
1232 let config: ConfigData = read_config_file_with_test_database(
1233 "/config/aquarium_control_test_generic.toml".to_string(),
1234 9,
1235 );
1236
1237 let mut sql_interface = match SqlInterface::new(config.sql_interface.clone()) {
1238 Ok(c) => c,
1239 Err(e) => {
1240 panic!("Could not connect to SQL database: {e:?}");
1241 }
1242 };
1243
1244 let mut sql_interface_feed_unboxed = SqlInterfaceFeed::new(
1245 sql_interface.get_connection().unwrap(),
1246 config.sql_interface.max_rows_feed_pattern,
1247 config.sql_interface.max_rows_feed_schedule,
1248 config.sql_interface.max_rows_feed_log,
1249 )
1250 .unwrap();
1251
1252 insert_feed_patterns(&mut sql_interface, &mut sql_interface_feed_unboxed);
1253
1254 let mut sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send> =
1255 Box::new(sql_interface_feed_unboxed);
1256
1257 // empty the feed schedule table
1258 match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
1259 Ok(_) => {}
1260 Err(e) => {
1261 panic!("Could not prepare test case: {e:?}")
1262 }
1263 }
1264
1265 let time_stamp_feed_schedule_entry_one_time =
1266 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, -2) {
1267 Ok(c) => c,
1268 Err(e) => {
1269 panic!(
1270 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
1271 );
1272 }
1273 };
1274
1275 let time_stamp_feed_schedule_entry_repeat =
1276 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, -4) {
1277 Ok(c) => c,
1278 Err(e) => {
1279 panic!(
1280 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
1281 );
1282 }
1283 };
1284
1285 // manipulation of the database: inserting feed schedule entries with future timestamp
1286 sql_interface_feed
1287 .insert_feed_schedule_entry(
1288 time_stamp_feed_schedule_entry_one_time,
1289 1,
1290 "testOneTimePast".to_string(),
1291 false,
1292 false,
1293 )
1294 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
1295
1296 sql_interface_feed
1297 .insert_feed_schedule_entry(
1298 time_stamp_feed_schedule_entry_repeat,
1299 2,
1300 "testRepeatPast".to_string(),
1301 false,
1302 true,
1303 )
1304 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
1305
1306 let mut feed = Feed::new(config.feed, Duration::from_millis(1000));
1307
1308 scope(|scope| {
1309 let mutex_device_scheduler_feed = Arc::new(Mutex::new(0));
1310
1311 // thread for mock signal handler
1312 scope.spawn(move || {
1313 create_mock_signal_handler(
1314 &mut channels.signal_handler.tx_signal_handler_to_feed,
1315 &mut channels.signal_handler.rx_signal_handler_from_feed,
1316 spin_sleeper,
1317 sleep_duration_8_seconds,
1318 );
1319 });
1320
1321 spin_sleeper.sleep(sleep_duration_100_millis);
1322
1323 // thread for the test object
1324 scope.spawn(move || {
1325 feed.execute(
1326 mutex_device_scheduler_feed.clone(),
1327 &mut channels.feed,
1328 &mut mock_food_injection,
1329 sql_interface_feed,
1330 );
1331
1332 // check if the right profiles have been executed in the right order
1333 assert_eq!(mock_food_injection.pop_profile_id_executed(), Some(1));
1334 assert_eq!(mock_food_injection.pop_profile_id_executed(), None);
1335 });
1336 });
1337 }
1338
1339 #[test]
1340 // Test case test implementation with multiple feed schedule entries in the past.
1341 // The oldest feed schedule entry shall be selected.
1342 // Test case uses test database #10.
1343 pub fn test_feed_schedule_execution_past_entries_select_oldest() {
1344 let sleep_duration_100_millis = Duration::from_millis(100);
1345 let sleep_duration_8_seconds = Duration::from_millis(10000);
1346 let spin_sleeper = SpinSleeper::default();
1347
1348 let mut channels = Channels::new_for_test();
1349
1350 setup_logger(LoggerConfig::default()).unwrap();
1351
1352 let mut mock_food_injection = MockFoodInjection::new();
1353
1354 let mut config: ConfigData = read_config_file_with_test_database(
1355 "/config/aquarium_control_test_generic.toml".to_string(),
1356 10,
1357 );
1358
1359 config.feed.strategy_multiple_schedule_entries_in_past = 99;
1360
1361 let mut sql_interface = match SqlInterface::new(config.sql_interface.clone()) {
1362 Ok(c) => c,
1363 Err(e) => {
1364 panic!("Could not connect to SQL database: {e:?}");
1365 }
1366 };
1367
1368 let mut sql_interface_feed_unboxed = SqlInterfaceFeed::new(
1369 sql_interface.get_connection().unwrap(),
1370 config.sql_interface.max_rows_feed_pattern,
1371 config.sql_interface.max_rows_feed_schedule,
1372 config.sql_interface.max_rows_feed_log,
1373 )
1374 .unwrap();
1375
1376 insert_feed_patterns(&mut sql_interface, &mut sql_interface_feed_unboxed);
1377
1378 let mut sql_interface_feed: Box<dyn DatabaseInterfaceFeedTrait + Sync + Send> =
1379 Box::new(sql_interface_feed_unboxed);
1380
1381 // empty the feed schedule table
1382 match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
1383 Ok(_) => {}
1384 Err(e) => {
1385 panic!("Could not prepare test case: {e:?}")
1386 }
1387 }
1388
1389 let time_stamp_feed_schedule_entry_one_time =
1390 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, -2) {
1391 Ok(c) => c,
1392 Err(e) => {
1393 panic!(
1394 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
1395 );
1396 }
1397 };
1398
1399 let time_stamp_feed_schedule_entry_repeat =
1400 match SqlInterface::get_current_timestamp_offset_seconds(&mut sql_interface.conn, -4) {
1401 Ok(c) => c,
1402 Err(e) => {
1403 panic!(
1404 "test_feed_schedule_execution: Could not calculate timestamp for stimuli: {e:?}"
1405 );
1406 }
1407 };
1408
1409 // manipulation of the database: inserting feed schedule entries with future timestamp
1410 sql_interface_feed
1411 .insert_feed_schedule_entry(
1412 time_stamp_feed_schedule_entry_one_time,
1413 1,
1414 "testOneTimePast".to_string(),
1415 false,
1416 false,
1417 )
1418 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
1419
1420 sql_interface_feed
1421 .insert_feed_schedule_entry(
1422 time_stamp_feed_schedule_entry_repeat,
1423 2,
1424 "testRepeatPast".to_string(),
1425 false,
1426 true,
1427 )
1428 .expect("test_feed_schedule_execution: Could not insert stimuli into database: {e:?}");
1429
1430 let mut feed = Feed::new(config.feed, Duration::from_millis(1000));
1431
1432 scope(|scope| {
1433 let mutex_device_scheduler_feed = Arc::new(Mutex::new(0));
1434
1435 // thread for mock signal handler
1436 scope.spawn(move || {
1437 create_mock_signal_handler(
1438 &mut channels.signal_handler.tx_signal_handler_to_feed,
1439 &mut channels.signal_handler.rx_signal_handler_from_feed,
1440 spin_sleeper,
1441 sleep_duration_8_seconds,
1442 );
1443 });
1444
1445 spin_sleeper.sleep(sleep_duration_100_millis);
1446
1447 // thread for the test object
1448 scope.spawn(move || {
1449 feed.execute(
1450 mutex_device_scheduler_feed.clone(),
1451 &mut channels.feed,
1452 &mut mock_food_injection,
1453 sql_interface_feed,
1454 );
1455
1456 // check if the right profiles have been executed in the right order
1457 assert_eq!(mock_food_injection.pop_profile_id_executed(), Some(2));
1458 assert_eq!(mock_food_injection.pop_profile_id_executed(), None);
1459 });
1460 });
1461 }
1462
1463 #[test]
1464 // Verifies that the `Feed::new` constructor correctly initializes the struct.
1465 //
1466 // This test checks that all fields are set to their expected default values upon creation,
1467 // ensuring a predictable starting state for the Feed controller.
1468 //
1469 // Test case uses test database #65 for configuration loading, as requested.
1470 fn test_feed_new() {
1471 println!("* Testing Feed::new constructor...");
1472
1473 // --- Setup ---
1474 // Load a test configuration. The database number is part of the config loading
1475 // but is not directly used by the `Feed::new` constructor itself.
1476 let config = read_config_file_with_test_database(
1477 "/config/aquarium_control_test_generic.toml".to_string(),
1478 65,
1479 );
1480 let ping_interval = Duration::from_secs(30);
1481
1482 // --- Execution ---
1483 let feed = Feed::new(config.feed.clone(), ping_interval);
1484
1485 // --- Assertions ---
1486
1487 // 1. Check that the configuration and interval were stored correctly.
1488 assert_eq!(
1489 &feed.config, &config.feed,
1490 "The provided FeedConfig should be stored in the struct."
1491 );
1492 assert_eq!(
1493 feed.database_ping_interval, ping_interval,
1494 "The provided database_ping_interval should be stored in the struct."
1495 );
1496 println!("* Succeeded: Config and ping interval are stored correctly.");
1497
1498 // 2. Check the initial state of command/request flags.
1499 assert_eq!(
1500 feed.execute_command_received, false,
1501 "execute_command_received should be initialized to false."
1502 );
1503 assert_eq!(
1504 feed.profile_id_requested, -1,
1505 "profile_id_requested should be initialized to -1."
1506 );
1507 println!("* Succeeded: Command flags are initialized correctly.");
1508
1509 // 3. Check the initial state of all blocking and locking flags.
1510 assert_eq!(
1511 feed.execution_blocked, false,
1512 "execution_blocked should be initialized to false."
1513 );
1514 assert_eq!(
1515 feed.lock_error_get_feedschedule_entry, false,
1516 "lock_error_get_feedschedule_entry should be initialized to false."
1517 );
1518 assert_eq!(
1519 feed.lock_warn_inapplicable_command_signal_handler, false,
1520 "lock_warn_inapplicable_command_signal_handler should be initialized to false."
1521 );
1522 assert_eq!(
1523 feed.lock_error_channel_receive_termination, false,
1524 "lock_error_channel_receive_termination should be initialized to false."
1525 );
1526 println!("* Succeeded: State and lock flags are initialized to false.");
1527
1528 // 4. Check the `last_ping_instant`.
1529 // We can't check for an exact time, but we can assert that it was created very recently.
1530 assert!(
1531 feed.last_ping_instant.elapsed() < Duration::from_millis(100),
1532 "last_ping_instant should be set to a very recent time."
1533 );
1534 println!("* Succeeded: last_ping_instant is initialized to the current time.");
1535
1536 println!("* Test for Feed::new completed successfully.");
1537 }
1538}