Files
ldr/src/tests.rs
T
2025-07-27 14:48:04 -04:00

590 lines
19 KiB
Rust

//! Comprehensive test suite for the parts todo system.
//!
//! Tests cover all core functionality including content manipulation,
//! command operations, filtering, and edge cases.
use crate::commands::*;
use crate::content::*;
/// Tests adding an entry to empty content
#[test]
fn test_add_entry_to_content_empty() {
let result = add_entry_to_content("", "First item");
assert_eq!(result, "First item\n");
}
/// Tests adding an entry to existing content
#[test]
fn test_add_entry_to_content_existing() {
let existing = "Second item\nThird item";
let result = add_entry_to_content(existing, "First item");
assert_eq!(result, "First item\nSecond item\nThird item");
}
/// Tests adding an entry when existing content has trailing newline
#[test]
fn test_add_entry_to_content_with_trailing_newline() {
let existing = "Second item\nThird item\n";
let result = add_entry_to_content(existing, "First item");
assert_eq!(result, "First item\nSecond item\nThird item");
}
/// Tests prioritizing a single item by moving it to the top
#[test]
fn test_prioritize_items_in_content_single_item() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
];
let result = prioritize_items_in_content(&lines, &[2]);
assert!(result.is_ok());
let (new_content, prioritized) = result.unwrap();
assert_eq!(new_content, "Second item\nFirst item\nThird item\n");
assert_eq!(prioritized, vec!["Second item"]);
}
/// Tests prioritizing multiple items in command-line order
#[test]
fn test_prioritize_items_in_content_multiple_items() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
"Fourth item".to_string(),
];
let result = prioritize_items_in_content(&lines, &[3, 1]);
assert!(result.is_ok());
let (new_content, prioritized) = result.unwrap();
assert_eq!(
new_content,
"Third item\nFirst item\nSecond item\nFourth item\n"
);
assert_eq!(prioritized, vec!["Third item", "First item"]);
}
/// Tests error handling for invalid item numbers
#[test]
fn test_prioritize_items_in_content_invalid_number() {
let lines = vec!["First item".to_string(), "Second item".to_string()];
let result = prioritize_items_in_content(&lines, &[3]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid item number: 3. Valid range: 1-2"
);
}
/// Tests error handling for zero item numbers
#[test]
fn test_prioritize_items_in_content_zero_number() {
let lines = vec!["First item".to_string()];
let result = prioritize_items_in_content(&lines, &[0]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid item number: 0. Valid range: 1-1"
);
}
/// Tests duplicate removal while preserving order
#[test]
fn test_prioritize_items_in_content_duplicates() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
];
let result = prioritize_items_in_content(&lines, &[2, 2, 1]);
assert!(result.is_ok());
let (new_content, prioritized) = result.unwrap();
assert_eq!(new_content, "Second item\nFirst item\nThird item\n");
assert_eq!(prioritized, vec!["Second item", "First item"]);
}
/// Tests archiving a single item
#[test]
fn test_archive_items_in_content_single_item() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
];
let result = archive_items_in_content(&lines, &[2]);
assert!(result.is_ok());
let (new_content, archived) = result.unwrap();
assert_eq!(new_content, "First item\nThird item\n");
assert_eq!(archived, vec!["Second item"]);
}
/// Tests archiving multiple items
#[test]
fn test_archive_items_in_content_multiple_items() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
"Fourth item".to_string(),
];
let result = archive_items_in_content(&lines, &[1, 3]);
assert!(result.is_ok());
let (new_content, archived) = result.unwrap();
assert_eq!(new_content, "Second item\nFourth item\n");
assert_eq!(archived, vec!["First item", "Third item"]);
}
/// Tests archiving all items results in empty content
#[test]
fn test_archive_items_in_content_all_items() {
let lines = vec!["First item".to_string(), "Second item".to_string()];
let result = archive_items_in_content(&lines, &[1, 2]);
assert!(result.is_ok());
let (new_content, archived) = result.unwrap();
assert_eq!(new_content, "");
assert_eq!(archived, vec!["First item", "Second item"]);
}
/// Tests error handling for invalid archive item numbers
#[test]
fn test_archive_items_in_content_invalid_number() {
let lines = vec!["First item".to_string()];
let result = archive_items_in_content(&lines, &[2]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid item number: 2. Valid range: 1-1"
);
}
/// Tests duplicate handling in archive operations
#[test]
fn test_archive_items_in_content_duplicates() {
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
];
let result = archive_items_in_content(&lines, &[1, 1, 3]);
assert!(result.is_ok());
let (new_content, archived) = result.unwrap();
assert_eq!(new_content, "Second item\n");
assert_eq!(archived, vec!["First item", "Third item"]);
}
/// Tests case-insensitive filtering functionality
#[test]
fn test_filter_functionality() {
let lines = vec![
"Read: Book about Rust".to_string(),
"Listen: Podcast episode".to_string(),
"@work: Review PR".to_string(),
"@home: Fix sink".to_string(),
"Read: Another book".to_string(),
];
// Test filtering by "read:"
let filtered: Vec<(usize, &String)> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.to_lowercase().contains("read:"))
.collect();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].0, 0);
assert_eq!(filtered[0].1, "Read: Book about Rust");
assert_eq!(filtered[1].0, 4);
assert_eq!(filtered[1].1, "Read: Another book");
// Test filtering by "@work"
let filtered: Vec<(usize, &String)> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.to_lowercase().contains("@work"))
.collect();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].0, 2);
assert_eq!(filtered[0].1, "@work: Review PR");
}
/// Tests that filtering is case-insensitive
#[test]
fn test_case_insensitive_filtering() {
let lines = vec![
"READ: Book about Rust".to_string(),
"read: Another book".to_string(),
"ReAd: Mixed case".to_string(),
];
let filtered: Vec<(usize, &String)> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.to_lowercase().contains("read:"))
.collect();
assert_eq!(filtered.len(), 3);
}
#[cfg(test)]
mod list_tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
/// Helper function to create temporary test files with content
fn create_test_file_with_content(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", content).unwrap();
file
}
/// Tests listing all items when all flag is true
#[test]
fn test_list_with_all_flag() {
let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6";
let file = create_test_file_with_content(content);
// Test that all=true shows all items regardless of num
let result = std::panic::catch_unwind(|| {
list_note(file.path(), 3, true, None).unwrap();
});
assert!(result.is_ok());
}
/// Tests respecting num limit when all flag is false
#[test]
fn test_list_without_all_flag() {
let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6";
let file = create_test_file_with_content(content);
// Test that all=false respects num limit
let result = std::panic::catch_unwind(|| {
list_note(file.path(), 3, false, None).unwrap();
});
assert!(result.is_ok());
}
/// Tests filtering with all flag shows all matching items
#[test]
fn test_list_with_filter_and_all() {
let content = "read: book 1\nwrite: article\nread: book 2\ntask: cleanup";
let file = create_test_file_with_content(content);
// Test that all=true with filter shows all matching items
let result = std::panic::catch_unwind(|| {
list_note(file.path(), 1, true, Some("read:")).unwrap();
});
assert!(result.is_ok());
}
/// Tests handling of empty files
#[test]
fn test_list_empty_file() {
let file = create_test_file_with_content("");
let result = std::panic::catch_unwind(|| {
list_note(file.path(), 5, false, None).unwrap();
});
assert!(result.is_ok());
}
}
#[cfg(test)]
mod review_tests {
use std::collections::HashSet;
/// Tests that prioritized items maintain their relative order during review
#[test]
fn test_review_prioritization_order() {
// Test that items prioritized during review maintain their relative order
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
"Fourth item".to_string(),
];
// Simulate review process: prioritize items 2 and 4 (in that encounter order during bottom-to-top review)
let mut prioritized: Vec<String> = Vec::new();
let mut remove_indices: HashSet<usize> = HashSet::new();
// Simulate top-to-bottom review order (indices 0, 1, 2, 3) - same as list order
// User prioritizes item at index 1 (Second item)
prioritized.push(lines[1].clone());
remove_indices.insert(1);
// User prioritizes item at index 3 (Fourth item)
prioritized.push(lines[3].clone());
remove_indices.insert(3);
// Build remaining items (items not prioritized)
let mut remaining: Vec<String> = Vec::new();
for (i, line) in lines.iter().enumerate() {
if !remove_indices.contains(&i) {
remaining.push(line.clone());
}
}
// Combine: prioritized items first (in order they were prioritized), then remaining
let mut new_lines = prioritized;
new_lines.extend(remaining);
// Expected result: prioritized items (Second, Fourth) should appear first,
// in the order they were prioritized, followed by remaining items (First, Third)
let expected = vec![
"Second item".to_string(),
"Fourth item".to_string(),
"First item".to_string(),
"Third item".to_string(),
];
assert_eq!(new_lines, expected);
}
/// Tests that quitting review still saves any changes made
#[test]
fn test_review_quit_saves_changes() {
// Test that quitting review still saves any changes made
let lines = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
];
// Simulate review process where user prioritizes one item then quits
let mut prioritized: Vec<String> = Vec::new();
let mut remove_indices: HashSet<usize> = HashSet::new();
// User prioritizes item at index 1 (Second item)
prioritized.push(lines[1].clone());
remove_indices.insert(1);
// User quits (but changes should still be applied)
// Build remaining items (items not prioritized or archived)
let mut remaining: Vec<String> = Vec::new();
for (i, line) in lines.iter().enumerate() {
if !remove_indices.contains(&i) {
remaining.push(line.clone());
}
}
// Combine: prioritized items first, then remaining
let mut new_lines = prioritized;
new_lines.extend(remaining);
// Expected result: prioritized item should be first, even though user quit
let expected = vec![
"Second item".to_string(),
"First item".to_string(),
"Third item".to_string(),
];
assert_eq!(new_lines, expected);
}
/// Tests that items are prioritized in exact command-line order
#[test]
fn test_prioritize_command_line_order() {
// Test that items are prioritized in the exact order specified on command line
let lines = vec![
"Item A".to_string(),
"Item B".to_string(),
"Item C".to_string(),
"Item D".to_string(),
"Item E".to_string(),
];
// Command: arn up 5 2 4 (prioritize items 5, 2, 4 in that order)
let result = crate::content::prioritize_items_in_content(&lines, &[5, 2, 4]);
assert!(result.is_ok());
let (new_content, prioritized) = result.unwrap();
// Expected: Item E, Item B, Item D should be at top in that order
let expected_content = "Item E\nItem B\nItem D\nItem A\nItem C\n";
let expected_prioritized = vec!["Item E", "Item B", "Item D"];
assert_eq!(new_content, expected_content);
assert_eq!(prioritized, expected_prioritized);
}
/// Tests duplicate removal while preserving command-line order
#[test]
fn test_prioritize_command_line_order_with_duplicates() {
// Test that duplicates are removed but order is preserved
let lines = vec![
"Item A".to_string(),
"Item B".to_string(),
"Item C".to_string(),
];
// Command: arn up 3 1 3 2 1 (should become 3, 1, 2)
let result = crate::content::prioritize_items_in_content(&lines, &[3, 1, 3, 2, 1]);
assert!(result.is_ok());
let (new_content, prioritized) = result.unwrap();
// Expected: Item C, Item A, Item B (duplicates removed, order preserved)
let expected_content = "Item C\nItem A\nItem B\n";
let expected_prioritized = vec!["Item C", "Item A", "Item B"];
assert_eq!(new_content, expected_content);
assert_eq!(prioritized, expected_prioritized);
}
}
#[cfg(test)]
mod remove_tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
/// Helper function to create temporary test files with content
fn create_test_file_with_content(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", content).unwrap();
file
}
/// Tests removing a single item
#[test]
fn test_remove_single_item() {
let content = "First item\nSecond item\nThird item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[2]).unwrap();
});
assert!(result.is_ok());
// Verify the item was removed from the file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "First item\nThird item\n");
}
/// Tests removing multiple items
#[test]
fn test_remove_multiple_items() {
let content = "First item\nSecond item\nThird item\nFourth item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1, 3]).unwrap();
});
assert!(result.is_ok());
// Verify the items were removed from the file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Second item\nFourth item\n");
}
/// Tests removing all items results in empty file
#[test]
fn test_remove_all_items() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1, 2]).unwrap();
});
assert!(result.is_ok());
// Verify the file is empty
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "");
}
/// Tests handling of invalid item numbers
#[test]
fn test_remove_invalid_item_number() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[3]).unwrap();
});
assert!(result.is_ok());
// Verify the file content is unchanged
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, content);
}
/// Tests handling of duplicate item numbers
#[test]
fn test_remove_duplicate_item_numbers() {
let content = "First item\nSecond item\nThird item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[2, 2, 1]).unwrap();
});
assert!(result.is_ok());
// Verify only unique items were removed
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Third item\n");
}
/// Tests handling of non-existent file
#[test]
fn test_remove_from_nonexistent_file() {
let temp_dir = tempfile::tempdir().unwrap();
let nonexistent_path = temp_dir.path().join("nonexistent.txt");
let result = std::panic::catch_unwind(|| {
remove_items(&nonexistent_path, &[1]).unwrap();
});
assert!(result.is_ok());
// Verify no file was created
assert!(!nonexistent_path.exists());
}
/// Tests handling of empty file
#[test]
fn test_remove_from_empty_file() {
let file = create_test_file_with_content("");
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1]).unwrap();
});
assert!(result.is_ok());
// Verify the file remains empty
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "");
}
/// Tests that remove doesn't create an archive file (unlike do command)
#[test]
fn test_remove_does_not_archive() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("archive.txt");
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1]).unwrap();
});
assert!(result.is_ok());
// Verify no archive file was created
assert!(!archive_path.exists());
// Verify the item was removed from the original file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Second item\n");
}
}