@@ -118,12 +118,12 @@ impl TaskReminder {
118118 /// Calculates task instances from definitions on demand.
119119 /// Generates enough instances to provide the requested page.
120120 ///
121- /// E.g., for page 0 with `PAGE_SIZE=30`, up to 30 instances are generated. For page 2 with the same `PAGE_SIZE`, up to 90 instances are returned .
121+ /// E.g., for page 0 with `PAGE_SIZE=30`, up to 30 instances per definition are generated. For page 2 with the same `PAGE_SIZE`, up to 90 instances per definition are generated .
122122 pub fn generate_task_instances ( & mut self , page : i32 ) {
123123 #[ cfg( debug_assertions) ]
124124 println ! ( "Beginning to recalculate new instances" ) ;
125-
126- let definitions = self . task_definitions . clone ( ) ;
125+
126+ let definitions = & self . task_definitions ;
127127
128128 // If any tasks already have a window spawned, the state must be kept and set accordingly on the new set of instances
129129 let mut window_spawned_map: HashMap < ( Uuid , DateTime < Utc > ) , bool > = HashMap :: new ( ) ;
@@ -195,14 +195,17 @@ impl TaskReminder {
195195 } ) ;
196196
197197 instances. sort_by_key ( |i| i. timestamp ) ;
198- instances. iter_mut ( ) . take ( PAGE_SIZE as usize ) . for_each ( |i| {
199- if window_spawned_map. contains_key ( & ( i. definition_id , i. timestamp ) ) {
200- i. window_spawned = * window_spawned_map
201- . get ( & ( i. definition_id , i. timestamp ) )
202- . unwrap_or ( & false ) ;
203- } ;
204- self . push_task_instance ( i. clone ( ) ) ;
205- } ) ;
198+ instances
199+ . iter_mut ( )
200+ . take ( instances_required as usize )
201+ . for_each ( |i| {
202+ if window_spawned_map. contains_key ( & ( i. definition_id , i. timestamp ) ) {
203+ i. window_spawned = * window_spawned_map
204+ . get ( & ( i. definition_id , i. timestamp ) )
205+ . unwrap_or ( & false ) ;
206+ } ;
207+ self . push_task_instance ( i. clone ( ) ) ;
208+ } ) ;
206209
207210 self . calculated_instances = self . task_instances . len ( ) ;
208211
@@ -296,3 +299,193 @@ impl TaskReminder {
296299 Ok ( ( ) )
297300 }
298301}
302+
303+ #[ cfg( test) ]
304+ mod tests {
305+ use std:: path:: PathBuf ;
306+
307+ use super :: * ;
308+ use chrono:: TimeZone ;
309+ use tempfile:: tempdir;
310+
311+ fn sample_task_definition ( ) -> TaskDefinition {
312+ TaskDefinition {
313+ id : Uuid :: new_v4 ( ) ,
314+ name : "Sample Task" . to_string ( ) ,
315+ desc : Some ( "Sample Description" . to_string ( ) ) ,
316+ start : Utc . with_ymd_and_hms ( 2025 , 1 , 1 , 12 , 0 , 0 ) . unwrap ( ) ,
317+ recurrence : Some ( Recurrence :: Recurring {
318+ last_recurrence : None ,
319+ minutes : 60 ,
320+ exceptions : None ,
321+ } ) ,
322+ }
323+ }
324+
325+ #[ test]
326+ fn test_push_and_get_task_definition ( ) {
327+ let mut reminder = TaskReminder {
328+ task_definitions : vec ! [ ] ,
329+ task_instances : BinaryHeap :: new ( ) ,
330+ calculated_instances : 0 ,
331+ } ;
332+
333+ let def = sample_task_definition ( ) ;
334+ let id = def. id ;
335+ reminder. push_task_definition ( def. clone ( ) ) ;
336+
337+ assert_eq ! (
338+ reminder. get_task_definition( id) . unwrap( ) . name,
339+ "Sample Task"
340+ ) ;
341+ }
342+
343+ #[ test]
344+ fn test_generate_single_instance ( ) {
345+ let mut reminder = TaskReminder {
346+ task_definitions : vec ! [ sample_task_definition( ) ] ,
347+ task_instances : BinaryHeap :: new ( ) ,
348+ calculated_instances : 0 ,
349+ } ;
350+
351+ reminder. generate_task_instances ( 0 ) ;
352+ let tasks = reminder. get_tasks ( 0 ) ;
353+
354+ assert ! ( !tasks. is_empty( ) ) ;
355+ assert_eq ! ( tasks[ 0 ] . name, "Sample Task" ) ;
356+ }
357+
358+ #[ test]
359+ fn test_mark_task_completed_updates_last_recurrence ( ) {
360+ let temp_dir = tempdir ( ) . expect ( "Failed to create temp dir" ) ;
361+ let test_data_path: PathBuf = temp_dir. path ( ) . join ( "tasks.json" ) ;
362+
363+ config:: init ( test_data_path. clone ( ) ) ;
364+
365+ let mut reminder = TaskReminder {
366+ task_definitions : vec ! [ sample_task_definition( ) ] ,
367+ task_instances : BinaryHeap :: new ( ) ,
368+ calculated_instances : 0 ,
369+ } ;
370+
371+ reminder. generate_task_instances ( 0 ) ;
372+ let task = reminder. get_next_task ( ) . unwrap ( ) . clone ( ) ;
373+ reminder. mark_task_completed ( task. clone ( ) ) . unwrap ( ) ;
374+
375+ let updated_def = reminder. get_task_definition ( task. definition_id ) . unwrap ( ) ;
376+ if let Some ( Recurrence :: Recurring {
377+ last_recurrence, ..
378+ } ) = & updated_def. recurrence
379+ {
380+ assert_eq ! ( * last_recurrence, Some ( task. timestamp) ) ;
381+ } else {
382+ panic ! ( "Expected recurring task" ) ;
383+ }
384+ }
385+
386+ #[ test]
387+ fn test_delete_task_definition ( ) {
388+ let temp_dir = tempdir ( ) . expect ( "Failed to create temp dir" ) ;
389+ let test_data_path: PathBuf = temp_dir. path ( ) . join ( "tasks.json" ) ;
390+
391+ config:: init ( test_data_path. clone ( ) ) ;
392+
393+ let mut reminder = TaskReminder {
394+ task_definitions : vec ! [ sample_task_definition( ) ] ,
395+ task_instances : BinaryHeap :: new ( ) ,
396+ calculated_instances : 0 ,
397+ } ;
398+
399+ let id = reminder. task_definitions [ 0 ] . id ;
400+ reminder. delete_task_definition ( id) . unwrap ( ) ;
401+
402+ assert ! ( reminder. get_task_definition( id) . is_none( ) ) ;
403+ }
404+
405+ #[ test]
406+ fn test_non_recurring_task_generates_single_instance ( ) {
407+ let mut reminder = TaskReminder {
408+ task_definitions : vec ! [ TaskDefinition {
409+ id: Uuid :: new_v4( ) ,
410+ name: "One-time Task" . to_string( ) ,
411+ desc: None ,
412+ start: Utc . with_ymd_and_hms( 2025 , 1 , 1 , 9 , 0 , 0 ) . unwrap( ) ,
413+ recurrence: Some ( Recurrence :: None ) ,
414+ } ] ,
415+ task_instances : BinaryHeap :: new ( ) ,
416+ calculated_instances : 0 ,
417+ } ;
418+
419+ reminder. generate_task_instances ( 0 ) ;
420+ let tasks = reminder. get_tasks ( 0 ) ;
421+
422+ assert_eq ! ( tasks. len( ) , 1 ) ;
423+ assert_eq ! ( tasks[ 0 ] . name, "One-time Task" ) ;
424+ }
425+
426+ #[ test]
427+ fn test_task_with_exception_does_not_generate_that_instance ( ) {
428+ let start = Utc . with_ymd_and_hms ( 2025 , 1 , 1 , 10 , 0 , 0 ) . unwrap ( ) ;
429+ let exception_time = start + Duration :: minutes ( 60 ) ;
430+
431+ let mut reminder = TaskReminder {
432+ task_definitions : vec ! [ TaskDefinition {
433+ id: Uuid :: new_v4( ) ,
434+ name: "With Exception" . to_string( ) ,
435+ desc: None ,
436+ start,
437+ recurrence: Some ( Recurrence :: Recurring {
438+ last_recurrence: None ,
439+ minutes: 60 ,
440+ exceptions: Some ( vec![ exception_time] ) ,
441+ } ) ,
442+ } ] ,
443+ task_instances : BinaryHeap :: new ( ) ,
444+ calculated_instances : 0 ,
445+ } ;
446+
447+ reminder. generate_task_instances ( 0 ) ;
448+ let tasks = reminder. get_tasks ( 0 ) ;
449+
450+ // Ensure that the exception timestamp is not in the generated list
451+ for task in tasks {
452+ assert_ne ! ( task. timestamp, exception_time) ;
453+ }
454+ }
455+
456+ #[ test]
457+ fn test_pagination_limits_task_output ( ) {
458+ let def = TaskDefinition {
459+ id : Uuid :: new_v4 ( ) ,
460+ name : "Paged Task" . to_string ( ) ,
461+ desc : None ,
462+ start : Utc . with_ymd_and_hms ( 2025 , 1 , 1 , 8 , 0 , 0 ) . unwrap ( ) ,
463+ recurrence : Some ( Recurrence :: Recurring {
464+ last_recurrence : None ,
465+ minutes : 5 , // small interval to fill multiple pages quickly
466+ exceptions : None ,
467+ } ) ,
468+ } ;
469+
470+ let mut reminder = TaskReminder {
471+ task_definitions : vec ! [ def] ,
472+ task_instances : BinaryHeap :: new ( ) ,
473+ calculated_instances : 0 ,
474+ } ;
475+
476+ // Generate page 0
477+ let page_0 = reminder. get_tasks ( 0 ) ;
478+ assert_eq ! ( page_0. len( ) , PAGE_SIZE as usize ) ;
479+
480+ // Generate page 1
481+ let page_1 = reminder. get_tasks ( 1 ) ;
482+ assert_eq ! ( page_1. len( ) , PAGE_SIZE as usize ) ;
483+
484+ // Ensure no duplicates between pages
485+ let timestamps_page_0: Vec < _ > = page_0. iter ( ) . map ( |t| t. timestamp ) . collect ( ) ;
486+ let timestamps_page_1: Vec < _ > = page_1. iter ( ) . map ( |t| t. timestamp ) . collect ( ) ;
487+ assert ! ( timestamps_page_0
488+ . iter( )
489+ . all( |ts| !timestamps_page_1. contains( ts) ) ) ;
490+ }
491+ }
0 commit comments