1use crate::tick::{full_value, Tick};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub struct Store {
7 pub root: PathBuf, }
9
10const DEFAULT_CONFIG: &str = "schema_version = 1\n\n\
11[runner]\n\
12template = \"pytest {selector}\"\n\
13green_exit_code = 0\n\n\
14[liveness]\n\
15platforms = [\"linux-ci\", \"mac\", \"ship-image\"]\n\
16staleness_days = 7\n\
17not_run_lookback_commits = 20\n";
18
19impl Store {
20 pub fn at(repo: &Path) -> Store {
21 Store {
22 root: repo.join(".evolving"),
23 }
24 }
25 pub fn ticks_dir(&self) -> PathBuf {
26 self.root.join("ticks")
27 }
28 pub fn head_path(&self) -> PathBuf {
29 self.root.join("HEAD")
30 }
31 pub fn config_path(&self) -> PathBuf {
32 self.root.join("config")
33 }
34 pub fn exists(&self) -> bool {
35 self.root.exists()
36 }
37
38 pub fn init(&self) -> std::io::Result<bool> {
40 if self.root.exists() {
41 return Ok(false);
42 }
43 fs::create_dir_all(self.ticks_dir())?;
44 fs::create_dir_all(self.root.join("results").join("receipts"))?;
45 fs::create_dir_all(self.root.join("results").join("state"))?;
46 fs::write(self.head_path(), "")?;
47 fs::write(self.config_path(), DEFAULT_CONFIG)?;
48 Ok(true)
49 }
50
51 pub fn write_tick(&self, t: &Tick) -> std::io::Result<()> {
53 let json = serde_json::to_string_pretty(&full_value(t)).expect("serializable");
54 fs::write(self.ticks_dir().join(&t.id), json)?;
55 fs::write(self.head_path(), &t.id)?;
56 Ok(())
57 }
58
59 pub fn read_head(&self) -> std::io::Result<String> {
61 match std::fs::read_to_string(self.head_path()) {
62 Ok(s) => Ok(s.trim().to_string()),
63 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
64 Err(e) => Err(e),
65 }
66 }
67
68 pub fn read_tick(&self, id: &str) -> std::io::Result<Option<crate::tick::Tick>> {
70 let p = self.ticks_dir().join(id);
71 if !p.is_file() {
72 return Ok(None);
73 }
74 let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&p)?)
75 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
76 crate::tick::from_value(&v)
77 .map(Some)
78 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
79 }
80
81 pub fn read_all(&self) -> std::io::Result<Vec<(String, serde_json::Value)>> {
83 let mut out = Vec::new();
84 for entry in fs::read_dir(self.ticks_dir())? {
85 let p = entry?.path();
86 if p.is_file() {
87 let name = p.file_name().unwrap().to_string_lossy().to_string();
88 let text = fs::read_to_string(&p)?;
89 let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
90 std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{name}: {e}"))
91 })?;
92 out.push((name, v));
93 }
94 }
95 Ok(out)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::tick::{Ground, Tick};
103
104 fn tmp() -> std::path::PathBuf {
105 use std::sync::atomic::{AtomicU64, Ordering};
106 static N: AtomicU64 = AtomicU64::new(0);
107 let p = std::env::temp_dir().join(format!(
108 "ev-store-test-{}-{}",
109 std::process::id(),
110 N.fetch_add(1, Ordering::Relaxed)
111 ));
112 let _ = std::fs::remove_dir_all(&p);
113 std::fs::create_dir_all(&p).unwrap();
114 p
115 }
116
117 fn a_tick(id: &str, parent: &str) -> Tick {
118 Tick {
119 id: id.into(),
120 parent_id: parent.into(),
121 observe: "o".into(),
122 decision: "d".into(),
123 grounds: vec![Ground {
124 claim: "c".into(),
125 supports: "chosen".into(),
126 check: None,
127 }],
128 status: "live".into(),
129 held_since: "".into(),
130 blame: "Wang Yu".into(),
131 }
132 }
133
134 #[test]
135 fn init_should_create_the_full_store_layout_when_the_store_is_new() {
136 let repo = tmp();
138 let s = Store::at(&repo);
139
140 let created = s.init().unwrap();
142
143 assert!(created); assert!(s.ticks_dir().is_dir());
146 assert!(s.head_path().is_file());
147 assert!(s.config_path().is_file());
148 assert!(repo.join(".evolving/results/receipts").is_dir());
149 }
150
151 #[test]
152 fn init_should_be_a_no_op_when_the_store_already_exists() {
153 let repo = tmp();
155 let s = Store::at(&repo);
156 assert!(s.init().unwrap());
157
158 let created_again = s.init().unwrap();
160
161 assert!(!created_again); }
164
165 #[test]
166 fn write_tick_should_persist_the_tick_and_advance_head_when_a_tick_is_written() {
167 let repo = tmp();
169 let s = Store::at(&repo);
170 s.init().unwrap();
171 let t = a_tick("aaaaaaaaaaaa", "");
172
173 s.write_tick(&t).unwrap();
175
176 assert!(s.ticks_dir().join("aaaaaaaaaaaa").is_file());
178 assert_eq!(
179 std::fs::read_to_string(s.head_path()).unwrap(),
180 "aaaaaaaaaaaa"
181 );
182 let all = s.read_all().unwrap();
183 assert_eq!(all.len(), 1);
184 assert_eq!(all[0].0, "aaaaaaaaaaaa");
185 }
186}