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\
18staleness_ref = \"live-origin\"\n";
19
20impl Store {
21 pub fn at(repo: &Path) -> Store {
22 Store {
23 root: repo.join(".evolving"),
24 }
25 }
26 pub fn ticks_dir(&self) -> PathBuf {
27 self.root.join("ticks")
28 }
29 pub fn head_path(&self) -> PathBuf {
30 self.root.join("HEAD")
31 }
32 pub fn config_path(&self) -> PathBuf {
33 self.root.join("config")
34 }
35 pub fn exists(&self) -> bool {
36 self.root.exists()
37 }
38
39 pub fn init(&self) -> std::io::Result<bool> {
41 if self.root.exists() {
42 return Ok(false);
43 }
44 fs::create_dir_all(self.ticks_dir())?;
45 fs::create_dir_all(self.root.join("results").join("receipts"))?;
46 fs::create_dir_all(self.root.join("results").join("state"))?;
47 fs::write(self.head_path(), "")?;
48 fs::write(self.config_path(), DEFAULT_CONFIG)?;
49 Ok(true)
50 }
51
52 pub fn write_tick(&self, t: &Tick) -> std::io::Result<()> {
54 let json = serde_json::to_string_pretty(&full_value(t)).expect("serializable");
55 fs::write(self.ticks_dir().join(&t.id), json)?;
56 fs::write(self.head_path(), &t.id)?;
57 Ok(())
58 }
59
60 pub fn read_head(&self) -> std::io::Result<String> {
62 match std::fs::read_to_string(self.head_path()) {
63 Ok(s) => Ok(s.trim().to_string()),
64 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
65 Err(e) => Err(e),
66 }
67 }
68
69 pub fn read_tick(&self, id: &str) -> std::io::Result<Option<crate::tick::Tick>> {
71 let p = self.ticks_dir().join(id);
72 if !p.is_file() {
73 return Ok(None);
74 }
75 let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&p)?)
76 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
77 crate::tick::from_value(&v)
78 .map(Some)
79 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
80 }
81
82 pub fn read_all(&self) -> std::io::Result<Vec<(String, serde_json::Value)>> {
84 let mut out = Vec::new();
85 for entry in fs::read_dir(self.ticks_dir())? {
86 let p = entry?.path();
87 if p.is_file() {
88 let name = p.file_name().unwrap().to_string_lossy().to_string();
89 let text = fs::read_to_string(&p)?;
90 let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
91 std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{name}: {e}"))
92 })?;
93 out.push((name, v));
94 }
95 }
96 Ok(out)
97 }
98
99 pub fn read_origin_sha(&self) -> Option<String> {
101 std::fs::read_to_string(self.root.join("results").join("origin-sha"))
102 .ok()
103 .map(|s| s.trim().to_string())
104 .filter(|s| !s.is_empty())
105 }
106
107 pub fn write_origin_sha(&self, sha: &str) -> std::io::Result<()> {
109 std::fs::write(self.root.join("results").join("origin-sha"), sha)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::tick::{Ground, Tick};
117
118 fn tmp() -> std::path::PathBuf {
119 use std::sync::atomic::{AtomicU64, Ordering};
120 static N: AtomicU64 = AtomicU64::new(0);
121 let p = std::env::temp_dir().join(format!(
122 "ev-store-test-{}-{}",
123 std::process::id(),
124 N.fetch_add(1, Ordering::Relaxed)
125 ));
126 let _ = std::fs::remove_dir_all(&p);
127 std::fs::create_dir_all(&p).unwrap();
128 p
129 }
130
131 fn a_tick(id: &str, parent: &str) -> Tick {
132 Tick {
133 id: id.into(),
134 parent_id: parent.into(),
135 observe: "o".into(),
136 decision: "d".into(),
137 grounds: vec![Ground {
138 claim: "c".into(),
139 supports: "chosen".into(),
140 check: None,
141 }],
142 status: "live".into(),
143 held_since: "".into(),
144 blame: "Wang Yu".into(),
145 authority: None,
146 }
147 }
148
149 #[test]
150 fn init_should_create_the_full_store_layout_when_the_store_is_new() {
151 let repo = tmp();
153 let s = Store::at(&repo);
154
155 let created = s.init().unwrap();
157
158 assert!(created); assert!(s.ticks_dir().is_dir());
161 assert!(s.head_path().is_file());
162 assert!(s.config_path().is_file());
163 assert!(repo.join(".evolving/results/receipts").is_dir());
164 }
165
166 #[test]
167 fn init_should_be_a_no_op_when_the_store_already_exists() {
168 let repo = tmp();
170 let s = Store::at(&repo);
171 assert!(s.init().unwrap());
172
173 let created_again = s.init().unwrap();
175
176 assert!(!created_again); }
179
180 #[test]
181 fn write_tick_should_persist_the_tick_and_advance_head_when_a_tick_is_written() {
182 let repo = tmp();
184 let s = Store::at(&repo);
185 s.init().unwrap();
186 let t = a_tick("aaaaaaaaaaaa", "");
187
188 s.write_tick(&t).unwrap();
190
191 assert!(s.ticks_dir().join("aaaaaaaaaaaa").is_file());
193 assert_eq!(
194 std::fs::read_to_string(s.head_path()).unwrap(),
195 "aaaaaaaaaaaa"
196 );
197 let all = s.read_all().unwrap();
198 assert_eq!(all.len(), 1);
199 assert_eq!(all[0].0, "aaaaaaaaaaaa");
200 }
201
202 #[test]
203 fn read_origin_sha_should_return_the_trimmed_sha_when_the_cache_file_exists() {
204 let repo = tmp();
206 let s = Store::at(&repo);
207 s.init().unwrap();
208 std::fs::write(
209 s.root.join("results").join("origin-sha"),
210 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901\n",
211 )
212 .unwrap();
213
214 let sha = s.read_origin_sha();
216
217 assert_eq!(
219 sha.as_deref(),
220 Some("d308afac1b2c3d4e5f60718293a4b5c6d7e8f901")
221 );
222 }
223
224 #[test]
225 fn read_origin_sha_should_be_none_when_no_cache_file_exists() {
226 let repo = tmp();
228 let s = Store::at(&repo);
229 s.init().unwrap();
230
231 let sha = s.read_origin_sha();
233
234 assert!(sha.is_none());
236 }
237}