From 3b5d5121a0e28d03921065bbb970ebfe0ec17173 Mon Sep 17 00:00:00 2001 From: Jonathan Baecker Date: Mon, 28 Oct 2024 16:31:53 +0100 Subject: [PATCH] add time machine for time manipulation mostly for testing/debugging --- engine/examples/mock_time.rs | 65 ++++++++++++++++++++++++++++++ engine/src/db/handles.rs | 2 +- engine/src/db/models.rs | 4 ++ engine/src/main.rs | 3 ++ engine/src/player/utils/mod.rs | 36 +---------------- engine/src/utils/args_parse.rs | 5 ++- engine/src/utils/logging.rs | 12 +++++- engine/src/utils/mod.rs | 1 + engine/src/utils/time_machine.rs | 43 ++++++++++++++++++++ migrations/00001_create_tables.sql | 5 ++- tests/src/engine_playlist.rs | 16 ++++---- tests/src/utils.rs | 15 +++---- 12 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 engine/examples/mock_time.rs create mode 100644 engine/src/utils/time_machine.rs diff --git a/engine/examples/mock_time.rs b/engine/examples/mock_time.rs new file mode 100644 index 00000000..187f148e --- /dev/null +++ b/engine/examples/mock_time.rs @@ -0,0 +1,65 @@ +use std::{ + process, + sync::{Arc, Mutex}, + thread::sleep, + time::Duration, +}; + +use chrono::{prelude::*, TimeDelta}; +use clap::Parser; + +// Struct to hold command-line arguments +#[derive(Parser, Debug, Clone)] +#[clap(version, about = "run time machine")] +struct Args { + #[clap(short, long, help = "set time")] + fake_time: Option, +} + +// Thread-local storage for time offset when mocking the time +lazy_static::lazy_static! { + static ref DATE_TIME_DIFF: Arc>> = Arc::new(Mutex::new(None)); +} + +// Set the mock time offset if `--fake-time` argument is provided +pub fn set_mock_time(fake_time: &Option) { + if let Some(time) = fake_time { + if let Ok(mock_time) = DateTime::parse_from_rfc3339(time) { + let mock_time = mock_time.with_timezone(&Local); + // Calculate the offset from the real current time + let mut diff = DATE_TIME_DIFF.lock().unwrap(); + *diff = Some(Local::now() - mock_time); + } else { + eprintln!( + "Error: Invalid date format for --fake-time, use time with offset in: 2024-10-27T00:59:00+02:00" + ); + process::exit(1); + } + } +} + +// Function to get the current time, using either real or mock time based on `--fake-time` +pub fn time_now() -> DateTime { + let diff = DATE_TIME_DIFF.lock().unwrap(); + + if let Some(d) = &*diff { + // If `--fake-time` is set, use the offset time + Local::now() - *d + } else { + // Otherwise, use the real current time + Local::now() + } +} + +fn main() { + let args = Args::parse(); + + // Initialize mock time if `--fake-time` is set + set_mock_time(&args.fake_time); + + loop { + println!("Current time (or mocked time): {}", time_now()); + + sleep(Duration::from_secs(1)); + } +} diff --git a/engine/src/db/handles.rs b/engine/src/db/handles.rs index 88b30cd9..575c4240 100644 --- a/engine/src/db/handles.rs +++ b/engine/src/db/handles.rs @@ -86,7 +86,7 @@ pub async fn select_related_channels( ) -> Result, sqlx::Error> { let query = match user_id { Some(id) => format!( - "SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.public, c.playlists, c.storage, c.last_date, c.time_shift FROM channels c + "SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.public, c.playlists, c.storage, c.last_date, c.time_shift, c.timezone FROM channels c left join user_channels uc on uc.channel_id = c.id left join user u on u.id = uc.user_id WHERE u.id = {id} ORDER BY c.id ASC;" diff --git a/engine/src/db/models.rs b/engine/src/db/models.rs index a58b3500..b1614238 100644 --- a/engine/src/db/models.rs +++ b/engine/src/db/models.rs @@ -74,6 +74,10 @@ pub struct Channel { pub storage: String, pub last_date: Option, pub time_shift: f64, + // not in use currently + #[sqlx(default)] + #[serde(default, skip_serializing)] + pub timezone: Option, #[sqlx(default)] #[serde(default)] diff --git a/engine/src/main.rs b/engine/src/main.rs index b55fdd96..54b26131 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -31,6 +31,7 @@ use ffplayout::{ config::get_config, logging::{init_logging, MailQueue}, playlist::generate_playlist, + time_machine::set_mock_time, }, validator, ARGS, }; @@ -61,6 +62,8 @@ async fn main() -> std::io::Result<()> { exit(c); } + set_mock_time(&ARGS.fake_time); + init_globales(&pool).await; init_logging(mail_queues.clone())?; diff --git a/engine/src/player/utils/mod.rs b/engine/src/player/utils/mod.rs index d07b692f..24969275 100644 --- a/engine/src/player/utils/mod.rs +++ b/engine/src/player/utils/mod.rs @@ -35,6 +35,7 @@ use crate::utils::{ config::{OutputMode::*, PlayoutConfig, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS}, errors::ProcessError, logging::Target, + time_machine::time_now, }; pub use json_serializer::{read_json, JsonPlaylist}; @@ -1182,38 +1183,3 @@ pub fn custom_format(template: &str, args: &[T]) -> String { filled_template } - -/// Get system time, in non test/debug case. -#[cfg(not(any(test, debug_assertions)))] -pub fn time_now() -> DateTime { - Local::now() -} - -/// Get mocked system time, in test/debug case. -#[cfg(any(test, debug_assertions))] -pub mod mock_time { - use super::*; - use std::cell::RefCell; - - thread_local! { - static DATE_TIME_DIFF: RefCell> = const { RefCell::new(None) }; - } - - pub fn time_now() -> DateTime { - DATE_TIME_DIFF.with(|cell| match cell.borrow().as_ref().cloned() { - Some(diff) => Local::now() - diff, - None => Local::now(), - }) - } - - pub fn set_mock_time(date_time: &str) { - if let Ok(d) = NaiveDateTime::parse_from_str(date_time, "%Y-%m-%dT%H:%M:%S") { - let time = Local.from_local_datetime(&d).unwrap(); - - DATE_TIME_DIFF.with(|cell| *cell.borrow_mut() = Some(Local::now() - time)); - } - } -} - -#[cfg(any(test, debug_assertions))] -pub use mock_time::time_now; diff --git a/engine/src/utils/args_parse.rs b/engine/src/utils/args_parse.rs index ad3278a9..152f5edc 100644 --- a/engine/src/utils/args_parse.rs +++ b/engine/src/utils/args_parse.rs @@ -27,7 +27,7 @@ use crate::ARGS; #[cfg(target_family = "unix")] use crate::utils::db_path; -#[derive(Parser, Debug, Clone)] +#[derive(Parser, Debug, Default, Clone)] #[clap(version, about = "ffplayout - 24/7 broadcasting solution", long_about = Some("ffplayout - 24/7 broadcasting solution\n @@ -140,6 +140,9 @@ pub struct Args { )] pub channels: Option>, + #[clap(long, hide = true, help = "set fake time (for debugging)")] + pub fake_time: Option, + #[clap( short, long, diff --git a/engine/src/utils/logging.rs b/engine/src/utils/logging.rs index 4676c882..76f2e060 100644 --- a/engine/src/utils/logging.rs +++ b/engine/src/utils/logging.rs @@ -23,7 +23,9 @@ use regex::Regex; use super::ARGS; use crate::db::models::GlobalSettings; -use crate::utils::{config::Mail, errors::ProcessError, round_to_nearest_ten}; +use crate::utils::{ + config::Mail, errors::ProcessError, round_to_nearest_ten, time_machine::time_now, +}; #[derive(Debug)] pub struct Target; @@ -263,12 +265,18 @@ fn console_formatter(w: &mut dyn Write, now: &mut DeferredNow, record: &Record) }; if ARGS.log_timestamp { + let time = if ARGS.fake_time.is_some() { + time_now() + } else { + *now.now() + }; + write!( w, "{} {}", colorize_string(format!( "[{}]", - now.now().format("%Y-%m-%d %H:%M:%S%.6f") + time.format("%Y-%m-%d %H:%M:%S%.6f") )), log_line ) diff --git a/engine/src/utils/mod.rs b/engine/src/utils/mod.rs index 6b1f6b29..27f73c17 100644 --- a/engine/src/utils/mod.rs +++ b/engine/src/utils/mod.rs @@ -32,6 +32,7 @@ pub mod logging; pub mod playlist; pub mod system; pub mod task_runner; +pub mod time_machine; use crate::db::models::GlobalSettings; use crate::player::utils::time_to_sec; diff --git a/engine/src/utils/time_machine.rs b/engine/src/utils/time_machine.rs new file mode 100644 index 00000000..0bcb05dc --- /dev/null +++ b/engine/src/utils/time_machine.rs @@ -0,0 +1,43 @@ +/// These functions are made for testing purposes. +/// It allows, with a hidden command line argument, to override the time in this program. +/// It is like a time machine where you can fake the time and make the hole program think it is running in the future or past. +use std::{ + process, + sync::{Arc, Mutex}, +}; + +use chrono::{prelude::*, TimeDelta}; + +// Thread-local storage for time offset when mocking the time +lazy_static::lazy_static! { + static ref DATE_TIME_DIFF: Arc>> = Arc::new(Mutex::new(None)); +} + +// Set the mock time offset if `--fake-time` argument is provided +pub fn set_mock_time(fake_time: &Option) { + if let Some(time) = fake_time { + if let Ok(mock_time) = DateTime::parse_from_rfc3339(time) { + let mock_time = mock_time.with_timezone(&Local); + // Calculate the offset from the real current time + let mut diff = DATE_TIME_DIFF.lock().unwrap(); + *diff = Some(Local::now() - mock_time); + } else { + eprintln!( + "Error: Invalid date format for --fake-time, use time with offset in: 2024-10-27T00:59:00+02:00" + ); + process::exit(1); + } + } +} + +// Function to get the current time, using either real or mock time based on `--fake-time` +pub fn time_now() -> DateTime { + let diff = DATE_TIME_DIFF.lock().unwrap(); + if let Some(d) = &*diff { + // If `--fake-time` is set, use the offset time + Local::now() - *d + } else { + // Otherwise, use the real current time + Local::now() + } +} diff --git a/migrations/00001_create_tables.sql b/migrations/00001_create_tables.sql index a6f42d5f..943ddb36 100644 --- a/migrations/00001_create_tables.sql +++ b/migrations/00001_create_tables.sql @@ -29,13 +29,14 @@ CREATE TABLE id INTEGER PRIMARY KEY, name TEXT NOT NULL, preview_url TEXT NOT NULL, - extra_extensions TEXT NOT NULL DEFAULT 'jpg,jpeg,png', + extra_extensions TEXT NOT NULL DEFAULT "jpg,jpeg,png", active INTEGER NOT NULL DEFAULT 0, public TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public", playlists TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists", storage TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media", last_date TEXT, - time_shift REAL NOT NULL DEFAULT 0 + time_shift REAL NOT NULL DEFAULT 0, + timezone TEXT ); CREATE TABLE diff --git a/tests/src/engine_playlist.rs b/tests/src/engine_playlist.rs index e473c050..1413b00e 100644 --- a/tests/src/engine_playlist.rs +++ b/tests/src/engine_playlist.rs @@ -9,10 +9,10 @@ use tokio::runtime::Runtime; use ffplayout::db::handles; use ffplayout::player::output::player; -use ffplayout::player::utils::*; use ffplayout::player::{controller::ChannelManager, input::playlist::gen_source, utils::Media}; use ffplayout::utils::config::OutputMode::Null; use ffplayout::utils::config::{PlayoutConfig, ProcessMode::Playlist}; +use ffplayout::utils::time_machine::set_mock_time; use ffplayout::vec_strings; async fn prepare_config() -> (PlayoutConfig, ChannelManager) { @@ -121,7 +121,7 @@ fn playlist_missing() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2023-02-07T23:59:45"); + set_mock_time(&Some("2023-02-07T23:59:45".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); @@ -155,7 +155,7 @@ fn playlist_next_missing() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2023-02-09T23:59:45"); + set_mock_time(&Some("2023-02-09T23:59:45".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); @@ -189,7 +189,7 @@ fn playlist_to_short() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2024-01-31T05:59:40"); + set_mock_time(&Some("2024-01-31T05:59:40".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); @@ -223,7 +223,7 @@ fn playlist_init_after_list_end() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2024-01-31T05:59:47"); + set_mock_time(&Some("2024-01-31T05:59:47".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); @@ -257,7 +257,7 @@ fn playlist_change_at_midnight() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2023-02-08T23:59:45"); + set_mock_time(&Some("2023-02-08T23:59:45".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); @@ -291,7 +291,7 @@ fn playlist_change_before_midnight() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2023-02-08T23:59:30"); + set_mock_time(&Some("2023-02-08T23:59:30".to_string())); thread::spawn(move || timed_stop(35, manager_clone)); @@ -325,7 +325,7 @@ fn playlist_change_at_six() { config.output.output_filter = None; config.output.output_cmd = Some(vec_strings!["-f", "null", "-"]); - mock_time::set_mock_time("2023-02-09T05:59:45"); + set_mock_time(&Some("2023-02-09T05:59:45".to_string())); thread::spawn(move || timed_stop(28, manager_clone)); diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 73cc31cd..c52dbe89 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -1,13 +1,14 @@ use sqlx::sqlite::SqlitePoolOptions; use tokio::runtime::Runtime; -#[cfg(test)] use chrono::prelude::*; -#[cfg(test)] use ffplayout::db::handles; use ffplayout::player::{controller::ChannelManager, utils::*}; -use ffplayout::utils::config::{PlayoutConfig, ProcessMode::Playlist}; +use ffplayout::utils::{ + config::{PlayoutConfig, ProcessMode::Playlist}, + time_machine::{set_mock_time, time_now}, +}; async fn prepare_config() -> (PlayoutConfig, ChannelManager) { let pool = SqlitePoolOptions::new() @@ -44,7 +45,7 @@ fn mock_date_time() { let date_obj = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S"); let time = Local.from_local_datetime(&date_obj.unwrap()).unwrap(); - mock_time::set_mock_time(time_str); + set_mock_time(&Some(time_str.to_string())); assert_eq!( time.format("%Y-%m-%dT%H:%M:%S.2f").to_string(), @@ -54,7 +55,7 @@ fn mock_date_time() { #[test] fn get_date_yesterday() { - mock_time::set_mock_time("2022-05-20T05:59:24"); + set_mock_time(&Some("2022-05-20T05:59:24".to_string())); let date = get_date(true, 21600.0, false); @@ -63,7 +64,7 @@ fn get_date_yesterday() { #[test] fn get_date_tomorrow() { - mock_time::set_mock_time("2022-05-20T23:59:58"); + set_mock_time(&Some("2022-05-20T23:59:58".to_string())); let date = get_date(false, 0.0, true); @@ -79,7 +80,7 @@ fn test_delta() { config.playlist.day_start = "00:00:00".into(); config.playlist.length = "24:00:00".into(); - mock_time::set_mock_time("2022-05-09T23:59:59"); + set_mock_time(&Some("2022-05-09T23:59:59".to_string())); let (delta, _) = get_delta(&config, &86401.0); assert!(delta < 2.0);