Skip to main content

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)
     }
 }