iac_forge/testing/
fixtures.rs1use std::path::Path;
36
37use crate::ir::{IacDataSource, IacProvider, IacResource};
38use crate::sexpr::{FromSExpr, SExpr, SExprError, ToSExpr};
39
40#[derive(Debug)]
42pub enum FixtureError {
43 Io(std::io::Error),
45 Sexpr(SExprError),
47}
48
49impl std::fmt::Display for FixtureError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::Io(e) => write!(f, "fixture io: {e}"),
53 Self::Sexpr(e) => write!(f, "fixture sexpr: {e}"),
54 }
55 }
56}
57
58impl std::error::Error for FixtureError {}
59
60impl From<std::io::Error> for FixtureError {
61 fn from(e: std::io::Error) -> Self {
62 Self::Io(e)
63 }
64}
65
66impl From<SExprError> for FixtureError {
67 fn from(e: SExprError) -> Self {
68 Self::Sexpr(e)
69 }
70}
71
72pub fn load<T: FromSExpr>(path: impl AsRef<Path>) -> Result<T, FixtureError> {
80 let text = std::fs::read_to_string(path)?;
81 load_str(&text)
82}
83
84pub fn load_str<T: FromSExpr>(text: &str) -> Result<T, FixtureError> {
89 let sexpr = SExpr::parse(text)?;
90 Ok(T::from_sexpr(&sexpr)?)
91}
92
93pub fn save<T: ToSExpr>(value: &T, path: impl AsRef<Path>) -> Result<(), FixtureError> {
99 let path = path.as_ref();
100 if let Some(parent) = path.parent() {
101 std::fs::create_dir_all(parent)?;
102 }
103 let mut text = value.to_sexpr().emit();
104 text.push('\n');
105 std::fs::write(path, text)?;
106 Ok(())
107}
108
109pub fn load_resource(path: impl AsRef<Path>) -> Result<IacResource, FixtureError> {
116 load::<IacResource>(path)
117}
118
119pub fn load_resource_str(text: &str) -> Result<IacResource, FixtureError> {
124 load_str::<IacResource>(text)
125}
126
127pub fn save_resource(resource: &IacResource, path: impl AsRef<Path>) -> Result<(), FixtureError> {
132 save(resource, path)
133}
134
135pub fn load_provider(path: impl AsRef<Path>) -> Result<IacProvider, FixtureError> {
140 load::<IacProvider>(path)
141}
142
143pub fn load_provider_str(text: &str) -> Result<IacProvider, FixtureError> {
148 load_str::<IacProvider>(text)
149}
150
151pub fn save_provider(provider: &IacProvider, path: impl AsRef<Path>) -> Result<(), FixtureError> {
156 save(provider, path)
157}
158
159pub fn load_data_source(path: impl AsRef<Path>) -> Result<IacDataSource, FixtureError> {
164 load::<IacDataSource>(path)
165}
166
167pub fn save_data_source(ds: &IacDataSource, path: impl AsRef<Path>) -> Result<(), FixtureError> {
172 save(ds, path)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::testing::{test_provider, test_resource};
179 use tempfile::TempDir;
180
181 #[test]
182 fn round_trip_resource_via_disk() {
183 let original = test_resource("widget");
184 let dir = TempDir::new().unwrap();
185 let path = dir.path().join("fx.sexpr");
186 save_resource(&original, &path).unwrap();
187
188 let loaded = load_resource(&path).unwrap();
189 assert_eq!(loaded.name, original.name);
190 assert_eq!(loaded.attributes, original.attributes);
191 }
192
193 #[test]
194 fn round_trip_resource_via_string() {
195 let original = test_resource("widget");
196 let text = original.to_sexpr().emit();
197 let loaded = load_resource_str(&text).unwrap();
198 assert_eq!(loaded.name, original.name);
199 assert_eq!(loaded.attributes, original.attributes);
200 }
201
202 #[test]
203 fn save_creates_parent_directories() {
204 let original = test_resource("widget");
205 let dir = TempDir::new().unwrap();
206 let nested = dir.path().join("a/b/c/widget.sexpr");
207 save_resource(&original, &nested).unwrap();
208 assert!(nested.exists());
209 }
210
211 #[test]
212 fn saved_file_ends_with_newline() {
213 let original = test_resource("widget");
214 let dir = TempDir::new().unwrap();
215 let path = dir.path().join("fx.sexpr");
216 save_resource(&original, &path).unwrap();
217 let contents = std::fs::read_to_string(&path).unwrap();
218 assert!(contents.ends_with('\n'));
219 }
220
221 #[test]
222 fn load_nonexistent_file_errors() {
223 let err = load_resource("/nonexistent/fixture.sexpr").unwrap_err();
224 assert!(matches!(err, FixtureError::Io(_)));
225 }
226
227 #[test]
228 fn load_malformed_sexpr_errors() {
229 let err = load_resource_str("(resource").unwrap_err();
230 assert!(matches!(err, FixtureError::Sexpr(_)));
231 }
232
233 #[test]
234 fn load_wrong_top_level_shape_errors() {
235 let err = load_resource_str("(unknown-top-level)").unwrap_err();
237 assert!(matches!(err, FixtureError::Sexpr(_)));
238 }
239
240 #[test]
241 fn round_trip_provider_via_disk() {
242 let original = test_provider("acme");
243 let dir = TempDir::new().unwrap();
244 let path = dir.path().join("provider.sexpr");
245 save_provider(&original, &path).unwrap();
246 let loaded = load_provider(&path).unwrap();
247 assert_eq!(loaded.name, original.name);
248 assert_eq!(loaded.platform_config, original.platform_config);
249 }
250
251 #[test]
252 fn round_trip_data_source_via_disk() {
253 use crate::testing::test_data_source;
254 let original = test_data_source("secret");
255 let dir = TempDir::new().unwrap();
256 let path = dir.path().join("ds.sexpr");
257 save_data_source(&original, &path).unwrap();
258 let loaded = load_data_source(&path).unwrap();
259 assert_eq!(loaded.name, original.name);
260 assert_eq!(loaded.attributes, original.attributes);
261 }
262
263 #[test]
264 fn generic_helpers_work_for_any_to_sexpr_type() {
265 let attr = crate::ir::IacAttribute {
267 api_name: "field".into(),
268 canonical_name: "field".into(),
269 ..Default::default()
270 };
271 let dir = TempDir::new().unwrap();
272 let path = dir.path().join("attr.sexpr");
273 save(&attr, &path).unwrap();
274 let loaded: crate::ir::IacAttribute = load(&path).unwrap();
275 assert_eq!(loaded, attr);
276 }
277
278 #[test]
279 fn fixture_error_display() {
280 let err = FixtureError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "x"));
281 assert!(err.to_string().contains("fixture io"));
282 let err2: FixtureError = SExprError::Parse("x".into()).into();
283 assert!(err2.to_string().contains("fixture sexpr"));
284 }
285}