create_thumbnail: switch to table-driven tests
- ID
3f0b70b- date
2026-09-24 05:35:22+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
f89b9c8- message
create_thumbnail: switch to table-driven tests- changed files
3 files, 204 additions, 196 deletions
Changed files
src/create_thumbnail.rs (8795 → 7687)
diff --git a/src/create_thumbnail.rs b/src/create_thumbnail.rs
index 04fe0c3..e2eec24 100644
--- a/src/create_thumbnail.rs
+++ b/src/create_thumbnail.rs
@@ -143,121 +143,90 @@ mod test_create_thumbnail {
use crate::dimensions::TargetDimension;
use crate::test_utils::{get_dimensions, test_dir};
- #[test]
- fn creates_an_animated_gif_thumbnail() {
- let gif_path = PathBuf::from("src/tests/animated_squares.gif");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(16);
-
- let thumbnail_path = create_thumbnail(&gif_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("animated_squares.mp4"));
- assert!(thumbnail_path.exists());
- }
-
- #[test]
- fn creates_an_animated_gif_thumbnail_with_odd_width() {
- let gif_path = PathBuf::from("src/tests/animated_squares.gif");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(15);
-
- let thumbnail_path = create_thumbnail(&gif_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("animated_squares.mp4"));
- assert!(thumbnail_path.exists());
- }
-
- #[test]
- fn creates_a_static_gif_thumbnail() {
- let img_path = PathBuf::from("src/tests/yellow.gif");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(16);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("yellow.gif"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (16, 8));
- }
-
- #[test]
- fn creates_a_png_thumbnail() {
- let img_path = PathBuf::from("src/tests/red.png");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(16);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("red.png"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (16, 32));
- }
-
- #[test]
- fn creates_a_jpeg_thumbnail() {
- let img_path = PathBuf::from("src/tests/noise.jpg");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(16);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("noise.jpg"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (16, 32));
- }
-
- #[test]
- fn creates_a_tif_thumbnail() {
- let img_path = PathBuf::from("src/tests/green.tiff");
- let out_dir = test_dir();
- let target = TargetDimension::MaxHeight(16);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("green.tiff"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (16, 16));
- }
-
- #[test]
- fn creates_a_webp_thumbnail() {
- let img_path = PathBuf::from("src/tests/purple.webp");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(16);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("purple.webp"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (16, 16));
- }
-
- #[test]
- fn it_creates_an_equal_size_thumbnail_if_dimension_larger_than_original() {
- let img_path = PathBuf::from("src/tests/noise.jpg");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(500);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("noise.jpg"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (128, 256));
+ macro_rules! image_thumbnail_tests {
+ ($($name:ident: {
+ img_path: $img_path:expr,
+ target: $target:expr,
+ expected_filename: $expected_filename:expr,
+ expected_dims: $expected_dims:expr,
+ },)*)
+ =>
+ {
+ $(
+ #[test]
+ fn $name() {
+ let path = PathBuf::from($img_path);
+ let out_dir = test_dir();
+ let thumbnail_path = create_thumbnail(&path, &out_dir, $target).unwrap();
+
+ assert_eq!(thumbnail_path, out_dir.join($expected_filename));
+ assert!(thumbnail_path.exists());
+ assert_eq!(get_dimensions(&thumbnail_path), $expected_dims);
+ }
+ )*
+ }
}
- #[test]
- fn it_applies_exif_orientation() {
+ image_thumbnail_tests! {
+ animated_gif: {
+ img_path: "src/tests/animated_squares.gif",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "animated_squares.mp4",
+ expected_dims: (16, 16),
+ },
+ // Regression test for "height not divisible by 2" error.
+ animated_gif_with_odd_width: {
+ img_path: "src/tests/animated_squares.gif",
+ target: TargetDimension::MaxWidth(15),
+ expected_filename: "animated_squares.mp4",
+ expected_dims: (16, 16),
+ },
+ static_gif: {
+ img_path: "src/tests/yellow.gif",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "yellow.gif",
+ expected_dims: (16, 8),
+ },
+ png: {
+ img_path: "src/tests/red.png",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "red.png",
+ expected_dims: (16, 32),
+ },
+ jpeg: {
+ img_path: "src/tests/noise.jpg",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "noise.jpg",
+ expected_dims: (16, 32),
+ },
+ tiff: {
+ img_path: "src/tests/green.tiff",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "green.tiff",
+ expected_dims: (16, 16),
+ },
+ webp: {
+ img_path: "src/tests/purple.webp",
+ target: TargetDimension::MaxWidth(16),
+ expected_filename: "purple.webp",
+ expected_dims: (16, 16),
+ },
+ // If the target dimension is larger than the original image,
+ // the thumbnail is created at original size.
+ clamps_to_original_size: {
+ img_path: "src/tests/noise.jpg",
+ target: TargetDimension::MaxWidth(500),
+ expected_filename: "noise.jpg",
+ expected_dims: (128, 256),
+ },
// This source image comes from Dave Perrett's exif-orientation-examples
// repo, and is used under MIT.
// See https://github.com/recurser/exif-orientation-examples
- let img_path = PathBuf::from("src/tests/Landscape_5.jpg");
- let out_dir = test_dir();
- let target = TargetDimension::MaxWidth(180);
-
- let thumbnail_path = create_thumbnail(&img_path, &out_dir, target).unwrap();
-
- assert_eq!(thumbnail_path, out_dir.join("Landscape_5.jpg"));
- assert!(thumbnail_path.exists());
- assert_eq!(get_dimensions(&thumbnail_path), (180, 120));
+ exif_orientation: {
+ img_path: "src/tests/Landscape_5.jpg",
+ target: TargetDimension::MaxWidth(180),
+ expected_filename: "Landscape_5.jpg",
+ expected_dims: (180, 120),
+ },
}
}
src/is_animated_gif.rs (1439 → 1615)
diff --git a/src/is_animated_gif.rs b/src/is_animated_gif.rs
index 011915a..7636a26 100644
--- a/src/is_animated_gif.rs
+++ b/src/is_animated_gif.rs
@@ -24,28 +24,38 @@ mod test_is_animated_gif {
use super::*;
- #[test]
- fn a_png_is_not_an_animated_gif() {
- let p = PathBuf::from("src/tests/blue.png");
- assert_eq!(is_animated_gif(&p).unwrap(), false);
- }
-
- #[test]
- fn a_static_gif_is_not_an_animated_gif() {
- let p = PathBuf::from("src/tests/static.gif");
- assert_eq!(is_animated_gif(&p).unwrap(), false);
+ macro_rules! is_animated_gif_tests {
+ ($($name:ident: {
+ path: $path:expr,
+ is_animated: $is_animated:expr,
+ },)*) => {
+ $(
+ #[test]
+ fn $name() {
+ let p = PathBuf::from($path);
+ assert_eq!(is_animated_gif(&p).unwrap(), $is_animated);
+ }
+ )*
+ }
}
- #[test]
- fn an_animated_gif_is_animated() {
- let p = PathBuf::from("src/tests/animated_squares.gif");
- assert_eq!(is_animated_gif(&p).unwrap(), true);
- }
-
- #[test]
- fn a_non_image_is_not_animated_gif() {
- let p = PathBuf::from("Cargo.toml");
- assert_eq!(is_animated_gif(&p).unwrap(), false);
+ is_animated_gif_tests! {
+ png: {
+ path: "src/tests/blue.png",
+ is_animated: false,
+ },
+ static_gif: {
+ path: "src/tests/static.gif",
+ is_animated: false,
+ },
+ animated_gif: {
+ path: "src/tests/animated_squares.gif",
+ is_animated: true,
+ },
+ non_image: {
+ path: "Cargo.toml",
+ is_animated: false,
+ },
}
#[test]
src/main.rs (7855 → 9026)
diff --git a/src/main.rs b/src/main.rs
index 6a3cdc2..729ee94 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -147,70 +147,52 @@ mod test_cli {
Ok(())
}
- #[test]
- fn it_fails_if_you_pass_neither_width_nor_height() {
- Command::cargo_bin("create_thumbnail")
- .unwrap()
- .args(&["src/tests/red.png", "--out-dir=/tmp"])
- .assert()
- .failure()
- .code(1)
- .stdout("")
- .stderr(
- "Failed to create thumbnail: you must pass at least one of --width or --height\n",
- );
- }
-
- #[test]
- fn it_fails_if_you_pass_a_non_existent_file() {
- Command::cargo_bin("create_thumbnail")
- .unwrap()
- .args(&["doesnotexist.txt", "--width=50", "--out-dir=/tmp"])
- .assert()
- .failure()
- .code(1)
- .stdout("")
- .stderr("Failed to open image: No such file or directory (os error 2)\n");
- }
-
- #[test]
- fn it_fails_if_you_pass_a_non_image() {
- Command::cargo_bin("create_thumbnail")
- .unwrap()
- .args(&["Cargo.toml", "--width=50", "--out-dir=/tmp"])
- .assert()
- .failure()
- .code(1)
- .stdout("")
- .stderr("Failed to open image: The file extension `.\"toml\"` was not recognized as an image format\n");
- }
-
- // TODO: Improve this error message.
- //
- // It's good to know the tool won't completely break when this happens, but ideally
- // we'd return a more meaningful error message in this case.
- #[test]
- fn it_fails_if_out_dir_is_a_file() {
- Command::cargo_bin("create_thumbnail")
- .unwrap()
- .args(&["src/images/noise.jpg", "--width=50", "--out-dir=README.md"])
- .assert()
- .failure()
- .code(1)
- .stdout("")
- .stderr("I/O error: File exists (os error 17)\n");
+ macro_rules! cli_error_tests {
+ ($($name:ident: {
+ args: $args:expr,
+ expect_error: $expect_error:expr,
+ },)*) => {
+ $(
+ #[test]
+ fn $name() {
+ Command::cargo_bin("create_thumbnail")
+ .unwrap()
+ .args($args)
+ .assert()
+ .failure()
+ .code(1)
+ .stdout("")
+ .stderr($expect_error);
+ }
+ )*
+ }
}
- #[test]
- fn it_fails_if_you_try_to_overwrite_the_original_file() {
- Command::cargo_bin("create_thumbnail")
- .unwrap()
- .args(&["src/images/noise.jpg", "--width=50", "--out-dir=src/images"])
- .assert()
- .failure()
- .code(1)
- .stdout("")
- .stderr("Cannot write thumbnail to the same path as the original image\n");
+ cli_error_tests! {
+ no_width_nor_height: {
+ args: &["src/tests/red.png"],
+ expect_error: "Failed to create thumbnail: you must pass at least one of --width or --height\n",
+ },
+ nonexistent_file: {
+ args: &["doesnotexist.png", "--width=50"],
+ expect_error: "Failed to open image: No such file or directory (os error 2)\n",
+ },
+ non_image: {
+ args: &["Cargo.toml", "--width=50"],
+ expect_error: "Failed to open image: The file extension `.\"toml\"` was not recognized as an image format\n",
+ },
+ // TODO: Improve this error message.
+ //
+ // It's good to know the tool won't completely break, but it'd
+ // be nice to return a more meaningful error message in this case.
+ out_dir_is_file: {
+ args: &["src/images/nose.jpg", "--width=50", "--out-dir=README.md"],
+ expect_error: "I/O error: File exists (os error 17)\n",
+ },
+ overwrite_original_file: {
+ args: &["src/images/noise.jpg", "--width=50", "--out-dir=src/images"],
+ expect_error: "Cannot write thumbnail to the same path as the original image\n",
+ },
}
#[test]
@@ -247,6 +229,7 @@ mod test_cli {
#[cfg(test)]
pub mod test_utils {
use std::path::{Path, PathBuf};
+ use std::process::Command;
use image::GenericImageView;
@@ -259,10 +242,56 @@ pub mod test_utils {
tmp_dir.path().to_owned()
}
- /// Return the dimensions for an image.
+ /// Returns the dimensions `(width, height)` of an image or video file.
pub fn get_dimensions(path: impl AsRef<Path>) -> (u32, u32) {
- let img = image::open(path).unwrap();
+ let path = path.as_ref();
+
+ if let Ok(img) = image::open(path) {
+ return img.dimensions();
+ }
+
+ get_video_dimensions(path)
+ }
- img.dimensions()
+ fn get_video_dimensions(path: &Path) -> (u32, u32) {
+ // Use ffprobe to get the video dimensions. This should print
+ // the dimensions as {width}x{height}, for example "1920x1080".
+ let output = Command::new("ffprobe")
+ .args([
+ "-v",
+ "error",
+ "-select_streams",
+ "v:0",
+ "-show_entries",
+ "stream=width,height",
+ "-of",
+ "csv=p=0:s=x",
+ ])
+ .arg(path)
+ .output()
+ .expect("failed to execute ffprobe — ensure ffmpeg/ffprobe is installed");
+
+ assert!(
+ output.status.success(),
+ "ffprobe failed for {:?}: {}",
+ path,
+ String::from_utf8_lossy(&output.stderr)
+ );
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let stdout = stdout.trim();
+
+ let (w_str, h_str) = stdout
+ .split_once('x')
+ .unwrap_or_else(|| panic!("unexpected ffprobe output: '{stdout}' for path {:?}", path));
+
+ let width = w_str
+ .parse::<u32>()
+ .unwrap_or_else(|_| panic!("failed to parse width '{w_str}' from ffprobe"));
+ let height = h_str
+ .parse::<u32>()
+ .unwrap_or_else(|_| panic!("failed to parse height '{h_str}' from ffprobe"));
+
+ (width, height)
}
}