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.
This commit is contained in:
2026-03-07 19:47:23 +02:00
parent 270a7db60d
commit b432cc7431
44 changed files with 5748 additions and 2221 deletions

View File

@@ -1,4 +1,7 @@
use pixstrip_core::operations::rename::{apply_template, resolve_collision};
use pixstrip_core::operations::rename::{
apply_template, apply_regex_replace, apply_space_replacement,
apply_special_chars, apply_case_conversion, resolve_collision,
};
#[test]
fn template_basic_variables() {
@@ -93,3 +96,185 @@ fn no_collision_returns_same() {
let resolved = resolve_collision(&path);
assert_eq!(resolved, path);
}
// --- Regex replace tests ---
#[test]
fn regex_replace_basic() {
let result = apply_regex_replace("hello_world", "_", "-");
assert_eq!(result, "hello-world");
}
#[test]
fn regex_replace_pattern() {
let result = apply_regex_replace("IMG_20260307_001", r"\d{8}", "DATE");
assert_eq!(result, "IMG_DATE_001");
}
#[test]
fn regex_replace_invalid_pattern_returns_original() {
let result = apply_regex_replace("hello", "[invalid", "x");
assert_eq!(result, "hello");
}
#[test]
fn regex_replace_empty_find_returns_original() {
let result = apply_regex_replace("hello", "", "x");
assert_eq!(result, "hello");
}
// --- Space replacement tests ---
#[test]
fn space_replacement_none() {
assert_eq!(apply_space_replacement("hello world", 0), "hello world");
}
#[test]
fn space_replacement_underscore() {
assert_eq!(apply_space_replacement("hello world", 1), "hello_world");
}
#[test]
fn space_replacement_hyphen() {
assert_eq!(apply_space_replacement("hello world", 2), "hello-world");
}
#[test]
fn space_replacement_dot() {
assert_eq!(apply_space_replacement("hello world", 3), "hello.world");
}
#[test]
fn space_replacement_camelcase() {
assert_eq!(apply_space_replacement("hello world", 4), "helloWorld");
}
#[test]
fn space_replacement_remove() {
assert_eq!(apply_space_replacement("hello world", 5), "helloworld");
}
// --- Special chars tests ---
#[test]
fn special_chars_keep_all() {
assert_eq!(apply_special_chars("file<name>.txt", 0), "file<name>.txt");
}
#[test]
fn special_chars_filesystem_safe() {
assert_eq!(apply_special_chars("file<name>", 1), "filename");
}
#[test]
fn special_chars_web_safe() {
assert_eq!(apply_special_chars("file name!@#", 2), "filename");
}
#[test]
fn special_chars_alphanumeric_only() {
assert_eq!(apply_special_chars("file-name_123", 5), "filename123");
}
// --- Case conversion tests ---
#[test]
fn case_conversion_none() {
assert_eq!(apply_case_conversion("Hello World", 0), "Hello World");
}
#[test]
fn case_conversion_lowercase() {
assert_eq!(apply_case_conversion("Hello World", 1), "hello world");
}
#[test]
fn case_conversion_uppercase() {
assert_eq!(apply_case_conversion("Hello World", 2), "HELLO WORLD");
}
#[test]
fn case_conversion_title_case() {
assert_eq!(apply_case_conversion("hello world", 3), "Hello World");
}
#[test]
fn case_conversion_title_preserves_separators() {
assert_eq!(apply_case_conversion("hello-world_foo", 3), "Hello-World_Foo");
}
// --- RenameConfig::apply_simple edge cases ---
use pixstrip_core::operations::RenameConfig;
fn default_rename_config() -> RenameConfig {
RenameConfig {
prefix: String::new(),
suffix: String::new(),
counter_start: 1,
counter_padding: 3,
counter_enabled: false,
counter_position: 3,
template: None,
case_mode: 0,
replace_spaces: 0,
special_chars: 0,
regex_find: String::new(),
regex_replace: String::new(),
}
}
#[test]
fn apply_simple_no_changes() {
let cfg = default_rename_config();
assert_eq!(cfg.apply_simple("photo", "jpg", 1), "photo.jpg");
}
#[test]
fn apply_simple_with_prefix_suffix() {
let mut cfg = default_rename_config();
cfg.prefix = "web_".into();
cfg.suffix = "_final".into();
assert_eq!(cfg.apply_simple("photo", "jpg", 1), "web_photo_final.jpg");
}
#[test]
fn apply_simple_counter_after_suffix() {
let mut cfg = default_rename_config();
cfg.counter_enabled = true;
cfg.counter_position = 3;
assert_eq!(cfg.apply_simple("photo", "jpg", 1), "photo_001.jpg");
}
#[test]
fn apply_simple_counter_replaces_name() {
let mut cfg = default_rename_config();
cfg.counter_enabled = true;
cfg.counter_position = 4;
assert_eq!(cfg.apply_simple("photo", "jpg", 1), "001.jpg");
}
#[test]
fn apply_simple_empty_name() {
let cfg = default_rename_config();
assert_eq!(cfg.apply_simple("", "png", 1), ".png");
}
#[test]
fn apply_simple_case_lowercase() {
let mut cfg = default_rename_config();
cfg.case_mode = 1;
assert_eq!(cfg.apply_simple("MyPhoto", "JPG", 1), "myphoto.JPG");
}
#[test]
fn apply_simple_large_counter_padding_capped() {
let mut cfg = default_rename_config();
cfg.counter_enabled = true;
cfg.counter_padding = 100; // should be capped to 10
cfg.counter_position = 3;
let result = cfg.apply_simple("photo", "jpg", 1);
// Counter portion should be at most 10 digits
assert!(result.len() <= 22); // "photo_" + 10 digits + ".jpg"
}