Skip to content

Commit 20a1e5c

Browse files
committed
Refactor, Add tests
1 parent 289172b commit 20a1e5c

File tree

6 files changed

+217
-26
lines changed

6 files changed

+217
-26
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ The built binary & installers will be created in `src-tauri/target/release`.
6161

6262
## TODO
6363

64-
- LICENCE
6564
- Try building for MacOS via GHA pipeline provided by tauri
66-
- Mention in readme the location of `tasks.json`, `settings.json` and `alarm.mp3`
6765
- Calendar units for recurrence
6866
- GUI paging
6967

7068
## Future Releases
7169

70+
- Update paging so that all generated instances are not held in memory and do not grow with each new page
71+
- Only hold the current page and check if it's needed to regenerate upon every request for a page of tasks
7272
- MacOS release
7373
- Fix title bar (<https://v2.tauri.app/learn/window-customization/#creating-a-custom-titlebar>)
7474
- Clean up the app data directory when uninstalling (maybe make it optional with a checkbox?)

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ features = [
3030
"v4",
3131
]
3232

33+
[dev-dependencies]
34+
tempfile = "3.10"
35+
3336
[profile.dev]
3437
incremental = true # Compile your binary in smaller steps.
3538

src-tauri/src/config.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,6 @@ fn setup_config(dir_path: &PathBuf) -> (PathBuf, Settings) {
3939
let task_path = dir_path.join(TASK_FILE_NAME);
4040
let settings_path = dir_path.join(SETTINGS_FILE_NAME);
4141

42-
// Path relative to the location of the executable
43-
// let exe_dir = std::env::current_exe()
44-
// .unwrap()
45-
// .parent()
46-
// .unwrap()
47-
// .to_path_buf();
4842
let resources_dir = dir_path.join("resources");
4943

5044
if !resources_dir.exists() {
@@ -110,10 +104,10 @@ pub fn update_settings(settings: Settings) -> Result<(), Box<dyn Error>> {
110104
pub fn init(data_dir_path: PathBuf) {
111105
let (data_path, settings) = setup_config(&data_dir_path);
112106

113-
CONFIG
114-
.set(Mutex::new(Config {
107+
CONFIG.get_or_init(|| {
108+
Mutex::new(Config {
115109
data_path,
116110
settings,
117-
}))
118-
.expect("Config already initialized");
111+
})
112+
});
119113
}

src-tauri/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ fn setup_tray(app: &mut App) {
6161
}
6262
}
6363
_ => {
64-
#[cfg(debug_assertions)]
65-
println!("unhandled event {event:?}");
64+
// #[cfg(debug_assertions)]
65+
// println!("unhandled event {event:?}");
6666
}
6767
})
6868
.build(app)
@@ -179,7 +179,7 @@ pub fn run() {
179179
if !next.0.window_spawned {
180180
#[cfg(debug_assertions)]
181181
println!("Reminding task!!!: {:?}", next.0.timestamp);
182-
182+
183183
window_counter += 1;
184184

185185
next.0.window_spawned = true;

src-tauri/src/tasks.rs

Lines changed: 204 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)