Skip to main content

spec_driven_docs/release/
fixture.rs

1//! A directory served as a bundle, for tests.
2//!
3//! The seam's third implementation exists so a test can describe a release
4//! the binary does not carry without reaching a registry. It is the only
5//! bundle whose bytes nobody verified, so nothing outside a test builds
6//! one.
7
8use std::collections::BTreeMap;
9
10use camino::Utf8Path;
11
12use crate::domain::ownership::Sha256;
13use crate::error::AppError;
14use crate::release::{
15    Provenance, ReleaseBundle, ReleaseManifest, ReleaseResolver, ResolvedRelease, Selector,
16    Version, blob_from, manifest_from,
17};
18
19/// A release read from a directory tree.
20#[derive(Debug, Clone)]
21pub struct FixtureReleaseBundle {
22    version: Version,
23    payload_schema: u32,
24    files: BTreeMap<String, Vec<u8>>,
25}
26
27impl FixtureReleaseBundle {
28    /// Read every file under `root` as one release.
29    ///
30    /// # Errors
31    ///
32    /// Any I/O error walking or reading the directory.
33    pub fn read(root: &Utf8Path, version: Version, payload_schema: u32) -> Result<Self, AppError> {
34        let mut files = BTreeMap::new();
35        for entry in walkdir::WalkDir::new(root)
36            .into_iter()
37            .filter_map(Result::ok)
38        {
39            if !entry.file_type().is_file() {
40                continue;
41            }
42            let Ok(path) = camino::Utf8PathBuf::from_path_buf(entry.path().to_path_buf()) else {
43                continue;
44            };
45            let Ok(relative) = path.strip_prefix(root) else {
46                continue;
47            };
48            files.insert(relative.to_string(), std::fs::read(&path)?);
49        }
50        Ok(Self {
51            version,
52            payload_schema,
53            files,
54        })
55    }
56
57    /// A release built from paths and bytes given directly.
58    #[must_use]
59    pub const fn of(
60        version: Version,
61        payload_schema: u32,
62        files: BTreeMap<String, Vec<u8>>,
63    ) -> Self {
64        Self {
65            version,
66            payload_schema,
67            files,
68        }
69    }
70}
71
72impl ReleaseBundle for FixtureReleaseBundle {
73    fn manifest(&self) -> Result<ReleaseManifest, AppError> {
74        Ok(manifest_from(
75            self.version.clone(),
76            self.payload_schema,
77            Provenance::Native,
78            None,
79            &self.files,
80            &BTreeMap::new(),
81        ))
82    }
83
84    fn blob(&self, digest: &Sha256) -> Result<Vec<u8>, AppError> {
85        blob_from(&self.files, &BTreeMap::new(), digest)
86    }
87}
88
89impl ReleaseResolver for FixtureReleaseBundle {
90    fn resolve(&self, selector: &Selector) -> Result<ResolvedRelease, AppError> {
91        let manifest = self.manifest()?;
92        Ok(ResolvedRelease {
93            selector: selector.clone(),
94            version: self.version.clone(),
95            registry_checksum: None,
96            payload_sha256: manifest.payload_sha256,
97            yanked: false,
98            bundle: Box::new(self.clone()),
99        })
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    #![allow(
106        clippy::unwrap_used,
107        reason = "a test panics as its failure signal, not as control flow"
108    )]
109
110    use super::*;
111
112    #[test]
113    fn the_fixture_source_serves_a_directory() {
114        let dir = tempfile::tempdir().unwrap();
115        let root = camino::Utf8PathBuf::from(dir.path().to_str().unwrap());
116        crate::adapters::fs::write_file(&root.join("method/one.md"), b"one\n").unwrap();
117        crate::adapters::fs::write_file(&root.join("templates/two.md"), b"two\n").unwrap();
118
119        let version: Version = "0.1.0".parse().unwrap();
120        let bundle = FixtureReleaseBundle::read(&root, version.clone(), 1).unwrap();
121        let manifest = bundle.manifest().unwrap();
122        let paths: Vec<&str> = manifest
123            .artifacts
124            .iter()
125            .map(|artifact| artifact.path.as_str())
126            .collect();
127        assert_eq!(paths, ["method/one.md", "templates/two.md"]);
128        assert_eq!(bundle.artifact("method/one.md").unwrap(), b"one\n");
129        assert!(bundle.artifact("absent.md").is_err());
130
131        let resolved = bundle.resolve(&Selector::Exact(version.clone())).unwrap();
132        assert_eq!(resolved.version, version);
133        assert_eq!(resolved.payload_sha256, manifest.payload_sha256);
134    }
135}