#!/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 total_reports=$(find "$TEST_REPORTS_DIR" -name "*.md" | wc -l | tr -d ' ') if [[ $total_reports -gt 10 ]]; then print_color "$BLUE" "Cleaning up old test reports (keeping last 10)..." local remove_count remove_count=$((total_reports - 10)) find "$TEST_REPORTS_DIR" -name "*.md" -type f | while IFS= read -r report_file; do local report_mtime if report_mtime=$(stat -f %m "$report_file" 2>/dev/null); then : else report_mtime=$(stat -c %Y "$report_file" 2>/dev/null || echo 0) fi printf '%s\t%s\n' "$report_mtime" "$report_file" done | sort -n | head -n "$remove_count" | cut -f2- | while IFS= read -r old_report; do rm -f "$old_report" done local remaining remaining=$(find "$TEST_REPORTS_DIR" -name "*.md" | wc -l | tr -d ' ') 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