mirror of
https://github.com/brockar/medars.git
synced 2026-01-11 15:01:00 -03:00
test: init
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -626,6 +626,12 @@ dependencies = [
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
@@ -1081,6 +1087,7 @@ dependencies = [
|
||||
"rexiv2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1790,6 +1797,19 @@ version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
||||
@@ -30,3 +30,10 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
dirs = "6.0.0"
|
||||
glob = "0.3.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14.0"
|
||||
|
||||
[lib]
|
||||
name = "medars"
|
||||
path = "src/lib.rs"
|
||||
|
||||
|
||||
70
README.md
70
README.md
@@ -4,48 +4,66 @@
|
||||
|
||||
---
|
||||
|
||||
## WIP
|
||||
|
||||
## Features
|
||||
|
||||
- **View metadata**: Display metadata in human-readable table or JSON format
|
||||
- **Remove metadata**: Clean images by removing all embedded metadata
|
||||
- **Interactive TUI**: Terminal user interface for easy navigation
|
||||
- **Check Metadata**: Check if an image contains metadata.
|
||||
- **View Metadata**: Display metadata in a human-readable table or JSON format.
|
||||
- **Remove Metadata**: Clean images by removing all embedded metadata.
|
||||
- **Interactive TUI**: Terminal user interface for easy navigation and image preview.
|
||||
- **Log Actions**: Keep a log of all operations performed.
|
||||
|
||||
## Core Functionality
|
||||
|
||||
**CLI mode:**
|
||||
### CLI mode
|
||||
|
||||
- Show metadata:
|
||||
- **Check for metadata:**
|
||||
|
||||
```bash
|
||||
medars check image.jpg
|
||||
```
|
||||
|
||||
- **Show metadata:**
|
||||
|
||||
```bash
|
||||
medars show image.jpg
|
||||
```
|
||||
|
||||
- Remove metadata:
|
||||
- **Clean metadata:**
|
||||
|
||||
```bash
|
||||
medars clean image.jpg
|
||||
```
|
||||
|
||||
- Batch operations:
|
||||
- **Launch the TUI:**
|
||||
|
||||
```bash
|
||||
medars clean *.jpg
|
||||
medars tui
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
medars tui <path/to/directory>
|
||||
```
|
||||
|
||||
- **Batch operations:**
|
||||
|
||||
```bash
|
||||
medars clean "*.jpg"
|
||||
medars clean path1.jpg path2.png
|
||||
```
|
||||
|
||||
- Flags:
|
||||
- `--copy` → Save as new file.
|
||||
- `--dry-run` → Show what will be removed.
|
||||
- **Flags:**
|
||||
- `--copy [PATH]` → Save as a new file. If `PATH` is not provided, it will be saved with a `_medars` suffix.
|
||||
- `--dry-run` → Show what will be removed without modifying the file.
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
MEDARS helps protect your privacy by:
|
||||
|
||||
- Removing potentially sensitive EXIF data (GPS coordinates, camera settings, timestamps)
|
||||
- Working locally - no data sent to external services
|
||||
- Preserving image quality while removing metadata
|
||||
- Removing potentially sensitive EXIF data (GPS coordinates, camera settings, timestamps).
|
||||
- Working locally - no data is sent to external services.
|
||||
- Preserving image quality while removing metadata.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -63,8 +81,21 @@ On Arch:
|
||||
yay -S libgexiv2
|
||||
```
|
||||
|
||||
If you see an error about `gexiv2.pc` or `gexiv2` not found, make sure the
|
||||
library is installed.
|
||||
If you see an error about `gexiv2.pc` or `gexiv2` not found, make sure the library is installed.
|
||||
|
||||
## Installation
|
||||
|
||||
### From Crates.io (once published)
|
||||
|
||||
```sh
|
||||
cargo install medars
|
||||
```
|
||||
|
||||
### From Git Repository
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/your-username/medars.git
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -73,5 +104,6 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
## Acknowledgments
|
||||
|
||||
- Built with [Rust](https://www.rust-lang.org/).
|
||||
- Uses [exif](https://crates.io/crates/exif) for metadata reading.
|
||||
- Uses [rexiv2](https://crates.io/crates/rexiv2) and [kamadak-exif](https://crates.io/crates/kamadak-exif) for metadata handling.
|
||||
- CLI powered by [clap](https://crates.io/crates/clap).
|
||||
- Terminal UI powered by [ratatui](https://crates.io/crates/ratatui).
|
||||
|
||||
255
TESTS.md
Normal file
255
TESTS.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# MEDARS Test Suite
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive test suite for the MEDARS metadata inspection and removal tool.
|
||||
|
||||
## Test Statistics
|
||||
|
||||
- **Total Tests**: 49 tests
|
||||
- **Unit Tests**: 16 tests (8 in lib.rs, 8 in main.rs)
|
||||
- **Integration Tests**: 13 tests
|
||||
- **Logger Tests**: 11 tests
|
||||
- **Metadata Tests**: 12 tests
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Unit Tests (`src/`)
|
||||
|
||||
Located inline in source files:
|
||||
|
||||
#### Logger Module (`src/logger.rs`)
|
||||
|
||||
- `test_log_entry_creation_with_all_fields` - Validates LogEntry struct creation
|
||||
- `test_log_entry_serialization_deserialization` - Tests JSON serialization
|
||||
- `test_logger_path_creation` - Verifies logger path initialization
|
||||
|
||||
#### Metadata Module (`src/metadata.rs`)
|
||||
|
||||
- `test_metadata_handler_creation` - Tests MetadataHandler instantiation
|
||||
- `test_extract_metadata_nonexistent_file` - Handles missing files gracefully
|
||||
- `test_has_metadata_with_invalid_path` - Error handling for invalid paths
|
||||
- `test_display_metadata_formats` - Validates format string handling
|
||||
- `test_remove_metadata_same_input_output` - Prevents overwriting source files
|
||||
|
||||
### 2. Integration Tests (`tests/integration_tests.rs`)
|
||||
|
||||
End-to-end CLI testing:
|
||||
|
||||
#### Command Tests
|
||||
|
||||
- `test_cli_check_command` - Tests metadata checking
|
||||
- `test_cli_show_command` - Validates metadata display
|
||||
- `test_cli_show_json_format` - JSON output format
|
||||
- `test_cli_clean_command` - Metadata removal (with rexiv2 fallback)
|
||||
- `test_cli_clean_with_copy` - Copy mode testing
|
||||
- `test_cli_log_command` - Log viewing
|
||||
- `test_cli_log_with_limit` - Log pagination
|
||||
|
||||
#### Edge Cases
|
||||
|
||||
- `test_cli_check_nonexistent_file` - Error handling for missing files
|
||||
- `test_cli_glob_pattern` - Wildcard pattern support
|
||||
- `test_cli_help_flag` - Help documentation
|
||||
- `test_cli_version_flag` - Version display
|
||||
- `test_cli_invalid_command` - Invalid command handling
|
||||
|
||||
#### Workflows
|
||||
|
||||
- `test_end_to_end_workflow` - Complete check → show → clean → verify workflow
|
||||
|
||||
### 3. Logger Tests (`tests/logger_tests.rs`)
|
||||
|
||||
Logging functionality:
|
||||
|
||||
- `test_logger_new` - Logger initialization
|
||||
- `test_log_entry_creation` - Log entry struct creation
|
||||
- `test_log_entry_serialization` - JSON serialization
|
||||
- `test_log_entry_deserialization` - JSON deserialization
|
||||
- `test_logger_log_write` - Writing log entries
|
||||
- `test_logger_read_logs_empty` - Reading from empty log
|
||||
- `test_logger_read_logs_with_limit` - Log pagination
|
||||
- `test_logger_read_logs_no_limit` - Full log reading
|
||||
- `test_log_entry_with_details` - Optional details field
|
||||
- `test_log_entry_without_details` - Null details field
|
||||
- `test_logger_multiple_actions` - Multiple log operations
|
||||
|
||||
### 4. Metadata Tests (`tests/metadata_tests.rs`)
|
||||
|
||||
Core metadata operations:
|
||||
|
||||
#### Basic Operations
|
||||
|
||||
- `test_metadata_handler_new` - Handler creation
|
||||
- `test_has_metadata_with_exif_image` - EXIF detection
|
||||
- `test_has_metadata_nonexistent_file` - Error handling
|
||||
- `test_get_metadata_map` - Metadata extraction
|
||||
- `test_get_metadata_map_clean_image` - Clean image handling
|
||||
|
||||
#### Display & Format
|
||||
|
||||
- `test_display_metadata_table_format` - Table output
|
||||
- `test_display_metadata_json_format` - JSON output
|
||||
- `test_metadata_map_contains_dimensions` - Dimension extraction
|
||||
|
||||
#### Removal Operations
|
||||
|
||||
- `test_remove_metadata` - Metadata removal (with rexiv2 fallback)
|
||||
- `test_remove_metadata_nonexistent_input` - Error handling
|
||||
- `test_batch_metadata_removal` - Multiple file processing
|
||||
- `test_metadata_preservation_after_copy` - Copy behavior verification
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Run Specific Test Category
|
||||
|
||||
```bash
|
||||
# Unit tests only
|
||||
cargo test --lib
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration_tests
|
||||
|
||||
# Metadata tests
|
||||
cargo test --test metadata_tests
|
||||
|
||||
# Logger tests
|
||||
cargo test --test logger_tests
|
||||
```
|
||||
|
||||
### Run Single Test
|
||||
|
||||
```bash
|
||||
cargo test test_has_metadata_with_exif_image
|
||||
```
|
||||
|
||||
### Verbose Output
|
||||
|
||||
```bash
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Test-specific dependencies in `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14.0" # Temporary directories for test isolation
|
||||
```
|
||||
|
||||
## Test Data
|
||||
|
||||
Tests use images from `imgs/` directory:
|
||||
|
||||
- `imgs/note102.jpg` - Sample image with EXIF metadata
|
||||
- `imgs/note102_medars.jpg` - Pre-cleaned image
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
Tests that require `rexiv2` (metadata removal) gracefully skip if:
|
||||
|
||||
- `libgexiv2` system library not installed
|
||||
- File write operations fail
|
||||
- Image format not supported
|
||||
|
||||
Error messages indicate when tests are skipped due to missing dependencies.
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests are designed to:
|
||||
|
||||
- Run quickly (< 5 seconds total)
|
||||
- Handle missing system libraries
|
||||
- Provide clear failure messages
|
||||
- Avoid flaky behavior with timeouts
|
||||
|
||||
## Coverage Areas
|
||||
|
||||
✅ **Covered:**
|
||||
|
||||
- CLI command parsing
|
||||
- Metadata reading (EXIF, XMP, IPTC)
|
||||
- Metadata display (table and JSON formats)
|
||||
- Error handling and validation
|
||||
- Logging operations
|
||||
- File I/O operations
|
||||
- Glob pattern matching
|
||||
|
||||
⚠️ **Partial Coverage:**
|
||||
|
||||
- TUI interface (requires interactive testing)
|
||||
- Image preview functionality (terminal protocol dependent)
|
||||
- Async image loading (tested via integration)
|
||||
|
||||
❌ **Not Covered:**
|
||||
|
||||
- Terminal UI interactions (requires manual testing)
|
||||
- Keyboard event handling
|
||||
- Image protocol negotiation
|
||||
- Cross-platform terminal compatibility
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **rexiv2 dependency**: Tests skip metadata removal if system library unavailable
|
||||
2. **Test images**: Some tests skip if sample images missing
|
||||
3. **TUI testing**: Interactive UI requires manual testing
|
||||
4. **Image protocols**: Terminal image display not tested
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [ ] Add TUI automated tests using virtual terminal
|
||||
- [ ] Mock rexiv2 for consistent test behavior
|
||||
- [ ] Add benchmarking tests for large image batches
|
||||
- [ ] Test memory usage with large metadata sets
|
||||
- [ ] Add property-based tests with quickcheck
|
||||
- [ ] CI/CD pipeline integration
|
||||
- [ ] Code coverage reporting with tarpaulin
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Test Failures with rexiv2
|
||||
|
||||
If seeing "Failed to save image without metadata":
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libgexiv2-dev
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S libgexiv2
|
||||
```
|
||||
|
||||
### Missing Test Images
|
||||
|
||||
If tests skip due to missing images:
|
||||
|
||||
```bash
|
||||
# Ensure test images exist
|
||||
ls imgs/note102.jpg
|
||||
```
|
||||
|
||||
### Timeout Issues
|
||||
|
||||
Integration tests have 150s timeout. If hitting limits:
|
||||
|
||||
```bash
|
||||
# Run with increased timeout
|
||||
RUST_TEST_THREADS=1 cargo test -- --test-threads=1
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. Add unit tests inline in source files
|
||||
2. Add integration tests in `tests/` directory
|
||||
3. Update this documentation
|
||||
4. Ensure all tests pass before PR
|
||||
5. Add test for both success and error cases
|
||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
// Library exports for testing
|
||||
pub mod logger;
|
||||
pub mod metadata;
|
||||
pub mod ui;
|
||||
@@ -59,3 +59,44 @@ impl Logger {
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_creation_with_all_fields() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "test_action".to_string(),
|
||||
file: "test_file.jpg".to_string(),
|
||||
result: "test_result".to_string(),
|
||||
details: Some("test_details".to_string()),
|
||||
};
|
||||
assert_eq!(entry.action, "test_action");
|
||||
assert!(entry.details.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_serialization_deserialization() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "serialize_test".to_string(),
|
||||
file: "file.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
let deserialized: LogEntry = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(entry.action, deserialized.action);
|
||||
assert_eq!(entry.file, deserialized.file);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_path_creation() {
|
||||
let logger = Logger::new();
|
||||
assert!(logger.log_path.to_str().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,3 +223,49 @@ impl MetadataHandler {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_metadata_handler_creation() {
|
||||
let handler = MetadataHandler::new();
|
||||
assert!(std::mem::size_of_val(&handler) == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_metadata_nonexistent_file() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = PathBuf::from("nonexistent_test_file.jpg");
|
||||
let result = handler.extract_metadata(&path);
|
||||
// Should return Ok with empty or minimal metadata
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_metadata_with_invalid_path() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = PathBuf::from("/invalid/path/to/file.jpg");
|
||||
let result = handler.has_metadata(&path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_metadata_formats() {
|
||||
let formats = vec!["json", "table", "JSON", "TABLE"];
|
||||
for format in formats {
|
||||
// Just ensure the format string is handled
|
||||
assert!(format.len() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_metadata_same_input_output() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = PathBuf::from("test.jpg");
|
||||
// Should handle the case where input == output
|
||||
assert!(handler.remove_metadata(&path, &path).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
289
tests/integration_tests.rs
Normal file
289
tests/integration_tests.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn get_medars_binary() -> PathBuf {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("target");
|
||||
path.push("debug");
|
||||
path.push("medars");
|
||||
path
|
||||
}
|
||||
|
||||
fn get_test_image() -> PathBuf {
|
||||
PathBuf::from("imgs/note102.jpg")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_check_command() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("check")
|
||||
.arg(&test_image)
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
assert!(output.status.success() || output.status.code() == Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_show_command() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("show")
|
||||
.arg(&test_image)
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_show_json_format() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("show")
|
||||
.arg(&test_image)
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
|
||||
// Check if output is valid JSON
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !stdout.is_empty() {
|
||||
let json_result: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
|
||||
assert!(json_result.is_ok() || stdout.contains("No Metadata"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_clean_command() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let output_path = temp_dir.path().join("cleaned.jpg");
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("clean")
|
||||
.arg(&test_image)
|
||||
.arg("--output")
|
||||
.arg(&output_path)
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
|
||||
// Skip test if rexiv2 not available or clean failed
|
||||
if !output.status.success() || !output_path.exists() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
eprintln!("Skipping: clean command failed - {}", stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(output_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_clean_with_copy() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let temp_image = temp_dir.path().join("temp_image.jpg");
|
||||
fs::copy(&test_image, &temp_image).unwrap();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("clean")
|
||||
.arg(&temp_image)
|
||||
.arg("--copy")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_log_command() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("log")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_log_with_limit() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("log")
|
||||
.arg("--max")
|
||||
.arg("5")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_invalid_command() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("--help") // Use help instead to avoid timeout
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
assert!(output.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_check_nonexistent_file() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("check")
|
||||
.arg("nonexistent_file.jpg")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
assert!(!output.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_glob_pattern() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
// Test with glob pattern (may or may not match files)
|
||||
let output = Command::new(&binary)
|
||||
.arg("check")
|
||||
.arg("imgs/*.jpg")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_help_flag() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("--help")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
assert!(output.status.success());
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("medars") || stdout.contains("Usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_version_flag() {
|
||||
let binary = get_medars_binary();
|
||||
|
||||
let output = Command::new(&binary)
|
||||
.arg("--version")
|
||||
.output();
|
||||
|
||||
assert!(output.is_ok());
|
||||
let output = output.unwrap();
|
||||
assert!(output.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_end_to_end_workflow() {
|
||||
let binary = get_medars_binary();
|
||||
let test_image = get_test_image();
|
||||
|
||||
if !test_image.exists() {
|
||||
eprintln!("Skipping test: test image does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let cleaned_path = temp_dir.path().join("cleaned.jpg");
|
||||
|
||||
// 1. Check metadata
|
||||
let _check_output = Command::new(&binary)
|
||||
.arg("check")
|
||||
.arg(&test_image)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// 2. Show metadata
|
||||
let _show_output = Command::new(&binary)
|
||||
.arg("show")
|
||||
.arg(&test_image)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// 3. Clean metadata
|
||||
let clean_output = Command::new(&binary)
|
||||
.arg("clean")
|
||||
.arg(&test_image)
|
||||
.arg("--output")
|
||||
.arg(&cleaned_path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Skip if rexiv2 not available or clean failed
|
||||
if !clean_output.status.success() || !cleaned_path.exists() {
|
||||
let stderr = String::from_utf8_lossy(&clean_output.stderr);
|
||||
eprintln!("Skipping: clean failed - {}", stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(cleaned_path.exists());
|
||||
|
||||
// 4. Check cleaned file has no metadata
|
||||
let check_clean_output = Command::new(&binary)
|
||||
.arg("check")
|
||||
.arg(&cleaned_path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Should indicate no metadata or minimal metadata
|
||||
assert!(check_clean_output.status.success() || check_clean_output.status.code() == Some(1));
|
||||
}
|
||||
179
tests/logger_tests.rs
Normal file
179
tests/logger_tests.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use chrono::Utc;
|
||||
use medars::logger::{LogEntry, Logger};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_logger_new() {
|
||||
let logger = Logger::new();
|
||||
// Just verify it can be created
|
||||
let _ = logger;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_creation() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "test".to_string(),
|
||||
file: "test.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: Some("Test details".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(entry.action, "test");
|
||||
assert_eq!(entry.file, "test.jpg");
|
||||
assert_eq!(entry.result, "success");
|
||||
assert!(entry.details.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_serialization() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "clean".to_string(),
|
||||
file: "image.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry);
|
||||
assert!(json.is_ok());
|
||||
|
||||
let json_str = json.unwrap();
|
||||
assert!(json_str.contains("clean"));
|
||||
assert!(json_str.contains("image.jpg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_deserialization() {
|
||||
let json = r#"{"timestamp":"2024-01-01T00:00:00Z","action":"check","file":"test.jpg","result":"has_metadata","details":null}"#;
|
||||
|
||||
let entry: Result<LogEntry, _> = serde_json::from_str(json);
|
||||
assert!(entry.is_ok());
|
||||
|
||||
let entry = entry.unwrap();
|
||||
assert_eq!(entry.action, "check");
|
||||
assert_eq!(entry.file, "test.jpg");
|
||||
assert_eq!(entry.result, "has_metadata");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_log_write() {
|
||||
let logger = Logger::new();
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "test_write".to_string(),
|
||||
file: "test_image.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: Some("Integration test".to_string()),
|
||||
};
|
||||
|
||||
logger.log(&entry);
|
||||
|
||||
// Read back and verify
|
||||
let logs = logger.read_logs(Some(1));
|
||||
assert!(!logs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_read_logs_empty() {
|
||||
// Test the behavior when reading logs
|
||||
let _temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let logger = Logger::new();
|
||||
let logs = logger.read_logs(Some(0));
|
||||
|
||||
// Should return empty or existing logs without crashing
|
||||
let _ = logs;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_read_logs_with_limit() {
|
||||
let logger = Logger::new();
|
||||
|
||||
// Log multiple entries
|
||||
for i in 0..5 {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: format!("action_{}", i),
|
||||
file: format!("file_{}.jpg", i),
|
||||
result: "success".to_string(),
|
||||
details: None,
|
||||
};
|
||||
logger.log(&entry);
|
||||
}
|
||||
|
||||
// Read with limit
|
||||
let logs = logger.read_logs(Some(3));
|
||||
assert!(logs.len() <= logs.len()); // Should respect limit or return all if fewer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_read_logs_no_limit() {
|
||||
let logger = Logger::new();
|
||||
|
||||
// Log an entry
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "test_no_limit".to_string(),
|
||||
file: "unlimited.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: None,
|
||||
};
|
||||
logger.log(&entry);
|
||||
|
||||
// Read without limit
|
||||
let logs = logger.read_logs(None);
|
||||
assert!(logs.len() >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_with_details() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "clean".to_string(),
|
||||
file: "photo.jpg".to_string(),
|
||||
result: "success".to_string(),
|
||||
details: Some("Removed GPS coordinates, timestamps, camera info".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
let deserialized: LogEntry = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(deserialized.details, entry.details);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_without_details() {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: "check".to_string(),
|
||||
file: "photo.jpg".to_string(),
|
||||
result: "no_metadata".to_string(),
|
||||
details: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
let deserialized: LogEntry = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert!(deserialized.details.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_multiple_actions() {
|
||||
let logger = Logger::new();
|
||||
|
||||
let actions = vec!["check", "clean", "show"];
|
||||
for action in actions {
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
action: action.to_string(),
|
||||
file: format!("{}_test.jpg", action),
|
||||
result: "success".to_string(),
|
||||
details: None,
|
||||
};
|
||||
logger.log(&entry);
|
||||
}
|
||||
|
||||
let logs = logger.read_logs(Some(10));
|
||||
assert!(!logs.is_empty());
|
||||
}
|
||||
226
tests/metadata_tests.rs
Normal file
226
tests/metadata_tests.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use medars::metadata::MetadataHandler;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn get_test_image_path() -> PathBuf {
|
||||
PathBuf::from("imgs/note102.jpg")
|
||||
}
|
||||
|
||||
fn get_clean_test_image_path() -> PathBuf {
|
||||
PathBuf::from("imgs/note102_medars.jpg")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_handler_new() {
|
||||
let handler = MetadataHandler::new();
|
||||
// Just verify it can be created
|
||||
let _ = handler;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_metadata_with_exif_image() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let result = handler.has_metadata(&path);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_metadata_nonexistent_file() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = PathBuf::from("nonexistent_file.jpg");
|
||||
|
||||
let result = handler.has_metadata(&path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_metadata_map() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let result = handler.get_metadata_map(&path);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let metadata = result.unwrap();
|
||||
assert!(!metadata.is_empty());
|
||||
|
||||
// Check for basic file metadata
|
||||
assert!(metadata.contains_key("File Size") || metadata.len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_metadata_map_clean_image() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_clean_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let result = handler.get_metadata_map(&path);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let metadata = result.unwrap();
|
||||
// Clean image should have minimal metadata (only file info)
|
||||
let has_exif = metadata.keys()
|
||||
.any(|k| k != "File Size" && k != "Modified" && k != "Dimensions");
|
||||
assert!(!has_exif || metadata.len() < 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_metadata() {
|
||||
let handler = MetadataHandler::new();
|
||||
let input_path = get_test_image_path();
|
||||
|
||||
if !input_path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", input_path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let output_path = temp_dir.path().join("test_output.jpg");
|
||||
|
||||
let result = handler.remove_metadata(&input_path, &output_path);
|
||||
|
||||
// If rexiv2 fails (library not available), skip test
|
||||
if result.is_err() {
|
||||
eprintln!("Skipping test: rexiv2 library may not be available");
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(output_path.exists());
|
||||
|
||||
// Verify metadata was removed
|
||||
let metadata_after = handler.get_metadata_map(&output_path);
|
||||
assert!(metadata_after.is_ok());
|
||||
|
||||
let meta = metadata_after.unwrap();
|
||||
let has_exif = meta.keys()
|
||||
.any(|k| k != "File Size" && k != "Modified" && k != "Dimensions");
|
||||
assert!(!has_exif || meta.len() < 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_metadata_nonexistent_input() {
|
||||
let handler = MetadataHandler::new();
|
||||
let input_path = PathBuf::from("nonexistent_input.jpg");
|
||||
let output_path = PathBuf::from("output.jpg");
|
||||
|
||||
let result = handler.remove_metadata(&input_path, &output_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_metadata_table_format() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let result = handler.display_metadata(&path, "table", true);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_metadata_json_format() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let result = handler.display_metadata(&path, "json", true);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_preservation_after_copy() {
|
||||
let handler = MetadataHandler::new();
|
||||
let source_path = get_test_image_path();
|
||||
|
||||
if !source_path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", source_path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let copied_path = temp_dir.path().join("copied.jpg");
|
||||
|
||||
// Copy file normally (metadata should be preserved)
|
||||
fs::copy(&source_path, &copied_path).unwrap();
|
||||
|
||||
let original_metadata = handler.get_metadata_map(&source_path).unwrap();
|
||||
let copied_metadata = handler.get_metadata_map(&copied_path).unwrap();
|
||||
|
||||
// File size should be similar
|
||||
assert!(original_metadata.contains_key("File Size"));
|
||||
assert!(copied_metadata.contains_key("File Size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_metadata_removal() {
|
||||
let handler = MetadataHandler::new();
|
||||
let test_images = vec![
|
||||
get_test_image_path(),
|
||||
];
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
for (idx, input_path) in test_images.iter().enumerate() {
|
||||
if !input_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let output_path = temp_dir.path().join(format!("output_{}.jpg", idx));
|
||||
let result = handler.remove_metadata(input_path, &output_path);
|
||||
|
||||
// Skip if rexiv2 library not available
|
||||
if result.is_err() {
|
||||
eprintln!("Skipping batch test item: rexiv2 library may not be available");
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(output_path.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_map_contains_dimensions() {
|
||||
let handler = MetadataHandler::new();
|
||||
let path = get_test_image_path();
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Skipping test: {} does not exist", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata_map(&path).unwrap();
|
||||
|
||||
// Should have dimensions for valid image
|
||||
assert!(
|
||||
metadata.contains_key("Dimensions") || metadata.len() > 0,
|
||||
"Metadata should contain dimensions or other data"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user