//! The golden lines of the task parser and the round trip of a task line. use notescraft_tasks::{OnCompletion, Priority, Task, format_line, parse_line}; use notescraft_templates::Date; /// Longest line the parser accepts, in characters. const MAX_LINE_CHARS: usize = 2000; /// The summary of a task, the way the golden fixture writes it. fn summary(task: &Task) -> String { let mut parts: Vec = Vec::with_capacity(16); parts.push(format!("indent={}", task.indent)); parts.push(format!("marker={}", task.marker)); parts.push(format!("status={}", status_word(task.status))); if task.priority != Priority::Normal { parts.push(format!("pri={:?}", task.priority)); } if let Some(date) = task.created { parts.push(format!("created={}", date_text(date))); } if let Some(date) = task.start { parts.push(format!("start={}", date_text(date))); } if let Some(date) = task.scheduled { parts.push(format!("scheduled={}", date_text(date))); } if let Some(date) = task.due { parts.push(format!("due={}", date_text(date))); } if let Some(date) = task.cancelled { parts.push(format!("cancelled={}", date_text(date))); } if let Some(date) = task.done { parts.push(format!("done={}", date_text(date))); } if let Some(rule) = &task.recurrence { parts.push(format!("recur={rule}")); } if task.on_completion == OnCompletion::Delete { parts.push("oncomplete=Delete".to_owned()); } if let Some(id) = &task.id { parts.push(format!("id={id}")); } if !task.depends_on.is_empty() { parts.push(format!("depends={}", task.depends_on.join(","))); } if !task.tags.is_empty() { parts.push(format!("tags={}", task.tags.join(","))); } parts.push(format!("desc={}", task.description)); parts.join(";") } /// The status character, spelled out when it is a space. fn status_word(status: char) -> String { if status == ' ' { "space".to_owned() } else { status.to_string() } } /// A date as `YYYY-MM-DD`. fn date_text(date: Date) -> String { format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()) } #[test] fn the_golden_lines_match_the_fixture() { let fixture = include_str!("fixtures/lines.txt"); let mut rows = 0; for (number, row) in fixture.lines().enumerate() { if row.trim().is_empty() { continue; } let (input, expected) = row.split_once(" ||| ").unwrap_or_else(|| panic!("row {} has no separator", number + 1)); let actual = parse_line(input).as_ref().map_or_else(|| "none".to_owned(), summary); assert_eq!(actual, expected, "row {}: {input}", number + 1); rows += 1; } assert!(rows > 20, "the fixture has only {rows} rows"); } #[test] fn a_line_that_is_not_a_task_is_refused() { for line in ["", "Buy milk", "- Buy milk", "* [x", "- [] Buy milk", "text 📅 2026-10-20", "# [ ] Buy milk"] { assert!(parse_line(line).is_none(), "{line}"); } } #[test] fn a_line_over_the_limit_is_refused() { let line = format!("- [ ] {}", "a".repeat(MAX_LINE_CHARS)); assert!(parse_line(&line).is_none()); } #[test] fn a_line_of_the_limit_is_a_task() { let line = format!("- [ ] {}", "a".repeat(MAX_LINE_CHARS - 6)); let task = parse_line(&line).expect("a task line"); assert_eq!(task.description.len(), MAX_LINE_CHARS - 6); } #[test] fn the_status_is_kept_verbatim() { let task = parse_line("- [z] Buy milk").expect("a task line"); assert_eq!(task.status, 'z'); assert_eq!(task.description, "Buy milk"); } #[test] fn the_dates_are_read() { let task = parse_line("- [ ] Buy milk ➕ 2026-01-01 🛫 2026-10-01 ⏳ 2026-10-05 📅 2026-10-20 ✅ 2026-10-19 ❌ 2026-10-21").expect("a task line"); assert_eq!(task.created, Some(Date::new(2026, 1, 1).unwrap())); assert_eq!(task.start, Some(Date::new(2026, 10, 1).unwrap())); assert_eq!(task.scheduled, Some(Date::new(2026, 10, 5).unwrap())); assert_eq!(task.due, Some(Date::new(2026, 10, 20).unwrap())); assert_eq!(task.done, Some(Date::new(2026, 10, 19).unwrap())); assert_eq!(task.cancelled, Some(Date::new(2026, 10, 21).unwrap())); assert_eq!(task.description, "Buy milk"); } #[test] fn a_date_that_does_not_parse_stays_in_the_description() { let task = parse_line("- [ ] Buy milk 📅 2026-13-40").expect("a task line"); assert_eq!(task.description, "Buy milk 📅 2026-13-40"); assert_eq!(task.due, None); } #[test] fn an_unknown_emoji_stays_in_the_description() { let task = parse_line("- [ ] Buy milk 🚀 rocket").expect("a task line"); assert_eq!(task.description, "Buy milk 🚀 rocket"); assert_eq!(task.tags, Vec::::new()); } #[test] fn the_priority_is_read() { for (emoji, priority) in [("🔺", Priority::Highest), ("⏫", Priority::High), ("🔼", Priority::Medium), ("🔽", Priority::Low), ("⏬", Priority::Lowest)] { let task = parse_line(&format!("- [ ] Buy milk {emoji}")).expect("a task line"); assert_eq!(task.priority, priority); assert_eq!(task.description, "Buy milk"); } } #[test] fn the_tags_are_collected() { let task = parse_line("- [ ] Buy #milk and #a/b").expect("a task line"); assert_eq!(task.tags, vec!["milk".to_owned(), "a/b".to_owned()]); } #[test] fn the_recurrence_rule_is_kept_whole() { let task = parse_line("- [ ] Buy milk 🔁 every week on Tuesday, Friday").expect("a task line"); assert_eq!(task.recurrence.as_deref(), Some("every week on Tuesday, Friday")); assert_eq!(task.description, "Buy milk"); } #[test] fn the_dependencies_are_split() { let task = parse_line("- [ ] Buy milk ⛔ abc,def").expect("a task line"); assert_eq!(task.depends_on, vec!["abc".to_owned(), "def".to_owned()]); } #[test] fn the_round_trip_keeps_a_canonical_line() { for line in [ "- [ ] Buy milk", "- [x] Buy milk", "* [ ] Buy milk 📅 2026-10-20", " - [ ] Nested", "- [ ] Buy milk 🆔 abc123 ⛔ abc,def 🔺 🔁 every week 🏁 delete ➕ 2026-01-01 🛫 2026-10-01 ⏳ 2026-10-05 📅 2026-10-20 ❌ 2026-10-21 ✅ 2026-10-19", ] { let task = parse_line(line).expect("a task line"); assert_eq!(format_line(&task), line); } } #[test] fn the_format_uses_the_fixed_order() { let mut task = Task { marker: "-".to_owned(), description: "Buy milk".to_owned(), ..Task::default() }; task.id = Some("abc123".to_owned()); task.depends_on = vec!["def".to_owned()]; task.priority = Priority::High; task.recurrence = Some("every week".to_owned()); task.on_completion = OnCompletion::Delete; task.due = Some(Date::new(2026, 10, 20).unwrap()); assert_eq!(format_line(&task), "- [ ] Buy milk 🆔 abc123 ⛔ def ⏫ 🔁 every week 🏁 delete 📅 2026-10-20"); } #[test] fn the_empty_task_is_formatted_without_a_description() { let task = Task { marker: "-".to_owned(), ..Task::default() }; assert_eq!(format_line(&task), "- [ ]"); }