//! Task Manager - A CLI application for managing tasks with priorities and subtasks. //! //! This module provides a complete task management system demonstrating //! idiomatic Rust patterns including error handling, traits, and collections. use std::collections::HashMap; use std::fmt; use std::io::{self, Write}; // ============================================================================ // Constants // ============================================================================ /// Maximum tasks before warning about performance. /// Chosen because HashMap operations remain O(1) up to this scale, /// and terminal output remains readable. const MAX_RECOMMENDED_TASKS: usize = 1000; // ============================================================================ // Enums // ============================================================================ /// Task priority levels, ordered from lowest to highest urgency. /// /// Priority affects the urgency score calculation and sort order /// when listing tasks. Critical tasks are flagged in statistics. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Priority { Low, Medium, High, Critical, } impl Priority { /// Parses a priority from user input string. /// /// Accepts both full names and single-letter abbreviations, /// case-insensitive. Returns `None` for unrecognized input. /// /// # Examples /// ``` /// assert_eq!(Priority::from_str("high"), Some(Priority::High)); /// assert_eq!(Priority::from_str("H"), Some(Priority::High)); /// assert_eq!(Priority::from_str("invalid"), None); /// ``` pub fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "low" | "l" => Some(Priority::Low), "medium" | "m" => Some(Priority::Medium), "high" | "h" => Some(Priority::High), "critical" | "c" => Some(Priority::Critical), _ => None, } } /// Returns a numeric score for sorting and urgency calculation. /// /// Higher scores indicate higher priority. Scale is 1-4 to allow /// multiplication in urgency formulas without overflow concerns. fn score(&self) -> u8 { match self { Priority::Low => 1, Priority::Medium => 2, Priority::High => 3, Priority::Critical => 4, } } } impl fmt::Display for Priority { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let symbol = match self { Priority::Low => "[ ]", Priority::Medium => "[=]", Priority::High => "[!]", Priority::Critical => "[*]", }; write!(f, "{:?} {}", self, symbol) } } /// Task workflow status. /// /// Status transitions follow a typical kanban flow: /// Todo -> InProgress -> Done, with Blocked as a side state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { Todo, InProgress, Blocked, Done, } impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (name, symbol) = match self { Status::Todo => ("Todo", "[ ]"), Status::InProgress => ("In Progress", "[~]"), Status::Blocked => ("Blocked", "[x]"), Status::Done => ("Done", "[v]"), }; write!(f, "{} {}", name, symbol) } } // ============================================================================ // Error Types // ============================================================================ /// Errors that can occur during task operations. /// /// Each variant provides context for user-friendly error messages. /// Implements std::error::Error for compatibility with error handling crates. #[derive(Debug)] pub enum TaskError { /// Task with the specified ID does not exist. NotFound(u32), /// User provided invalid input (with explanation). InvalidInput(String), /// Attempted to add a duplicate tag to a task. DuplicateTag(String), } impl fmt::Display for TaskError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TaskError::NotFound(id) => write!(f, "Task with ID {} not found", id), TaskError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), TaskError::DuplicateTag(tag) => write!(f, "Tag '{}' already exists", tag), } } } impl std::error::Error for TaskError {} /// Convenience type alias for Results with TaskError. pub type Result = std::result::Result; // ============================================================================ // Traits // ============================================================================ /// Provides a one-line summary of an item for list views. pub trait Summarize { fn summary(&self) -> String; } /// Calculates an urgency/priority score for sorting. pub trait Scoreable { fn calculate_score(&self) -> u32; } // ============================================================================ // Core Data Structures // ============================================================================ /// A task with metadata, subtasks, and tags. /// /// Tasks are the core entity in the system. Each task has a unique ID /// assigned by the TaskManager, along with user-defined attributes. /// /// # Invariants /// - `id` is unique within a TaskManager instance /// - `tags` contains no duplicates (enforced by `add_tag`) #[derive(Debug, Clone)] pub struct Task { id: u32, title: String, description: Option, priority: Priority, status: Status, tags: Vec, subtasks: Vec, } /// A subtask within a parent task. /// /// Subtasks are simpler than tasks - they only have a title and /// completion status. They cannot be nested further. #[derive(Debug, Clone)] pub struct Subtask { title: String, completed: bool, } impl Task { /// Creates a new task with the given title and priority. /// /// The task starts in `Todo` status with no description, tags, or subtasks. /// /// # Arguments /// * `id` - Unique identifier (assigned by TaskManager) /// * `title` - Brief description of the task /// * `priority` - Initial priority level fn new(id: u32, title: impl Into, priority: Priority) -> Self { Task { id, title: title.into(), description: None, priority, status: Status::Todo, tags: Vec::new(), subtasks: Vec::new(), } } /// Adds a tag to this task. /// /// Tags are case-sensitive and must be unique within a task. /// Returns an error if the tag already exists. pub fn add_tag(&mut self, tag: impl Into) -> Result<()> { let tag = tag.into(); if self.tags.contains(&tag) { return Err(TaskError::DuplicateTag(tag)); } self.tags.push(tag); Ok(()) } /// Adds a new subtask to this task. /// /// Subtasks start as incomplete. The order of subtasks is preserved. pub fn add_subtask(&mut self, title: impl Into) { self.subtasks.push(Subtask { title: title.into(), completed: false, }); } /// Marks a subtask as completed by its index. /// /// # Arguments /// * `index` - Zero-based index of the subtask /// /// # Errors /// Returns `InvalidInput` if the index is out of bounds. pub fn complete_subtask(&mut self, index: usize) -> Result<()> { self.subtasks .get_mut(index) .ok_or_else(|| { TaskError::InvalidInput(format!("Subtask index {} out of bounds", index)) })? .completed = true; Ok(()) } /// Calculates the percentage of completed subtasks. /// /// If there are no subtasks, returns 100% for Done tasks, 0% otherwise. /// This provides meaningful progress even for tasks without subtasks. pub fn progress_percentage(&self) -> f64 { if self.subtasks.is_empty() { return if self.status == Status::Done { 100.0 } else { 0.0 }; } let completed = self.subtasks.iter().filter(|s| s.completed).count(); (completed as f64 / self.subtasks.len() as f64) * 100.0 } /// Returns true if this task is critical and not yet done. /// /// Used to flag high-priority incomplete work in statistics. fn is_critical_incomplete(&self) -> bool { self.priority == Priority::Critical && self.status != Status::Done } } impl Summarize for Task { fn summary(&self) -> String { let tags_str = if self.tags.is_empty() { String::from("none") } else { self.tags.join(", ") }; format!( "[#{}] {} | Priority: {} | Status: {} | Tags: {} | Progress: {:.0}%", self.id, self.title, self.priority, self.status, tags_str, self.progress_percentage() ) } } impl Scoreable for Task { /// Calculates urgency score for task prioritization. /// /// Formula: (priority * 10) + status_penalty + incomplete_subtasks /// /// This weights priority highest, then penalizes blocked/todo status, /// then adds urgency for incomplete subtasks. Higher score = more urgent. fn calculate_score(&self) -> u32 { let priority_score = self.priority.score() as u32 * 10; let status_penalty = match self.status { Status::Done => 0, Status::InProgress => 5, Status::Todo => 10, Status::Blocked => 15, // Blocked items need attention }; let subtask_bonus = self.subtasks.iter().filter(|s| !s.completed).count() as u32; priority_score + status_penalty + subtask_bonus } } impl fmt::Display for Task { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "+----------------------------------------------------------------+")?; writeln!(f, "| Task #{}: {}", self.id, self.title)?; writeln!(f, "+----------------------------------------------------------------+")?; writeln!(f, "| Priority: {}", self.priority)?; writeln!(f, "| Status: {}", self.status)?; if let Some(ref desc) = self.description { writeln!(f, "| Description: {}", desc)?; } if !self.tags.is_empty() { writeln!(f, "| Tags: [{}]", self.tags.join(", "))?; } if !self.subtasks.is_empty() { writeln!(f, "| Subtasks:")?; for (i, subtask) in self.subtasks.iter().enumerate() { let check = if subtask.completed { "v" } else { " " }; writeln!(f, "| {}. [{}] {}", i + 1, check, subtask.title)?; } } writeln!(f, "| Progress: {:.1}%", self.progress_percentage())?; writeln!(f, "| Urgency Score: {}", self.calculate_score())?; write!(f, "+----------------------------------------------------------------+") } } // ============================================================================ // Task Manager // ============================================================================ /// Manages a collection of tasks with CRUD operations and queries. /// /// TaskManager owns all tasks and assigns unique IDs. It provides /// filtering, searching, and statistical aggregation. /// /// # Example /// ``` /// let mut manager = TaskManager::new(); /// let id = manager.create_task("My task", Priority::High); /// manager.update_status(id, Status::InProgress)?; /// ``` pub struct TaskManager { tasks: HashMap, next_id: u32, } impl TaskManager { /// Creates an empty TaskManager. pub fn new() -> Self { TaskManager { tasks: HashMap::new(), next_id: 1, } } /// Creates a new task and returns its assigned ID. /// /// IDs are assigned sequentially starting from 1. /// Warns if approaching the recommended task limit. pub fn create_task(&mut self, title: impl Into, priority: Priority) -> u32 { let id = self.next_id; self.next_id += 1; if self.tasks.len() >= MAX_RECOMMENDED_TASKS { eprintln!( "Warning: {} tasks exceeds recommended limit of {}", self.tasks.len(), MAX_RECOMMENDED_TASKS ); } let task = Task::new(id, title, priority); self.tasks.insert(id, task); id } /// Returns an immutable reference to a task by ID. pub fn get_task(&self, id: u32) -> Result<&Task> { self.tasks.get(&id).ok_or(TaskError::NotFound(id)) } /// Returns a mutable reference to a task by ID. pub fn get_task_mut(&mut self, id: u32) -> Result<&mut Task> { self.tasks.get_mut(&id).ok_or(TaskError::NotFound(id)) } /// Deletes a task and returns it. pub fn delete_task(&mut self, id: u32) -> Result { self.tasks.remove(&id).ok_or(TaskError::NotFound(id)) } /// Updates a task's status. pub fn update_status(&mut self, id: u32, status: Status) -> Result<()> { self.get_task_mut(id)?.status = status; Ok(()) } /// Returns all tasks sorted by urgency score (highest first). pub fn list_all(&self) -> Vec<&Task> { let mut tasks: Vec<&Task> = self.tasks.values().collect(); tasks.sort_by(|a, b| b.calculate_score().cmp(&a.calculate_score())); tasks } /// Returns tasks matching the given status. pub fn filter_by_status(&self, status: Status) -> Vec<&Task> { self.tasks .values() .filter(|t| t.status == status) .collect() } /// Returns tasks containing the given tag (case-insensitive). pub fn filter_by_tag(&self, tag: &str) -> Vec<&Task> { self.tasks .values() .filter(|t| t.tags.iter().any(|t| t.eq_ignore_ascii_case(tag))) .collect() } /// Searches tasks by title and description (case-insensitive substring). pub fn search(&self, query: &str) -> Vec<&Task> { let query_lower = query.to_lowercase(); self.tasks .values() .filter(|t| { t.title.to_lowercase().contains(&query_lower) || t.description .as_ref() .map(|d| d.to_lowercase().contains(&query_lower)) .unwrap_or(false) }) .collect() } /// Computes aggregate statistics across all tasks. pub fn statistics(&self) -> Statistics { let total = self.tasks.len(); let by_status = |s: Status| self.tasks.values().filter(|t| t.status == s).count(); let by_priority = |p: Priority| self.tasks.values().filter(|t| t.priority == p).count(); let avg_progress = if total > 0 { self.tasks .values() .map(|t| t.progress_percentage()) .sum::() / total as f64 } else { 0.0 }; let critical_incomplete = self .tasks .values() .filter(|t| t.is_critical_incomplete()) .count(); Statistics { total, todo: by_status(Status::Todo), in_progress: by_status(Status::InProgress), blocked: by_status(Status::Blocked), done: by_status(Status::Done), low_priority: by_priority(Priority::Low), medium_priority: by_priority(Priority::Medium), high_priority: by_priority(Priority::High), critical_priority: by_priority(Priority::Critical), average_progress: avg_progress, critical_incomplete, } } } impl Default for TaskManager { fn default() -> Self { Self::new() } } // ============================================================================ // Statistics // ============================================================================ /// Aggregated statistics across all tasks. pub struct Statistics { pub total: usize, pub todo: usize, pub in_progress: usize, pub blocked: usize, pub done: usize, pub low_priority: usize, pub medium_priority: usize, pub high_priority: usize, pub critical_priority: usize, pub average_progress: f64, pub critical_incomplete: usize, } impl fmt::Display for Statistics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "\n## Task Statistics")?; writeln!(f, "========================================")?; writeln!(f, "Total Tasks: {}", self.total)?; writeln!(f)?; writeln!(f, "By Status:")?; writeln!(f, " [ ] Todo: {}", self.todo)?; writeln!(f, " [~] In Progress: {}", self.in_progress)?; writeln!(f, " [x] Blocked: {}", self.blocked)?; writeln!(f, " [v] Done: {}", self.done)?; writeln!(f)?; writeln!(f, "By Priority:")?; writeln!(f, " Low: {}", self.low_priority)?; writeln!(f, " Medium: {}", self.medium_priority)?; writeln!(f, " High: {}", self.high_priority)?; writeln!(f, " Critical: {}", self.critical_priority)?; writeln!(f)?; writeln!(f, "Average Progress: {:.1}%", self.average_progress)?; if self.critical_incomplete > 0 { writeln!( f, "!! Critical tasks incomplete: {}", self.critical_incomplete )?; } write!(f, "========================================") } } // ============================================================================ // CLI Command Handlers // ============================================================================ // Each handler is a small function (<50 lines) following the single // responsibility principle. This makes testing individual commands easy. /// Result of executing a command. enum CommandResult { Continue, Exit, } /// Parses a task ID from a string slice. fn parse_id(s: Option<&&str>, usage: &str) -> Result { s.and_then(|s| s.parse().ok()) .ok_or_else(|| TaskError::InvalidInput(usage.into())) } /// Handles the 'add' command. fn cmd_add(manager: &mut TaskManager, parts: &[&str]) -> Result { if parts.len() < 2 { return Err(TaskError::InvalidInput("Usage: add [priority]".into())); } let priority = parts .get(2) .and_then(|p| Priority::from_str(p)) .unwrap_or(Priority::Medium); let id = manager.create_task(parts[1], priority); println!("[OK] Created task #{}", id); Ok(CommandResult::Continue) } /// Handles the 'list' command. fn cmd_list(manager: &TaskManager) -> Result<CommandResult> { let tasks = manager.list_all(); if tasks.is_empty() { println!("No tasks found. Use 'demo' to load sample data."); } else { println!("\n## All Tasks (sorted by urgency):\n"); for task in tasks { println!(" {}", task.summary()); } } Ok(CommandResult::Continue) } /// Handles the 'show' command. fn cmd_show(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { let id = parse_id(parts.get(1), "Usage: show <id>")?; let task = manager.get_task(id)?; println!("{}", task); Ok(CommandResult::Continue) } /// Handles status update commands (done, progress, block). fn cmd_update_status( manager: &mut TaskManager, parts: &[&str], status: Status, label: &str, ) -> Result<CommandResult> { let usage = format!("Usage: {} <id>", label); let id = parse_id(parts.get(1), &usage)?; manager.update_status(id, status)?; println!("[OK] Task #{} marked as {}", id, label); Ok(CommandResult::Continue) } /// Handles the 'delete' command. fn cmd_delete(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { let id = parse_id(parts.get(1), "Usage: delete <id>")?; let task = manager.delete_task(id)?; println!("[OK] Deleted task: {}", task.title); Ok(CommandResult::Continue) } /// Handles the 'tag' command. fn cmd_tag(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { if parts.len() < 3 { return Err(TaskError::InvalidInput("Usage: tag <id> <tag>".into())); } let id: u32 = parts[1] .parse() .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; manager.get_task_mut(id)?.add_tag(parts[2])?; println!("[OK] Added tag '{}' to task #{}", parts[2], id); Ok(CommandResult::Continue) } /// Handles the 'subtask' command. fn cmd_subtask(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { if parts.len() < 3 { return Err(TaskError::InvalidInput("Usage: subtask <id> <title>".into())); } let id: u32 = parts[1] .parse() .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; manager.get_task_mut(id)?.add_subtask(parts[2]); println!("[OK] Added subtask to task #{}", id); Ok(CommandResult::Continue) } /// Handles the 'check' command. fn cmd_check(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { if parts.len() < 3 { return Err(TaskError::InvalidInput("Usage: check <id> <subtask#>".into())); } let id: u32 = parts[1] .parse() .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; let subtask_idx: usize = parts[2] .parse::<usize>() .map_err(|_| TaskError::InvalidInput("Invalid subtask number".into()))? .saturating_sub(1); // Convert 1-based to 0-based index manager.get_task_mut(id)?.complete_subtask(subtask_idx)?; println!("[OK] Completed subtask"); Ok(CommandResult::Continue) } /// Handles the 'desc' command. fn cmd_desc(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { if parts.len() < 3 { return Err(TaskError::InvalidInput( "Usage: desc <id> <description>".into(), )); } let id: u32 = parts[1] .parse() .map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?; manager.get_task_mut(id)?.description = Some(parts[2].into()); println!("[OK] Updated description for task #{}", id); Ok(CommandResult::Continue) } /// Handles the 'filter' command. fn cmd_filter(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { let status_str = parts.get(1).unwrap_or(&""); let tasks = match status_str.to_lowercase().as_str() { "todo" => manager.filter_by_status(Status::Todo), "progress" | "inprogress" => manager.filter_by_status(Status::InProgress), "blocked" => manager.filter_by_status(Status::Blocked), "done" => manager.filter_by_status(Status::Done), _ => { return Err(TaskError::InvalidInput( "Usage: filter <todo|progress|blocked|done>".into(), )); } }; println!("\nFiltered tasks:"); if tasks.is_empty() { println!(" No tasks match the filter."); } else { for task in tasks { println!(" {}", task.summary()); } } Ok(CommandResult::Continue) } /// Handles the 'search' command. fn cmd_search(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> { if parts.len() < 2 { return Err(TaskError::InvalidInput("Usage: search <query>".into())); } let query = parts[1..].join(" "); let tasks = manager.search(&query); println!("\nSearch results for '{}':", query); if tasks.is_empty() { println!(" No tasks found."); } else { for task in tasks { println!(" {}", task.summary()); } } Ok(CommandResult::Continue) } // ============================================================================ // CLI Infrastructure // ============================================================================ fn print_help() { println!("\n## Task Manager Commands:"); println!("=========================================================="); println!(" add <title> [priority] - Add a new task (priority: l/m/h/c)"); println!(" list - List all tasks"); println!(" show <id> - Show task details"); println!(" done <id> - Mark task as done"); println!(" progress <id> - Mark task as in progress"); println!(" block <id> - Mark task as blocked"); println!(" delete <id> - Delete a task"); println!(" tag <id> <tag> - Add tag to task"); println!(" subtask <id> <title> - Add subtask"); println!(" check <id> <subtask#> - Complete subtask"); println!(" desc <id> <description> - Add description"); println!(" filter <status> - Filter by status"); println!(" search <query> - Search tasks"); println!(" stats - Show statistics"); println!(" demo - Load demo data"); println!(" help - Show this help"); println!(" quit - Exit"); println!("==========================================================\n"); } /// Loads sample tasks for demonstration and testing. fn load_demo_data(manager: &mut TaskManager) { let id1 = manager.create_task("Deploy production server", Priority::Critical); if let Ok(task) = manager.get_task_mut(id1) { task.description = Some("Deploy the new version to production with zero downtime".into()); let _ = task.add_tag("devops"); let _ = task.add_tag("urgent"); task.add_subtask("Backup database"); task.add_subtask("Run migrations"); task.add_subtask("Deploy new containers"); task.add_subtask("Verify health checks"); let _ = task.complete_subtask(0); task.status = Status::InProgress; } let id2 = manager.create_task("Fix authentication bug", Priority::High); if let Ok(task) = manager.get_task_mut(id2) { task.description = Some("Users getting logged out unexpectedly".into()); let _ = task.add_tag("bug"); let _ = task.add_tag("security"); task.status = Status::Blocked; } let id3 = manager.create_task("Implement user dashboard", Priority::Medium); if let Ok(task) = manager.get_task_mut(id3) { let _ = task.add_tag("feature"); let _ = task.add_tag("frontend"); task.add_subtask("Design mockups"); task.add_subtask("Create React components"); task.add_subtask("Add API integration"); task.add_subtask("Write tests"); let _ = task.complete_subtask(0); let _ = task.complete_subtask(1); } let id4 = manager.create_task("Update documentation", Priority::Low); if let Ok(task) = manager.get_task_mut(id4) { let _ = task.add_tag("docs"); } let id5 = manager.create_task("Setup CI/CD pipeline", Priority::High); if let Ok(task) = manager.get_task_mut(id5) { let _ = task.add_tag("devops"); task.add_subtask("Configure GitHub Actions"); task.add_subtask("Add test stage"); task.add_subtask("Add deploy stage"); let _ = task.complete_subtask(0); let _ = task.complete_subtask(1); let _ = task.complete_subtask(2); task.status = Status::Done; } println!("[OK] Loaded 5 demo tasks!"); } /// Routes a command to the appropriate handler. /// /// This dispatcher keeps each command handler small and testable. /// Adding new commands requires only adding a new match arm and handler. fn execute_command(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> { let command = parts[0].to_lowercase(); match command.as_str() { "quit" | "q" | "exit" => { println!("Goodbye!"); Ok(CommandResult::Exit) } "help" | "h" | "?" => { print_help(); Ok(CommandResult::Continue) } "demo" => { load_demo_data(manager); Ok(CommandResult::Continue) } "add" => cmd_add(manager, parts), "list" | "ls" => cmd_list(manager), "show" => cmd_show(manager, parts), "done" => cmd_update_status(manager, parts, Status::Done, "done"), "progress" => cmd_update_status(manager, parts, Status::InProgress, "in progress"), "block" => cmd_update_status(manager, parts, Status::Blocked, "blocked"), "delete" | "rm" => cmd_delete(manager, parts), "tag" => cmd_tag(manager, parts), "subtask" => cmd_subtask(manager, parts), "check" => cmd_check(manager, parts), "desc" => cmd_desc(manager, parts), "filter" => cmd_filter(manager, parts), "search" => cmd_search(manager, parts), "stats" => { println!("{}", manager.statistics()); Ok(CommandResult::Continue) } _ => Err(TaskError::InvalidInput(format!( "Unknown command: '{}'. Type 'help' for commands.", command ))), } } /// Main REPL loop for the CLI. fn run_interactive(manager: &mut TaskManager) { println!("\n## Welcome to Rust Task Manager!\n"); print_help(); loop { print!("> "); io::stdout().flush().unwrap(); let mut input = String::new(); if io::stdin().read_line(&mut input).is_err() { println!("Error reading input"); continue; } let parts: Vec<&str> = input.trim().splitn(3, ' ').collect(); if parts.is_empty() || parts[0].is_empty() { continue; } match execute_command(manager, &parts) { Ok(CommandResult::Exit) => break, Ok(CommandResult::Continue) => {} Err(e) => println!("[ERROR] {}", e), } } } // ============================================================================ // Entry Point // ============================================================================ fn main() { let mut manager = TaskManager::new(); run_interactive(&mut manager); } // ============================================================================ // Unit Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; // ------------------------------------------------------------------------- // Priority Tests // ------------------------------------------------------------------------- #[test] fn test_priority_from_str_full_names() { assert_eq!(Priority::from_str("low"), Some(Priority::Low)); assert_eq!(Priority::from_str("medium"), Some(Priority::Medium)); assert_eq!(Priority::from_str("high"), Some(Priority::High)); assert_eq!(Priority::from_str("critical"), Some(Priority::Critical)); } #[test] fn test_priority_from_str_abbreviations() { assert_eq!(Priority::from_str("l"), Some(Priority::Low)); assert_eq!(Priority::from_str("m"), Some(Priority::Medium)); assert_eq!(Priority::from_str("h"), Some(Priority::High)); assert_eq!(Priority::from_str("c"), Some(Priority::Critical)); } #[test] fn test_priority_from_str_case_insensitive() { assert_eq!(Priority::from_str("HIGH"), Some(Priority::High)); assert_eq!(Priority::from_str("High"), Some(Priority::High)); assert_eq!(Priority::from_str("H"), Some(Priority::High)); } #[test] fn test_priority_from_str_invalid() { assert_eq!(Priority::from_str("invalid"), None); assert_eq!(Priority::from_str(""), None); assert_eq!(Priority::from_str("x"), None); } #[test] fn test_priority_score_ordering() { assert!(Priority::Low.score() < Priority::Medium.score()); assert!(Priority::Medium.score() < Priority::High.score()); assert!(Priority::High.score() < Priority::Critical.score()); } // ------------------------------------------------------------------------- // Task Tests // ------------------------------------------------------------------------- #[test] fn test_task_new_defaults() { let task = Task::new(1, "Test task", Priority::Medium); assert_eq!(task.id, 1); assert_eq!(task.title, "Test task"); assert_eq!(task.priority, Priority::Medium); assert_eq!(task.status, Status::Todo); assert!(task.description.is_none()); assert!(task.tags.is_empty()); assert!(task.subtasks.is_empty()); } #[test] fn test_task_add_tag_success() { let mut task = Task::new(1, "Test", Priority::Low); assert!(task.add_tag("rust").is_ok()); assert_eq!(task.tags, vec!["rust"]); } #[test] fn test_task_add_tag_duplicate_error() { let mut task = Task::new(1, "Test", Priority::Low); task.add_tag("rust").unwrap(); let result = task.add_tag("rust"); assert!(result.is_err()); match result { Err(TaskError::DuplicateTag(tag)) => assert_eq!(tag, "rust"), _ => panic!("Expected DuplicateTag error"), } } #[test] fn test_task_add_subtask() { let mut task = Task::new(1, "Test", Priority::Low); task.add_subtask("Subtask 1"); task.add_subtask("Subtask 2"); assert_eq!(task.subtasks.len(), 2); assert_eq!(task.subtasks[0].title, "Subtask 1"); assert!(!task.subtasks[0].completed); } #[test] fn test_task_complete_subtask_success() { let mut task = Task::new(1, "Test", Priority::Low); task.add_subtask("Subtask 1"); assert!(task.complete_subtask(0).is_ok()); assert!(task.subtasks[0].completed); } #[test] fn test_task_complete_subtask_out_of_bounds() { let mut task = Task::new(1, "Test", Priority::Low); task.add_subtask("Subtask 1"); let result = task.complete_subtask(5); assert!(result.is_err()); } #[test] fn test_task_progress_no_subtasks_todo() { let task = Task::new(1, "Test", Priority::Low); assert_eq!(task.progress_percentage(), 0.0); } #[test] fn test_task_progress_no_subtasks_done() { let mut task = Task::new(1, "Test", Priority::Low); task.status = Status::Done; assert_eq!(task.progress_percentage(), 100.0); } #[test] fn test_task_progress_with_subtasks() { let mut task = Task::new(1, "Test", Priority::Low); task.add_subtask("Sub 1"); task.add_subtask("Sub 2"); task.add_subtask("Sub 3"); task.add_subtask("Sub 4"); task.complete_subtask(0).unwrap(); task.complete_subtask(1).unwrap(); assert_eq!(task.progress_percentage(), 50.0); } #[test] fn test_task_urgency_score() { let mut task = Task::new(1, "Test", Priority::High); task.status = Status::Todo; task.add_subtask("Incomplete subtask"); // priority(3) * 10 + status_todo(10) + 1 incomplete subtask = 41 assert_eq!(task.calculate_score(), 41); } // ------------------------------------------------------------------------- // TaskManager Tests // ------------------------------------------------------------------------- #[test] fn test_manager_create_task_assigns_sequential_ids() { let mut manager = TaskManager::new(); let id1 = manager.create_task("Task 1", Priority::Low); let id2 = manager.create_task("Task 2", Priority::Low); let id3 = manager.create_task("Task 3", Priority::Low); assert_eq!(id1, 1); assert_eq!(id2, 2); assert_eq!(id3, 3); } #[test] fn test_manager_get_task_success() { let mut manager = TaskManager::new(); let id = manager.create_task("Test", Priority::High); let task = manager.get_task(id).unwrap(); assert_eq!(task.title, "Test"); assert_eq!(task.priority, Priority::High); } #[test] fn test_manager_get_task_not_found() { let manager = TaskManager::new(); let result = manager.get_task(999); assert!(result.is_err()); match result { Err(TaskError::NotFound(id)) => assert_eq!(id, 999), _ => panic!("Expected NotFound error"), } } #[test] fn test_manager_delete_task() { let mut manager = TaskManager::new(); let id = manager.create_task("To delete", Priority::Low); let deleted = manager.delete_task(id).unwrap(); assert_eq!(deleted.title, "To delete"); assert!(manager.get_task(id).is_err()); } #[test] fn test_manager_update_status() { let mut manager = TaskManager::new(); let id = manager.create_task("Test", Priority::Low); manager.update_status(id, Status::InProgress).unwrap(); assert_eq!(manager.get_task(id).unwrap().status, Status::InProgress); } #[test] fn test_manager_list_all_sorted_by_urgency() { let mut manager = TaskManager::new(); manager.create_task("Low priority", Priority::Low); manager.create_task("Critical priority", Priority::Critical); manager.create_task("Medium priority", Priority::Medium); let tasks = manager.list_all(); assert_eq!(tasks[0].priority, Priority::Critical); assert_eq!(tasks[2].priority, Priority::Low); } #[test] fn test_manager_filter_by_status() { let mut manager = TaskManager::new(); let id1 = manager.create_task("Task 1", Priority::Low); let id2 = manager.create_task("Task 2", Priority::Low); manager.create_task("Task 3", Priority::Low); manager.update_status(id1, Status::Done).unwrap(); manager.update_status(id2, Status::Done).unwrap(); let done_tasks = manager.filter_by_status(Status::Done); assert_eq!(done_tasks.len(), 2); } #[test] fn test_manager_search_by_title() { let mut manager = TaskManager::new(); manager.create_task("Fix authentication bug", Priority::High); manager.create_task("Add new feature", Priority::Medium); manager.create_task("Fix database bug", Priority::High); let results = manager.search("fix"); assert_eq!(results.len(), 2); } #[test] fn test_manager_search_case_insensitive() { let mut manager = TaskManager::new(); manager.create_task("Authentication Module", Priority::High); let results = manager.search("authentication"); assert_eq!(results.len(), 1); } #[test] fn test_manager_statistics() { let mut manager = TaskManager::new(); let id1 = manager.create_task("Task 1", Priority::Critical); let id2 = manager.create_task("Task 2", Priority::High); manager.create_task("Task 3", Priority::Low); manager.update_status(id1, Status::Done).unwrap(); manager.update_status(id2, Status::InProgress).unwrap(); let stats = manager.statistics(); assert_eq!(stats.total, 3); assert_eq!(stats.done, 1); assert_eq!(stats.in_progress, 1); assert_eq!(stats.todo, 1); assert_eq!(stats.critical_priority, 1); assert_eq!(stats.critical_incomplete, 0); // Critical task is done } // ------------------------------------------------------------------------- // Command Handler Tests // ------------------------------------------------------------------------- #[test] fn test_parse_id_success() { let parts = vec!["show", "42"]; let id = parse_id(parts.get(1), "Usage: show <id>").unwrap(); assert_eq!(id, 42); } #[test] fn test_parse_id_missing() { let parts: Vec<&str> = vec!["show"]; let result = parse_id(parts.get(1), "Usage: show <id>"); assert!(result.is_err()); } #[test] fn test_parse_id_invalid() { let parts = vec!["show", "abc"]; let result = parse_id(parts.get(1), "Usage: show <id>"); assert!(result.is_err()); } }