Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// The 0008 specification, as executable tests. Do not edit this file — make it pass.
|
||||
// Copy to: tasks/tests/collections.rs
|
||||
// Run with: cargo test
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use tasks::stats::tally;
|
||||
use tasks::store::Store;
|
||||
use tasks::task::Priority;
|
||||
|
||||
// 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-coll-{}-{}.txt", std::process::id(), n))
|
||||
}
|
||||
|
||||
// ---------- tally: one generic function, any element type, any key type ----------
|
||||
|
||||
#[test]
|
||||
fn tally_counts_how_often_each_key_appears() {
|
||||
let words = ["red", "blue", "red", "green", "red"];
|
||||
let counts = tally(&words, |word| *word);
|
||||
assert_eq!(counts[&"red"], 3);
|
||||
assert_eq!(counts[&"blue"], 1);
|
||||
assert_eq!(counts.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tally_accepts_a_key_type_that_is_not_the_element_type() {
|
||||
let words = ["a", "bb", "cc", "ddd"];
|
||||
let by_length = tally(&words, |word| word.len());
|
||||
assert_eq!(by_length, HashMap::from([(1, 1), (2, 2), (3, 1)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tally_of_nothing_is_an_empty_map() {
|
||||
let nothing: [u32; 0] = [];
|
||||
assert!(tally(¬hing, |n| *n).is_empty());
|
||||
}
|
||||
|
||||
// ---------- counting the store, built on that same function ----------
|
||||
|
||||
#[test]
|
||||
fn count_by_priority_counts_every_priority_present() {
|
||||
let mut store = Store::new();
|
||||
store.add("buy milk", Priority::High);
|
||||
store.add("call bank", Priority::High);
|
||||
store.add("water plants", Priority::Low);
|
||||
|
||||
assert_eq!(
|
||||
store.count_by_priority(),
|
||||
HashMap::from([(Priority::High, 2), (Priority::Low, 1)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_by_priority_has_no_entry_for_an_absent_priority() {
|
||||
let mut store = Store::new();
|
||||
store.add("buy milk", Priority::High);
|
||||
|
||||
let counts = store.count_by_priority();
|
||||
assert_eq!(counts.len(), 1, "absent priorities must not appear as zero");
|
||||
assert_eq!(counts.get(&Priority::Low), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_by_priority_of_an_empty_store_is_empty() {
|
||||
assert!(Store::new().count_by_priority().is_empty());
|
||||
}
|
||||
|
||||
// ---------- selecting titles: filter, map, collect ----------
|
||||
|
||||
#[test]
|
||||
fn titles_with_returns_matching_titles_in_order() {
|
||||
let mut store = Store::new();
|
||||
store.add("buy milk", Priority::High);
|
||||
store.add("water plants", Priority::Low);
|
||||
store.add("call bank", Priority::High);
|
||||
|
||||
assert_eq!(store.titles_with(Priority::High), ["buy milk", "call bank"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn titles_with_returns_nothing_when_no_task_matches() {
|
||||
let mut store = Store::new();
|
||||
store.add("buy milk", Priority::High);
|
||||
|
||||
assert!(store.titles_with(Priority::Medium).is_empty());
|
||||
}
|
||||
|
||||
// ---------- clearing out finished work: retain ----------
|
||||
|
||||
#[test]
|
||||
fn remove_completed_drops_done_tasks_and_reports_how_many() {
|
||||
let mut store = Store::new();
|
||||
let first = store.add("buy milk", Priority::High);
|
||||
store.add("water plants", Priority::Low);
|
||||
let third = store.add("call bank", Priority::Medium);
|
||||
store.complete(first).unwrap();
|
||||
store.complete(third).unwrap();
|
||||
|
||||
assert_eq!(store.remove_completed(), 2);
|
||||
assert_eq!(store.titles_with(Priority::Low), ["water plants"]);
|
||||
assert_eq!(store.tasks().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_completed_removes_nothing_when_nothing_is_done() {
|
||||
let mut store = Store::new();
|
||||
store.add("buy milk", Priority::High);
|
||||
store.add("call bank", Priority::Low);
|
||||
|
||||
assert_eq!(store.remove_completed(), 0);
|
||||
assert_eq!(store.tasks().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_completed_keeps_the_remaining_ids_unchanged() {
|
||||
let mut store = Store::new();
|
||||
let first = store.add("buy milk", Priority::High);
|
||||
store.add("call bank", Priority::Low);
|
||||
store.complete(first).unwrap();
|
||||
store.remove_completed();
|
||||
|
||||
assert_eq!(store.tasks()[0].id, 2, "surviving tasks keep their own id");
|
||||
assert_eq!(
|
||||
store.add("new one", Priority::Low),
|
||||
3,
|
||||
"and the counter is untouched"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- load: one collect, and the line rules that come with it ----------
|
||||
|
||||
#[test]
|
||||
fn a_file_without_a_trailing_newline_still_loads() {
|
||||
let path = temp_path();
|
||||
std::fs::write(&path, "1|todo|low|fine").unwrap();
|
||||
|
||||
let store = Store::load(&path).unwrap();
|
||||
assert_eq!(store.tasks().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_blank_line_inside_the_file_is_a_bad_line() {
|
||||
let path = temp_path();
|
||||
std::fs::write(&path, "1|todo|low|fine\n\n2|todo|low|also fine\n").unwrap();
|
||||
|
||||
let err = Store::load(&path).unwrap_err();
|
||||
assert_eq!(err.to_string(), "cannot read saved line: ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_bad_line_loses_the_whole_load_not_part_of_it() {
|
||||
let path = temp_path();
|
||||
std::fs::write(&path, "1|todo|low|fine\nrubbish\n3|todo|low|also fine\n").unwrap();
|
||||
|
||||
assert!(Store::load(&path).is_err());
|
||||
// the file itself is untouched by a failed load — nothing was half-written
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).unwrap(),
|
||||
"1|todo|low|fine\nrubbish\n3|todo|low|also fine\n"
|
||||
);
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use tasks::store::Store;
|
||||
use tasks::task::Priority;
|
||||
|
||||
pub 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-{}-{}.txt", std::process::id(), n))
|
||||
}
|
||||
|
||||
pub fn args(list: &[&str]) -> Vec<String> {
|
||||
list.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
/// One task per priority, ids 1..=3, task 1 already completed.
|
||||
pub fn three_tasks() -> Store {
|
||||
let mut store = Store::new();
|
||||
store.add("low task", Priority::Low);
|
||||
store.add("medium task", Priority::Medium);
|
||||
store.add("high task", Priority::High);
|
||||
store.complete(1).expect("task 1 was just added");
|
||||
store
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// The 0006 specification, as executable tests. Do not edit this file — make it pass.
|
||||
// Copy to: tasks/tests/errors.rs
|
||||
// Run with: cargo test
|
||||
|
||||
use std::error::Error;
|
||||
use tasks::command::Command;
|
||||
use tasks::error::TaskError;
|
||||
use tasks::store::Store;
|
||||
use tasks::task::Priority;
|
||||
|
||||
fn args(list: &[&str]) -> Vec<String> {
|
||||
list.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
// ---------- one variant per way of failing ----------
|
||||
|
||||
#[test]
|
||||
fn parse_errors_name_the_exact_failure() {
|
||||
assert_eq!(Command::parse(&args(&[])).unwrap_err(), TaskError::NoCommand);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["fly"])).unwrap_err(),
|
||||
TaskError::UnknownCommand("fly".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["add"])).unwrap_err(),
|
||||
TaskError::MissingTitle
|
||||
);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["add", "x", "urgent"])).unwrap_err(),
|
||||
TaskError::BadPriority("urgent".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["done"])).unwrap_err(),
|
||||
TaskError::MissingId
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_reports_which_id_was_missing() {
|
||||
let mut store = Store::new();
|
||||
store.add("only task", Priority::Low);
|
||||
assert_eq!(store.complete(99).unwrap_err(), TaskError::NotFound(99));
|
||||
assert_eq!(store.remove(7).unwrap_err(), TaskError::NotFound(7));
|
||||
}
|
||||
|
||||
// ---------- Display: the sentence the user reads ----------
|
||||
|
||||
#[test]
|
||||
fn messages_are_lowercase_and_unpunctuated() {
|
||||
assert_eq!(TaskError::NoCommand.to_string(), "no command given");
|
||||
assert_eq!(
|
||||
TaskError::UnknownCommand("fly".to_string()).to_string(),
|
||||
"unknown command: fly"
|
||||
);
|
||||
assert_eq!(TaskError::MissingTitle.to_string(), "add needs a title");
|
||||
assert_eq!(
|
||||
TaskError::MissingId.to_string(),
|
||||
"this command needs a task id"
|
||||
);
|
||||
assert_eq!(
|
||||
TaskError::BadPriority("urgent".to_string()).to_string(),
|
||||
"unknown priority: urgent"
|
||||
);
|
||||
assert_eq!(TaskError::NotFound(9).to_string(), "no task with id 9");
|
||||
}
|
||||
|
||||
// ---------- From + ? : the parse error is wrapped, not thrown away ----------
|
||||
|
||||
#[test]
|
||||
fn a_bad_id_wraps_the_parse_error() {
|
||||
let err = Command::parse(&args(&["done", "abc"])).unwrap_err();
|
||||
assert!(matches!(err, TaskError::BadId(_)), "expected BadId");
|
||||
assert!(
|
||||
Command::parse(&args(&["remove", "-1"])).is_err(),
|
||||
"a negative id is not a u32"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrapped_error_is_reported_once_and_reachable() {
|
||||
let err = Command::parse(&args(&["done", "abc"])).unwrap_err();
|
||||
// Display says your sentence and does not repeat std's.
|
||||
assert_eq!(err.to_string(), "task id must be a number");
|
||||
// std's sentence is still reachable, through Error::source().
|
||||
let inner = err.source().expect("BadId must expose its source");
|
||||
assert_eq!(inner.to_string(), "invalid digit found in string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_that_carry_nothing_have_no_source() {
|
||||
assert!(TaskError::NoCommand.source().is_none());
|
||||
assert!(TaskError::NotFound(1).source().is_none());
|
||||
}
|
||||
|
||||
// ---------- the Error trait, and the bounds the ecosystem expects ----------
|
||||
|
||||
#[test]
|
||||
fn task_error_is_a_real_error() {
|
||||
fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}
|
||||
assert_usable_as_error::<TaskError>();
|
||||
|
||||
let boxed: Box<dyn Error> = Box::new(TaskError::NotFound(1));
|
||||
assert_eq!(boxed.to_string(), "no task with id 1");
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::fs;
|
||||
|
||||
use tasks::error::TaskError;
|
||||
use tasks::store::Store;
|
||||
use tasks::task::Status;
|
||||
|
||||
use crate::common::{temp_path, three_tasks};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn completing_task_that_are_done_is_not_an_error() -> Result<(), TaskError> {
|
||||
let mut m = three_tasks();
|
||||
|
||||
m.complete(1)?;
|
||||
|
||||
let t = m.find(1).unwrap();
|
||||
assert_eq!(t.status, Status::Done);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_saved_working() -> Result<(), TaskError> {
|
||||
let mut m = three_tasks();
|
||||
let p = temp_path();
|
||||
m.remove_completed();
|
||||
m.save(&p)?;
|
||||
|
||||
let m2 = Store::load(&p)?;
|
||||
assert_eq!(m2, m);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smaller_store_smaller_saved_files() -> Result<(), TaskError> {
|
||||
let mut m = three_tasks();
|
||||
let p = temp_path();
|
||||
m.save(&p)?;
|
||||
|
||||
m.remove_completed();
|
||||
m.save(&p)?;
|
||||
|
||||
let s = fs::read_to_string(p)?;
|
||||
assert_eq!(s.lines().count(), 2);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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::<Task>().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::<Task>().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");
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// The specification, as executable tests. Do not edit this file — make it pass.
|
||||
// Copy to: tasks/tests/spec.rs
|
||||
// Run with: cargo test
|
||||
|
||||
use tasks::command::Command;
|
||||
use tasks::store::Store;
|
||||
use tasks::task::{Priority, Status, Task};
|
||||
|
||||
/// Helper: build an argument list the way main() would pass it in
|
||||
/// (program name already stripped).
|
||||
fn args(list: &[&str]) -> Vec<String> {
|
||||
list.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
// ---------- task.rs ----------
|
||||
|
||||
#[test]
|
||||
fn new_task_starts_as_todo() {
|
||||
let task = Task::new(1, "write the spec", Priority::High);
|
||||
assert_eq!(task.id, 1);
|
||||
assert_eq!(task.title, "write the spec");
|
||||
assert_eq!(task.status, Status::Todo);
|
||||
assert_eq!(task.priority, Priority::High);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_labels() {
|
||||
assert_eq!(Status::Todo.label(), "todo");
|
||||
assert_eq!(Status::InProgress.label(), "in-progress");
|
||||
assert_eq!(Status::Done.label(), "done");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority_labels() {
|
||||
assert_eq!(Priority::Low.label(), "low");
|
||||
assert_eq!(Priority::Medium.label(), "medium");
|
||||
assert_eq!(Priority::High.label(), "high");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority_parses_known_words_only() {
|
||||
assert_eq!(Priority::parse("low"), Some(Priority::Low));
|
||||
assert_eq!(Priority::parse("medium"), Some(Priority::Medium));
|
||||
assert_eq!(Priority::parse("high"), Some(Priority::High));
|
||||
assert_eq!(Priority::parse("urgent"), None);
|
||||
assert_eq!(Priority::parse(""), None);
|
||||
}
|
||||
|
||||
// ---------- command.rs ----------
|
||||
|
||||
#[test]
|
||||
fn parses_add_with_default_priority() {
|
||||
let command = Command::parse(&args(&["add", "buy milk"])).unwrap();
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::Add {
|
||||
title: "buy milk".to_string(),
|
||||
priority: Priority::Medium,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_add_with_explicit_priority() {
|
||||
let command = Command::parse(&args(&["add", "ship it", "high"])).unwrap();
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::Add {
|
||||
title: "ship it".to_string(),
|
||||
priority: Priority::High,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_list_done_and_remove() {
|
||||
assert_eq!(Command::parse(&args(&["list"])).unwrap(), Command::List);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["done", "7"])).unwrap(),
|
||||
Command::Done { id: 7 }
|
||||
);
|
||||
assert_eq!(
|
||||
Command::parse(&args(&["remove", "12"])).unwrap(),
|
||||
Command::Remove { id: 12 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_input() {
|
||||
assert!(Command::parse(&args(&[])).is_err(), "no command at all");
|
||||
assert!(Command::parse(&args(&["fly"])).is_err(), "unknown command");
|
||||
assert!(Command::parse(&args(&["add"])).is_err(), "add with no title");
|
||||
assert!(
|
||||
Command::parse(&args(&["add", "x", "urgent"])).is_err(),
|
||||
"unknown priority word"
|
||||
);
|
||||
assert!(Command::parse(&args(&["done"])).is_err(), "done with no id");
|
||||
assert!(
|
||||
Command::parse(&args(&["done", "abc"])).is_err(),
|
||||
"id is not a number"
|
||||
);
|
||||
assert!(
|
||||
Command::parse(&args(&["remove", "-1"])).is_err(),
|
||||
"negative id is not a u32"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- store.rs ----------
|
||||
|
||||
#[test]
|
||||
fn add_returns_ids_starting_at_one() {
|
||||
let mut store = Store::new();
|
||||
assert_eq!(store.add("first", Priority::Low), 1);
|
||||
assert_eq!(store.add("second", Priority::Low), 2);
|
||||
assert_eq!(store.add("third", Priority::Low), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_come_back_in_insertion_order() {
|
||||
let mut store = Store::new();
|
||||
store.add("first", Priority::Low);
|
||||
store.add("second", Priority::High);
|
||||
|
||||
let listed = store.tasks();
|
||||
assert_eq!(listed.len(), 2);
|
||||
assert_eq!(listed[0].title, "first");
|
||||
assert_eq!(listed[1].title, "second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_store_is_empty() {
|
||||
let store = Store::new();
|
||||
assert!(store.tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_sets_status_to_done() {
|
||||
let mut store = Store::new();
|
||||
let id = store.add("do the thing", Priority::Medium);
|
||||
|
||||
assert_eq!(store.find(id).unwrap().status, Status::Todo);
|
||||
assert!(store.complete(id).is_ok());
|
||||
assert_eq!(store.find(id).unwrap().status, Status::Done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_unknown_id_is_an_error() {
|
||||
let mut store = Store::new();
|
||||
store.add("only task", Priority::Low);
|
||||
assert!(store.complete(99).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_deletes_only_that_task() {
|
||||
let mut store = Store::new();
|
||||
let first = store.add("first", Priority::Low);
|
||||
let second = store.add("second", Priority::Low);
|
||||
|
||||
assert!(store.remove(first).is_ok());
|
||||
assert_eq!(store.tasks().len(), 1);
|
||||
assert!(store.find(first).is_none());
|
||||
assert!(store.find(second).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_unknown_id_is_an_error() {
|
||||
let mut store = Store::new();
|
||||
assert!(store.remove(1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_returns_none_for_missing_id() {
|
||||
let store = Store::new();
|
||||
assert!(store.find(1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_are_not_reused_after_remove() {
|
||||
let mut store = Store::new();
|
||||
let first = store.add("first", Priority::Low);
|
||||
store.remove(first).unwrap();
|
||||
let second = store.add("second", Priority::Low);
|
||||
assert_ne!(first, second, "a removed id must not be handed out again");
|
||||
}
|
||||
Reference in New Issue
Block a user