1use std::{
2 fmt::Write as _,
3 fs,
4 path::{Path, PathBuf},
5};
6
7use thiserror::Error;
8
9use crate::{Config, DiscoveredFixtureFile, DiscoveryError, discover_fixture_files};
10
11pub const GENERATED_FIXTURES_PATH: &str = ".hblank/generated/fixtures.rs";
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct GenerationResult {
15 pub fixture_files: Vec<DiscoveredFixtureFile>,
16 pub changed: bool,
17}
18
19pub fn refresh_generated_fixtures(
24 project_root: &Path,
25 config: &Config,
26) -> Result<GenerationResult, GenerationError> {
27 let fixture_files = discover_fixture_files(project_root, config)?;
28 let source = generated_source(&fixture_files);
29 let path = project_root.join(GENERATED_FIXTURES_PATH);
30 let changed = fs::read(&path).map_or(true, |current| current != source.as_bytes());
31 if changed {
32 write_atomically(&path, source.as_bytes())?;
33 }
34 Ok(GenerationResult {
35 fixture_files,
36 changed,
37 })
38}
39
40#[must_use]
41pub fn generated_source(fixture_files: &[DiscoveredFixtureFile]) -> String {
42 let mut source = String::from(
43 "// Generated by hblank dev. Do not edit.
44",
45 );
46 for fixture_file in fixture_files {
47 let path = rust_string(&fixture_file.absolute_path);
48 source.push_str(
49 "#[allow(dead_code, unused_imports)]
50",
51 );
52 write!(
53 &mut source,
54 "#[path = {path}]\nmod {};\n",
55 fixture_file.module_name
56 )
57 .expect("writing generated imports to a String cannot fail");
58 }
59 source
60}
61
62fn rust_string(path: &Path) -> String {
63 format!("{:?}", path.to_string_lossy())
64}
65
66fn write_atomically(path: &Path, contents: &[u8]) -> Result<(), GenerationError> {
67 let parent = path
68 .parent()
69 .ok_or_else(|| GenerationError::MissingParent(path.to_path_buf()))?;
70 fs::create_dir_all(parent).map_err(|source| GenerationError::CreateDirectory {
71 path: parent.to_path_buf(),
72 source,
73 })?;
74 let temporary = parent.join(format!(".fixtures.rs.{}.tmp", std::process::id()));
75 fs::write(&temporary, contents).map_err(|source| GenerationError::Write {
76 path: temporary.clone(),
77 source,
78 })?;
79 if let Err(source) = fs::rename(&temporary, path) {
80 let _ = fs::remove_file(&temporary);
81 return Err(GenerationError::Write {
82 path: path.to_path_buf(),
83 source,
84 });
85 }
86 Ok(())
87}
88
89#[derive(Debug, Error)]
90pub enum GenerationError {
91 #[error(transparent)]
92 Discovery(#[from] DiscoveryError),
93 #[error("generated fixture path has no parent: {0}")]
94 MissingParent(PathBuf),
95 #[error("could not create generated source directory {path}: {source}")]
96 CreateDirectory {
97 path: PathBuf,
98 source: std::io::Error,
99 },
100 #[error("could not write generated fixture source {path}: {source}")]
101 Write {
102 path: PathBuf,
103 source: std::io::Error,
104 },
105}