Files
pixstrip/pixstrip-core/tests/executor_tests.rs
lashman b432cc7431 Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir),
JPEG scanline write errors propagated, selective metadata write result returned.

High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio
rejection, FM integration toggle infinite recursion guard, saturating counter
arithmetic in executor.

Medium: PNG compression level passed to oxipng, pct mode updates job_config,
external file loading updates step indicator, CLI undo removes history entries,
watch config write failures reported, fast-copy path reads image dimensions for
rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl),
CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot
fix, generation guards on all preview threads to cancel stale results, default
DPI aligned to 0, watermark text width uses char count not byte length.

Low: binary path escaped in Nautilus extension, file dialog filter aligned with
discovery, reset_wizard clears preset_mode and output_dir.
2026-03-07 19:47:23 +02:00

140 lines
4.5 KiB
Rust

use pixstrip_core::executor::PipelineExecutor;
use pixstrip_core::operations::*;
use pixstrip_core::pipeline::ProcessingJob;
use pixstrip_core::types::*;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
fn create_test_jpeg(path: &std::path::Path) {
let img = image::RgbImage::from_fn(200, 150, |x, y| {
image::Rgb([(x % 256) as u8, (y % 256) as u8, 128])
});
img.save_with_format(path, image::ImageFormat::Jpeg).unwrap();
}
fn setup_test_dir() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let input = dir.path().join("input");
let output = dir.path().join("output");
std::fs::create_dir_all(&input).unwrap();
std::fs::create_dir_all(&output).unwrap();
(dir, input, output)
}
#[test]
fn execute_single_image_resize() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("photo.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("photo.jpg"));
job.resize = Some(ResizeConfig::ByWidth(100));
let executor = PipelineExecutor::new();
let result = executor.execute(&job, |_| {}).unwrap();
assert_eq!(result.succeeded, 1);
assert_eq!(result.failed, 0);
assert!(output.join("photo.jpg").exists());
}
#[test]
fn execute_resize_and_convert() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("photo.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("photo.jpg"));
job.resize = Some(ResizeConfig::ByWidth(100));
job.convert = Some(ConvertConfig::SingleFormat(ImageFormat::WebP));
let executor = PipelineExecutor::new();
let result = executor.execute(&job, |_| {}).unwrap();
assert_eq!(result.succeeded, 1);
assert!(output.join("photo.webp").exists());
}
#[test]
fn execute_multiple_images() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("a.jpg"));
create_test_jpeg(&input.join("b.jpg"));
create_test_jpeg(&input.join("c.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("a.jpg"));
job.add_source(input.join("b.jpg"));
job.add_source(input.join("c.jpg"));
job.resize = Some(ResizeConfig::ByWidth(50));
let executor = PipelineExecutor::new();
let result = executor.execute(&job, |_| {}).unwrap();
assert_eq!(result.succeeded, 3);
assert_eq!(result.failed, 0);
assert_eq!(result.total, 3);
}
#[test]
fn execute_collects_progress() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("photo.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("photo.jpg"));
job.resize = Some(ResizeConfig::ByWidth(100));
let updates = Arc::new(Mutex::new(Vec::new()));
let updates_clone = updates.clone();
let executor = PipelineExecutor::new();
executor.execute(&job, move |update| {
updates_clone.lock().unwrap().push(update);
}).unwrap();
let updates = updates.lock().unwrap();
assert!(!updates.is_empty());
}
#[test]
fn execute_with_cancellation() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("a.jpg"));
create_test_jpeg(&input.join("b.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("a.jpg"));
job.add_source(input.join("b.jpg"));
job.resize = Some(ResizeConfig::ByWidth(100));
let cancel = Arc::new(AtomicBool::new(true)); // cancel immediately
let executor = PipelineExecutor::with_cancel(cancel);
let result = executor.execute(&job, |_| {}).unwrap();
// Cancellation flag should be set
assert!(result.cancelled, "result.cancelled should be true when cancel flag is set");
// Total processed should be less than total sources (at least some skipped)
assert!(
result.succeeded + result.failed <= 2,
"processed count ({}) should not exceed total (2)",
result.succeeded + result.failed
);
}
#[test]
fn batch_result_tracks_sizes() {
let (_dir, input, output) = setup_test_dir();
create_test_jpeg(&input.join("photo.jpg"));
let mut job = ProcessingJob::new(&input, &output);
job.add_source(input.join("photo.jpg"));
job.compress = Some(CompressConfig::Preset(QualityPreset::Low));
let executor = PipelineExecutor::new();
let result = executor.execute(&job, |_| {}).unwrap();
assert!(result.total_input_bytes > 0);
assert!(result.total_output_bytes > 0);
}