Skip to main content

turtle_build/infrastructure/database/
log.rs

1mod hash;
2mod header_input;
3mod output;
4mod utility;
5
6use self::{hash::HashLog, header_input::HeaderInputLog, output::OutputLog};
7use crate::{
8    build_hash::BuildHash,
9    infrastructure::{Database, DatabaseError},
10    ir::BuildId,
11    path_pool::PathPool,
12};
13use alloc::sync::Arc;
14use async_trait::async_trait;
15use std::{fs::create_dir_all, path::Path};
16use tokio::try_join;
17
18const HASH_FILENAME: &str = "hashes";
19const HEADER_INPUT_DIRECTORY: &str = "header_inputs";
20const OUTPUT_FILENAME: &str = "outputs";
21
22/// A log database.
23pub struct LogDatabase {
24    hash_log: HashLog,
25    header_input_log: HeaderInputLog,
26    output_log: OutputLog,
27}
28
29impl LogDatabase {
30    /// Creates a database.
31    pub async fn new(directory: &Path, path_pool: &PathPool) -> Result<Self, DatabaseError> {
32        create_dir_all(directory)?;
33
34        let hash_path = directory.join(HASH_FILENAME);
35        let header_input_path = directory.join(HEADER_INPUT_DIRECTORY);
36        let output_path = directory.join(OUTPUT_FILENAME);
37        let (hash_log, header_input_log, output_log) = try_join!(
38            HashLog::new(&hash_path),
39            HeaderInputLog::new(&header_input_path, path_pool),
40            OutputLog::new(&output_path)
41        )?;
42
43        Ok(Self {
44            hash_log,
45            header_input_log,
46            output_log,
47        })
48    }
49}
50
51#[async_trait]
52impl Database for LogDatabase {
53    async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
54        Ok(self.hash_log.get(id))
55    }
56
57    async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
58        self.hash_log.set(id, hash).await
59    }
60
61    async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
62        Ok(self.header_input_log.get(id))
63    }
64
65    async fn set_header_inputs(
66        &self,
67        id: BuildId,
68        inputs: &[Arc<str>],
69    ) -> Result<(), DatabaseError> {
70        self.header_input_log.set(id, inputs).await
71    }
72
73    async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
74        Ok(self.output_log.get())
75    }
76
77    async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
78        self.output_log.set(path, source).await
79    }
80
81    async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
82        Ok(self.output_log.get_source(output))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use pretty_assertions::assert_eq;
90    use std::fs::{exists, write};
91    use tempfile::{TempDir, tempdir};
92
93    async fn open() -> (LogDatabase, TempDir) {
94        let directory = tempdir().unwrap();
95
96        (reopen(&directory).await, directory)
97    }
98
99    async fn reopen(directory: &TempDir) -> LogDatabase {
100        LogDatabase::new(directory.path(), &Default::default())
101            .await
102            .unwrap()
103    }
104
105    #[tokio::test]
106    async fn new() {
107        let (_database, directory) = open().await;
108
109        assert!(exists(directory.path().join("hashes")).unwrap());
110        assert!(exists(directory.path().join("header_inputs").join("paths")).unwrap());
111        assert!(exists(directory.path().join("header_inputs").join("indices")).unwrap());
112        assert!(exists(directory.path().join("outputs")).unwrap());
113    }
114
115    #[tokio::test]
116    async fn new_in_missing_directory() {
117        let directory = tempdir().unwrap();
118
119        LogDatabase::new(
120            &directory.path().join("foo").join("bar"),
121            &Default::default(),
122        )
123        .await
124        .unwrap();
125    }
126
127    #[tokio::test]
128    async fn hash() {
129        let (database, _directory) = open().await;
130
131        database
132            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
133            .await
134            .unwrap();
135
136        assert_eq!(
137            database.get_hash(BuildId::new(0)).await.unwrap(),
138            Some(BuildHash::new(1, 2))
139        );
140    }
141
142    #[tokio::test]
143    async fn get_no_hash() {
144        let (database, _directory) = open().await;
145
146        database
147            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
148            .await
149            .unwrap();
150
151        assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
152    }
153
154    #[tokio::test]
155    async fn header_inputs() {
156        let (database, _directory) = open().await;
157
158        database
159            .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
160            .await
161            .unwrap();
162
163        assert_eq!(
164            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
165            vec!["foo".into(), "bar".into()]
166        );
167    }
168
169    #[tokio::test]
170    async fn get_no_header_inputs() {
171        let (database, _directory) = open().await;
172
173        database
174            .set_header_inputs(BuildId::new(0), &["foo".into()])
175            .await
176            .unwrap();
177
178        assert_eq!(
179            database.get_header_inputs(BuildId::new(1)).await.unwrap(),
180            Vec::<Arc<str>>::new()
181        );
182    }
183
184    #[tokio::test]
185    async fn intern_header_inputs() {
186        let (database, directory) = open().await;
187
188        database
189            .set_header_inputs(BuildId::new(0), &["foo".into()])
190            .await
191            .unwrap();
192
193        drop(database);
194
195        let path_pool = PathPool::new();
196        let database = LogDatabase::new(directory.path(), &path_pool)
197            .await
198            .unwrap();
199
200        assert!(Arc::ptr_eq(
201            &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
202            &path_pool.intern("foo")
203        ));
204    }
205
206    #[tokio::test]
207    async fn set_output() {
208        let (database, _directory) = open().await;
209
210        database.set_output("foo", None).await.unwrap();
211
212        assert_eq!(database.get_outputs().await.unwrap(), ["foo"]);
213    }
214
215    #[tokio::test]
216    async fn get_no_output() {
217        let (database, _directory) = open().await;
218
219        assert_eq!(database.get_outputs().await.unwrap(), Vec::<String>::new());
220    }
221
222    #[tokio::test]
223    async fn get_output_with_source() {
224        let (database, _directory) = open().await;
225
226        database.set_output("foo", Some("bar")).await.unwrap();
227
228        assert_eq!(database.get_outputs().await.unwrap(), ["foo"]);
229    }
230
231    #[tokio::test]
232    async fn get_source() {
233        let (database, _directory) = open().await;
234
235        database.set_output("foo", Some("bar")).await.unwrap();
236
237        assert_eq!(
238            database.get_source("foo").await.unwrap(),
239            Some("bar".into())
240        );
241    }
242
243    #[tokio::test]
244    async fn get_no_source() {
245        let (database, _directory) = open().await;
246
247        database.set_output("foo", Some("bar")).await.unwrap();
248
249        assert_eq!(database.get_source("baz").await.unwrap(), None);
250    }
251
252    #[tokio::test]
253    async fn reopen_database() {
254        let (database, directory) = open().await;
255
256        database
257            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
258            .await
259            .unwrap();
260        database
261            .set_header_inputs(BuildId::new(0), &["foo".into()])
262            .await
263            .unwrap();
264        database.set_output("foo", Some("bar")).await.unwrap();
265
266        drop(database);
267
268        let database = reopen(&directory).await;
269
270        assert_eq!(
271            database.get_hash(BuildId::new(0)).await.unwrap(),
272            Some(BuildHash::new(1, 2))
273        );
274        assert_eq!(
275            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
276            vec!["foo".into()]
277        );
278        assert_eq!(database.get_outputs().await.unwrap(), ["foo"]);
279        assert_eq!(
280            database.get_source("foo").await.unwrap(),
281            Some("bar".into())
282        );
283    }
284
285    #[tokio::test]
286    async fn fail_to_open_invalid_log() {
287        let directory = tempdir().unwrap();
288
289        write(directory.path().join("outputs"), [0xff, b'\n']).unwrap();
290
291        assert!(
292            LogDatabase::new(directory.path(), &Default::default())
293                .await
294                .is_err()
295        );
296    }
297}