163 lines
5.0 KiB
Rust
163 lines
5.0 KiB
Rust
// 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"
|
|
);
|
|
}
|