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}