From 07dc5ca5eee3106702cb12e3a405d6254c44fc75 Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:33:47 -0700 Subject: [PATCH] comment out v1 --- pttodoest/src/main.rs | 2142 +++++++++++++++++++++-------------------- 1 file changed, 1073 insertions(+), 1069 deletions(-) diff --git a/pttodoest/src/main.rs b/pttodoest/src/main.rs index f4e4c24..e8dd76b 100755 --- a/pttodoest/src/main.rs +++ b/pttodoest/src/main.rs @@ -1,1136 +1,1140 @@ -use clap::Parser; -use serde::{Deserialize, Serialize}; -use serde_yaml; -use std::io::{BufRead, Read, Write}; +mod v1 { + use clap::Parser; + use serde::{Deserialize, Serialize}; + use serde_yaml; + use std::io::{BufRead, Read, Write}; -fn main() { - let flags = Flags::new().expect("failed to flags"); - let files = flags.files().expect("failed to files"); - - if !flags.dry_run { - files.reconcile().expect("failed to reconcile"); - - if let Some(add) = flags.add { - let task = match flags.add_schedule.clone() { - None => Task(serde_yaml::Value::String(add)), - Some(add_schedule) => { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), add_schedule.into()); - m.insert("do".into(), add.into()); - Task(serde_yaml::Value::Mapping(m)) - } - }; - let now = Delta::now_time(); - files.files[0] - .append(match task.next_due(now.clone()) { - None => Delta::add(task), - Some(due) => Delta::add_at(task, if due > now { due } else { now }), - }) - .expect("failed to add"); + fn main() { + let flags = Flags::new().expect("failed to flags"); + let files = flags.files().expect("failed to files"); + if !flags.dry_run { files.reconcile().expect("failed to reconcile"); - } - } - for file in files.files.iter() { - println!( - "{}", - serde_yaml::to_string(&file.events().unwrap().snapshot().unwrap()).unwrap(), - ); - } - - if flags.edit { - edit::files(&files); - } -} - -#[derive(Debug, Parser)] -struct Flags { - #[arg(short = 'f', long = "path", default_value = "$PTTODO_FILE")] - path: String, - - #[arg(short = 'a', long = "add")] - add: Option, - - #[arg(short = 'e', long = "edit", default_value = "false")] - edit: bool, - - #[arg(short = 'd', long = "dry-run", default_value = "false")] - dry_run: bool, - - #[arg(short = 's', long = "add-schedule")] - add_schedule: Option, -} - -impl Flags { - pub fn new() -> Result { - let mut result = Flags::parse(); - - if result.path.get(..1) == Some("$") { - result.path = match std::env::var(result.path.get(1..).unwrap()) { - Ok(v) => Ok(v), - Err(msg) => Err(format!("'{}' unset: {}", result.path, msg)), - }?; - } - - let _ = result.files()?; - - Ok(result) - } - - pub fn files(&self) -> Result { - Self::files_with(&self.path) - } - - pub fn files_with(p: &String) -> Result { - let metadata = match std::fs::metadata(p.clone()) { - Ok(v) => Ok(v), - Err(msg) => Err(format!("failed to load {}: {}", p.clone(), msg)), - }?; - let files = match metadata.is_dir() { - false => Ok(vec![p.clone()]), - true => match std::fs::read_dir(p.clone()) { - Ok(paths) => Ok(paths - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) - .filter(|x| x.metadata().unwrap().is_file()) - .map(|x| x.path().display().to_string()) - .filter(|x| !x.contains("/.")) - .collect()), - Err(msg) => Err(format!("failed to read {}: {}", p.clone(), msg)), - }, - }?; - assert!(files.len() > 0, "no files"); - Ok(Files::new(&files)) - } -} - -#[cfg(test)] -mod test_flags { - use super::*; - - #[test] - fn test_flags_files_unhidden_only() { - tests::with_dir(|d| { - std::fs::File::create(d.path().join("plain")).unwrap(); - std::fs::File::create(d.path().join(".hidden")).unwrap(); - - let flags = Flags { - path: d.path().to_str().unwrap().to_string(), - add: None, - edit: false, - dry_run: true, - add_schedule: None, - }; - let files = flags.files().expect("failed to files from dir"); - assert_eq!(1, files.files.len()); - }); - } -} - -#[derive(Debug, Clone)] -struct Files { - files: Vec, -} - -impl Files { - pub fn new(files: &Vec) -> Files { - let mut files = files.clone(); - files.sort(); - Files { - files: files.into_iter().map(|x| File::new(&x)).collect(), - } - } - - pub fn reconcile(&self) -> Result<(), String> { - for file in self.files.iter() { - file.persist_stage()?; - file.stage_persisted()?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -struct File { - file: String, -} - -impl File { - pub fn new(file: &String) -> File { - File { file: file.clone() } - } - - pub fn events(&self) -> Result { - Events::new(&self.file) - } - - pub fn persist_stage(&self) -> Result<(), String> { - let old_snapshot = self.events()?.last_snapshot(); - let stage_mod_time = std::fs::metadata(&self.file) - .unwrap() - .modified() - .unwrap() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - self.persist_delta_at(old_snapshot, self.stage()?, stage_mod_time) - } - - pub fn stage_persisted(&self) -> Result<(), String> { - let persisted_as_snapshot = self.events()?.snapshot()?; - if persisted_as_snapshot != self.events()?.last_snapshot() { - self.append(Delta::snapshot(persisted_as_snapshot.clone()))?; - } - let plaintext = serde_yaml::to_string(&persisted_as_snapshot).unwrap(); - let mut f = std::fs::File::create(&self.file).expect("failed to open file for writing"); - writeln!(f, "{}", plaintext).expect("failed to write"); - Ok(()) - } - - pub fn persist_delta(&self, before: Vec, after: Vec) -> Result<(), String> { - self.persist_delta_at(before, after, Delta::now_time()) - } - - fn persist_delta_at( - &self, - before: Vec, - after: Vec, - now: u64, - ) -> Result<(), String> { - for before in before.iter() { - if !after.contains(before) { - self.append(Delta::remove_at(before.clone(), now))?; + if let Some(add) = flags.add { + let task = match flags.add_schedule.clone() { + None => Task(serde_yaml::Value::String(add)), + Some(add_schedule) => { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), add_schedule.into()); + m.insert("do".into(), add.into()); + Task(serde_yaml::Value::Mapping(m)) + } + }; let now = Delta::now_time(); - let due = before.must_next_due(now.clone()); - if due >= now { - self.append(Delta::add_at(before.clone(), due))?; + files.files[0] + .append(match task.next_due(now.clone()) { + None => Delta::add(task), + Some(due) => Delta::add_at(task, if due > now { due } else { now }), + }) + .expect("failed to add"); + + files.reconcile().expect("failed to reconcile"); + } + } + + for file in files.files.iter() { + println!( + "{}", + serde_yaml::to_string(&file.events().unwrap().snapshot().unwrap()).unwrap(), + ); + } + + if flags.edit { + edit::files(&files); + } + } + + #[derive(Debug, Parser)] + struct Flags { + #[arg(short = 'f', long = "path", default_value = "$PTTODO_FILE")] + path: String, + + #[arg(short = 'a', long = "add")] + add: Option, + + #[arg(short = 'e', long = "edit", default_value = "false")] + edit: bool, + + #[arg(short = 'd', long = "dry-run", default_value = "false")] + dry_run: bool, + + #[arg(short = 's', long = "add-schedule")] + add_schedule: Option, + } + + impl Flags { + pub fn new() -> Result { + let mut result = Flags::parse(); + + if result.path.get(..1) == Some("$") { + result.path = match std::env::var(result.path.get(1..).unwrap()) { + Ok(v) => Ok(v), + Err(msg) => Err(format!("'{}' unset: {}", result.path, msg)), + }?; + } + + let _ = result.files()?; + + Ok(result) + } + + pub fn files(&self) -> Result { + Self::files_with(&self.path) + } + + pub fn files_with(p: &String) -> Result { + let metadata = match std::fs::metadata(p.clone()) { + Ok(v) => Ok(v), + Err(msg) => Err(format!("failed to load {}: {}", p.clone(), msg)), + }?; + let files = match metadata.is_dir() { + false => Ok(vec![p.clone()]), + true => match std::fs::read_dir(p.clone()) { + Ok(paths) => Ok(paths + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .filter(|x| x.metadata().unwrap().is_file()) + .map(|x| x.path().display().to_string()) + .filter(|x| !x.contains("/.")) + .collect()), + Err(msg) => Err(format!("failed to read {}: {}", p.clone(), msg)), + }, + }?; + assert!(files.len() > 0, "no files"); + Ok(Files::new(&files)) + } + } + + #[cfg(test)] + mod test_flags { + use super::*; + + #[test] + fn test_flags_files_unhidden_only() { + tests::with_dir(|d| { + std::fs::File::create(d.path().join("plain")).unwrap(); + std::fs::File::create(d.path().join(".hidden")).unwrap(); + + let flags = Flags { + path: d.path().to_str().unwrap().to_string(), + add: None, + edit: false, + dry_run: true, + add_schedule: None, + }; + let files = flags.files().expect("failed to files from dir"); + assert_eq!(1, files.files.len()); + }); + } + } + + #[derive(Debug, Clone)] + struct Files { + files: Vec, + } + + impl Files { + pub fn new(files: &Vec) -> Files { + let mut files = files.clone(); + files.sort(); + Files { + files: files.into_iter().map(|x| File::new(&x)).collect(), + } + } + + pub fn reconcile(&self) -> Result<(), String> { + for file in self.files.iter() { + file.persist_stage()?; + file.stage_persisted()?; + } + Ok(()) + } + } + + #[derive(Debug, Clone)] + struct File { + file: String, + } + + impl File { + pub fn new(file: &String) -> File { + File { file: file.clone() } + } + + pub fn events(&self) -> Result { + Events::new(&self.file) + } + + pub fn persist_stage(&self) -> Result<(), String> { + let old_snapshot = self.events()?.last_snapshot(); + let stage_mod_time = std::fs::metadata(&self.file) + .unwrap() + .modified() + .unwrap() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + self.persist_delta_at(old_snapshot, self.stage()?, stage_mod_time) + } + + pub fn stage_persisted(&self) -> Result<(), String> { + let persisted_as_snapshot = self.events()?.snapshot()?; + if persisted_as_snapshot != self.events()?.last_snapshot() { + self.append(Delta::snapshot(persisted_as_snapshot.clone()))?; + } + let plaintext = serde_yaml::to_string(&persisted_as_snapshot).unwrap(); + let mut f = std::fs::File::create(&self.file).expect("failed to open file for writing"); + writeln!(f, "{}", plaintext).expect("failed to write"); + Ok(()) + } + + pub fn persist_delta(&self, before: Vec, after: Vec) -> Result<(), String> { + self.persist_delta_at(before, after, Delta::now_time()) + } + + fn persist_delta_at( + &self, + before: Vec, + after: Vec, + now: u64, + ) -> Result<(), String> { + for before in before.iter() { + if !after.contains(before) { + self.append(Delta::remove_at(before.clone(), now))?; + let now = Delta::now_time(); + let due = before.must_next_due(now.clone()); + if due >= now { + self.append(Delta::add_at(before.clone(), due))?; + } } } + for after in after.iter() { + if !before.contains(after) { + self.append(Delta::add_at(after.clone(), now))?; + } + } + Ok(()) } - for after in after.iter() { - if !before.contains(after) { - self.append(Delta::add_at(after.clone(), now))?; + + fn stage(&self) -> Result, String> { + let mut r = match std::fs::File::open(self.file.clone()) { + Ok(f) => Ok(f), + Err(msg) => Err(format!("could not open {}: {}", &self.file, msg)), + }?; + + let mut buff = String::new(); + match r.read_to_string(&mut buff) { + Err(msg) => Err(format!("failed reading {}: {}", &self.file, msg)), + _ => Ok({}), + }?; + + let mut result = vec![]; + match serde_yaml::from_str::>(&buff) { + Ok(v) => { + result.extend(v.iter().map(|x| Task(x.clone()))); + Ok({}) + } + Err(msg) => Err(format!("failed parsing {}: {}", &self.file, msg)), + }?; + + Ok(result) + } + + fn append(&self, delta: Delta) -> Result<(), String> { + use std::fs::OpenOptions; + let hostname = gethostname::gethostname(); + assert!(hostname.len() > 0, "empty hostname"); + let log = format!( + "{}{}", + Events::log_prefix(&self.file), + hostname.into_string().unwrap() + ); + let mut file = match OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(&log) + { + Ok(f) => Ok(f), + Err(msg) => Err(format!("failed to open {} for appending: {}", &log, msg)), + }?; + let line = serde_json::to_string(&delta).unwrap(); + match writeln!(file, "{}", line) { + Ok(_) => Ok(()), + Err(msg) => Err(format!("failed to append: {}", msg)), } } - Ok(()) } - fn stage(&self) -> Result, String> { - let mut r = match std::fs::File::open(self.file.clone()) { - Ok(f) => Ok(f), - Err(msg) => Err(format!("could not open {}: {}", &self.file, msg)), - }?; + #[cfg(test)] + mod test_file { + use super::*; - let mut buff = String::new(); - match r.read_to_string(&mut buff) { - Err(msg) => Err(format!("failed reading {}: {}", &self.file, msg)), - _ => Ok({}), - }?; + #[test] + fn test_file_empty_empty() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "[]"); - let mut result = vec![]; - match serde_yaml::from_str::>(&buff) { - Ok(v) => { - result.extend(v.iter().map(|x| Task(x.clone()))); - Ok({}) + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(0, f.events().unwrap().0.len()); + assert_eq!(0, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[]"); + + f.persist_stage().unwrap(); + assert_eq!(0, f.events().unwrap().0.len()); + assert_eq!(0, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[]"); + + f.stage_persisted().unwrap(); + assert_eq!(0, f.events().unwrap().0.len()); + assert_eq!(0, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[]"); + }); + } + + #[test] + fn test_file_empty_stage_fills_events() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "[hello, world]"); + + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(0, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[hello, world]"); + + f.persist_stage().unwrap(); + assert_eq!(2, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "hello"); + tests::file_contains(&d, "plain", "world"); + + f.stage_persisted().unwrap(); + assert_eq!(3, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "- hello\n- world"); + }); + } + + #[test] + fn test_file_persist_empty_drains_events() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "[]"); + tests::write_file( + &d, + ".plain.host_a", + r#" + {"ts":1, "op":"Add", "task": "initial"} + {"ts":3, "op":"Add", "task": {"k":"v"}} + {"ts":3, "op":"Snapshot", "task": null, "tasks": ["initial", 1, {"k":"v"}]} + "#, + ); + tests::write_file( + &d, + ".plain.host_b", + r#" + {"ts":2, "op":"Add", "task": 1} + "#, + ); + + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(4, f.events().unwrap().0.len()); + assert_eq!(0, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[]"); + + f.persist_stage().unwrap(); + assert_eq!(7, f.events().unwrap().0.len()); + assert_eq!(0, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "[]"); + + f.stage_persisted().unwrap(); + assert_eq!( + 0, + f.events().unwrap().snapshot().unwrap().len(), + "{:?}", + f.events().unwrap().snapshot().unwrap(), + ); + assert_eq!(8, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage().unwrap()); + tests::file_contains(&d, "plain", "[]"); + }); + } + + #[test] + fn test_file_deletion_to_persist() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- initial\n- 1"); + tests::write_file( + &d, + ".plain.host_a", + r#" + {"ts":1, "op":"Add", "task": "initial"} + {"ts":3, "op":"Add", "task": {"k":"v"}} + "#, + ); + tests::write_file( + &d, + ".plain.host_b", + r#" + {"ts":2, "op":"Add", "task": 1} + {"ts":2, "op":"Snapshot", "task": null, "tasks": ["initial", 1]} + "#, + ); + + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(4, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "- initial\n- 1"); + + f.persist_stage().unwrap(); + assert_eq!(4, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "- initial\n- 1"); + + f.stage_persisted().unwrap(); + assert_eq!(5, f.events().unwrap().0.len()); + assert_eq!(3, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "- initial\n- 1\n- k: v"); + }); + } + + #[test] + fn test_persist_stage() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- old\n- new"); + tests::write_file( + &d, + ".plain.host", + format!( + r#" + {{"ts":1, "op":"Add", "task": "removed"}} + {{"ts":2, "op":"Add", "task": "old"}} + {{"ts":2, "op":"Snapshot", "task": null, "tasks": ["removed", "old"]}} + {{"ts":{}, "op":"Add", "task": "persisted but not snapshotted"}} + {{"ts":{}, "op":"Add", "task": "doesnt exist yet"}} + "#, + Delta::now_time() - 50, + Delta::now_time() + 50, + ) + .as_str(), + ); + + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(5, f.events().unwrap().0.len()); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "old"); + tests::file_contains(&d, "plain", "new"); + + f.persist_stage().unwrap(); + assert_eq!( + 7, + f.events().unwrap().0.len(), + "events: {:?}", + f.events().unwrap() + ); + assert_eq!(2, f.stage().unwrap().len()); + tests::file_contains(&d, "plain", "new"); + + f.stage_persisted().unwrap(); + assert_eq!(8, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(3, f.stage().unwrap().len(), "{:?}", f.stage().unwrap()); + tests::file_contains(&d, "plain", "new"); + tests::file_contains(&d, "plain", "old"); + tests::file_contains(&d, "plain", "persisted but not snapshotted"); + }); + } + + #[test] + fn test_schedule_date_future() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "[]"); + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "2036-01-02".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + + f.append(Delta::add(task)).unwrap(); + assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); + + f.persist_stage().unwrap(); + assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); + + f.stage_persisted().unwrap(); + assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); + }); + } + + #[test] + fn test_schedule_cron_resolve_reschedules() { + //panic!("not impl"); + } + + #[test] + fn test_schedule_duration_resolve_reschedules() { + //panic!("not impl"); + } + + #[test] + fn test_schedule_date_resolve_does_not_reschedule() { + //panic!("not impl"); + } + + #[test] + fn test_schedule_date_future_with_snapshot_between_scheduled_and_fired() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- stage"); + tests::write_file( + &d, + ".plain.host", + format!( + r#" + {{"ts":3, "op":"Add", "task": "scheduled add for after snapshot"}} + {{"ts":2, "op":"Snapshot", "task": null, "tasks": ["removed"]}} + "#, + ) + .as_str(), + ); + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + assert_eq!(2, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); + + f.persist_stage().unwrap(); + assert_eq!(4, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); + + f.stage_persisted().unwrap(); + assert_eq!(5, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(2, f.stage().unwrap().len(), "{:?}", f.stage()); + }); + } + + #[test] + fn test_schedule_date_past() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "[]"); + let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "2006-01-02".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + + f.append(Delta::add(task)).unwrap(); + assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); + + f.persist_stage().unwrap(); + assert_eq!( + 1, + f.events().unwrap().0.len(), + "events after 1 add scheduled: {:?}", + f.events().unwrap() + ); + assert_eq!( + 1, + f.events().unwrap().snapshot().unwrap().len(), + "events.snapshot after 1 add scheduled: {:?}", + f.events().unwrap().snapshot().unwrap(), + ); + tests::file_contains(&d, "plain", "[]"); + assert_eq!( + 0, + f.stage().unwrap().len(), + "stage after 1 add scheduled: {:?}", + f.stage() + ); + + f.stage_persisted().unwrap(); + assert_eq!(2, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); + assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); + }); + } + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + struct Delta { + ts: u64, + op: Op, + task: Task, + tasks: Option>, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + enum Op { + Add, + Remove, + Snapshot, + } + + impl Delta { + pub fn new(ts: u64, op: Op, task: Task) -> Delta { + Delta { + ts: ts, + op: op, + task: task, + tasks: None, } - Err(msg) => Err(format!("failed parsing {}: {}", &self.file, msg)), - }?; - - Ok(result) - } - - fn append(&self, delta: Delta) -> Result<(), String> { - use std::fs::OpenOptions; - let hostname = gethostname::gethostname(); - assert!(hostname.len() > 0, "empty hostname"); - let log = format!( - "{}{}", - Events::log_prefix(&self.file), - hostname.into_string().unwrap() - ); - let mut file = match OpenOptions::new() - .write(true) - .append(true) - .create(true) - .open(&log) - { - Ok(f) => Ok(f), - Err(msg) => Err(format!("failed to open {} for appending: {}", &log, msg)), - }?; - let line = serde_json::to_string(&delta).unwrap(); - match writeln!(file, "{}", line) { - Ok(_) => Ok(()), - Err(msg) => Err(format!("failed to append: {}", msg)), } - } -} -#[cfg(test)] -mod test_file { - use super::*; + pub fn snapshot(tasks: Vec) -> Delta { + Delta { + ts: Self::now_time(), + op: Op::Snapshot, + task: Task(serde_yaml::Value::Null), + tasks: Some(tasks), + } + } - #[test] - fn test_file_empty_empty() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "[]"); + pub fn add(task: Task) -> Delta { + Self::add_at(task, Self::now_time()) + } - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); + pub fn add_at(task: Task, at: u64) -> Delta { + Self::new(at, Op::Add, task) + } - assert_eq!(0, f.events().unwrap().0.len()); - assert_eq!(0, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[]"); + pub fn remove_at(task: Task, at: u64) -> Delta { + Self::new(at, Op::Remove, task) + } - f.persist_stage().unwrap(); - assert_eq!(0, f.events().unwrap().0.len()); - assert_eq!(0, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[]"); - - f.stage_persisted().unwrap(); - assert_eq!(0, f.events().unwrap().0.len()); - assert_eq!(0, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[]"); - }); - } - - #[test] - fn test_file_empty_stage_fills_events() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "[hello, world]"); - - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - assert_eq!(0, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[hello, world]"); - - f.persist_stage().unwrap(); - assert_eq!(2, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "hello"); - tests::file_contains(&d, "plain", "world"); - - f.stage_persisted().unwrap(); - assert_eq!(3, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "- hello\n- world"); - }); - } - - #[test] - fn test_file_persist_empty_drains_events() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "[]"); - tests::write_file( - &d, - ".plain.host_a", - r#" - {"ts":1, "op":"Add", "task": "initial"} - {"ts":3, "op":"Add", "task": {"k":"v"}} - {"ts":3, "op":"Snapshot", "task": null, "tasks": ["initial", 1, {"k":"v"}]} - "#, - ); - tests::write_file( - &d, - ".plain.host_b", - r#" - {"ts":2, "op":"Add", "task": 1} - "#, - ); - - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - assert_eq!(4, f.events().unwrap().0.len()); - assert_eq!(0, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[]"); - - f.persist_stage().unwrap(); - assert_eq!(7, f.events().unwrap().0.len()); - assert_eq!(0, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "[]"); - - f.stage_persisted().unwrap(); - assert_eq!( - 0, - f.events().unwrap().snapshot().unwrap().len(), - "{:?}", - f.events().unwrap().snapshot().unwrap(), - ); - assert_eq!(8, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage().unwrap()); - tests::file_contains(&d, "plain", "[]"); - }); - } - - #[test] - fn test_file_deletion_to_persist() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- initial\n- 1"); - tests::write_file( - &d, - ".plain.host_a", - r#" - {"ts":1, "op":"Add", "task": "initial"} - {"ts":3, "op":"Add", "task": {"k":"v"}} - "#, - ); - tests::write_file( - &d, - ".plain.host_b", - r#" - {"ts":2, "op":"Add", "task": 1} - {"ts":2, "op":"Snapshot", "task": null, "tasks": ["initial", 1]} - "#, - ); - - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - assert_eq!(4, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "- initial\n- 1"); - - f.persist_stage().unwrap(); - assert_eq!(4, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "- initial\n- 1"); - - f.stage_persisted().unwrap(); - assert_eq!(5, f.events().unwrap().0.len()); - assert_eq!(3, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "- initial\n- 1\n- k: v"); - }); - } - - #[test] - fn test_persist_stage() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- old\n- new"); - tests::write_file( - &d, - ".plain.host", - format!( - r#" - {{"ts":1, "op":"Add", "task": "removed"}} - {{"ts":2, "op":"Add", "task": "old"}} - {{"ts":2, "op":"Snapshot", "task": null, "tasks": ["removed", "old"]}} - {{"ts":{}, "op":"Add", "task": "persisted but not snapshotted"}} - {{"ts":{}, "op":"Add", "task": "doesnt exist yet"}} - "#, - Delta::now_time() - 50, - Delta::now_time() + 50, - ) - .as_str(), - ); - - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - assert_eq!(5, f.events().unwrap().0.len()); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "old"); - tests::file_contains(&d, "plain", "new"); - - f.persist_stage().unwrap(); - assert_eq!( - 7, - f.events().unwrap().0.len(), - "events: {:?}", - f.events().unwrap() - ); - assert_eq!(2, f.stage().unwrap().len()); - tests::file_contains(&d, "plain", "new"); - - f.stage_persisted().unwrap(); - assert_eq!(8, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(3, f.stage().unwrap().len(), "{:?}", f.stage().unwrap()); - tests::file_contains(&d, "plain", "new"); - tests::file_contains(&d, "plain", "old"); - tests::file_contains(&d, "plain", "persisted but not snapshotted"); - }); - } - - #[test] - fn test_schedule_date_future() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "[]"); - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "2036-01-02".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - - f.append(Delta::add(task)).unwrap(); - assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); - - f.persist_stage().unwrap(); - assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); - - f.stage_persisted().unwrap(); - assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); - }); - } - - #[test] - fn test_schedule_cron_resolve_reschedules() { - panic!("not impl"); - } - - #[test] - fn test_schedule_duration_resolve_reschedules() { - panic!("not impl"); - } - - #[test] - fn test_schedule_date_resolve_does_not_reschedule() { - panic!("not impl"); - } - - #[test] - fn test_schedule_date_future_with_snapshot_between_scheduled_and_fired() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- stage"); - tests::write_file( - &d, - ".plain.host", - format!( - r#" - {{"ts":3, "op":"Add", "task": "scheduled add for after snapshot"}} - {{"ts":2, "op":"Snapshot", "task": null, "tasks": ["removed"]}} - "#, - ) - .as_str(), - ); - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - assert_eq!(2, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); - - f.persist_stage().unwrap(); - assert_eq!(4, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); - - f.stage_persisted().unwrap(); - assert_eq!(5, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(2, f.stage().unwrap().len(), "{:?}", f.stage()); - }); - } - - #[test] - fn test_schedule_date_past() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "[]"); - let f = File::new(&d.path().join("plain").to_str().unwrap().to_string()); - - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "2006-01-02".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - - f.append(Delta::add(task)).unwrap(); - assert_eq!(1, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(0, f.stage().unwrap().len(), "{:?}", f.stage()); - - f.persist_stage().unwrap(); - assert_eq!( - 1, - f.events().unwrap().0.len(), - "events after 1 add scheduled: {:?}", - f.events().unwrap() - ); - assert_eq!( - 1, - f.events().unwrap().snapshot().unwrap().len(), - "events.snapshot after 1 add scheduled: {:?}", - f.events().unwrap().snapshot().unwrap(), - ); - tests::file_contains(&d, "plain", "[]"); - assert_eq!( - 0, - f.stage().unwrap().len(), - "stage after 1 add scheduled: {:?}", - f.stage() - ); - - f.stage_persisted().unwrap(); - assert_eq!(2, f.events().unwrap().0.len(), "{:?}", f.events().unwrap()); - assert_eq!(1, f.stage().unwrap().len(), "{:?}", f.stage()); - }); - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Delta { - ts: u64, - op: Op, - task: Task, - tasks: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum Op { - Add, - Remove, - Snapshot, -} - -impl Delta { - pub fn new(ts: u64, op: Op, task: Task) -> Delta { - Delta { - ts: ts, - op: op, - task: task, - tasks: None, + fn now_time() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .try_into() + .unwrap() } } - pub fn snapshot(tasks: Vec) -> Delta { - Delta { - ts: Self::now_time(), - op: Op::Snapshot, - task: Task(serde_yaml::Value::Null), - tasks: Some(tasks), - } - } + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + struct Task(serde_yaml::Value); - pub fn add(task: Task) -> Delta { - Self::add_at(task, Self::now_time()) - } - - pub fn add_at(task: Task, at: u64) -> Delta { - Self::new(at, Op::Add, task) - } - - pub fn remove_at(task: Task, at: u64) -> Delta { - Self::new(at, Op::Remove, task) - } - - fn now_time() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - .try_into() - .unwrap() - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -struct Task(serde_yaml::Value); - -impl Task { - pub fn _due(&self, after: u64) -> bool { - match self.next_due(after) { - Some(ts) => Delta::now_time() > ts, - None => true, - } - } - - pub fn must_next_due(&self, after: u64) -> u64 { - self.next_due(after).unwrap_or(1) - } - - pub fn next_due(&self, after: u64) -> Option { - match self.schedule() { - Some(schedule) => self.parse_schedule_next(schedule, after), - None => None, - } - } - - fn parse_schedule_next(&self, schedule: String, after: u64) -> Option { - let mut schedule = schedule; - - if regex::Regex::new(r"^[0-9]+h$").unwrap().is_match(&schedule) { - let hours = &schedule[..schedule.len() - 1]; - match hours.parse::() { - Ok(hours) => return Some(after + hours * 60 * 60), - _ => {} - }; + impl Task { + pub fn _due(&self, after: u64) -> bool { + match self.next_due(after) { + Some(ts) => Delta::now_time() > ts, + None => true, + } } - if regex::Regex::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2}$") - .unwrap() - .is_match(&schedule) - { - schedule += "T00"; + pub fn must_next_due(&self, after: u64) -> u64 { + self.next_due(after).unwrap_or(1) } - if regex::Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}$") - .unwrap() - .is_match(&schedule) - { - let date = schedule.clone() + ":00:00"; - match chrono::NaiveDateTime::parse_from_str(&date, "%Y-%m-%dT%H:%M:%S") { - Ok(datehour) => { - let seconds = datehour.format("%s").to_string(); + pub fn next_due(&self, after: u64) -> Option { + match self.schedule() { + Some(schedule) => self.parse_schedule_next(schedule, after), + None => None, + } + } + + fn parse_schedule_next(&self, schedule: String, after: u64) -> Option { + let mut schedule = schedule; + + if regex::Regex::new(r"^[0-9]+h$").unwrap().is_match(&schedule) { + let hours = &schedule[..schedule.len() - 1]; + match hours.parse::() { + Ok(hours) => return Some(after + hours * 60 * 60), + _ => {} + }; + } + + if regex::Regex::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2}$") + .unwrap() + .is_match(&schedule) + { + schedule += "T00"; + } + + if regex::Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}$") + .unwrap() + .is_match(&schedule) + { + let date = schedule.clone() + ":00:00"; + match chrono::NaiveDateTime::parse_from_str(&date, "%Y-%m-%dT%H:%M:%S") { + Ok(datehour) => { + let seconds = datehour.format("%s").to_string(); + match seconds.parse::() { + Ok(n) => return Some(n), + _ => {} + }; + } + Err(msg) => panic!("{}", msg), + }; + } + + if regex::Regex::new(r"^([^ ]+ ){4}[^ ]+$") + .unwrap() + .is_match(&schedule) + { + let after = chrono::DateTime::from_timestamp(after as i64, 0).unwrap(); + if let Ok(next) = cron_parser::parse(&schedule, &after) { + let seconds = next.format("%s").to_string(); match seconds.parse::() { Ok(n) => return Some(n), _ => {} }; } - Err(msg) => panic!("{}", msg), - }; - } - - if regex::Regex::new(r"^([^ ]+ ){4}[^ ]+$") - .unwrap() - .is_match(&schedule) - { - let after = chrono::DateTime::from_timestamp(after as i64, 0).unwrap(); - if let Ok(next) = cron_parser::parse(&schedule, &after) { - let seconds = next.format("%s").to_string(); - match seconds.parse::() { - Ok(n) => return Some(n), - _ => {} - }; } + + None } - None - } - - fn schedule(&self) -> Option { - match &self.0 { - serde_yaml::Value::Mapping(m) => match m.get("schedule".to_string()) { - Some(schedule) => match schedule { - serde_yaml::Value::String(s) => Some(s.clone()), + fn schedule(&self) -> Option { + match &self.0 { + serde_yaml::Value::Mapping(m) => match m.get("schedule".to_string()) { + Some(schedule) => match schedule { + serde_yaml::Value::String(s) => Some(s.clone()), + _ => None, + }, _ => None, }, _ => None, - }, - _ => None, + } } } -} -#[cfg(test)] -mod test_task { - use super::*; + #[cfg(test)] + mod test_task { + use super::*; - #[test] - fn test_unscheduled() { - let task = Task(serde_yaml::Value::String("hello world".to_string())); - assert_eq!(None, task.schedule()); - assert_eq!(1 as u64, task.must_next_due(100)); - assert!(task._due(100)); - } - - #[test] - fn test_scheduled_date_before() { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "2006-01-02".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - assert_eq!(Some("2006-01-02".to_string()), task.schedule()); - assert_eq!(Some(1136160000 as u64), task.next_due(100)); - assert!(task._due(100)); - } - - #[test] - fn test_scheduled_date_after() { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "2036-01-02".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - assert_eq!(Some("2036-01-02".to_string()), task.schedule()); - assert_eq!(Some(2082844800 as u64), task.next_due(100)); - assert!(!task._due(100)); - } - - #[test] - fn test_scheduled_hour_after() { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "2036-01-02T16".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - assert_eq!(Some("2036-01-02T16".to_string()), task.schedule()); - assert_eq!(Some(2082902400 as u64), task.next_due(100)); - assert!(!task._due(100)); - } - - #[test] - fn test_scheduled_duration() { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "1h".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - assert_eq!(Some("1h".to_string()), task.schedule()); - assert_eq!(Some(3700), task.next_due(100)); - assert!(task._due(100)); - } - - #[test] - fn test_scheduled_cron() { - let mut m = serde_yaml::Mapping::new(); - m.insert("schedule".into(), "* * * * *".into()); - let task = Task(serde_yaml::Value::Mapping(m)); - assert_eq!(Some("* * * * *".to_string()), task.schedule()); - assert_eq!(Some(120 as u64), task.next_due(100)); - assert!(task._due(100)); - } -} - -#[derive(Clone)] -struct Events(Vec); - -impl std::fmt::Debug for Events { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut arr = vec![]; - for i in self.0.iter() { - arr.push(format!("{:?}", i.clone())); + #[test] + fn test_unscheduled() { + let task = Task(serde_yaml::Value::String("hello world".to_string())); + assert_eq!(None, task.schedule()); + assert_eq!(1 as u64, task.must_next_due(100)); + assert!(task._due(100)); } - write!(f, "[\n {}\n]", arr.join("\n ")) - } -} -impl Events { - pub fn new(file: &String) -> Result { - let logs = match std::fs::read_dir(Self::dir(&file)) { - Ok(files) => Ok(files + #[test] + fn test_scheduled_date_before() { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "2006-01-02".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + assert_eq!(Some("2006-01-02".to_string()), task.schedule()); + assert_eq!(Some(1136160000 as u64), task.next_due(100)); + assert!(task._due(100)); + } + + #[test] + fn test_scheduled_date_after() { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "2036-01-02".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + assert_eq!(Some("2036-01-02".to_string()), task.schedule()); + assert_eq!(Some(2082844800 as u64), task.next_due(100)); + assert!(!task._due(100)); + } + + #[test] + fn test_scheduled_hour_after() { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "2036-01-02T16".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + assert_eq!(Some("2036-01-02T16".to_string()), task.schedule()); + assert_eq!(Some(2082902400 as u64), task.next_due(100)); + assert!(!task._due(100)); + } + + #[test] + fn test_scheduled_duration() { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "1h".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + assert_eq!(Some("1h".to_string()), task.schedule()); + assert_eq!(Some(3700), task.next_due(100)); + assert!(task._due(100)); + } + + #[test] + fn test_scheduled_cron() { + let mut m = serde_yaml::Mapping::new(); + m.insert("schedule".into(), "* * * * *".into()); + let task = Task(serde_yaml::Value::Mapping(m)); + assert_eq!(Some("* * * * *".to_string()), task.schedule()); + assert_eq!(Some(120 as u64), task.next_due(100)); + assert!(task._due(100)); + } + } + + #[derive(Clone)] + struct Events(Vec); + + impl std::fmt::Debug for Events { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut arr = vec![]; + for i in self.0.iter() { + arr.push(format!("{:?}", i.clone())); + } + write!(f, "[\n {}\n]", arr.join("\n ")) + } + } + + impl Events { + pub fn new(file: &String) -> Result { + let logs = match std::fs::read_dir(Self::dir(&file)) { + Ok(files) => Ok(files + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .filter(|x| x.metadata().unwrap().is_file()) + .map(|x| x.path().display().to_string()) + .filter(|x| x.starts_with(&Self::log_prefix(&file))) + .collect::>()), + Err(msg) => Err(format!("failed to read dir {}: {}", Self::dir(&file), msg)), + }?; + + let mut result: Vec = vec![]; + for log in logs.iter() { + match std::fs::File::open(&log) { + Ok(f) => { + for line in std::io::BufReader::new(f).lines() { + let line = line.unwrap(); + let line = line.trim(); + if line.len() > 0 { + let delta = match serde_json::from_str(&line) { + Ok(v) => Ok(v), + Err(msg) => { + Err(format!("failed to parse line {}: {}", &line, msg)) + } + }?; + result.push(delta); + } + } + Ok(()) + } + Err(msg) => Err(format!("failed to read {}: {}", &log, msg)), + }?; + } + + result.sort_by(|a, b| a.ts.cmp(&b.ts)); + + Ok(Events(result)) + } + + fn log_prefix(file: &String) -> String { + format!("{}/.{}.", Self::dir(&file), Self::basename(&file)).to_string() + } + + fn dir(file: &String) -> String { + let path = std::path::Path::new(&file); + path.parent() + .expect("cannot get dirname") + .to_str() + .expect("cannot stringify dirname") + .to_string() + } + + pub fn basename(file: &String) -> String { + let path = std::path::Path::new(&file); + path.file_name() + .expect("cannot get basename") + .to_str() + .expect("cannot stringify basename") + .to_string() + } + + fn last_snapshot(&self) -> Vec { + let reversed_events = { + let mut e = self.0.clone(); + e.reverse(); + e + }; + for event in reversed_events.iter() { + match &event.op { + Op::Snapshot => return event.tasks.clone().unwrap(), + _ => {} + }; + } + vec![] + } + + fn snapshot(&self) -> Result, String> { + let mut result = vec![]; + for event in self.0.iter().filter(|t| t.ts <= Delta::now_time()) { + match &event.op { + Op::Add => match event.task.next_due(event.ts) { + Some(next_due) => match next_due <= Delta::now_time() { + true => result.push(event.task.clone()), + false => {} + }, + None => result.push(event.task.clone()), + }, + Op::Remove => { + let mut i = (result.len() - 1) as i32; + while i >= 0 { + if event.task == result[i as usize] { + result.remove(i as usize); + if i == result.len() as i32 { + i -= 1 + } + } else { + i -= 1; + } + } + } + Op::Snapshot => result = event.tasks.clone().unwrap(), + }; + } + Ok(result) + } + } + + #[cfg(test)] + mod test_events { + use super::*; + + #[test] + fn test_events_op_snapshot() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- who cares"); + tests::write_file( + &d, + ".plain.some_host", + r#" + {"ts":1, "op":"Snapshot", "task":"", "tasks":["snapshotted"]} + "#, + ); + + let events = + Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); + assert_eq!(1, events.0.len(), "events: {:?}", events); + + let snapshot = events.snapshot().unwrap(); + assert_eq!(1, snapshot.len()); + assert_eq!( + serde_yaml::Value::String("snapshotted".to_string()), + snapshot[0].0 + ); + }); + } + + #[test] + fn test_events_oplog_to_snapshot_one() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- persisted\n- stage only"); + tests::write_file( + &d, + ".plain.some_host", + r#" + {"ts":1, "op":"Add", "task":"persisted"} + "#, + ); + + let events = + Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); + assert_eq!(1, events.0.len(), "events: {:?}", events); + + let snapshot = events.snapshot().unwrap(); + assert_eq!(1, snapshot.len()); + assert_eq!( + serde_yaml::Value::String("persisted".to_string()), + snapshot[0].0 + ); + }); + } + + #[test] + fn test_events_oplog_to_snapshot_complex() { + tests::with_dir(|d| { + tests::write_file(&d, "plain", "- ignored"); + tests::write_file( + &d, + ".plain.host_a", + r#" + {"ts":1, "op":"Add", "task":"persisted"} + {"ts":3, "op":"Add", "task":"persisted 3"} + {"ts":2, "op":"Add", "task":"persisted 2"} + {"ts":6, "op":"Remove", "task":"persisted 5"} + {"ts":6, "op":"Add", "task":"persisted 5'"} + {"ts":7, "op":"Remove", "task":"persisted 4"} + "#, + ); + tests::write_file( + &d, + ".plain.host_b", + r#" + {"ts":4, "op":"Add", "task":"persisted 4"} + {"ts":5, "op":"Add", "task":"persisted 5"} + "#, + ); + + let events = + Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); + + let snapshot = events.snapshot().unwrap(); + assert_eq!(4, snapshot.len()); + assert_eq!( + serde_yaml::Value::String("persisted".to_string()), + snapshot[0].0 + ); + assert_eq!( + serde_yaml::Value::String("persisted 2".to_string()), + snapshot[1].0 + ); + assert_eq!( + serde_yaml::Value::String("persisted 3".to_string()), + snapshot[2].0 + ); + assert_eq!( + serde_yaml::Value::String("persisted 5'".to_string()), + snapshot[3].0 + ); + }); + } + } + + mod tests { + use super::*; + + pub fn with_dir(mut foo: impl FnMut(tempdir::TempDir)) { + foo(tempdir::TempDir::new("").unwrap()); + } + + pub fn write_file(d: &tempdir::TempDir, fname: &str, content: &str) { + let mut f = std::fs::File::create(d.path().join(&fname)).unwrap(); + writeln!(f, "{}", &content).unwrap(); + f.sync_all().unwrap(); + } + + #[allow(dead_code)] + pub fn file_contains(d: &tempdir::TempDir, fname: &str, content: &str) { + let p = d.path().join(&fname); + let file_content = file_content(&p.to_str().unwrap().to_string()); + assert!( + file_content.contains(content), + "expected {:?} but got {:?}", + content, + file_content + ); + } + + pub fn file_content(p: &String) -> String { + std::fs::read_to_string(p).unwrap() + } + + pub fn list_dir(d: &tempdir::TempDir) -> Vec { + std::fs::read_dir(d) + .unwrap() .filter(|x| x.is_ok()) .map(|x| x.unwrap()) .filter(|x| x.metadata().unwrap().is_file()) .map(|x| x.path().display().to_string()) - .filter(|x| x.starts_with(&Self::log_prefix(&file))) - .collect::>()), - Err(msg) => Err(format!("failed to read dir {}: {}", Self::dir(&file), msg)), - }?; - - let mut result: Vec = vec![]; - for log in logs.iter() { - match std::fs::File::open(&log) { - Ok(f) => { - for line in std::io::BufReader::new(f).lines() { - let line = line.unwrap(); - let line = line.trim(); - if line.len() > 0 { - let delta = match serde_json::from_str(&line) { - Ok(v) => Ok(v), - Err(msg) => Err(format!("failed to parse line {}: {}", &line, msg)), - }?; - result.push(delta); - } - } - Ok(()) - } - Err(msg) => Err(format!("failed to read {}: {}", &log, msg)), - }?; - } - - result.sort_by(|a, b| a.ts.cmp(&b.ts)); - - Ok(Events(result)) - } - - fn log_prefix(file: &String) -> String { - format!("{}/.{}.", Self::dir(&file), Self::basename(&file)).to_string() - } - - fn dir(file: &String) -> String { - let path = std::path::Path::new(&file); - path.parent() - .expect("cannot get dirname") - .to_str() - .expect("cannot stringify dirname") - .to_string() - } - - pub fn basename(file: &String) -> String { - let path = std::path::Path::new(&file); - path.file_name() - .expect("cannot get basename") - .to_str() - .expect("cannot stringify basename") - .to_string() - } - - fn last_snapshot(&self) -> Vec { - let reversed_events = { - let mut e = self.0.clone(); - e.reverse(); - e - }; - for event in reversed_events.iter() { - match &event.op { - Op::Snapshot => return event.tasks.clone().unwrap(), - _ => {} - }; - } - vec![] - } - - fn snapshot(&self) -> Result, String> { - let mut result = vec![]; - for event in self.0.iter().filter(|t| t.ts <= Delta::now_time()) { - match &event.op { - Op::Add => match event.task.next_due(event.ts) { - Some(next_due) => match next_due <= Delta::now_time() { - true => result.push(event.task.clone()), - false => {} - }, - None => result.push(event.task.clone()), - }, - Op::Remove => { - let mut i = (result.len() - 1) as i32; - while i >= 0 { - if event.task == result[i as usize] { - result.remove(i as usize); - if i == result.len() as i32 { - i -= 1 - } - } else { - i -= 1; - } - } - } - Op::Snapshot => result = event.tasks.clone().unwrap(), - }; - } - Ok(result) - } -} - -#[cfg(test)] -mod test_events { - use super::*; - - #[test] - fn test_events_op_snapshot() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- who cares"); - tests::write_file( - &d, - ".plain.some_host", - r#" - {"ts":1, "op":"Snapshot", "task":"", "tasks":["snapshotted"]} - "#, - ); - - let events = - Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); - assert_eq!(1, events.0.len(), "events: {:?}", events); - - let snapshot = events.snapshot().unwrap(); - assert_eq!(1, snapshot.len()); - assert_eq!( - serde_yaml::Value::String("snapshotted".to_string()), - snapshot[0].0 - ); - }); - } - - #[test] - fn test_events_oplog_to_snapshot_one() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- persisted\n- stage only"); - tests::write_file( - &d, - ".plain.some_host", - r#" - {"ts":1, "op":"Add", "task":"persisted"} - "#, - ); - - let events = - Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); - assert_eq!(1, events.0.len(), "events: {:?}", events); - - let snapshot = events.snapshot().unwrap(); - assert_eq!(1, snapshot.len()); - assert_eq!( - serde_yaml::Value::String("persisted".to_string()), - snapshot[0].0 - ); - }); - } - - #[test] - fn test_events_oplog_to_snapshot_complex() { - tests::with_dir(|d| { - tests::write_file(&d, "plain", "- ignored"); - tests::write_file( - &d, - ".plain.host_a", - r#" - {"ts":1, "op":"Add", "task":"persisted"} - {"ts":3, "op":"Add", "task":"persisted 3"} - {"ts":2, "op":"Add", "task":"persisted 2"} - {"ts":6, "op":"Remove", "task":"persisted 5"} - {"ts":6, "op":"Add", "task":"persisted 5'"} - {"ts":7, "op":"Remove", "task":"persisted 4"} - "#, - ); - tests::write_file( - &d, - ".plain.host_b", - r#" - {"ts":4, "op":"Add", "task":"persisted 4"} - {"ts":5, "op":"Add", "task":"persisted 5"} - "#, - ); - - let events = - Events::new(&d.path().join("plain").to_str().unwrap().to_string()).unwrap(); - - let snapshot = events.snapshot().unwrap(); - assert_eq!(4, snapshot.len()); - assert_eq!( - serde_yaml::Value::String("persisted".to_string()), - snapshot[0].0 - ); - assert_eq!( - serde_yaml::Value::String("persisted 2".to_string()), - snapshot[1].0 - ); - assert_eq!( - serde_yaml::Value::String("persisted 3".to_string()), - snapshot[2].0 - ); - assert_eq!( - serde_yaml::Value::String("persisted 5'".to_string()), - snapshot[3].0 - ); - }); - } -} - -mod tests { - use super::*; - - pub fn with_dir(mut foo: impl FnMut(tempdir::TempDir)) { - foo(tempdir::TempDir::new("").unwrap()); - } - - pub fn write_file(d: &tempdir::TempDir, fname: &str, content: &str) { - let mut f = std::fs::File::create(d.path().join(&fname)).unwrap(); - writeln!(f, "{}", &content).unwrap(); - f.sync_all().unwrap(); - } - - #[allow(dead_code)] - pub fn file_contains(d: &tempdir::TempDir, fname: &str, content: &str) { - let p = d.path().join(&fname); - let file_content = file_content(&p.to_str().unwrap().to_string()); - assert!( - file_content.contains(content), - "expected {:?} but got {:?}", - content, - file_content - ); - } - - pub fn file_content(p: &String) -> String { - std::fs::read_to_string(p).unwrap() - } - - pub fn list_dir(d: &tempdir::TempDir) -> Vec { - std::fs::read_dir(d) - .unwrap() - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) - .filter(|x| x.metadata().unwrap().is_file()) - .map(|x| x.path().display().to_string()) - .filter(|x| !x.contains("/.")) - .collect() - } -} - -mod edit { - use super::*; - - pub fn files(files: &Files) { - tests::with_dir(|d| { - build_dir(&d, &files).expect("failed to build dir"); - edit_dir(&d).expect("failed to edit dir"); - persist_edits(&d, &files).expect("couldnt persist edited"); - }); - } - - fn build_dir(d: &tempdir::TempDir, files: &Files) -> Result<(), String> { - for file in files.files.iter() { - let content = tests::file_content(&file.file); - tests::write_file(d, Events::basename(&file.file).as_str(), content.as_str()); - } - Ok(()) - } - - fn edit_dir(d: &tempdir::TempDir) -> Result<(), String> { - let mut cmd = std::process::Command::new("vim"); - cmd.stdout(std::process::Stdio::inherit()); - cmd.stdin(std::process::Stdio::inherit()); - cmd.stderr(std::process::Stdio::inherit()); - cmd.arg("-p"); - for f in tests::list_dir(&d).iter() { - cmd.arg(Events::basename(f)); - } - cmd.current_dir(d.path().display().to_string()); - match cmd.output() { - Ok(_) => Ok(()), - Err(msg) => Err(format!("failed to vim: {}", msg)), + .filter(|x| !x.contains("/.")) + .collect() } } - fn persist_edits(d: &tempdir::TempDir, files: &Files) -> Result<(), String> { - let new_files = Flags::files_with(&d.path().display().to_string())?; - assert_eq!(files.files.len(), new_files.files.len()); - - for i in 0..files.files.len() { - let file = &files.files[i]; - let before = file.stage()?; - let after = new_files.files[i].stage()?; - file.persist_delta(before, after)?; - file.stage_persisted()?; - } - - Ok(()) - } - - #[cfg(test)] - mod test_edit { + mod edit { use super::*; - #[test] - fn test_build_empty() { + pub fn files(files: &Files) { tests::with_dir(|d| { - build_dir(&d, &Files { files: vec![] }).expect("failed to build empty dir"); + build_dir(&d, &files).expect("failed to build dir"); + edit_dir(&d).expect("failed to edit dir"); + persist_edits(&d, &files).expect("couldnt persist edited"); }); } - #[test] - fn test_build_with_files() { - tests::with_dir(|d1| { - tests::write_file(&d1, "file_a", "hello world a"); - tests::write_file(&d1, "file_b", "hello world b"); - let p1 = d1.path().join("file_a").display().to_string(); - let p2 = d1.path().join("file_b").display().to_string(); - let files = Files::new(&vec![p1, p2]); - tests::with_dir(|d2| { - build_dir(&d2, &files).expect("failed to build non-empty dir"); - tests::file_contains(&d2, "file_a", "hello world a"); - tests::file_contains(&d2, "file_b", "hello world b"); + fn build_dir(d: &tempdir::TempDir, files: &Files) -> Result<(), String> { + for file in files.files.iter() { + let content = tests::file_content(&file.file); + tests::write_file(d, Events::basename(&file.file).as_str(), content.as_str()); + } + Ok(()) + } + + fn edit_dir(d: &tempdir::TempDir) -> Result<(), String> { + let mut cmd = std::process::Command::new("vim"); + cmd.stdout(std::process::Stdio::inherit()); + cmd.stdin(std::process::Stdio::inherit()); + cmd.stderr(std::process::Stdio::inherit()); + cmd.arg("-p"); + for f in tests::list_dir(&d).iter() { + cmd.arg(Events::basename(f)); + } + cmd.current_dir(d.path().display().to_string()); + match cmd.output() { + Ok(_) => Ok(()), + Err(msg) => Err(format!("failed to vim: {}", msg)), + } + } + + fn persist_edits(d: &tempdir::TempDir, files: &Files) -> Result<(), String> { + let new_files = Flags::files_with(&d.path().display().to_string())?; + assert_eq!(files.files.len(), new_files.files.len()); + + for i in 0..files.files.len() { + let file = &files.files[i]; + let before = file.stage()?; + let after = new_files.files[i].stage()?; + file.persist_delta(before, after)?; + file.stage_persisted()?; + } + + Ok(()) + } + + #[cfg(test)] + mod test_edit { + use super::*; + + #[test] + fn test_build_empty() { + tests::with_dir(|d| { + build_dir(&d, &Files { files: vec![] }).expect("failed to build empty dir"); }); - }); - } + } - #[test] - fn test_persist_edits_with_files() { - let hostname = gethostname::gethostname().into_string().unwrap(); - let log_file = format!(".file.{}", hostname); - let log_file = log_file.as_str(); - tests::with_dir(|d1| { - tests::write_file(&d1, "file", "- foo"); - let p = d1.path().join("file").display().to_string(); - let files = Files::new(&vec![p]); - files.files[0].persist_stage().expect("failed to init log"); - tests::file_contains(&d1, log_file, r#""foo""#); - tests::with_dir(|d2| { - build_dir(&d2, &files).expect("failed to build dir"); - tests::file_contains(&d2, "file", "- foo"); - tests::write_file(&d2, "file", "- foobar\n- bar"); + #[test] + fn test_build_with_files() { + tests::with_dir(|d1| { + tests::write_file(&d1, "file_a", "hello world a"); + tests::write_file(&d1, "file_b", "hello world b"); + let p1 = d1.path().join("file_a").display().to_string(); + let p2 = d1.path().join("file_b").display().to_string(); + let files = Files::new(&vec![p1, p2]); + tests::with_dir(|d2| { + build_dir(&d2, &files).expect("failed to build non-empty dir"); + tests::file_contains(&d2, "file_a", "hello world a"); + tests::file_contains(&d2, "file_b", "hello world b"); + }); + }); + } - persist_edits(&d2, &files).expect("failed to persist edits"); - tests::file_contains(&d1, "file", "- foobar\n- bar"); + #[test] + fn test_persist_edits_with_files() { + let hostname = gethostname::gethostname().into_string().unwrap(); + let log_file = format!(".file.{}", hostname); + let log_file = log_file.as_str(); + tests::with_dir(|d1| { + tests::write_file(&d1, "file", "- foo"); + let p = d1.path().join("file").display().to_string(); + let files = Files::new(&vec![p]); + files.files[0].persist_stage().expect("failed to init log"); tests::file_contains(&d1, log_file, r#""foo""#); - tests::file_contains(&d1, log_file, r#""foobar""#); - tests::file_contains(&d1, log_file, r#""bar""#); + tests::with_dir(|d2| { + build_dir(&d2, &files).expect("failed to build dir"); + tests::file_contains(&d2, "file", "- foo"); + tests::write_file(&d2, "file", "- foobar\n- bar"); + + persist_edits(&d2, &files).expect("failed to persist edits"); + tests::file_contains(&d1, "file", "- foobar\n- bar"); + tests::file_contains(&d1, log_file, r#""foo""#); + tests::file_contains(&d1, log_file, r#""foobar""#); + tests::file_contains(&d1, log_file, r#""bar""#); + }); }); - }); + } } } }