diff --git a/Cargo.lock b/Cargo.lock index a07ac5f..8f24364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "aligned-vec" version = "0.5.0" @@ -295,6 +304,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -471,6 +503,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -665,7 +721,9 @@ version = "0.2.0" dependencies = [ "anyhow", "clap", + "env_logger", "image", + "log", "mktemp", "serde", "serde_yml", @@ -691,6 +749,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -858,6 +931,35 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rgb" version = "0.8.50" diff --git a/Cargo.toml b/Cargo.toml index b20d6c3..e8ee543 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ edition = "2024" [dependencies] anyhow = "^1.0" clap = { version = "^4.5", features = ["derive"] } +env_logger = "^0.11.8" image = "^0.25.6" +log = "^0.4.27" serde = { version = "^1.0", features = ["derive"] } serde_yml = "^0.0.12" thiserror = "^2.0" diff --git a/Makefile b/Makefile index 0d082a2..177267f 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ lint: cargo clippy test: - cargo test + RUST_BACKTRACE=1 cargo test test-watch: diff --git a/src/config.rs b/src/config.rs index 7bf7f9b..10157b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,23 @@ +use anyhow::Context; use serde::Deserialize; use std::fs; use std::path::PathBuf; #[derive(Deserialize, Debug, PartialEq)] #[serde(default)] -struct Config { - thumbnail_size: (u32, u32), - view_size: (u32, u32), - output_dir: PathBuf, +pub struct Config { + pub thumbnail_size: (u32, u32), + pub view_size: (u32, u32), + pub output_dir: PathBuf, } impl Config { - fn from_album(path: PathBuf) -> anyhow::Result { - let content = fs::read(path.join("photojawn.conf.yml"))?; - let cfg = serde_yml::from_slice(&content)?; + pub fn from_album(path: PathBuf) -> anyhow::Result { + let config_path = path.join("photojawn.conf.yml"); + let content = fs::read(&config_path) + .with_context(|| format!("Failed to read config from {}", config_path.display()))?; + let cfg = serde_yml::from_slice(&content) + .with_context(|| format!("Failed to parse config from {}", config_path.display()))?; Ok(cfg) } } diff --git a/src/generate.rs b/src/generate.rs index 614234b..7a7624a 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,28 +1,27 @@ mod album_dir; -use album_dir::AlbumDir; +use crate::config::Config; +pub use album_dir::AlbumDir; use std::env; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -const OUTPUT_PATH: &'static str = "site"; - -pub fn generate(root_path: &PathBuf) -> anyhow::Result<()> { +pub fn generate(root_path: &PathBuf) -> anyhow::Result { + let config = Config::from_album(root_path.to_path_buf())?; let orig_path = env::current_dir()?; let album = AlbumDir::try_from(root_path)?; env::set_current_dir(&root_path)?; - generate_images(&album)?; - generate_html(&album)?; + generate_images(&config, &album)?; + generate_html(&config, &album)?; env::set_current_dir(orig_path)?; + Ok(root_path.join(config.output_dir)) +} + +fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> { Ok(()) } -fn generate_images(album: &AlbumDir) -> anyhow::Result<()> { - let output_path = album.path.join(OUTPUT_PATH); - Ok(()) -} - -fn generate_html(album: &AlbumDir) -> anyhow::Result<()> { +fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> { Ok(()) } diff --git a/src/generate/album_dir.rs b/src/generate/album_dir.rs index a4dac5a..df460ea 100644 --- a/src/generate/album_dir.rs +++ b/src/generate/album_dir.rs @@ -35,7 +35,6 @@ impl TryFrom<&PathBuf> for AlbumDir { let entry_path = entry?.path(); if entry_path.is_file() { - println!("Found file: {}", entry_path.display()); if let Some(filename) = entry_path.file_name() { if filename == "description.txt" { description = fs::read_to_string(entry_path)?; @@ -65,8 +64,6 @@ impl TryFrom<&PathBuf> for AlbumDir { } } } else if entry_path.is_dir() { - println!("Found dir: {}", entry_path.display()); - if let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) { if dirname.starts_with("_") { // Likely a templates or static dir diff --git a/src/main.rs b/src/main.rs index 5bcec59..a1cbc4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,8 @@ use photojawn::skel::make_skeleton; use std::path::Path; fn main() -> anyhow::Result<()> { + env_logger::init(); let cli = Cli::parse(); - let album_path = Path::new(&cli.album_path); match cli.subcommand { diff --git a/tests/generate.rs b/tests/generate.rs new file mode 100644 index 0000000..d62a2ce --- /dev/null +++ b/tests/generate.rs @@ -0,0 +1,115 @@ +// TODO: Is this really an intergration test? Or should it go into generate.rs? +// orrrr make a function in our CLI which does everything and test _that_ +use mktemp::Temp; +use photojawn::generate::generate; +use std::collections::VecDeque; +use std::fs; +use std::path::{Path, PathBuf}; + +#[test] +/// Test that the generate function creates a rendered site as we expect it +fn test_generate() { + init(); + let album_path = make_test_album(); + let output_path = generate(&album_path.to_path_buf()).unwrap(); + + check_album(output_path).unwrap(); +} + +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +/// Copies the test album to a tempdir and returns the path to it +fn make_test_album() -> Temp { + let tmpdir = Temp::new_dir().unwrap(); + let source_path = Path::new("resources/test_album"); + + let mut dirs: VecDeque = VecDeque::from([source_path.to_path_buf()]); + while let Some(dir) = dirs.pop_front() { + for entry in dir.read_dir().unwrap() { + let entry_path = entry.unwrap().path(); + let path_in_album = entry_path.strip_prefix(&source_path).unwrap(); + if entry_path.is_dir() { + dirs.push_back(entry_path); + } else { + let dest_path = tmpdir.join(&path_in_album); + fs::create_dir_all(dest_path.parent().unwrap()).unwrap(); + fs::copy(&entry_path, &dest_path).unwrap(); + log::debug!("{} -> {}", entry_path.display(), dest_path.display()); + } + } + } + + tmpdir +} + +/// Does basic sanity checks on an output album +fn check_album(album_dir: PathBuf) -> anyhow::Result<()> { + log::debug!("Checking dir {}", album_dir.display()); + + let mut dirs: VecDeque = VecDeque::from([album_dir]); + while let Some(dir) = dirs.pop_front() { + for entry in dir.read_dir().unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + check_album(path.clone())?; + } + let files: Vec = path + .read_dir() + .unwrap() + .into_iter() + .map(|e| e.unwrap().path()) + .filter(|e| e.is_file()) + .collect(); + + // There should be an index.html + assert!(path.join("index.html").exists()); + + // There should be a cover image + let cover_path = path.join("cover.jpg"); + assert!(&cover_path.exists()); + + // The cover should be equal contents to some other image + let cover_contents = fs::read(&cover_path).unwrap(); + let mut found = false; + for file in &files { + if file != &cover_path { + let file_contents = fs::read(file).unwrap(); + if file_contents == cover_contents { + found = true; + } + } + } + if !found { + panic!( + "cover.jpg in {} does not have a matching file", + path.display() + ); + } + + // There should be a slides dir + let slides_path = path.join("slides"); + assert!(slides_path.is_dir()); + + // For each image in the album (including the cover), in slides there should be a: + // - .html + // - .screen. + // - .thumb. + for file in &files { + for ext in ["html", "screen.jpg", "thumb.jpg"] { + assert!(slides_path.join(file.with_extension(ext)).exists()); + } + } + + // There shouldn't be any .txt or .md files hanging around + for file in &files { + if let Some(ext) = file.extension() { + assert_ne!(ext, "md"); + assert_ne!(ext, "txt"); + } + } + } + } + Ok(()) +}