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.
140 lines
4.5 KiB
Rust
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);
|
|
}
|