From c30562782207068c502bf1677a2e5ea7e894a96e Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Mar 2026 20:04:48 -0300 Subject: [PATCH] Add dev::test module for filesystem tests setup/teardown --- src/dev/test.rs | 135 ++++++++++++++++++++++++++++++++ src/graph.rs | 15 ++-- src/router/handlers/template.rs | 106 ++++++++++++++++++++----- 3 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 src/dev/test.rs diff --git a/src/dev/test.rs b/src/dev/test.rs new file mode 100644 index 0000000..4813531 --- /dev/null +++ b/src/dev/test.rs @@ -0,0 +1,135 @@ +use std::{env, fs, io, path::PathBuf}; + +use crate::prelude::*; + +pub struct Directories { + pub original: PathBuf, + pub templates: PathBuf, + pub test: PathBuf, +} + +impl Directories { + /// Sets up self-cleaning original, temporary and 'templates' directories. + /// + /// # Errors + /// May return Error when: + /// - Current directory does not exist or lacking permissions + /// - Several I/O possibilities from directory creation failures + /// - Several I/O possibilities from working directory changing failures + pub fn setup(dir_name: &str) -> Result { + let original = env::current_dir()?; + let test = original.join(format!("target/mocks/{dir_name}")); + let templates = test.join("templates"); + + drop(fs::remove_dir_all(&test)); + + if let Err(error) = fs::create_dir_all(&test) { + return Err(Error::with_io( + "Failed test's directory creation", + error, + )) + } + + if let Err(error) = fs::create_dir_all(&templates) { + return Err(Error::with_io( + "Failed 'templates' directory creation", + error, + )) + } + + if let Err(error) = env::set_current_dir(&test) { + return Err(Error::with_io("Failed current directory change", error)) + } + + Ok(Directories { + original, + templates, + test, + }) + } +} + +impl Drop for Directories { + fn drop(&mut self) { + if let Err(error) = std::env::set_current_dir(&self.original) { + log!(ERROR, "Couldn't reset to original directory: {error}"); + } + if let Err(error) = std::fs::remove_dir_all(&self.test) { + log!(WARN, "Couldn't cleanup test directory: {error}"); + } + } +} + +#[derive(Debug)] +pub struct Error { + pub message: String, + pub inner_io: Option, + pub inner_tera: Option, +} + +impl Error { + fn with_io(message: &str, inner_io: io::Error) -> Error { + Error { + message: String::from(message), + inner_io: Some(inner_io), + inner_tera: None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut message = self.message.clone(); + + if let Some(inner_io) = &self.inner_io { + message = format!("{message}\n{inner_io}"); + } + + if let Some(inner_tera) = &self.inner_tera { + message = format!("{message}\n{inner_tera}"); + } + + write!(f, "{message}") + } +} + +impl From for Error { + fn from(string: String) -> Error { + Error { + message: string, + inner_io: None, + inner_tera: None, + } + } +} + +impl From<&str> for Error { + fn from(str: &str) -> Error { Error::from(String::from(str)) } +} + +impl From for Error { + fn from(inner: io::Error) -> Error { + let mut error = Error::from(inner.to_string()); + error.inner_io = Some(inner); + error + } +} + +impl From for Error { + fn from(inner: tera::Error) -> Error { + let mut error = Error::from(inner.to_string()); + error.inner_tera = Some(inner); + error + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bad_test_directory_name() { + let dirs = Directories::setup("\0"); + assert!(dirs.is_err()); + } +} diff --git a/src/graph.rs b/src/graph.rs index 7255479..7cdb7b6 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1308,24 +1308,19 @@ mod tests { } #[cfg(test)] +#[expect(clippy::panic_in_result_fn)] mod serial_tests { use super::*; + use crate::dev::test::{Directories, Error}; #[test] - fn bad_graph_path() { - let original_working_directory = std::env::current_dir().unwrap(); - - assert!( - std::env::set_current_dir(std::path::Path::new( - "tests/mocks/no_graph" - )) - .is_ok() - ); + fn bad_graph_path() -> Result<(), Error> { + let _dirs = Directories::setup("bad_graph_path")?; let graph = Graph::load(); let message = graph.meta.messages.first().unwrap(); assert!(message.contains("Failed reading file at")); - assert!(std::env::set_current_dir(original_working_directory).is_ok()); + Ok(()) } } diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 8a375ee..2fd7a83 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -496,33 +496,101 @@ mod tests { } #[cfg(test)] +#[expect(clippy::panic_in_result_fn)] mod serial_tests { + use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _}; + use super::*; + use crate::dev::test::{Directories, Error}; - #[cfg_attr(not(unix), ignore)] #[test] - fn invalid_utf8_template_filename() { - use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::PathBuf}; + #[cfg_attr(not(unix), ignore)] + fn invalid_utf8_template_filename() -> Result<(), Error> { + let dirs = Directories::setup("encoding")?; - let original_working_directory = std::env::current_dir().unwrap(); - let base_dir = PathBuf::from("tests/mocks/encoding/temp"); - let templates_dir = base_dir.clone().join("templates"); - assert!(std::fs::create_dir_all(&base_dir).is_ok()); - assert!( - std::env::set_current_dir(&base_dir) - .is_ok() - ); - assert!(std::fs::create_dir_all(&templates_dir).is_ok()); + let invalid_name = OsStr::from_bytes(&[0xff, 0xfe, 0x80]); + let file_path = dirs.templates.join(invalid_name); + fs::write(file_path, b"")?; + let template_load_result = load_templates(); + let err = template_load_result.err().unwrap(); - let invalid_name = OsStr::from_bytes(&[0xff, 0xfe, 0x00]); - let file_path = templates_dir.join(invalid_name); - assert!(std::fs::write(&file_path, b"eNJq4FPUqSKoozdg").is_ok()); + let error_message = err.to_string(); + assert!(error_message.contains("not valid unicode")); - let result = load_templates(); - assert!(result.is_err()); + Ok(()) + } - assert!(std::fs::remove_dir_all(&base_dir).is_ok()); - assert!(std::env::set_current_dir(original_working_directory).is_ok()); + #[test] + fn custom_template() -> Result<(), Error> { + let dirs = Directories::setup("custom_template")?; + + let file_name = "custom.html"; + let file_path = dirs.templates.join(file_name); + fs::write(file_path, b"")?; + + let engine = load_templates()?; + assert!(engine.get_template_names().any(|t| t == "custom.html")); + + Ok(()) + } + + #[test] + fn custom_template_inheritance_error() -> Result<(), Error> { + let dirs = Directories::setup("custom_template")?; + + let file_name = "custom.html"; + let file_path = dirs.templates.join(file_name); + fs::write(file_path, br#"{% extends "nonexistent.html" %}"#)?; + + let template_load_result = load_templates(); + assert!(template_load_result.is_err()); + + Ok(()) + } + + #[test] + fn inner_template_no_op() -> Result<(), Error> { + let dirs = Directories::setup("inner_template")?; + + let inner_dir = dirs.templates.join("inner"); + fs::create_dir(&inner_dir)?; + let inner_template = inner_dir.join("inner.html"); + fs::write(inner_template, br#"{% extends "nonexistent.html" %}"#)?; + + let engine = load_templates()?; + let default_count = dirs.original.join("templates").read_dir()?.count(); + let template_count = engine.get_template_names().count(); + assert!(template_count == default_count); + + Ok(()) + } + + #[test] + fn templates_dir_not_found_ok() -> Result<(), Error> { + let dirs = Directories::setup("not_found_error")?; + + std::fs::remove_dir_all(&dirs.templates)?; + let template_load_result = load_templates(); + template_load_result?; + + Ok(()) + } + + #[test] + // Unexpected here means any error other than 'not found' + fn templates_dir_unexpected_error() -> Result<(), Error> { + let dirs = Directories::setup("unexpected_error")?; + + log!(DEBUG, "Working directory is {:?}", std::env::current_dir()); + + std::fs::remove_dir_all(&dirs.templates)?; + let templates = dirs.test.join("templates"); + fs::write(&templates, b"")?; + + let template_load_result = load_templates(); + assert!(template_load_result.is_err()); + + Ok(()) } }