#[cfg(test)]
mod tests {
use chrono::{Duration, NaiveDate};
use kasl::db::{breaks::Breaks, workdays::Workdays};
use kasl::libs::{config::ProductivityConfig, pause::Pause, productivity::Productivity};
use tempfile::TempDir;
use test_context::{test_context, TestContext};
struct BreaksTestContext {
_temp_dir: TempDir,
}
impl TestContext for BreaksTestContext {
fn setup() -> Self {
let temp_dir = tempfile::tempdir().unwrap();
std::env::set_var("HOME", temp_dir.path());
std::env::set_var("LOCALAPPDATA", temp_dir.path());
BreaksTestContext { _temp_dir: temp_dir }
}
}
#[test_context(BreaksTestContext)]
#[test]
fn test_break_crud_operations(_ctx: &mut BreaksTestContext) {
let breaks_db = Breaks::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
let start_time = date.and_hms_opt(12, 0, 0).unwrap();
let end_time = date.and_hms_opt(12, 30, 0).unwrap();
let duration = Duration::minutes(30);
let break_record = kasl::db::breaks::Break {
id: None,
date,
start: start_time,
end: end_time,
duration,
reason: Some("Lunch break".to_string()),
created_at: None,
};
let break_id = breaks_db.insert(&break_record).unwrap();
assert!(break_id > 0);
let retrieved_break = breaks_db.get_by_id(break_id).unwrap().unwrap();
assert_eq!(retrieved_break.date, date);
assert_eq!(retrieved_break.start, start_time);
assert_eq!(retrieved_break.end, end_time);
assert_eq!(retrieved_break.duration, duration);
assert_eq!(retrieved_break.reason, Some("Lunch break".to_string()));
let daily_breaks = breaks_db.get_daily_breaks(date).unwrap();
assert_eq!(daily_breaks.len(), 1);
assert_eq!(daily_breaks[0].id, Some(break_id));
breaks_db.delete(break_id).unwrap();
let deleted_break = breaks_db.get_by_id(break_id).unwrap();
assert!(deleted_break.is_none());
}
#[test_context(BreaksTestContext)]
#[test]
fn test_multiple_breaks_ordering(_ctx: &mut BreaksTestContext) {
let breaks_db = Breaks::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
let break2 = kasl::db::breaks::Break {
id: None,
date,
start: date.and_hms_opt(15, 0, 0).unwrap(),
end: date.and_hms_opt(15, 15, 0).unwrap(),
duration: Duration::minutes(15),
reason: Some("Afternoon break".to_string()),
created_at: None,
};
let break1 = kasl::db::breaks::Break {
id: None,
date,
start: date.and_hms_opt(12, 0, 0).unwrap(),
end: date.and_hms_opt(12, 45, 0).unwrap(),
duration: Duration::minutes(45),
reason: Some("Lunch break".to_string()),
created_at: None,
};
breaks_db.insert(&break2).unwrap();
breaks_db.insert(&break1).unwrap();
let daily_breaks = breaks_db.get_daily_breaks(date).unwrap();
assert_eq!(daily_breaks.len(), 2);
assert_eq!(daily_breaks[0].start, date.and_hms_opt(12, 0, 0).unwrap());
assert_eq!(daily_breaks[1].start, date.and_hms_opt(15, 0, 0).unwrap());
}
#[test_context(BreaksTestContext)]
#[test]
fn test_productivity_calculation_with_breaks(_ctx: &mut BreaksTestContext) {
let mut workdays_db = Workdays::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
workdays_db.insert_start(date).unwrap();
workdays_db.conn.execute(
"UPDATE workdays SET start = '2025-01-15 09:00:00', end = '2025-01-15 17:00:00' WHERE date = ?",
[&date.to_string()],
).unwrap();
let workday = workdays_db.fetch(date).unwrap().unwrap();
let pauses = vec![
Pause {
id: 1,
start: date.and_hms_opt(10, 30, 0).unwrap(),
end: Some(date.and_hms_opt(10, 45, 0).unwrap()),
duration: Some(Duration::minutes(15)),
},
Pause {
id: 2,
start: date.and_hms_opt(14, 0, 0).unwrap(),
end: Some(date.and_hms_opt(14, 15, 0).unwrap()),
duration: Some(Duration::minutes(15)),
},
];
let breaks_db = Breaks::new().unwrap();
let break_record = kasl::db::breaks::Break {
id: None,
date,
start: date.and_hms_opt(12, 0, 0).unwrap(),
end: date.and_hms_opt(13, 0, 0).unwrap(),
duration: Duration::minutes(60),
reason: Some("Lunch break".to_string()),
created_at: None,
};
breaks_db.insert(&break_record).unwrap();
let breaks = breaks_db.get_daily_breaks(date).unwrap();
let productivity_calc = Productivity::with_test_data(&workday, breaks, pauses, vec![]);
let productivity = productivity_calc.calculate_productivity();
println!("Expected: 85.0-100.0, Got: {}", productivity);
assert!(productivity >= 85.0 && productivity <= 100.0);
}
#[test_context(BreaksTestContext)]
#[test]
fn test_productivity_threshold_validation(_ctx: &mut BreaksTestContext) {
let mut workdays_db = Workdays::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
workdays_db.insert_start(date).unwrap();
workdays_db.conn.execute(
"UPDATE workdays SET start = '2025-01-15 09:00:00', end = '2025-01-15 17:00:00' WHERE date = ?",
[&date.to_string()],
).unwrap();
let workday = workdays_db.fetch(date).unwrap().unwrap();
let pauses = vec![
Pause {
id: 1,
start: date.and_hms_opt(10, 0, 0).unwrap(),
end: Some(date.and_hms_opt(11, 0, 0).unwrap()),
duration: Some(Duration::minutes(60)),
},
Pause {
id: 2,
start: date.and_hms_opt(13, 0, 0).unwrap(),
end: Some(date.and_hms_opt(14, 0, 0).unwrap()),
duration: Some(Duration::minutes(60)),
},
Pause {
id: 3,
start: date.and_hms_opt(15, 0, 0).unwrap(),
end: Some(date.and_hms_opt(16, 0, 0).unwrap()),
duration: Some(Duration::minutes(60)),
},
];
let breaks = vec![];
let productivity_calc = Productivity::with_test_data(&workday, breaks.clone(), pauses.clone(), vec![]);
let initial_productivity = productivity_calc.calculate_productivity();
println!("Expected initial productivity: 62.5, Got: {}", initial_productivity);
assert!((initial_productivity - 62.5).abs() < 0.01);
let needed_break_minutes = productivity_calc.calculate_needed_break_duration(Some(75.0));
println!("Expected needed break minutes: 80, Got: {}", needed_break_minutes);
assert!(needed_break_minutes <= 80); }
#[test_context(BreaksTestContext)]
#[test]
fn test_break_placement_algorithms(_ctx: &mut BreaksTestContext) {
let mut workdays_db = Workdays::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
workdays_db.insert_start(date).unwrap();
workdays_db.conn.execute(
"UPDATE workdays SET start = '2025-01-15 09:00:00', end = '2025-01-15 17:00:00' WHERE date = ?",
[&date.to_string()],
).unwrap();
let workday = workdays_db.fetch(date).unwrap().unwrap();
let pauses = vec![
Pause {
id: 1,
start: date.and_hms_opt(10, 30, 0).unwrap(),
end: Some(date.and_hms_opt(10, 45, 0).unwrap()),
duration: Some(Duration::minutes(15)),
},
Pause {
id: 2,
start: date.and_hms_opt(14, 0, 0).unwrap(),
end: Some(date.and_hms_opt(14, 15, 0).unwrap()),
duration: Some(Duration::minutes(15)),
},
];
let intervals = kasl::libs::report::calculate_work_intervals(&workday, &pauses);
assert_eq!(intervals.len(), 3);
let longest_interval = intervals.iter().max_by_key(|i| i.duration.num_minutes()).unwrap();
assert_eq!(longest_interval.duration.num_minutes(), 195);
assert_eq!(longest_interval.start, date.and_hms_opt(10, 45, 0).unwrap());
assert_eq!(longest_interval.end, date.and_hms_opt(14, 0, 0).unwrap());
}
#[test_context(BreaksTestContext)]
#[test]
fn test_productivity_suggestion_timing(_ctx: &mut BreaksTestContext) {
let workday_start = NaiveDate::from_ymd_opt(2020, 1, 15).unwrap()
.and_hms_opt(9, 0, 0).unwrap();
let workday = kasl::db::workdays::Workday {
id: 1,
date: workday_start.date(),
start: workday_start,
end: None,
};
let mut productivity_calc = Productivity::with_test_data(&workday, vec![], vec![], vec![]);
productivity_calc.config.workday_hours = 8.0;
productivity_calc.config.min_workday_fraction_before_suggest = 0.0;
let always_suggest_zero = productivity_calc.should_suggest_productivity_improvements();
assert!(always_suggest_zero);
productivity_calc.config.min_workday_fraction_before_suggest = 0.5;
let should_suggest_half = productivity_calc.should_suggest_productivity_improvements();
assert!(should_suggest_half);
productivity_calc.config.min_workday_fraction_before_suggest = 1.0;
let should_suggest_full = productivity_calc.should_suggest_productivity_improvements();
assert!(should_suggest_full);
}
#[test_context(BreaksTestContext)]
#[test]
fn test_productivity_config_defaults(_ctx: &mut BreaksTestContext) {
let default_config = ProductivityConfig::default();
assert_eq!(default_config.min_productivity_threshold, 75.0);
assert_eq!(default_config.workday_hours, 8.0);
assert_eq!(default_config.min_break_duration, 20);
assert_eq!(default_config.max_break_duration, 180);
assert_eq!(default_config.min_workday_fraction_before_suggest, 0.5);
}
#[test_context(BreaksTestContext)]
#[test]
fn test_break_validation_constraints(_ctx: &mut BreaksTestContext) {
let config = ProductivityConfig::default();
assert!(config.min_break_duration <= config.max_break_duration);
assert!(config.min_break_duration > 0);
assert!(config.max_break_duration <= 480);
assert!(config.min_productivity_threshold > 0.0);
assert!(config.min_productivity_threshold <= 100.0);
assert!(config.workday_hours > 0.0);
assert!(config.workday_hours <= 24.0);
assert!(config.min_workday_fraction_before_suggest >= 0.0);
assert!(config.min_workday_fraction_before_suggest <= 1.0);
}
#[test_context(BreaksTestContext)]
#[test]
fn test_break_database_constraints(_ctx: &mut BreaksTestContext) {
let breaks_db = Breaks::new().unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
let valid_break = kasl::db::breaks::Break {
id: None,
date,
start: date.and_hms_opt(12, 0, 0).unwrap(),
end: date.and_hms_opt(13, 0, 0).unwrap(),
duration: Duration::minutes(60),
reason: None,
created_at: None,
};
let break_id = breaks_db.insert(&valid_break).unwrap();
assert!(break_id > 0);
let delete_result = breaks_db.delete(9999);
assert!(delete_result.is_err());
let non_existent = breaks_db.get_by_id(9999).unwrap();
assert!(non_existent.is_none());
}
}