1 contributor
#!/bin/bash
# Media Importer Test Runner
# Comprehensive testing framework based on Development.md specifications
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$SCRIPT_DIR/test"
SOURCE_DIR="$TEST_DIR/source"
DEST_DIR="$TEST_DIR/destination"
SAMPLES_DIR="$SCRIPT_DIR/sample" # Fixed: was 'samples' but should be 'sample'
MEDIA_IMPORTER="$SCRIPT_DIR/media-importer.sh"
TEST_REPORTS_DIR="$SCRIPT_DIR/test_reports"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_color() {
local color="$1"
local message="$2"
echo -e "${color}${message}${NC}"
}
# Function to create test directories
create_test_dirs() {
print_color "$BLUE" "Creating test directories..."
mkdir -p "$SOURCE_DIR"
mkdir -p "$DEST_DIR"
mkdir -p "$TEST_REPORTS_DIR"
}
# Function to clean test directory
clean_test_dir() {
print_color "$BLUE" "Cleaning test directory..."
rm -rf "$TEST_DIR"/*
}
# Function to copy sample files
copy_sample_files() {
local source_pattern="$1"
local target_dir="$2"
if [[ -d "$SAMPLES_DIR/$source_pattern" ]]; then
# Copy all files recursively from source to target directory
find "$SAMPLES_DIR/$source_pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) -exec cp {} "$target_dir/" \; 2>/dev/null || true
print_color "$GREEN" "Copied $source_pattern files to $target_dir"
else
print_color "$YELLOW" "Warning: $SAMPLES_DIR/$source_pattern not found. Path: $SAMPLES_DIR/$source_pattern"
fi
}
# Function to capture directory state
capture_state() {
local dir_path="$1"
local output_file="$2"
local label="$3"
print_color "$BLUE" "Capturing $label directory state..."
if [[ -d "$dir_path" ]]; then
find "$dir_path" -type f | sort > "$output_file"
else
echo "# Directory does not exist or is empty" > "$output_file"
fi
}
# Function to run test command and capture output
run_test_command() {
local command="$1"
local log_file="$2"
print_color "$YELLOW" "Executing: $command"
echo "$command" > "$log_file"
echo "" >> "$log_file"
echo "=== SCRIPT OUTPUT ===" >> "$log_file"
# Run command and capture both stdout and stderr
if eval "$command" >> "$log_file" 2>&1; then
echo "" >> "$log_file"
echo "=== COMMAND COMPLETED SUCCESSFULLY ===" >> "$log_file"
return 0
else
echo "" >> "$log_file"
echo "=== COMMAND FAILED ===" >> "$log_file"
return 1
fi
}
# Function to generate test report
generate_test_report() {
local test_name="$1"
local scenario="$2"
local objective="$3"
local files_used="$4"
local command="$5"
local result_status="$6"
# Determine success/failure prefix
local status_prefix
if [[ $result_status -eq 0 ]]; then
status_prefix="s"
else
status_prefix="f"
fi
# Format date as YYYYMMDD
local date_stamp=$(date '+%Y%m%d')
# Create filename with new format: [s|f]_date_restul_numelei
local report_file="$TEST_REPORTS_DIR/${status_prefix}_${date_stamp}_${test_name}.md"
print_color "$BLUE" "Generating test report: $report_file"
cat > "$report_file" << EOF
# Test Report: $test_name
## Test Information
- **Date**: $(date)
- **Scenario**: $scenario
- **Objective**: $objective
- **Files Used**: $files_used
## Pre-Test State
### Source Directory Structure
\`\`\`
$(cat "$TEST_DIR/source_before.txt" 2>/dev/null || echo "# No pre-test source state captured")
\`\`\`
### Destination Directory Structure
\`\`\`
$(cat "$TEST_DIR/dest_before.txt" 2>/dev/null || echo "# No pre-test destination state captured")
\`\`\`
## Test Execution
### Command Used
\`\`\`bash
$command
\`\`\`
### Script Output
\`\`\`
$(cat "$TEST_DIR/import_log.txt" 2>/dev/null || echo "# No script output captured")
\`\`\`
## Post-Test State
### Source Directory Structure
\`\`\`
$(cat "$TEST_DIR/source_after.txt" 2>/dev/null || echo "# No post-test source state captured")
\`\`\`
### Destination Directory Structure
\`\`\`
$(cat "$TEST_DIR/dest_after.txt" 2>/dev/null || echo "# No post-test destination state captured")
\`\`\`
## Analysis and Verification
### Expected Results
- Files should be processed according to the test scenario
- No data loss should occur
- Proper error handling for edge cases
### Actual Results
- Test execution completed with status: $result_status
### Issues Found
- [ ] No issues detected
- [ ] Issues found (see notes below)
### Protections Verified
- [ ] Destination exclusion working
- [ ] Move confirmation functional
- [ ] No data loss detected
- [ ] UTC conversion correct (for QuickTime files)
- [ ] Unimportable files handling (if applicable)
## Conclusion
### Test Result
- [ ] PASSED
- [ ] FAILED
- [ ] PARTIAL (with notes)
### Notes
Test completed as per Development.md specifications.
### Files Generated
- \`test/source_before.txt\` - Pre-test source structure
- \`test/dest_before.txt\` - Pre-test destination structure
- \`test/source_after.txt\` - Post-test source structure
- \`test/dest_after.txt\` - Post-test destination structure
- \`test/import_log.txt\` - Full script execution log
- \`test/test_report.md\` - This report
EOF
print_color "$GREEN" "Test report generated: $report_file"
# Clean up old reports, keeping only the last 10
cleanup_old_reports
}
# Function to clean up old test reports, keeping only the last 10
cleanup_old_reports() {
if [[ -d "$TEST_REPORTS_DIR" ]]; then
# Count total reports
local total_reports=$(find "$TEST_REPORTS_DIR" -name "*.md" | wc -l)
if [[ $total_reports -gt 10 ]]; then
print_color "$BLUE" "Cleaning up old test reports (keeping last 10)..."
# Find and remove oldest reports, keeping the 10 most recent
find "$TEST_REPORTS_DIR" -name "*.md" -type f -printf '%T@ %p\n' | \
sort -n | \
head -n -$((10)) | \
cut -d' ' -f2- | \
xargs -r rm
local remaining=$(find "$TEST_REPORTS_DIR" -name "*.md" | wc -l)
print_color "$GREEN" "Kept $remaining most recent test reports"
fi
fi
}
# Test Case 0: Basic Functionality Test
run_basic_functionality_test() {
print_color "$GREEN" "=== Running Test 0: Basic Functionality Test ==="
clean_test_dir
create_test_dirs
# Setup test files
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Basic_Functionality" \
"Processing files with valid EXIF data" \
"Verify correct sorting and organization of media files" \
"Sample media files with EXIF data from samples/media/sortable/" \
"$command" \
"$result"
}
# Test Case 1: Unimportable Files Test
run_unimportable_files_test() {
print_color "$GREEN" "=== Running Test 1: Unimportable Files Test ==="
clean_test_dir
create_test_dirs
# Setup test files - mix of sortable and unimportable
copy_sample_files "media/sortable" "$SOURCE_DIR"
copy_sample_files "media/unimportable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test (without --collect-unimportable flag)
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Unimportable_Files_Handling" \
"Testing files without EXIF data in root and subfolders" \
"Verify proper handling of unimportable files without collection flag" \
"Mixed sortable and unimportable files from samples/media/" \
"$command" \
"$result"
}
# Test Case 2: Mixed Content Test
run_mixed_content_test() {
print_color "$GREEN" "=== Running Test 2: Mixed Content Test ==="
clean_test_dir
create_test_dirs
# Setup separate folders for sortable vs unimportable
mkdir -p "$SOURCE_DIR/sortable" "$SOURCE_DIR/unimportable"
copy_sample_files "media/sortable" "$SOURCE_DIR/sortable"
copy_sample_files "media/unimportable" "$SOURCE_DIR/unimportable"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Mixed_Content" \
"Processing sortable and unimportable files in separate folders" \
"Verify cleanup behavior for folders with different file types" \
"Separate folders: sortable/ and unimportable/ with respective file types" \
"$command" \
"$result"
}
# Test Case 3: Safety Protections Test
run_safety_protections_test() {
print_color "$GREEN" "=== Running Test 3: Safety Protections Test ==="
clean_test_dir
create_test_dirs
# Setup test files
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Create a file in destination to test exclusion
echo "test file" > "$DEST_DIR/test.txt"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Safety_Protections" \
"Testing destination exclusion and move confirmation" \
"Verify data integrity protections prevent data loss" \
"Sample files with pre-existing destination content" \
"$command" \
"$result"
}
# Test Case 4: UTC Conversion Test
run_utc_conversion_test() {
print_color "$GREEN" "=== Running Test 4: UTC Conversion Test ==="
clean_test_dir
create_test_dirs
# Setup test files (assuming QuickTime files are available)
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"UTC_Conversion" \
"Testing UTC timestamp conversion for QuickTime files" \
"Verify correct UTC to local time conversion" \
"QuickTime/Apple media files with UTC timestamps" \
"$command" \
"$result"
}
# Test Case 5: Subdirectory Processing Test
run_subdirectory_processing_test() {
print_color "$GREEN" "=== Running Test 5: Subdirectory Processing Test ==="
clean_test_dir
create_test_dirs
# Create nested directory structure
mkdir -p "$SOURCE_DIR/level1/level2"
copy_sample_files "media/sortable" "$SOURCE_DIR/level1/level2"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Subdirectory_Processing" \
"Testing processing of files in nested subdirectories" \
"Verify recursive file discovery and processing" \
"Files in nested directory structure (level1/level2/)" \
"$command" \
"$result"
}
# Test Case 6: Keep Empty Directories Test
run_keep_empty_dirs_test() {
print_color "$GREEN" "=== Running Test 6: Keep Empty Directories Test ==="
clean_test_dir
create_test_dirs
# Create directory structure with some empty dirs
mkdir -p "$SOURCE_DIR/sortable" "$SOURCE_DIR/empty1" "$SOURCE_DIR/empty2"
copy_sample_files "media/sortable" "$SOURCE_DIR/sortable"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
# Run test with --keep-empty-dirs
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" --keep-empty-dirs -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Keep_Empty_Directories" \
"Testing --keep-empty-dirs functionality" \
"Verify empty directory preservation when flag is used" \
"Directory structure with sortable files and empty directories" \
"$command" \
"$result"
}
# Test Case 7: Source Only Test
run_source_only_test() {
print_color "$GREEN" "=== Running Test 7: Source Only Test ==="
clean_test_dir
create_test_dirs
# Setup test files
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_before.txt" "auto-destination"
# Run test with only source parameter
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_after.txt" "auto-destination"
# Generate report
generate_test_report \
"Source_Only_Test" \
"Testing processing with only source parameter" \
"Verify automatic creation of sorted subdirectory" \
"Sample media files, no explicit destination specified" \
"$command" \
"$result"
}
# Test Case 8: Destination Inside Source Test
run_destination_inside_source_test() {
print_color "$GREEN" "=== Running Test 8: Destination Inside Source Test ==="
clean_test_dir
create_test_dirs
# Setup test files
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_before.txt" "destination-inside-source"
# Run test with destination explicitly inside source
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$SOURCE_DIR/sorted\" -v"
local result=0
run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$?
# Guard 1: no files should remain outside sorted for sortable-only dataset
local outside_count
outside_count=$(find "$SOURCE_DIR" -type f ! -path "$SOURCE_DIR/sorted/*" | wc -l | tr -d ' ')
if [[ "$outside_count" != "0" ]]; then
echo "Destination exclusion check failed: found $outside_count file(s) outside sorted" >> "$TEST_DIR/import_log.txt"
result=1
fi
# Guard 2: destination exclusion should prevent sorted/sorted recursion
local recursive_sorted_count
recursive_sorted_count=$(find "$SOURCE_DIR/sorted" -type d -path "*/sorted/sorted*" 2>/dev/null | wc -l | tr -d ' ')
if [[ "$recursive_sorted_count" != "0" ]]; then
echo "Destination recursion check failed: found nested sorted/sorted directories" >> "$TEST_DIR/import_log.txt"
result=1
fi
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_after.txt" "destination-inside-source"
# Generate report
generate_test_report \
"Destination_Inside_Source" \
"Testing explicit destination inside source directory" \
"Verify destination exclusion and no sorted/sorted recursion" \
"Sortable sample media with destination set to source/sorted" \
"$command" \
"$result"
}
# Test Case 9: Verify Mode Test
run_verify_mode_test() {
print_color "$GREEN" "=== Running Test 9: Verify Mode Test ==="
clean_test_dir
create_test_dirs
# Setup test files
copy_sample_files "media/sortable" "$SOURCE_DIR"
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_before.txt" "auto-destination"
local log_file="$TEST_DIR/import_log.txt"
local result=0
: > "$log_file"
local command_default="cd \"$SOURCE_DIR\" && \"$MEDIA_IMPORTER\" --dry-run"
local command_strict="cd \"$SOURCE_DIR\" && \"$MEDIA_IMPORTER\" --dry-run --verify-mode strict"
local command="$command_default && $command_strict"
echo "$command_default" >> "$log_file"
echo "" >> "$log_file"
echo "=== DEFAULT VERIFY MODE OUTPUT ===" >> "$log_file"
if eval "$command_default" >> "$log_file" 2>&1; then
if ! grep -q "Verify mode:[[:space:]]*size" "$log_file"; then
echo "Expected default verify mode 'size' not found in output" >> "$log_file"
result=1
fi
else
result=1
fi
echo "" >> "$log_file"
echo "$command_strict" >> "$log_file"
echo "" >> "$log_file"
echo "=== STRICT VERIFY MODE OUTPUT ===" >> "$log_file"
if eval "$command_strict" >> "$log_file" 2>&1; then
if ! grep -q "Verify mode:[[:space:]]*strict" "$log_file"; then
echo "Expected strict verify mode not found in output" >> "$log_file"
result=1
fi
else
result=1
fi
if [[ $result -eq 0 ]]; then
echo "" >> "$log_file"
echo "=== COMMAND COMPLETED SUCCESSFULLY ===" >> "$log_file"
else
echo "" >> "$log_file"
echo "=== COMMAND FAILED ===" >> "$log_file"
fi
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_after.txt" "auto-destination"
# Generate report
generate_test_report \
"Verify_Mode" \
"Testing verify-mode defaults and strict override" \
"Verify default mode is size and strict mode is accepted" \
"Sample sortable media in source-only dry-run mode" \
"$command" \
"$result"
}
# Test Case 10: Timestamp Collision No-Overwrite Test
run_timestamp_collision_no_overwrite_test() {
print_color "$GREEN" "=== Running Test 10: Timestamp Collision No-Overwrite Test ==="
clean_test_dir
create_test_dirs
local log_file="$TEST_DIR/import_log.txt"
local result=0
: > "$log_file"
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg is required for this regression test" >> "$log_file"
result=1
elif ! command -v exiftool >/dev/null 2>&1; then
echo "exiftool is required for this regression test" >> "$log_file"
result=1
else
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=blue:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX011621.MP4" >> "$log_file" 2>&1 || result=1
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=red:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX021621.MP4" >> "$log_file" 2>&1 || result=1
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=green:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX031621.MP4" >> "$log_file" 2>&1 || result=1
exiftool -overwrite_original -CreateDate="2026:05:15 19:20:09" "$SOURCE_DIR/GX011621.MP4" "$SOURCE_DIR/GX021621.MP4" "$SOURCE_DIR/GX031621.MP4" >> "$log_file" 2>&1 || result=1
touch -t 202605151920.09 "$SOURCE_DIR/GX011621.MP4" "$SOURCE_DIR/GX021621.MP4" "$SOURCE_DIR/GX031621.MP4" || result=1
fi
# Capture pre-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" --verify-mode size -v"
if [[ $result -eq 0 ]]; then
run_test_command "$command" "$log_file" || result=$?
fi
local dest_count
dest_count=$(find "$DEST_DIR" -type f -iname "*.mp4" | wc -l | tr -d ' ')
if [[ "$dest_count" != "3" ]]; then
echo "Expected 3 destination MP4 files after timestamp collision import, found $dest_count" >> "$log_file"
result=1
fi
local source_count
source_count=$(find "$SOURCE_DIR" -type f -iname "*.mp4" | wc -l | tr -d ' ')
if [[ "$source_count" != "0" ]]; then
echo "Expected source MP4 files to be moved, found $source_count remaining" >> "$log_file"
result=1
fi
local unique_names
unique_names=$(find "$DEST_DIR" -type f -iname "*.mp4" -exec basename {} \; | sort -u | wc -l | tr -d ' ')
if [[ "$unique_names" != "3" ]]; then
echo "Expected 3 unique destination filenames, found $unique_names" >> "$log_file"
result=1
fi
local expected_base="$DEST_DIR/2026-05-15/2026-05-15_19-20-09.mp4"
local expected_suffix_1="$DEST_DIR/2026-05-15/2026-05-15_19-20-09_1.mp4"
local expected_suffix_2="$DEST_DIR/2026-05-15/2026-05-15_19-20-09_2.mp4"
if [[ ! -f "$expected_base" || ! -f "$expected_suffix_1" || ! -f "$expected_suffix_2" ]]; then
echo "Expected unattended numeric conflict suffixes _1 and _2 were not created" >> "$log_file"
result=1
fi
local legacy_suffix_count
legacy_suffix_count=$(find "$DEST_DIR" -type f -name "*__GX*.mp4" | wc -l | tr -d ' ')
if [[ "$legacy_suffix_count" != "0" ]]; then
echo "Expected no legacy original-name conflict suffixes, found $legacy_suffix_count" >> "$log_file"
result=1
fi
# Capture post-test state
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
# Generate report
generate_test_report \
"Timestamp_Collision_No_Overwrite" \
"Processing multiple GoPro-style chapters with identical CreateDate values" \
"Verify timestamp filename collisions do not overwrite prior imports or delete sources" \
"Synthetic MP4 files with identical QuickTime CreateDate metadata" \
"$command" \
"$result"
}
# Test Case 11: GoPro Sidecar Metadata Sync Test
run_gopro_sidecar_metadata_sync_test() {
print_color "$GREEN" "=== Running Test 11: GoPro Sidecar Metadata Sync Test ==="
clean_test_dir
create_test_dirs
local log_file="$TEST_DIR/import_log.txt"
local result=0
: > "$log_file"
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg is required for this regression test" >> "$log_file"
result=1
elif ! command -v exiftool >/dev/null 2>&1; then
echo "exiftool is required for this regression test" >> "$log_file"
result=1
else
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=blue:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX011622.MP4" >> "$log_file" 2>&1 || result=1
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=red:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX021622.MP4" >> "$log_file" 2>&1 || result=1
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=green:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX031622.MP4" >> "$log_file" 2>&1 || result=1
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=yellow:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX041622.MP4" >> "$log_file" 2>&1 || result=1
exiftool -overwrite_original -QuickTime:CreateDate="2026:05:16 19:03:19" "$SOURCE_DIR/GX011622.MP4" "$SOURCE_DIR/GX021622.MP4" "$SOURCE_DIR/GX031622.MP4" "$SOURCE_DIR/GX041622.MP4" >> "$log_file" 2>&1 || result=1
touch -t 202605161903.20 "$SOURCE_DIR/GX011622.THM" || result=1
touch -t 202605161915.08 "$SOURCE_DIR/GX021622.THM" || result=1
touch -t 202605161926.56 "$SOURCE_DIR/GX031622.LRV" || result=1
touch -t 202605161938.44 "$SOURCE_DIR/GX041622.MP4" || result=1
fi
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" --verify-mode size -v"
if [[ $result -eq 0 ]]; then
run_test_command "$command" "$log_file" || result=$?
fi
local first_dest="$DEST_DIR/2026-05-16/2026-05-16_19-03-20.mp4"
local second_dest="$DEST_DIR/2026-05-16/2026-05-16_19-15-08.mp4"
local third_dest="$DEST_DIR/2026-05-16/2026-05-16_19-26-56.mp4"
local fourth_dest="$DEST_DIR/2026-05-16/2026-05-16_19-38-44.mp4"
if [[ ! -f "$first_dest" || ! -f "$second_dest" || ! -f "$third_dest" || ! -f "$fourth_dest" ]]; then
echo "Expected GoPro filesystem-based destination filenames were not created" >> "$log_file"
result=1
fi
local first_metadata second_metadata third_metadata fourth_metadata
if [[ -f "$first_dest" ]]; then
first_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$first_dest" 2>/dev/null | head -1)
if [[ "$first_metadata" != "2026:05:16 19:03:20" ]]; then
echo "Expected first imported metadata CreateDate 2026:05:16 19:03:20, got ${first_metadata:-none}" >> "$log_file"
result=1
fi
fi
if [[ -f "$third_dest" ]]; then
third_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$third_dest" 2>/dev/null | head -1)
if [[ "$third_metadata" != "2026:05:16 19:26:56" ]]; then
echo "Expected third imported metadata CreateDate 2026:05:16 19:26:56, got ${third_metadata:-none}" >> "$log_file"
result=1
fi
fi
if [[ -f "$fourth_dest" ]]; then
fourth_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$fourth_dest" 2>/dev/null | head -1)
if [[ "$fourth_metadata" != "2026:05:16 19:38:44" ]]; then
echo "Expected fourth imported metadata CreateDate 2026:05:16 19:38:44, got ${fourth_metadata:-none}" >> "$log_file"
result=1
fi
fi
if [[ -f "$second_dest" ]]; then
second_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$second_dest" 2>/dev/null | head -1)
if [[ "$second_metadata" != "2026:05:16 19:15:08" ]]; then
echo "Expected second imported metadata CreateDate 2026:05:16 19:15:08, got ${second_metadata:-none}" >> "$log_file"
result=1
fi
fi
local source_count
source_count=$(find "$SOURCE_DIR" -type f -iname "*.mp4" | wc -l | tr -d ' ')
if [[ "$source_count" != "0" ]]; then
echo "Expected GoPro source MP4 files to be moved, found $source_count remaining" >> "$log_file"
result=1
fi
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
generate_test_report \
"GoPro_Sidecar_Metadata_Sync" \
"Processing GoPro-style MP4 chapters with THM, LRV, and MP4 filesystem timestamps" \
"Verify GoPro imports use filesystem fallback dates and sync destination metadata automatically" \
"Synthetic GoPro MP4 files with shared QuickTime CreateDate plus THM/LRV/no-sidecar mtime variants" \
"$command" \
"$result"
}
# Test Case 12: GoPro No-Sidecar Reimport Test
run_gopro_no_sidecar_reimport_test() {
print_color "$GREEN" "=== Running Test 12: GoPro No-Sidecar Reimport Test ==="
clean_test_dir
create_test_dirs
local log_file="$TEST_DIR/import_log.txt"
local result=0
: > "$log_file"
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg is required for this regression test" >> "$log_file"
result=1
elif ! command -v exiftool >/dev/null 2>&1; then
echo "exiftool is required for this regression test" >> "$log_file"
result=1
else
ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=purple:s=160x120:d=1 -c:v libx264 -pix_fmt yuv420p "$SOURCE_DIR/GX051622.MP4" >> "$log_file" 2>&1 || result=1
exiftool -overwrite_original -QuickTime:CreateDate="2026:05:16 19:03:19" "$SOURCE_DIR/GX051622.MP4" >> "$log_file" 2>&1 || result=1
rm -f "$SOURCE_DIR/GX051622.THM" "$SOURCE_DIR/GX051622.thm" "$SOURCE_DIR/GX051622.LRV" "$SOURCE_DIR/GX051622.lrv"
touch -t 202605161950.32 "$SOURCE_DIR/GX051622.MP4" || result=1
fi
capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_before.txt" "destination"
local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" --verify-mode size -k -v && \"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$DEST_DIR\" --verify-mode size -k -v"
if [[ $result -eq 0 ]]; then
run_test_command "$command" "$log_file" || result=$?
fi
local first_dest="$DEST_DIR/2026-05-16/2026-05-16_19-50-32.mp4"
local reimport_dest="$DEST_DIR/2026-05-16/2026-05-16_19-50-32_1.mp4"
if [[ ! -f "$first_dest" || ! -f "$reimport_dest" ]]; then
echo "Expected no-sidecar GoPro reimport destinations were not created" >> "$log_file"
result=1
fi
local dest_count
dest_count=$(find "$DEST_DIR" -type f -iname "*.mp4" | wc -l | tr -d ' ')
if [[ "$dest_count" != "2" ]]; then
echo "Expected 2 destination MP4 files after reimport, found $dest_count" >> "$log_file"
result=1
fi
local source_count
source_count=$(find "$SOURCE_DIR" -type f -iname "*.mp4" | wc -l | tr -d ' ')
if [[ "$source_count" != "1" ]]; then
echo "Expected source MP4 to remain in keep-originals reimport test, found $source_count" >> "$log_file"
result=1
fi
local sidecar_count
sidecar_count=$(find "$SOURCE_DIR" -type f \( -iname "*.thm" -o -iname "*.lrv" \) | wc -l | tr -d ' ')
if [[ "$sidecar_count" != "0" ]]; then
echo "Expected no THM/LRV sidecars in no-sidecar test, found $sidecar_count" >> "$log_file"
result=1
fi
if ! grep -q "Filesystem:GX051622.MP4" "$log_file"; then
echo "Expected date source fallback to Filesystem:GX051622.MP4" >> "$log_file"
result=1
fi
local first_metadata reimport_metadata
if [[ -f "$first_dest" ]]; then
first_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$first_dest" 2>/dev/null | head -1)
if [[ "$first_metadata" != "2026:05:16 19:50:32" ]]; then
echo "Expected first import metadata CreateDate 2026:05:16 19:50:32, got ${first_metadata:-none}" >> "$log_file"
result=1
fi
fi
if [[ -f "$reimport_dest" ]]; then
reimport_metadata=$(exiftool -api QuickTimeUTC=0 -s -s -s -QuickTime:CreateDate "$reimport_dest" 2>/dev/null | head -1)
if [[ "$reimport_metadata" != "2026:05:16 19:50:32" ]]; then
echo "Expected reimport metadata CreateDate 2026:05:16 19:50:32, got ${reimport_metadata:-none}" >> "$log_file"
result=1
fi
fi
capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source"
capture_state "$DEST_DIR" "$TEST_DIR/dest_after.txt" "destination"
generate_test_report \
"GoPro_No_Sidecar_Reimport" \
"Importing and reimporting a GoPro MP4 with no THM/LRV sidecars" \
"Verify MP4 filesystem mtime fallback, metadata sync, and numeric suffix on reimport" \
"Synthetic GoPro MP4 with no sidecars and wrong QuickTime metadata" \
"$command" \
"$result"
}
# Function to show menu
show_menu() {
echo ""
print_color "$GREEN" "Media Importer Test Runner"
echo "=========================="
echo ""
echo "Available Tests:"
echo "0. Basic Functionality Test"
echo "1. Unimportable Files Test"
echo "2. Mixed Content Test"
echo "3. Safety Protections Test"
echo "4. UTC Conversion Test"
echo "5. Subdirectory Processing Test"
echo "6. Keep Empty Directories Test"
echo "7. Source Only Test"
echo "8. Destination Inside Source Test"
echo "9. Verify Mode Test"
echo "10. Timestamp Collision No-Overwrite Test"
echo "11. GoPro Sidecar Metadata Sync Test"
echo "12. GoPro No-Sidecar Reimport Test"
echo "13. Run All Tests"
echo "q. Quit"
echo ""
echo -n "Select test to run (0-13, q to quit): "
}
# Function to run all tests
run_all_tests() {
print_color "$GREEN" "Running all tests sequentially..."
run_basic_functionality_test
echo ""
run_unimportable_files_test
echo ""
run_mixed_content_test
echo ""
run_safety_protections_test
echo ""
run_utc_conversion_test
echo ""
run_subdirectory_processing_test
echo ""
run_keep_empty_dirs_test
echo ""
run_source_only_test
echo ""
run_destination_inside_source_test
echo ""
run_verify_mode_test
echo ""
run_timestamp_collision_no_overwrite_test
echo ""
run_gopro_sidecar_metadata_sync_test
echo ""
run_gopro_no_sidecar_reimport_test
print_color "$GREEN" "All tests completed!"
}
# Main menu loop
main() {
while true; do
show_menu
read -r choice
case $choice in
0)
run_basic_functionality_test
;;
1)
run_unimportable_files_test
;;
2)
run_mixed_content_test
;;
3)
run_safety_protections_test
;;
4)
run_utc_conversion_test
;;
5)
run_subdirectory_processing_test
;;
6)
run_keep_empty_dirs_test
;;
7)
run_source_only_test
;;
8)
run_destination_inside_source_test
;;
9)
run_verify_mode_test
;;
10)
run_timestamp_collision_no_overwrite_test
;;
11)
run_gopro_sidecar_metadata_sync_test
;;
12)
run_gopro_no_sidecar_reimport_test
;;
13)
run_all_tests
;;
q|Q)
print_color "$GREEN" "Exiting test runner."
exit 0
;;
*)
print_color "$RED" "Invalid choice. Please select 0-13 or q to quit."
;;
esac
echo ""
echo -n "Press Enter to continue..."
read -r
done
}
# Check if script exists
if [[ ! -f "$MEDIA_IMPORTER" ]]; then
print_color "$RED" "Error: $MEDIA_IMPORTER not found!"
exit 1
fi
# Check if samples directory exists
if [[ ! -d "$SAMPLES_DIR" ]]; then
print_color "$YELLOW" "Warning: $SAMPLES_DIR not found. Some tests may not work properly."
fi
# Run main menu if no arguments provided
if [[ $# -eq 0 ]]; then
main
else
# Handle command line arguments
case "$1" in
"basic"|"0")
run_basic_functionality_test
;;
"unimportable"|"1")
run_unimportable_files_test
;;
"mixed"|"2")
run_mixed_content_test
;;
"safety"|"3")
run_safety_protections_test
;;
"utc"|"4")
run_utc_conversion_test
;;
"subdir"|"5")
run_subdirectory_processing_test
;;
"keep-empty"|"6")
run_keep_empty_dirs_test
;;
"source-only"|"7")
run_source_only_test
;;
"dest-in-source"|"8")
run_destination_inside_source_test
;;
"verify-mode"|"9")
run_verify_mode_test
;;
"timestamp-collision"|"collision"|"10")
run_timestamp_collision_no_overwrite_test
;;
"gopro-sidecar-sync"|"gopro-sync"|"11")
run_gopro_sidecar_metadata_sync_test
;;
"gopro-no-sidecar-reimport"|"gopro-reimport"|"12")
run_gopro_no_sidecar_reimport_test
;;
"all"|"13")
run_all_tests
;;
"clean")
clean_test_dir
;;
"setup")
create_test_dirs
;;
*)
print_color "$RED" "Unknown test: $1"
echo "Usage: $0 [basic|unimportable|mixed|safety|utc|subdir|keep-empty|source-only|dest-in-source|verify-mode|timestamp-collision|gopro-sidecar-sync|gopro-no-sidecar-reimport|all|clean|setup] or [0-13]"
exit 1
;;
esac
fi