// The 0007 specification, as executable tests. Do not edit this file — make it pass. // Copy to: tasks/tests/persist.rs // Run with: cargo test use std::error::Error; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; use tasks::error::TaskError; use tasks::store::Store; use tasks::task::{Priority, Status, Task}; // a fresh path per test, inside the OS temp dir — no file is ever left in your crate fn temp_path() -> PathBuf { static N: AtomicU32 = AtomicU32::new(0); let n = N.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!("tasks-test-{}-{}.txt", std::process::id(), n)) } // ---------- a Task survives the trip to text and back ---------- #[test] fn a_task_becomes_a_line_and_a_line_becomes_a_task() { let task = Task { id: 3, title: String::from("buy milk"), priority: Priority::High, status: Status::Done, }; assert_eq!(task.to_line(), "3|done|high|buy milk"); assert_eq!("3|done|high|buy milk".parse::().unwrap(), task); } #[test] fn a_title_may_contain_the_separator() { let line = "1|todo|low|read a|b testing"; let task: Task = line.parse().unwrap(); assert_eq!(task.title, "read a|b testing"); assert_eq!(task.to_line(), line); } #[test] fn a_corrupt_line_names_itself() { for bad in ["", "nonsense", "1|todo|low", "x|todo|low|t", "1|sleeping|low|t", "1|todo|urgent|t"] { assert_eq!( bad.parse::().unwrap_err(), TaskError::BadLine(bad.to_string()), "line {:?} should be reported as a bad line", bad ); } } // ---------- the store round trip ---------- #[test] fn every_task_survives_save_then_load() { let path = temp_path(); let mut store = Store::new(); store.add("buy milk", Priority::High); let second = store.add("call bank", Priority::Medium); store.complete(second).unwrap(); store.save(&path).unwrap(); let loaded = Store::load(&path).unwrap(); assert_eq!(loaded.tasks(), store.tasks()); } #[test] fn ids_do_not_restart_after_a_reload() { let path = temp_path(); let mut store = Store::new(); store.add("first", Priority::Low); store.add("second", Priority::Low); store.save(&path).unwrap(); let mut loaded = Store::load(&path).unwrap(); assert_eq!(loaded.add("third", Priority::Low), 3); } #[test] fn a_missing_file_is_an_empty_store_not_an_error() { let path = temp_path(); // never created let store = Store::load(&path).expect("first run must not fail"); assert!(store.tasks().is_empty()); } #[test] fn a_corrupt_file_fails_loudly() { let path = temp_path(); std::fs::write(&path, "1|todo|low|fine\nrubbish\n").unwrap(); assert_eq!( Store::load(&path).unwrap_err(), TaskError::BadLine(String::from("rubbish")) ); } // ---------- io::Error becomes TaskError, and keeps its cause ---------- #[test] fn a_write_that_cannot_happen_is_an_io_error() { let path = temp_path().join("no-such-dir").join("t.txt"); let err = Store::new().save(&path).unwrap_err(); assert!(matches!(err, TaskError::Io(_)), "got {:?}", err); assert_eq!(err.to_string(), "cannot read or write the task file"); assert!(err.source().is_some(), "the io::Error must stay reachable"); }