turtle-build 0.5.15

Ninja-compatible build system for high-level programming languages
Documentation
use crate::{
    build_hash::BuildHash,
    infrastructure::{Database, DatabaseError},
    ir::BuildId,
    path_pool::PathPool,
};
use alloc::sync::Arc;
use async_trait::async_trait;
use redb::{
    Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
};
use std::{fs::create_dir_all, path::Path};

const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
    TableDefinition::new("header_dependencies");
const OUTPUTS: TableDefinition<&str, Option<&str>> = TableDefinition::new("outputs");

/// A redb database.
pub struct RedbDatabase {
    database: redb::Database,
    path_pool: Arc<PathPool>,
}

impl RedbDatabase {
    /// Creates a database.
    pub fn new(path: &Path, path_pool: Arc<PathPool>) -> Result<Self, DatabaseError> {
        Ok(Self {
            database: Self::open(path)?,
            path_pool,
        })
    }

    fn open(path: &Path) -> Result<redb::Database, redb::Error> {
        if let Some(directory) = path.parent() {
            create_dir_all(directory)?;
        }

        let database = redb::Database::create(path)?;
        let mut transaction = database.begin_write()?;

        transaction.set_durability(Durability::None)?;
        transaction.open_table(HASHES)?;
        transaction.open_table(HEADER_DEPENDENCIES)?;
        transaction.open_table(OUTPUTS)?;
        transaction.commit()?;

        Ok(database)
    }

    fn read<K: Key + 'static, V: Value + 'static, T>(
        &self,
        table: TableDefinition<K, V>,
        read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
    ) -> Result<T, redb::Error> {
        read(self.database.begin_read()?.open_table(table)?)
    }

    // Writes become durable when the database is closed.
    fn write<K: Key + 'static, V: Value + 'static>(
        &self,
        table: TableDefinition<K, V>,
        key: K::SelfType<'_>,
        value: V::SelfType<'_>,
    ) -> Result<(), redb::Error> {
        let mut transaction = self.database.begin_write()?;

        transaction.set_durability(Durability::None)?;
        transaction.open_table(table)?.insert(key, value)?;
        transaction.commit()?;

        Ok(())
    }
}

#[async_trait]
impl Database for RedbDatabase {
    async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
        Ok(self.read(HASHES, |table| {
            Ok(table
                .get(id.to_bytes())?
                .map(|hash| hash.value())
                .map(|(timestamp, content)| BuildHash::new(timestamp, content)))
        })?)
    }

    async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
        Ok(self.write(HASHES, id.to_bytes(), (hash.timestamp(), hash.content()))?)
    }

    async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
        Ok(self.read(HEADER_DEPENDENCIES, |table| {
            Ok(table
                .get(id.to_bytes())?
                .map(|dependencies| {
                    dependencies
                        .value()
                        .into_iter()
                        .map(|path| self.path_pool.intern(path))
                        .collect()
                })
                .unwrap_or_default())
        })?)
    }

    async fn set_header_inputs(
        &self,
        id: BuildId,
        dependencies: &[Arc<str>],
    ) -> Result<(), DatabaseError> {
        Ok(self.write(
            HEADER_DEPENDENCIES,
            id.to_bytes(),
            dependencies.iter().map(AsRef::as_ref).collect(),
        )?)
    }

    async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
        Ok(self.read(OUTPUTS, |table| {
            table
                .iter()?
                .map(|entry| Ok(entry?.0.value().into()))
                .collect()
        })?)
    }

    async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
        Ok(self.write(OUTPUTS, path, source)?)
    }

    async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
        Ok(self.read(OUTPUTS, |table| {
            Ok(table
                .get(output)?
                .and_then(|source| source.value().map(Into::into)))
        })?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::{TempDir, tempdir};

    const FILENAME: &str = "database";

    fn open() -> (RedbDatabase, TempDir) {
        let directory = tempdir().unwrap();

        (
            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
            directory,
        )
    }

    #[test]
    fn new() {
        open();
    }

    #[test]
    fn new_in_missing_directory() {
        let directory = tempdir().unwrap();

        RedbDatabase::new(
            &directory.path().join("foo").join("bar").join(FILENAME),
            Default::default(),
        )
        .unwrap();
    }

    #[tokio::test]
    async fn hash() {
        let (database, _directory) = open();

        database
            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
            .await
            .unwrap();

        assert_eq!(
            database.get_hash(BuildId::new(0)).await.unwrap(),
            Some(BuildHash::new(1, 2))
        );
    }

    #[tokio::test]
    async fn get_no_hash() {
        let (database, _directory) = open();

        database
            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
            .await
            .unwrap();

        assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
    }

    #[tokio::test]
    async fn update_hash() {
        let (database, _directory) = open();

        database
            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
            .await
            .unwrap();
        database
            .set_hash(BuildId::new(0), BuildHash::new(3, 4))
            .await
            .unwrap();

        assert_eq!(
            database.get_hash(BuildId::new(0)).await.unwrap(),
            Some(BuildHash::new(3, 4))
        );
    }

    #[tokio::test]
    async fn set_output() {
        let (database, _directory) = open();

        database.set_output("foo", None).await.unwrap();
    }

    #[tokio::test]
    async fn get_output() {
        let (database, _directory) = open();

        database.set_output("foo", None).await.unwrap();

        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
    }

    #[tokio::test]
    async fn get_no_output() {
        let (database, _directory) = open();

        assert_eq!(database.get_outputs().await.unwrap(), Vec::<String>::new());
    }

    #[tokio::test]
    async fn get_output_with_source() {
        let (database, _directory) = open();

        database.set_output("foo", Some("bar")).await.unwrap();

        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
    }

    #[tokio::test]
    async fn header_dependencies() {
        let (database, _directory) = open();

        database
            .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
            .await
            .unwrap();

        assert_eq!(
            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
            vec!["foo".into(), "bar".into()]
        );
    }

    #[tokio::test]
    async fn get_no_header_dependencies() {
        let (database, _directory) = open();

        assert_eq!(
            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
            Vec::<Arc<str>>::new()
        );
    }

    #[tokio::test]
    async fn intern_header_dependencies() {
        let directory = tempdir().unwrap();
        let path_pool = Arc::new(PathPool::new());
        let database =
            RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();

        database
            .set_header_inputs(BuildId::new(0), &["foo".into()])
            .await
            .unwrap();

        assert!(Arc::ptr_eq(
            &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
            &path_pool.intern("foo")
        ));
    }

    #[tokio::test]
    async fn get_source() {
        let (database, _directory) = open();

        database.set_output("foo", Some("bar")).await.unwrap();

        assert_eq!(
            database.get_source("foo").await.unwrap(),
            Some("bar".into())
        );
    }

    #[tokio::test]
    async fn get_no_source() {
        let (database, _directory) = open();

        database.set_output("foo", None).await.unwrap();

        assert_eq!(database.get_source("foo").await.unwrap(), None);
    }

    #[tokio::test]
    async fn get_no_source_of_missing_output() {
        let (database, _directory) = open();

        assert_eq!(database.get_source("foo").await.unwrap(), None);
    }

    #[tokio::test]
    async fn update_source() {
        let (database, _directory) = open();

        database.set_output("foo", Some("bar")).await.unwrap();
        database.set_output("foo", Some("baz")).await.unwrap();

        assert_eq!(
            database.get_source("foo").await.unwrap(),
            Some("baz".into())
        );
    }

    #[tokio::test]
    async fn remove_source() {
        let (database, _directory) = open();

        database.set_output("foo", Some("bar")).await.unwrap();
        database.set_output("foo", None).await.unwrap();

        assert_eq!(database.get_source("foo").await.unwrap(), None);
    }

    #[tokio::test]
    async fn reopen() {
        let (database, directory) = open();

        database
            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
            .await
            .unwrap();
        database
            .set_header_inputs(BuildId::new(0), &["foo".into()])
            .await
            .unwrap();
        database.set_output("foo", Some("bar")).await.unwrap();

        drop(database);

        let database =
            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();

        assert_eq!(
            database.get_hash(BuildId::new(0)).await.unwrap(),
            Some(BuildHash::new(1, 2))
        );
        assert_eq!(
            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
            vec!["foo".into()]
        );
        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
        assert_eq!(
            database.get_source("foo").await.unwrap(),
            Some("bar".into())
        );
    }
}