use anyhow::{Context, Result};
use chrono::{DateTime, Local};
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use uuid::Uuid;
use super::{
SortOptions,
active_task::{self, ActiveTask},
filter::{DateRange, Filter},
model::{Priority, Task, TaskStatus},
sort_tasks, storage,
};
use crate::{
config::{GitConfig, PathConfig},
display::Display,
git::{MergeStrategy, repo::GitRepo},
};
#[derive(Default)]
pub struct TaskManager {
path_config: PathConfig,
git_config: GitConfig,
}
impl TaskManager {
fn is_time_in_range(time: &str, range: &DateRange) -> bool {
let time = DateTime::parse_from_rfc3339(time)
.unwrap()
.with_timezone(&Local);
range.from.map(|from| time >= from).unwrap_or(true)
&& range.to.map(|to| time < to).unwrap_or(true)
}
fn matches_filters(task: &Task, filter_options: &Filter) -> bool {
if let Some(p) = &filter_options.priority {
if task.priority != *p {
return false;
}
}
if let Some(s) = &filter_options.task_scope {
if task.scope.as_deref() != Some(s) {
return false;
}
}
if let (Some(t), Some(task_type)) = (&filter_options.task_type, &task.task_type) {
if task_type != t {
return false;
}
}
if let Some(st) = &filter_options.status {
if task.status != *st {
return false;
}
}
if let Some(date_range) = &filter_options.creation_time {
if !Self::is_time_in_range(&task.created_at, date_range) {
return false;
}
}
if let Some(date_range) = &filter_options.update_time {
let updated_at = task.updated_at.as_deref().unwrap_or(&task.created_at);
if !Self::is_time_in_range(updated_at, date_range) {
return false;
}
}
if let Some(date_range) = &filter_options.completion_time {
let Some(completed_at) = &task.completed_at else {
return false;
};
if !Self::is_time_in_range(completed_at, date_range) {
return false;
}
}
if let Some(query) = &filter_options.fuzzy {
if !query.is_empty() {
let matcher = SkimMatcherV2::default();
if matcher.fuzzy_match(&task.description, query).is_none() {
return false;
}
}
}
true
}
}
impl TaskManager {
pub const fn new(path_config: PathConfig, git_config: GitConfig) -> Self {
Self {
path_config,
git_config,
}
}
pub fn add_task(
&self,
description: &str,
priority: Priority,
scope: Option<String>,
task_type: Option<String>,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let task = Task::new(
id.clone(),
description.to_string(),
priority,
scope,
task_type,
);
storage::save_task(
&self.path_config.task_dir_path(),
&task,
"create",
"Create task",
)?;
Ok(id)
}
pub fn list_tasks(
&self,
filter_options: &Filter,
sort_options: Option<&SortOptions>,
) -> Result<Vec<Task>> {
let tasks = storage::load_all_tasks(&self.path_config.task_dir_path())?;
let mut filtered_tasks = tasks
.into_iter()
.filter(|task| Self::matches_filters(task, filter_options))
.collect::<Vec<Task>>();
let Some(sort_options) = sort_options else {
return Ok(filtered_tasks);
};
sort_tasks(&mut filtered_tasks, sort_options);
Ok(filtered_tasks)
}
pub fn mark_task_done(&self, task_id: &str) -> Result<()> {
let mut task = storage::load_task(&self.path_config.task_dir_path(), task_id)?;
if task.status == TaskStatus::Done {
anyhow::bail!("Task is already completed");
}
let is_active_task =
match active_task::load_active_task(&self.path_config.active_task_file_path())? {
Some(active) => {
if active.task_id == task_id {
let started_time = DateTime::parse_from_rfc3339(&active.started_at)
.context("Failed to parse started_at time from active task record")?;
let now = Local::now();
let duration =
now.signed_duration_since(started_time.with_timezone(&Local));
let seconds_spent = duration.num_seconds().max(0) as u64;
task.time_spent = Some(task.time_spent.unwrap_or(0) + seconds_spent);
true
} else {
false
}
}
None => false,
};
task.status = TaskStatus::Done;
task.updated_at = Some(Local::now().to_rfc3339());
task.completed_at = Some(Local::now().to_rfc3339());
storage::save_task(
&self.path_config.task_dir_path(),
&task,
"finish",
"Mark task as done",
)?;
if is_active_task {
active_task::clear_active_task(&self.path_config.active_task_file_path())?;
log::debug!("Completed active task: {task_id} and cleared active task file");
} else {
log::debug!("Completed task: {task_id}");
}
Ok(())
}
pub fn start_task(&self, task_id: &str) -> Result<String> {
if let Some(active) =
active_task::load_active_task(&self.path_config.active_task_file_path())?
{
let active_task_obj =
storage::load_task(&self.path_config.task_dir_path(), &active.task_id)?;
anyhow::bail!(
"There's already an active task: {} - {}. Stop it first.",
active.task_id,
active_task_obj.description
)
}
let task = storage::load_task(&self.path_config.task_dir_path(), task_id)?;
if task.status == TaskStatus::Done {
anyhow::bail!("Cannot start a completed task")
}
if task.status == TaskStatus::Aborted {
anyhow::bail!("Cannot start an aborted task")
}
let now = Local::now().to_rfc3339();
let active = ActiveTask::new(task.id.clone(), now);
active_task::save_active_task(&self.path_config.active_task_file_path(), &active)?;
log::debug!("Started task: {} and saved to active task file", task.id);
Ok(task.id)
}
pub fn stop_task(&self) -> Result<String> {
let Some(active_task_info) =
active_task::load_active_task(&self.path_config.active_task_file_path())?
else {
anyhow::bail!("No active task found. Task might not be in progress.")
};
let mut task =
storage::load_task(&self.path_config.task_dir_path(), &active_task_info.task_id)?;
let started_time = DateTime::parse_from_rfc3339(&active_task_info.started_at)
.context("Failed to parse started_at time from active task record")?;
let now = Local::now();
let duration = now.signed_duration_since(started_time.with_timezone(&Local));
let seconds_spent = duration.num_seconds().max(0) as u64;
task.time_spent = Some(task.time_spent.unwrap_or(0) + seconds_spent);
task.updated_at = Some(Local::now().to_rfc3339());
storage::save_task(
&self.path_config.task_dir_path(),
&task,
"update",
"Update time spent on task",
)?;
active_task::clear_active_task(&self.path_config.active_task_file_path())?;
log::debug!(
"Stopped task: {} and cleared active task file",
&active_task_info.task_id
);
Ok(active_task_info.task_id)
}
pub fn abort_task(&self, task_id: &Option<String>) -> Result<String> {
let task_id = match task_id {
Some(task_id) => task_id.to_owned(),
None => {
let Some(active_task) =
active_task::load_active_task(&self.path_config.active_task_file_path())?
else {
anyhow::bail!("No active task found");
};
active_task.task_id
}
};
let mut task = storage::load_task(&self.path_config.task_dir_path(), &task_id)?;
if task.status == TaskStatus::Done {
anyhow::bail!("Cannot abort a completed task");
}
if task.status == TaskStatus::Aborted {
anyhow::bail!("Task is already aborted");
}
let is_active_task =
match active_task::load_active_task(&self.path_config.active_task_file_path())? {
Some(active) if active.task_id == task_id => {
let started_time = DateTime::parse_from_rfc3339(&active.started_at)
.context("Failed to parse started_at time from active task record")?;
let now = Local::now();
let duration = now.signed_duration_since(started_time.with_timezone(&Local));
let seconds_spent = duration.num_seconds().max(0) as u64;
task.time_spent = Some(task.time_spent.unwrap_or(0) + seconds_spent);
true
}
_ => false,
};
task.status = TaskStatus::Aborted;
task.updated_at = Some(Local::now().to_rfc3339());
task.completed_at = Some(Local::now().to_rfc3339());
storage::save_task(
&self.path_config.task_dir_path(),
&task,
"cancel",
"Cancel task",
)?;
if is_active_task {
active_task::clear_active_task(&self.path_config.active_task_file_path())?;
log::debug!("Aborted active task: {task_id} and cleared active task file");
} else {
log::debug!("Aborted task: {task_id}");
}
Ok(task_id)
}
pub fn edit_task_description<D: Display>(
&self,
task_id: &str,
display_manager: &D,
) -> Result<String> {
let mut task = storage::load_task(&self.path_config.task_dir_path(), task_id)?;
let Some(new_description) = display_manager.edit(&task.description)? else {
anyhow::bail!("No changes made to the task description");
};
let new_description = new_description.trim().to_string();
if new_description != task.description {
task.description = new_description;
task.updated_at = Some(Local::now().to_rfc3339());
storage::save_task(
&self.path_config.task_dir_path(),
&task,
"update",
"Update task description",
)?;
}
Ok(task.id)
}
pub fn clean_tasks<D: Display>(
&self,
filter_options: &Filter,
force: bool,
display_manager: &D,
) -> Result<usize> {
let tasks = self.list_tasks(filter_options, None)?;
let count = tasks.len();
if count > 0 && !force {
let message = format!("Are you sure to delete {count} tasks?");
if !display_manager.confirm(&message)? {
return Ok(0);
}
}
storage::delete_task(
&self.path_config.task_dir_path(),
&tasks
.iter()
.map(|task| task.id.as_str())
.collect::<Vec<_>>(),
)?;
Ok(count)
}
pub fn clone_repo(&self, url: &str) -> Result<()> {
GitRepo::clone(self.path_config.task_dir_path(), url, &self.git_config)?;
Ok(())
}
pub fn sync(&self, prefer: MergeStrategy) -> Result<()> {
let git_repo = GitRepo::init(self.path_config.task_dir_path())?;
git_repo.sync(prefer, &self.git_config)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fs;
use anyhow::Result;
use chrono::Local;
use tempfile::tempdir;
use super::*;
use crate::{
config::{GitConfig, PathConfig},
display::Display,
task::{Filter, TaskStatus},
};
struct MockDisplay {
confirm_result: bool,
edit_result: Option<String>,
}
impl MockDisplay {
fn new(confirm_result: bool, edit_result: Option<String>) -> Self {
Self {
confirm_result,
edit_result,
}
}
}
impl Display for MockDisplay {
fn confirm(&self, _message: &str) -> Result<bool> {
Ok(self.confirm_result)
}
fn edit(&self, _message: &str) -> Result<Option<String>> {
Ok(self.edit_result.clone())
}
fn show_success(&self, _message: &str) {}
fn show_failure(&self, _message: &str) {}
fn show_tasks_list(&self, _tasks: &[Task]) {}
fn show_task_stats(&self, _tasks: &[Task]) {}
fn show_task_detail(&self, _task: &Task) {}
}
fn create_test_task_manager() -> (TaskManager, tempfile::TempDir) {
let temp_dir = tempdir().unwrap();
let path_config = PathConfig {
root_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let git_config = GitConfig::default();
let task_manager = TaskManager::new(path_config, git_config);
(task_manager, temp_dir)
}
#[test]
fn test_add_task() {
let (task_manager, _temp_dir) = create_test_task_manager();
let result = task_manager.add_task(
"Test task",
Priority::Normal,
Some("test-scope".to_string()),
Some("test-type".to_string()),
);
if let Ok(task_id) = result {
let task_dir = task_manager.path_config.task_dir_path();
let task_file = task_dir.join(format!("{task_id}.toml"));
assert!(task_file.exists());
}
}
#[test]
fn test_list_tasks() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
for i in 1..=3 {
let task = Task {
id: format!("test-{i}"),
description: format!("Test task {i}"),
priority: Priority::Normal,
scope: Some("test-scope".to_string()),
task_type: Some("test-type".to_string()),
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(file_path, toml::to_string(&task).unwrap()).unwrap();
}
let filter = Filter::default();
let result = task_manager.list_tasks(&filter, None);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 3);
let filter = Filter {
priority: Some(Priority::High),
..Default::default()
};
let result = task_manager.list_tasks(&filter, None);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_mark_task_done() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let task_id = "test-complete";
let task = Task {
id: task_id.to_string(),
description: "Test task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(&file_path, toml::to_string(&task).unwrap()).unwrap();
let result = task_manager.mark_task_done(task_id);
if result.is_ok() {
let content = fs::read_to_string(&file_path).unwrap();
let updated_task: Task = toml::from_str(&content).unwrap();
assert_eq!(updated_task.status, TaskStatus::Done);
assert!(updated_task.completed_at.is_some());
}
}
#[test]
fn test_start_and_stop_task() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let task_id = "test-start-stop";
let task = Task {
id: task_id.to_string(),
description: "Test task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(&file_path, toml::to_string(&task).unwrap()).unwrap();
let result = task_manager.start_task(task_id);
if result.is_ok() {
let active_task_file = task_manager.path_config.active_task_file_path();
assert!(active_task_file.exists());
let stop_result = task_manager.stop_task();
if stop_result.is_ok() {
assert!(!active_task_file.exists());
let content = fs::read_to_string(&file_path).unwrap();
let updated_task: Task = toml::from_str(&content).unwrap();
assert!(updated_task.time_spent.is_some());
}
}
}
#[test]
fn test_abort_task() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let task_id = "test-abort";
let task = Task {
id: task_id.to_string(),
description: "Test task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(&file_path, toml::to_string(&task).unwrap()).unwrap();
let result = task_manager.abort_task(&Some(task_id.to_string()));
if result.is_ok() {
let content = fs::read_to_string(&file_path).unwrap();
let updated_task: Task = toml::from_str(&content).unwrap();
assert_eq!(updated_task.status, TaskStatus::Aborted);
}
}
#[test]
fn test_edit_task_description() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let task_id = "test-edit";
let task = Task {
id: task_id.to_string(),
description: "Original description".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(&file_path, toml::to_string(&task).unwrap()).unwrap();
let new_description = "Updated description";
let display = MockDisplay::new(true, Some(new_description.to_string()));
let result = task_manager.edit_task_description(task_id, &display);
if result.is_ok() {
let content = fs::read_to_string(&file_path).unwrap();
let updated_task: Task = toml::from_str(&content).unwrap();
assert_eq!(updated_task.description, new_description);
assert!(updated_task.updated_at.is_some());
}
}
#[test]
fn test_clean_tasks() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let done_task = Task {
id: "done-task".to_string(),
description: "Done task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Done,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: Some(Local::now().to_rfc3339()),
time_spent: None,
};
let todo_task = Task {
id: "todo-task".to_string(),
description: "Todo task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
fs::write(
task_dir.join(format!("{}.toml", done_task.id)),
toml::to_string(&done_task).unwrap(),
)
.unwrap();
fs::write(
task_dir.join(format!("{}.toml", todo_task.id)),
toml::to_string(&todo_task).unwrap(),
)
.unwrap();
let filter = Filter {
status: Some(TaskStatus::Done),
..Default::default()
};
let display = MockDisplay::new(true, None);
let result = task_manager.clean_tasks(&filter, false, &display);
if let Ok(count) = result {
assert_eq!(count, 1);
assert!(!task_dir.join(format!("{}.toml", done_task.id)).exists());
assert!(task_dir.join(format!("{}.toml", todo_task.id)).exists());
}
}
#[test]
fn test_edge_cases_for_task_status_changes() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let todo_task = Task {
id: "todo-task".to_string(),
description: "Todo task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let done_task = Task {
id: "done-task".to_string(),
description: "Done task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Done,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: Some(Local::now().to_rfc3339()),
time_spent: None,
};
let aborted_task = Task {
id: "aborted-task".to_string(),
description: "Aborted task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Aborted,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: Some(Local::now().to_rfc3339()),
time_spent: None,
};
fs::write(
task_dir.join(format!("{}.toml", todo_task.id)),
toml::to_string(&todo_task).unwrap(),
)
.unwrap();
fs::write(
task_dir.join(format!("{}.toml", done_task.id)),
toml::to_string(&done_task).unwrap(),
)
.unwrap();
fs::write(
task_dir.join(format!("{}.toml", aborted_task.id)),
toml::to_string(&aborted_task).unwrap(),
)
.unwrap();
let result = task_manager.mark_task_done(&done_task.id);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("already completed")
);
let result = task_manager.start_task(&done_task.id);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("completed task"));
let result = task_manager.start_task(&aborted_task.id);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("aborted task"));
let result = task_manager.abort_task(&Some(done_task.id));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("completed task"));
let result = task_manager.abort_task(&Some(aborted_task.id));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already aborted"));
}
#[test]
fn test_task_filtering_logic() {
let task = Task {
id: "test-task".to_string(),
description: "Test filtering logic".to_string(),
priority: Priority::High,
scope: Some("test-scope".to_string()),
task_type: Some("feature".to_string()),
status: TaskStatus::Todo,
created_at: "2023-05-15T12:00:00+00:00".to_string(),
updated_at: Some("2023-05-16T14:30:00+00:00".to_string()),
completed_at: None,
time_spent: None,
};
let filter = Filter::default();
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
priority: Some(Priority::High),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
priority: Some(Priority::Low),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
let filter = Filter {
task_scope: Some("test-scope".to_string()),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
task_scope: Some("wrong-scope".to_string()),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
let filter = Filter {
task_type: Some("feature".to_string()),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
task_type: Some("bug".to_string()),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
let filter = Filter {
status: Some(TaskStatus::Todo),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
status: Some(TaskStatus::Done),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
let filter = Filter {
fuzzy: Some("filtering".to_string()),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
fuzzy: Some("nonexistent".to_string()),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
let filter = Filter {
priority: Some(Priority::High),
task_scope: Some("test-scope".to_string()),
status: Some(TaskStatus::Todo),
..Default::default()
};
assert!(TaskManager::matches_filters(&task, &filter));
let filter = Filter {
priority: Some(Priority::High),
task_scope: Some("wrong-scope".to_string()),
..Default::default()
};
assert!(!TaskManager::matches_filters(&task, &filter));
}
#[test]
fn test_active_task_tracking() {
let (task_manager, _temp_dir) = create_test_task_manager();
let task_dir = task_manager.path_config.task_dir_path();
fs::create_dir_all(&task_dir).unwrap();
let task_id = "active-task-test";
let task = Task {
id: task_id.to_string(),
description: "Task for active tracking".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let file_path = task_dir.join(format!("{}.toml", task.id));
fs::write(&file_path, toml::to_string(&task).unwrap()).unwrap();
let start_result = task_manager.start_task(task_id);
assert!(start_result.is_ok());
let second_task_id = "second-task";
let second_task = Task {
id: second_task_id.to_string(),
description: "Second task".to_string(),
priority: Priority::Normal,
scope: None,
task_type: None,
status: TaskStatus::Todo,
created_at: Local::now().to_rfc3339(),
updated_at: None,
completed_at: None,
time_spent: None,
};
let second_file_path = task_dir.join(format!("{}.toml", second_task.id));
fs::write(&second_file_path, toml::to_string(&second_task).unwrap()).unwrap();
let start_second_result = task_manager.start_task(second_task_id);
assert!(start_second_result.is_err());
assert!(
start_second_result
.unwrap_err()
.to_string()
.contains("already an active task")
);
let complete_result = task_manager.mark_task_done(task_id);
assert!(complete_result.is_ok());
let active_task_file = task_manager.path_config.active_task_file_path();
assert!(!active_task_file.exists());
let restart_result = task_manager.start_task(second_task_id);
assert!(restart_result.is_ok());
let content = fs::read_to_string(&second_file_path).unwrap();
let second_task: Task = toml::from_str(&content).unwrap();
assert!(second_task.time_spent.is_none());
let stop_result = task_manager.stop_task();
assert!(stop_result.is_ok());
let content = fs::read_to_string(&second_file_path).unwrap();
let updated_task: Task = toml::from_str(&content).unwrap();
assert!(updated_task.time_spent.is_some());
let stop_nothing_result = task_manager.stop_task();
assert!(stop_nothing_result.is_err());
assert!(
stop_nothing_result
.unwrap_err()
.to_string()
.contains("No active task found")
);
}
#[test]
fn test_is_time_in_range() {
let time1 = "2023-01-15T12:00:00+00:00";
let time2 = "2023-02-15T12:00:00+00:00";
let time3 = "2023-03-15T12:00:00+00:00";
let from = DateTime::parse_from_rfc3339("2023-02-01T00:00:00+00:00")
.unwrap()
.with_timezone(&Local);
let to = DateTime::parse_from_rfc3339("2023-03-01T00:00:00+00:00")
.unwrap()
.with_timezone(&Local);
let date_range = DateRange {
from: Some(from),
to: Some(to),
};
assert!(TaskManager::is_time_in_range(time2, &date_range));
assert!(!TaskManager::is_time_in_range(time1, &date_range));
assert!(!TaskManager::is_time_in_range(time3, &date_range));
let open_ended_range = DateRange {
from: Some(from),
to: None,
};
assert!(!TaskManager::is_time_in_range(time1, &open_ended_range));
assert!(TaskManager::is_time_in_range(time2, &open_ended_range));
assert!(TaskManager::is_time_in_range(time3, &open_ended_range));
let open_ended_range = DateRange {
from: None,
to: Some(to),
};
assert!(TaskManager::is_time_in_range(time1, &open_ended_range));
assert!(TaskManager::is_time_in_range(time2, &open_ended_range));
assert!(!TaskManager::is_time_in_range(time3, &open_ended_range));
}
}