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:
145
pixstrip-core/tests/adjustments_tests.rs
Normal file
145
pixstrip-core/tests/adjustments_tests.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use pixstrip_core::operations::AdjustmentsConfig;
|
||||
use pixstrip_core::operations::adjustments::apply_adjustments;
|
||||
use image::DynamicImage;
|
||||
|
||||
fn noop_config() -> AdjustmentsConfig {
|
||||
AdjustmentsConfig {
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
sharpen: false,
|
||||
grayscale: false,
|
||||
sepia: false,
|
||||
crop_aspect_ratio: None,
|
||||
trim_whitespace: false,
|
||||
canvas_padding: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_default() {
|
||||
assert!(noop_config().is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_brightness() {
|
||||
let mut config = noop_config();
|
||||
config.brightness = 10;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_sharpen() {
|
||||
let mut config = noop_config();
|
||||
config.sharpen = true;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_crop() {
|
||||
let mut config = noop_config();
|
||||
config.crop_aspect_ratio = Some((16.0, 9.0));
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_trim() {
|
||||
let mut config = noop_config();
|
||||
config.trim_whitespace = true;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_padding() {
|
||||
let mut config = noop_config();
|
||||
config.canvas_padding = 20;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_grayscale() {
|
||||
let mut config = noop_config();
|
||||
config.grayscale = true;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop_with_sepia() {
|
||||
let mut config = noop_config();
|
||||
config.sepia = true;
|
||||
assert!(!config.is_noop());
|
||||
}
|
||||
|
||||
// --- crop_to_aspect_ratio edge cases ---
|
||||
|
||||
fn make_test_image(w: u32, h: u32) -> DynamicImage {
|
||||
DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(w, h, image::Rgba([128, 128, 128, 255])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_zero_ratio_returns_original() {
|
||||
let img = make_test_image(100, 100);
|
||||
let config = AdjustmentsConfig {
|
||||
crop_aspect_ratio: Some((0.0, 9.0)),
|
||||
..noop_config()
|
||||
};
|
||||
let result = apply_adjustments(img, &config).unwrap();
|
||||
assert_eq!(result.width(), 100);
|
||||
assert_eq!(result.height(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_zero_height_ratio_returns_original() {
|
||||
let img = make_test_image(100, 100);
|
||||
let config = AdjustmentsConfig {
|
||||
crop_aspect_ratio: Some((16.0, 0.0)),
|
||||
..noop_config()
|
||||
};
|
||||
let result = apply_adjustments(img, &config).unwrap();
|
||||
assert_eq!(result.width(), 100);
|
||||
assert_eq!(result.height(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_square_on_landscape() {
|
||||
let img = make_test_image(200, 100);
|
||||
let config = AdjustmentsConfig {
|
||||
crop_aspect_ratio: Some((1.0, 1.0)),
|
||||
..noop_config()
|
||||
};
|
||||
let result = apply_adjustments(img, &config).unwrap();
|
||||
assert_eq!(result.width(), 100);
|
||||
assert_eq!(result.height(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_16_9_on_square() {
|
||||
let img = make_test_image(100, 100);
|
||||
let config = AdjustmentsConfig {
|
||||
crop_aspect_ratio: Some((16.0, 9.0)),
|
||||
..noop_config()
|
||||
};
|
||||
let result = apply_adjustments(img, &config).unwrap();
|
||||
assert_eq!(result.width(), 100);
|
||||
assert_eq!(result.height(), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_padding_large_value() {
|
||||
let img = make_test_image(10, 10);
|
||||
let config = AdjustmentsConfig {
|
||||
canvas_padding: 500,
|
||||
..noop_config()
|
||||
};
|
||||
let result = apply_adjustments(img, &config).unwrap();
|
||||
assert_eq!(result.width(), 1010);
|
||||
assert_eq!(result.height(), 1010);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noop_config_returns_same_dimensions() {
|
||||
let img = make_test_image(200, 100);
|
||||
let result = apply_adjustments(img, &noop_config()).unwrap();
|
||||
assert_eq!(result.width(), 200);
|
||||
assert_eq!(result.height(), 100);
|
||||
}
|
||||
@@ -112,9 +112,14 @@ fn execute_with_cancellation() {
|
||||
let executor = PipelineExecutor::with_cancel(cancel);
|
||||
let result = executor.execute(&job, |_| {}).unwrap();
|
||||
|
||||
// With immediate cancellation, fewer images should be processed
|
||||
assert!(result.succeeded + result.failed <= 2);
|
||||
assert!(result.cancelled);
|
||||
// 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]
|
||||
|
||||
@@ -42,3 +42,84 @@ fn privacy_mode_strips_gps() {
|
||||
strip_metadata(&input, &output, &MetadataConfig::Privacy).unwrap();
|
||||
assert!(output.exists());
|
||||
}
|
||||
|
||||
fn create_test_png(path: &Path) {
|
||||
let img = image::RgbaImage::from_fn(100, 80, |x, y| {
|
||||
image::Rgba([(x % 256) as u8, (y % 256) as u8, 128, 255])
|
||||
});
|
||||
img.save_with_format(path, image::ImageFormat::Png).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_png_metadata_produces_valid_png() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let input = dir.path().join("test.png");
|
||||
let output = dir.path().join("stripped.png");
|
||||
create_test_png(&input);
|
||||
|
||||
strip_metadata(&input, &output, &MetadataConfig::StripAll).unwrap();
|
||||
assert!(output.exists());
|
||||
// Output must be a valid PNG that can be opened
|
||||
let img = image::open(&output).unwrap();
|
||||
assert_eq!(img.width(), 100);
|
||||
assert_eq!(img.height(), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_png_removes_text_chunks() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let input = dir.path().join("test.png");
|
||||
let output = dir.path().join("stripped.png");
|
||||
create_test_png(&input);
|
||||
|
||||
strip_metadata(&input, &output, &MetadataConfig::StripAll).unwrap();
|
||||
// Read output and verify no tEXt chunks remain
|
||||
let data = std::fs::read(&output).unwrap();
|
||||
let mut pos = 8; // skip PNG signature
|
||||
while pos + 12 <= data.len() {
|
||||
let chunk_len = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
|
||||
let chunk_type = &data[pos+4..pos+8];
|
||||
assert_ne!(chunk_type, b"tEXt", "tEXt chunk should be stripped");
|
||||
assert_ne!(chunk_type, b"iTXt", "iTXt chunk should be stripped");
|
||||
assert_ne!(chunk_type, b"zTXt", "zTXt chunk should be stripped");
|
||||
pos += 12 + chunk_len;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_png_output_smaller_or_equal() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let input = dir.path().join("test.png");
|
||||
let output = dir.path().join("stripped.png");
|
||||
create_test_png(&input);
|
||||
|
||||
let input_size = std::fs::metadata(&input).unwrap().len();
|
||||
strip_metadata(&input, &output, &MetadataConfig::StripAll).unwrap();
|
||||
let output_size = std::fs::metadata(&output).unwrap().len();
|
||||
assert!(output_size <= input_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_jpeg_removes_app1_exif() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let input = dir.path().join("test.jpg");
|
||||
let output = dir.path().join("stripped.jpg");
|
||||
create_test_jpeg(&input);
|
||||
|
||||
strip_metadata(&input, &output, &MetadataConfig::StripAll).unwrap();
|
||||
// Verify no APP1 (0xFFE1) markers remain
|
||||
let data = std::fs::read(&output).unwrap();
|
||||
let mut i = 2; // skip SOI
|
||||
while i + 1 < data.len() {
|
||||
if data[i] != 0xFF { break; }
|
||||
let marker = data[i + 1];
|
||||
if marker == 0xDA { break; } // SOS - rest is image data
|
||||
assert_ne!(marker, 0xE1, "APP1/EXIF marker should be stripped");
|
||||
if i + 3 < data.len() {
|
||||
let len = ((data[i + 2] as usize) << 8) | (data[i + 3] as usize);
|
||||
i += 2 + len;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +91,12 @@ fn rename_config_simple_template() {
|
||||
suffix: String::new(),
|
||||
counter_start: 1,
|
||||
counter_padding: 3,
|
||||
counter_enabled: true,
|
||||
counter_position: 3,
|
||||
template: None,
|
||||
case_mode: 0,
|
||||
replace_spaces: 0,
|
||||
special_chars: 0,
|
||||
regex_find: String::new(),
|
||||
regex_replace: String::new(),
|
||||
};
|
||||
@@ -107,11 +111,101 @@ fn rename_config_with_suffix() {
|
||||
suffix: "_web".into(),
|
||||
counter_start: 1,
|
||||
counter_padding: 2,
|
||||
counter_enabled: true,
|
||||
counter_position: 3,
|
||||
template: None,
|
||||
case_mode: 0,
|
||||
replace_spaces: 0,
|
||||
special_chars: 0,
|
||||
regex_find: String::new(),
|
||||
regex_replace: String::new(),
|
||||
};
|
||||
let result = config.apply_simple("photo", "webp", 5);
|
||||
assert_eq!(result, "photo_web_05.webp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_counter_overflow_saturates() {
|
||||
let config = RenameConfig {
|
||||
prefix: String::new(),
|
||||
suffix: String::new(),
|
||||
counter_start: u32::MAX,
|
||||
counter_padding: 1,
|
||||
counter_enabled: true,
|
||||
counter_position: 3,
|
||||
template: None,
|
||||
case_mode: 0,
|
||||
replace_spaces: 0,
|
||||
special_chars: 0,
|
||||
regex_find: String::new(),
|
||||
regex_replace: String::new(),
|
||||
};
|
||||
// Should not panic - saturating arithmetic
|
||||
let result = config.apply_simple("photo", "jpg", u32::MAX);
|
||||
assert!(result.contains("photo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_config_custom_selective() {
|
||||
let config = MetadataConfig::Custom {
|
||||
strip_gps: true,
|
||||
strip_camera: false,
|
||||
strip_software: true,
|
||||
strip_timestamps: false,
|
||||
strip_copyright: false,
|
||||
};
|
||||
assert!(config.should_strip_gps());
|
||||
assert!(!config.should_strip_camera());
|
||||
assert!(!config.should_strip_copyright());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_config_custom_all_off() {
|
||||
let config = MetadataConfig::Custom {
|
||||
strip_gps: false,
|
||||
strip_camera: false,
|
||||
strip_software: false,
|
||||
strip_timestamps: false,
|
||||
strip_copyright: false,
|
||||
};
|
||||
assert!(!config.should_strip_gps());
|
||||
assert!(!config.should_strip_camera());
|
||||
assert!(!config.should_strip_copyright());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_config_custom_all_on() {
|
||||
let config = MetadataConfig::Custom {
|
||||
strip_gps: true,
|
||||
strip_camera: true,
|
||||
strip_software: true,
|
||||
strip_timestamps: true,
|
||||
strip_copyright: true,
|
||||
};
|
||||
assert!(config.should_strip_gps());
|
||||
assert!(config.should_strip_camera());
|
||||
assert!(config.should_strip_copyright());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watermark_rotation_variants_exist() {
|
||||
let rotations = [
|
||||
WatermarkRotation::Degrees45,
|
||||
WatermarkRotation::DegreesNeg45,
|
||||
WatermarkRotation::Degrees90,
|
||||
WatermarkRotation::Custom(30.0),
|
||||
];
|
||||
assert_eq!(rotations.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotation_auto_orient_variant() {
|
||||
let rotation = Rotation::AutoOrient;
|
||||
assert!(matches!(rotation, Rotation::AutoOrient));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwrite_action_default_is_auto_rename() {
|
||||
let default = OverwriteAction::default();
|
||||
assert!(matches!(default, OverwriteAction::AutoRename));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn preset_serialization_roundtrip() {
|
||||
#[test]
|
||||
fn all_builtin_presets() {
|
||||
let presets = Preset::all_builtins();
|
||||
assert_eq!(presets.len(), 8);
|
||||
assert_eq!(presets.len(), 9);
|
||||
let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect();
|
||||
assert!(names.contains(&"Blog Photos"));
|
||||
assert!(names.contains(&"Social Media"));
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ fn add_and_list_history_entries() {
|
||||
],
|
||||
};
|
||||
|
||||
history.add(entry.clone()).unwrap();
|
||||
history.add(entry.clone(), 50, 30).unwrap();
|
||||
let entries = history.list().unwrap();
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
@@ -236,7 +236,7 @@ fn history_appends_entries() {
|
||||
total_output_bytes: 500,
|
||||
elapsed_ms: 100,
|
||||
output_files: vec![],
|
||||
})
|
||||
}, 50, 30)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ fn clear_history() {
|
||||
total_output_bytes: 500,
|
||||
elapsed_ms: 100,
|
||||
output_files: vec![],
|
||||
})
|
||||
}, 50, 30)
|
||||
.unwrap();
|
||||
|
||||
history.clear().unwrap();
|
||||
|
||||
@@ -81,3 +81,39 @@ fn quality_preset_values() {
|
||||
assert!(QualityPreset::High.jpeg_quality() > QualityPreset::Medium.jpeg_quality());
|
||||
assert!(QualityPreset::Medium.jpeg_quality() > QualityPreset::Low.jpeg_quality());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_zero_height_aspect_ratio() {
|
||||
let dims = Dimensions { width: 1920, height: 0 };
|
||||
assert_eq!(dims.aspect_ratio(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_zero_both_aspect_ratio() {
|
||||
let dims = Dimensions { width: 0, height: 0 };
|
||||
assert_eq!(dims.aspect_ratio(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_fit_within_zero_self_width() {
|
||||
let original = Dimensions { width: 0, height: 600 };
|
||||
let max_box = Dimensions { width: 1200, height: 1200 };
|
||||
let fitted = original.fit_within(max_box, false);
|
||||
assert_eq!(fitted, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_fit_within_zero_self_height() {
|
||||
let original = Dimensions { width: 800, height: 0 };
|
||||
let max_box = Dimensions { width: 1200, height: 1200 };
|
||||
let fitted = original.fit_within(max_box, false);
|
||||
assert_eq!(fitted, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_fit_within_zero_max() {
|
||||
let original = Dimensions { width: 800, height: 600 };
|
||||
let max_box = Dimensions { width: 0, height: 0 };
|
||||
let fitted = original.fit_within(max_box, false);
|
||||
assert_eq!(fitted, original);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ fn watcher_detects_new_image() {
|
||||
watcher.start(&folder, tx).unwrap();
|
||||
|
||||
// Wait for watcher to be ready
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Create an image file
|
||||
let img_path = dir.path().join("new_photo.jpg");
|
||||
@@ -87,7 +87,7 @@ fn watcher_ignores_non_image_files() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
watcher.start(&folder, tx).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Create a non-image file
|
||||
std::fs::write(dir.path().join("readme.txt"), b"text file").unwrap();
|
||||
|
||||
@@ -109,3 +109,137 @@ fn position_bottom_left() {
|
||||
assert_eq!(x, 10);
|
||||
assert_eq!(y, 1020);
|
||||
}
|
||||
|
||||
// --- Margin variation tests ---
|
||||
|
||||
#[test]
|
||||
fn margin_zero_top_left() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::TopLeft,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
0,
|
||||
);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_zero_bottom_right() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::BottomRight,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
0,
|
||||
);
|
||||
assert_eq!(x, 1720); // 1920 - 200
|
||||
assert_eq!(y, 1030); // 1080 - 50
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_margin_top_left() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::TopLeft,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
100,
|
||||
);
|
||||
assert_eq!(x, 100);
|
||||
assert_eq!(y, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_margin_bottom_right() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::BottomRight,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
100,
|
||||
);
|
||||
assert_eq!(x, 1620); // 1920 - 200 - 100
|
||||
assert_eq!(y, 930); // 1080 - 50 - 100
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_does_not_affect_center() {
|
||||
let (x1, y1) = calculate_position(
|
||||
WatermarkPosition::Center,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
0,
|
||||
);
|
||||
let (x2, y2) = calculate_position(
|
||||
WatermarkPosition::Center,
|
||||
Dimensions { width: 1920, height: 1080 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
100,
|
||||
);
|
||||
assert_eq!(x1, x2);
|
||||
assert_eq!(y1, y2);
|
||||
}
|
||||
|
||||
// --- Edge case tests ---
|
||||
|
||||
#[test]
|
||||
fn watermark_larger_than_image() {
|
||||
// Watermark is bigger than the image - should not panic, saturating_sub clamps to 0
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::BottomRight,
|
||||
Dimensions { width: 100, height: 100 },
|
||||
Dimensions { width: 200, height: 200 },
|
||||
10,
|
||||
);
|
||||
// (100 - 200 - 10) saturates to 0
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watermark_exact_image_size() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::Center,
|
||||
Dimensions { width: 200, height: 100 },
|
||||
Dimensions { width: 200, height: 100 },
|
||||
0,
|
||||
);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_size_image() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::Center,
|
||||
Dimensions { width: 0, height: 0 },
|
||||
Dimensions { width: 200, height: 50 },
|
||||
10,
|
||||
);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_exceeds_available_space() {
|
||||
// Margin is huge relative to image size
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::BottomRight,
|
||||
Dimensions { width: 100, height: 100 },
|
||||
Dimensions { width: 50, height: 50 },
|
||||
200,
|
||||
);
|
||||
// saturating_sub: 100 - 50 - 200 = 0
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_pixel_image() {
|
||||
let (x, y) = calculate_position(
|
||||
WatermarkPosition::TopLeft,
|
||||
Dimensions { width: 1, height: 1 },
|
||||
Dimensions { width: 1, height: 1 },
|
||||
0,
|
||||
);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user