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 jurisdiction: None,
147 source_ref: None,
148 provenance: None,
149 }
150 }
151
152 #[test]
153 fn init_should_create_the_full_store_layout_when_the_store_is_new() {
154 let repo = tmp();
156 let s = Store::at(&repo);
157
158 let created = s.init().unwrap();
160
161 assert!(created); assert!(s.ticks_dir().is_dir());
164 assert!(s.head_path().is_file());
165 assert!(s.config_path().is_file());
166 assert!(repo.join(".evolving/results/receipts").is_dir());
167 }
168
169 #[test]
170 fn init_should_be_a_no_op_when_the_store_already_exists() {
171 let repo = tmp();
173 let s = Store::at(&repo);
174 assert!(s.init().unwrap());
175
176 let created_again = s.init().unwrap();
178
179 assert!(!created_again); }
182
183 #[test]
184 fn write_tick_should_persist_the_tick_and_advance_head_when_a_tick_is_written() {
185 let repo = tmp();
187 let s = Store::at(&repo);
188 s.init().unwrap();
189 let t = a_tick("aaaaaaaaaaaa", "");
190
191 s.write_tick(&t).unwrap();
193
194 assert!(s.ticks_dir().join("aaaaaaaaaaaa").is_file());
196 assert_eq!(
197 std::fs::read_to_string(s.head_path()).unwrap(),
198 "aaaaaaaaaaaa"
199 );
200 let all = s.read_all().unwrap();
201 assert_eq!(all.len(), 1);
202 assert_eq!(all[0].0, "aaaaaaaaaaaa");
203 }
204
205 #[test]
206 fn read_origin_sha_should_return_the_trimmed_sha_when_the_cache_file_exists() {
207 let repo = tmp();
209 let s = Store::at(&repo);
210 s.init().unwrap();
211 std::fs::write(
212 s.root.join("results").join("origin-sha"),
213 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901\n",
214 )
215 .unwrap();
216
217 let sha = s.read_origin_sha();
219
220 assert_eq!(
222 sha.as_deref(),
223 Some("d308afac1b2c3d4e5f60718293a4b5c6d7e8f901")
224 );
225 }
226
227 #[test]
228 fn read_origin_sha_should_be_none_when_no_cache_file_exists() {
229 let repo = tmp();
231 let s = Store::at(&repo);
232 s.init().unwrap();
233
234 let sha = s.read_origin_sha();
236
237 assert!(sha.is_none());
239 }
240}