Created
September 29, 2025 18:50
-
-
Save piotrkochan/1eb15d8ecb85c866e716bd07ee48d203 to your computer and use it in GitHub Desktop.
Photorec Image Filter Test Suite
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Starting PhotoRec Image Filter Test Suite... | |
| Running baseline: No filters - reference test... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 8s | |
| Analysis phase: Validating results... | |
| -> JPG: 21, PNG: 5835, Total: 5856 files (PhotoRec: 8s, Analysis: 0s, Total: 8s) - PASSED | |
| Analyzing baseline files to determine test ranges... | |
| File size analysis: 0k - 134k (25%=0.4k, 50%=0.6k, 75%=1k) | |
| Analyzing image dimensions... | |
| Dimension analysis: 1x6 - 3840x1200 (median: 200x150) | |
| Pixel analysis: 6 - 4608000 (median: 30000) | |
| Running test_filesize_min: File size ≥0.6k (median+)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 8s | |
| Analysis phase: Validating results... | |
| -> JPG: 15, PNG: 3020, Total: 3035 files (PhotoRec: 8s, Analysis: 0s, Total: 8s) - PASSED | |
| Running test_filesize_max: File size ≤1k (75th percentile)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 1, PNG: 4154, Total: 4155 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_filesize_range: File size 0.4k-1k (IQR)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 1, PNG: 2457, Total: 2458 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_decimal_filesize: Decimal file size 0.6k-1k... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 1, PNG: 1339, Total: 1340 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_large_filesize: Large files ≥100KB... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 6s | |
| Analysis phase: Validating results... | |
| -> JPG: 3, PNG: 1, Total: 4 files (PhotoRec: 6s, Analysis: 0s, Total: 6s) - PASSED | |
| Running test_dimensions_min: Dimensions ≥200x150 (median+)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 7, PNG: 298, Total: 305 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_dimensions_range: Dimensions 200-400 x 150-300... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 2s | |
| Analysis phase: Validating results... | |
| -> JPG: 0, PNG: 110, Total: 110 files (PhotoRec: 2s, Analysis: 0s, Total: 2s) - PASSED | |
| Running test_small_dimensions: Small dimensions ≤100x100... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 1s | |
| Analysis phase: Validating results... | |
| -> JPG: 5, PNG: 418, Total: 423 files (PhotoRec: 1s, Analysis: 0s, Total: 1s) - PASSED | |
| Running test_square_images: Square-ish images 100-500x100-500... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 2s | |
| Analysis phase: Validating results... | |
| -> JPG: 0, PNG: 244, Total: 244 files (PhotoRec: 2s, Analysis: 0s, Total: 2s) - PASSED | |
| Running test_restrictive: Restrictive filter ≥800x600... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 6s | |
| Analysis phase: Validating results... | |
| -> JPG: 6, PNG: 24, Total: 30 files (PhotoRec: 6s, Analysis: 0s, Total: 6s) - PASSED | |
| Running test_pixels_min: Pixels ≥30000 (median+)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 7, PNG: 336, Total: 343 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_pixels_range: Pixels 30000-100000 (median-75th)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 2s | |
| Analysis phase: Validating results... | |
| -> JPG: 0, PNG: 154, Total: 154 files (PhotoRec: 2s, Analysis: 0s, Total: 2s) - PASSED | |
| Running test_pixels_format: Pixels WIDTHxHEIGHT format 200x150-400x300... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 2s | |
| Analysis phase: Validating results... | |
| -> JPG: 0, PNG: 165, Total: 165 files (PhotoRec: 2s, Analysis: 0s, Total: 2s) - PASSED | |
| Running test_high_resolution: High resolution ≥1M pixels... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 6s | |
| Analysis phase: Validating results... | |
| -> JPG: 6, PNG: 5, Total: 11 files (PhotoRec: 6s, Analysis: 0s, Total: 6s) - PASSED | |
| Running test_combined: Combined filters (filesize + dimensions)... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 7s | |
| Analysis phase: Validating results... | |
| -> JPG: 7, PNG: 295, Total: 302 files (PhotoRec: 7s, Analysis: 0s, Total: 7s) - PASSED | |
| Running test_mixed_units: Mixed: 50KB-2MB files, width ≥300px... | |
| PhotoRec phase: Starting recovery... | |
| PhotoRec phase: Completed in 11s | |
| Analysis phase: Validating results... | |
| -> JPG: 5, PNG: 5, Total: 10 files (PhotoRec: 11s, Analysis: 0s, Total: 11s) - PASSED | |
| All tests completed! | |
| === TEST SUMMARY === | |
| Total tests: 17 | |
| Passed: 17 | |
| Failed: 0 | |
| Baseline files: 5856 | |
| Total execution time: 96.4s |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| PhotoRec Image Filter Test Suite | |
| Comprehensive test suite for PhotoRec's image filtering functionality. | |
| Tests file size, dimension, and pixel count filters across JPG and PNG formats. | |
| """ | |
| import os | |
| import sys | |
| import subprocess | |
| import shutil | |
| import time | |
| import re | |
| from pathlib import Path | |
| from dataclasses import dataclass | |
| from typing import List, Dict, Optional, Tuple | |
| @dataclass | |
| class TestResult: | |
| name: str | |
| description: str | |
| args: str | |
| jpg_count: int | |
| png_count: int | |
| total_count: int | |
| duration: float | |
| photorec_duration: float | |
| analysis_duration: float | |
| status: str | |
| failed_files: List[str] = None | |
| class PhotoRecTestSuite: | |
| def __init__(self, disk_image: str, output_dir: str, photorec_bin: str, debug: bool = False): | |
| self.disk_image = Path(disk_image) | |
| self.output_dir = Path(output_dir) | |
| self.photorec_bin = Path(photorec_bin) | |
| self.results: List[TestResult] = [] | |
| self.baseline_count = 0 | |
| self.debug = debug | |
| # Validate inputs | |
| if not self.disk_image.exists(): | |
| raise FileNotFoundError(f"Disk image not found: {disk_image}") | |
| if not self.photorec_bin.exists(): | |
| raise FileNotFoundError(f"PhotoRec binary not found: {photorec_bin}") | |
| # Ensure output directory exists | |
| self.output_dir.mkdir(parents=True, exist_ok=True) | |
| def cleanup_outputs(self): | |
| """Clean all output directories""" | |
| if self.output_dir.exists(): | |
| shutil.rmtree(self.output_dir) | |
| self.output_dir.mkdir(parents=True, exist_ok=True) | |
| def run_photorec(self, test_name: str, args: str) -> Tuple[float, int, int]: | |
| """Run PhotoRec with given arguments and return timing info and counts""" | |
| # Clean previous results (including .1, .2, etc. suffixes) | |
| for path in self.output_dir.glob(f"{test_name}*"): | |
| if path.is_dir(): | |
| shutil.rmtree(path) | |
| # Remove PhotoRec session files that might interfere | |
| session_files = ["photorec.ses", "photorec.cfg"] | |
| for session_file in session_files: | |
| if os.path.exists(session_file): | |
| os.remove(session_file) | |
| # Construct PhotoRec command | |
| cmd_args = "partition_none,fileopt,everything,disable,jpg,enable,png,enable" | |
| if args: | |
| cmd_args += "," + args | |
| cmd_args += ",search" | |
| cmd = [ | |
| str(self.photorec_bin), | |
| "/log", | |
| "/d", str(self.output_dir / test_name), | |
| "/cmd", str(self.disk_image), | |
| cmd_args | |
| ] | |
| if self.debug: | |
| print(f" DEBUG: PhotoRec command: {' '.join(cmd)}") | |
| print(f" PhotoRec phase: Starting recovery...") | |
| start_time = time.time() | |
| # Run PhotoRec | |
| if self.debug: | |
| subprocess.run(cmd) | |
| else: | |
| with open(os.devnull, 'w') as devnull: | |
| subprocess.run(cmd, stdout=devnull, stderr=devnull) | |
| end_time = time.time() | |
| duration = end_time - start_time | |
| print(f" PhotoRec phase: Completed in {duration:.0f}s") | |
| # Count recovered files | |
| pattern = test_name + ".*" | |
| jpg_files = list(self.output_dir.glob(f"{pattern}/*.jpg")) | |
| png_files = list(self.output_dir.glob(f"{pattern}/*.png")) | |
| return duration, len(jpg_files), len(png_files) | |
| def get_image_dimensions(self, image_path: Path) -> Optional[Tuple[int, int]]: | |
| """Get image dimensions using identify command""" | |
| try: | |
| result = subprocess.run( | |
| ["identify", "-ping", "-format", "%wx%h", str(image_path)], | |
| capture_output=True, text=True, timeout=10 | |
| ) | |
| if result.returncode == 0: | |
| dimensions = result.stdout.strip() | |
| if 'x' in dimensions: | |
| w, h = dimensions.split('x') | |
| return int(w), int(h) | |
| except (subprocess.TimeoutExpired, subprocess.CalledProcessError, ValueError): | |
| pass | |
| return None | |
| def validate_filter(self, files: List[Path], args: str, filter_type: str) -> List[str]: | |
| """Generic validation method for all filter types""" | |
| failed_files = [] | |
| validators = { | |
| 'size': self._validate_filesize, | |
| 'width': self._validate_dimensions, | |
| 'height': self._validate_dimensions, | |
| 'pixels': self._validate_pixels | |
| } | |
| # Check which filters are active in args | |
| for ftype, validator in validators.items(): | |
| if ftype in args and (filter_type == ftype or | |
| (filter_type in ['width', 'height'] and ftype in ['width', 'height'])): | |
| failed_files.extend(validator(files, args)) | |
| return failed_files | |
| def _validate_filesize(self, files: List[Path], args: str) -> List[str]: | |
| """Validate files against filesize criteria""" | |
| failed_files = [] | |
| filesize_match = re.search(r',size,([^,]+)', args) | |
| if not filesize_match: | |
| return failed_files | |
| min_size, max_size = self._parse_range(filesize_match.group(1), self._parse_size) | |
| for file_path in files: | |
| try: | |
| file_size = file_path.stat().st_size | |
| if not (min_size <= file_size <= max_size): | |
| failed_files.append(f"{file_path.name}: {file_size} bytes (expected {min_size}-{max_size})") | |
| except OSError: | |
| failed_files.append(f"{file_path.name}: Could not get file size") | |
| return failed_files | |
| def _validate_dimensions(self, files: List[Path], args: str) -> List[str]: | |
| """Validate files against dimension criteria""" | |
| failed_files = [] | |
| # Parse width and height ranges | |
| ranges = {} | |
| for dim in ['width', 'height']: | |
| match = re.search(f'{dim},([^,]+)', args) | |
| if match: | |
| ranges[dim] = self._parse_range(match.group(1)) | |
| else: | |
| ranges[dim] = (0, float('inf')) | |
| for file_path in files: | |
| dimensions = self.get_image_dimensions(file_path) | |
| if dimensions is None: | |
| failed_files.append(f"{file_path.name}: Could not get dimensions") | |
| continue | |
| width, height = dimensions | |
| width_ok = ranges['width'][0] <= width <= ranges['width'][1] | |
| height_ok = ranges['height'][0] <= height <= ranges['height'][1] | |
| if not (width_ok and height_ok): | |
| failed_files.append( | |
| f"{file_path.name}: {width}x{height} " | |
| f"(expected w:{ranges['width'][0]}-{ranges['width'][1]}, " | |
| f"h:{ranges['height'][0]}-{ranges['height'][1]})" | |
| ) | |
| return failed_files | |
| def _validate_pixels(self, files: List[Path], args: str) -> List[str]: | |
| """Validate files against pixel criteria""" | |
| failed_files = [] | |
| pixels_match = re.search(r'pixels,([^,]+)', args) | |
| if not pixels_match: | |
| return failed_files | |
| if 'x' in pixels_match.group(1): | |
| min_pixels, max_pixels = self._parse_range(pixels_match.group(1), self._parse_pixel_value) | |
| else: | |
| min_pixels, max_pixels = self._parse_range(pixels_match.group(1)) | |
| for file_path in files: | |
| dimensions = self.get_image_dimensions(file_path) | |
| if dimensions is None: | |
| failed_files.append(f"{file_path.name}: Could not get dimensions") | |
| continue | |
| width, height = dimensions | |
| pixels = width * height | |
| if not (min_pixels <= pixels <= max_pixels): | |
| failed_files.append( | |
| f"{file_path.name}: {width}x{height}={pixels} pixels " | |
| f"(expected {min_pixels}-{max_pixels})" | |
| ) | |
| return failed_files | |
| def _parse_size(self, size_str: str) -> int: | |
| """Parse size string like '10k', '1.5m' into bytes""" | |
| if not size_str: | |
| return 0 | |
| size_str = size_str.lower() | |
| multipliers = {'k': 1024, 'm': 1024**2, 'g': 1024**3} | |
| for suffix, mult in multipliers.items(): | |
| if size_str.endswith(suffix): | |
| return int(float(size_str[:-1]) * mult) | |
| return int(size_str) | |
| def _parse_range(self, range_arg: str, parser_func=int) -> Tuple: | |
| """Generic range parser for dimensions, sizes, or pixels""" | |
| if range_arg.startswith('-'): | |
| return 0, parser_func(range_arg[1:]) | |
| elif range_arg.endswith('-'): | |
| return parser_func(range_arg[:-1]), float('inf') | |
| elif '-' in range_arg: | |
| min_val, max_val = range_arg.split('-', 1) | |
| return (parser_func(min_val) if min_val else 0, | |
| parser_func(max_val) if max_val else float('inf')) | |
| else: | |
| val = parser_func(range_arg) | |
| return val, val | |
| def _parse_pixel_value(self, pixel_str: str) -> int: | |
| """Parse pixel value in WIDTHxHEIGHT format or numeric""" | |
| if 'x' in pixel_str: | |
| w, h = pixel_str.split('x') | |
| return int(w) * int(h) | |
| return int(pixel_str) | |
| def run_test(self, test_name: str, args: str, description: str) -> TestResult: | |
| """Run a single test and return results""" | |
| print(f"Running {test_name}: {description}...") | |
| # Run PhotoRec | |
| start_time = time.time() | |
| photorec_duration, jpg_count, png_count = self.run_photorec(test_name, args) | |
| # Analysis phase | |
| print(" Analysis phase: Validating results...") | |
| analysis_start = time.time() | |
| total_count = jpg_count + png_count | |
| failed_files = [] | |
| # Get all recovered files | |
| pattern = test_name + ".*" | |
| all_files = (list(self.output_dir.glob(f"{pattern}/*.jpg")) + | |
| list(self.output_dir.glob(f"{pattern}/*.png"))) | |
| # Validate based on filter type | |
| if test_name == "baseline": | |
| self.baseline_count = total_count | |
| status = "PASSED" | |
| elif any(f in args for f in ["size,", "width", "height", "pixels"]): | |
| filter_type = next((f.rstrip(',') for f in ["size", "width", "height", "pixels"] if f in args), "size") | |
| failed_files = self.validate_filter(all_files, args, filter_type) | |
| status = "PASSED" if len(failed_files) == 0 and total_count > 0 else "FAILED" | |
| else: | |
| status = "PASSED" if total_count == 0 else "FAILED" | |
| analysis_duration = time.time() - analysis_start | |
| total_duration = time.time() - start_time | |
| print(f" -> JPG: {jpg_count}, PNG: {png_count}, Total: {total_count} files " | |
| f"(PhotoRec: {photorec_duration:.0f}s, Analysis: {analysis_duration:.0f}s, " | |
| f"Total: {total_duration:.0f}s) - {status}") | |
| if failed_files: | |
| print(f" -> Failed files: {len(failed_files)}") | |
| for failure in failed_files[:5]: # Show first 5 failures | |
| print(f" {failure}") | |
| if len(failed_files) > 5: | |
| print(f" ... and {len(failed_files) - 5} more") | |
| result = TestResult( | |
| name=test_name, | |
| description=description, | |
| args=args, | |
| jpg_count=jpg_count, | |
| png_count=png_count, | |
| total_count=total_count, | |
| duration=total_duration, | |
| photorec_duration=photorec_duration, | |
| analysis_duration=analysis_duration, | |
| status=status, | |
| failed_files=failed_files | |
| ) | |
| self.results.append(result) | |
| return result | |
| def analyze_baseline(self) -> Dict: | |
| """Analyze baseline files to determine test ranges""" | |
| print("Analyzing baseline files to determine test ranges...") | |
| baseline_files = (list(self.output_dir.glob("baseline.*/*.jpg")) + | |
| list(self.output_dir.glob("baseline.*/*.png"))) | |
| if not baseline_files: | |
| print(" Using fallback values - no baseline files found") | |
| return { | |
| 'p25_kb': 5, 'p50_kb': 10, 'p75_kb': 20, | |
| 'p50_width': 200, 'p50_height': 150, | |
| 'p75_width': 400, 'p75_height': 300, | |
| 'p50_pixels': 30000, 'p75_pixels': 100000 | |
| } | |
| # Analyze file sizes | |
| sizes = [f.stat().st_size for f in baseline_files if f.exists()] | |
| sizes.sort() | |
| def format_kb(bytes_val): | |
| return f"{bytes_val/1024:.1f}k" if bytes_val < 1024 else f"{bytes_val//1024}k" | |
| if sizes: | |
| percentiles = [sizes[i] for i in [len(sizes)//4, len(sizes)//2, 3*len(sizes)//4]] | |
| p25_kb, p50_kb, p75_kb = map(format_kb, percentiles) | |
| min_kb, max_kb = sizes[0] // 1024, sizes[-1] // 1024 | |
| else: | |
| min_kb, p25_kb, p50_kb, p75_kb, max_kb = 1, "0.5k", "1k", "2k", 100 | |
| print(f" File size analysis: {min_kb}k - {max_kb}k " | |
| f"(25%={p25_kb}, 50%={p50_kb}, 75%={p75_kb})") | |
| # Analyze dimensions (sample for performance) | |
| print(" Analyzing image dimensions...") | |
| dimensions_data = [self.get_image_dimensions(f) for f in baseline_files[:100]] | |
| valid_dims = [d for d in dimensions_data if d] | |
| if valid_dims: | |
| widths, heights = zip(*valid_dims) | |
| pixels = [w * h for w, h in valid_dims] | |
| def get_percentiles(data, defaults): | |
| sorted_data = sorted(data) | |
| p50 = max(defaults[0], sorted_data[len(sorted_data)//2]) | |
| p75 = max(defaults[1], sorted_data[3*len(sorted_data)//4]) | |
| return p50, p75 | |
| p50_width, p75_width = get_percentiles(widths, (200, 400)) | |
| p50_height, p75_height = get_percentiles(heights, (150, 300)) | |
| p50_pixels, p75_pixels = get_percentiles(pixels, (30000, 100000)) | |
| print(f" Dimension analysis: {min(widths)}x{min(heights)} - " | |
| f"{max(widths)}x{max(heights)} (median: {p50_width}x{p50_height})") | |
| print(f" Pixel analysis: {min(pixels)} - {max(pixels)} (median: {p50_pixels})") | |
| else: | |
| p50_width, p75_width = 200, 400 | |
| p50_height, p75_height = 150, 300 | |
| p50_pixels, p75_pixels = 30000, 100000 | |
| print(" Using fallback dimension values") | |
| return { | |
| 'p25_kb': p25_kb, 'p50_kb': p50_kb, 'p75_kb': p75_kb, | |
| 'p50_width': p50_width, 'p50_height': p50_height, | |
| 'p75_width': p75_width, 'p75_height': p75_height, | |
| 'p50_pixels': p50_pixels, 'p75_pixels': p75_pixels | |
| } | |
| def get_all_test_definitions(self, ranges): | |
| """Get all test definitions""" | |
| r = ranges # Short alias | |
| base_cmd = "imagesize" | |
| tests = { | |
| "baseline": ("", "No filters - reference test"), | |
| # File size tests | |
| "test_filesize_min": (f"{base_cmd},size,{r['p50_kb']}-", f"File size ≥{r['p50_kb']} (median+)"), | |
| "test_filesize_max": (f"{base_cmd},size,-{r['p75_kb']}", f"File size ≤{r['p75_kb']} (75th percentile)"), | |
| "test_filesize_range": (f"{base_cmd},size,{r['p25_kb']}-{r['p75_kb']}", f"File size {r['p25_kb']}-{r['p75_kb']} (IQR)"), | |
| "test_decimal_filesize": (f"{base_cmd},size,{r['p50_kb']}-{r['p75_kb']}", f"Decimal file size {r['p50_kb']}-{r['p75_kb']}"), | |
| "test_large_filesize": (f"{base_cmd},size,100k-", "Large files ≥100KB"), | |
| # Dimension tests | |
| "test_dimensions_min": (f"{base_cmd},width,{r['p50_width']}-,height,{r['p50_height']}-", f"Dimensions ≥{r['p50_width']}x{r['p50_height']} (median+)"), | |
| "test_dimensions_range": (f"{base_cmd},width,{r['p50_width']}-{r['p75_width']},height,{r['p50_height']}-{r['p75_height']}", f"Dimensions {r['p50_width']}-{r['p75_width']} x {r['p50_height']}-{r['p75_height']}"), | |
| "test_small_dimensions": (f"{base_cmd},width,-100,height,-100", "Small dimensions ≤100x100"), | |
| "test_square_images": (f"{base_cmd},width,100-500,height,100-500", "Square-ish images 100-500x100-500"), | |
| "test_restrictive": (f"{base_cmd},width,{r['p75_width'] * 2}-,height,{r['p75_height'] * 2}-", f"Restrictive filter ≥{r['p75_width'] * 2}x{r['p75_height'] * 2}"), | |
| # Pixel tests | |
| "test_pixels_min": (f"{base_cmd},pixels,{r['p50_pixels']}-", f"Pixels ≥{r['p50_pixels']} (median+)"), | |
| "test_pixels_range": (f"{base_cmd},pixels,{r['p50_pixels']}-{r['p75_pixels']}", f"Pixels {r['p50_pixels']}-{r['p75_pixels']} (median-75th)"), | |
| "test_pixels_format": (f"{base_cmd},pixels,{r['p50_width']}x{r['p50_height']}-{r['p75_width']}x{r['p75_height']}", f"Pixels WIDTHxHEIGHT format {r['p50_width']}x{r['p50_height']}-{r['p75_width']}x{r['p75_height']}"), | |
| "test_high_resolution": (f"{base_cmd},pixels,1000000-", "High resolution ≥1M pixels"), | |
| # Combined tests | |
| "test_combined": (f"{base_cmd},size,{r['p25_kb']}-,width,{r['p50_width']}-,height,{r['p50_height']}-", "Combined filters (filesize + dimensions)"), | |
| "test_mixed_units": (f"{base_cmd},size,50k-2m,width,300-", "Mixed: 50KB-2MB files, width ≥300px") | |
| } | |
| return tests | |
| def run_specific_tests(self, test_names: List[str]): | |
| """Run specific tests by name""" | |
| print(f"Starting PhotoRec Image Filter Test Suite (running: {', '.join(test_names)})...") | |
| # Always run baseline first if not already specified | |
| if "baseline" not in test_names: | |
| self.run_test("baseline", "", "No filters - reference test") | |
| # Analyze baseline to determine ranges | |
| ranges = self.analyze_baseline() | |
| # Get test definitions | |
| test_definitions = self.get_all_test_definitions(ranges) | |
| # Run specified tests | |
| for test_name in test_names: | |
| if test_name in test_definitions: | |
| args, description = test_definitions[test_name] | |
| self.run_test(test_name, args, description) | |
| else: | |
| print(f"Warning: Unknown test '{test_name}' - skipping") | |
| available_tests = list(test_definitions.keys()) | |
| print(f"Available tests: {', '.join(available_tests)}") | |
| print("\nSelected tests completed!") | |
| def run_all_tests(self): | |
| """Run the complete test suite""" | |
| print("Starting PhotoRec Image Filter Test Suite...") | |
| # Baseline test | |
| self.run_test("baseline", "", "No filters - reference test") | |
| # Analyze baseline to determine ranges | |
| ranges = self.analyze_baseline() | |
| # Get test definitions and run all | |
| test_definitions = self.get_all_test_definitions(ranges) | |
| for test_name, (args, description) in test_definitions.items(): | |
| if test_name != "baseline": # Already run | |
| self.run_test(test_name, args, description) | |
| print("\nAll tests completed!") | |
| def print_summary(self): | |
| """Print test summary""" | |
| total_tests = len(self.results) | |
| passed_tests = sum(1 for r in self.results if r.status == "PASSED") | |
| failed_tests = total_tests - passed_tests | |
| print(f"\n=== TEST SUMMARY ===") | |
| print(f"Total tests: {total_tests}") | |
| print(f"Passed: {passed_tests}") | |
| print(f"Failed: {failed_tests}") | |
| if failed_tests > 0: | |
| print(f"\nFAILED TESTS:") | |
| for result in self.results: | |
| if result.status == "FAILED": | |
| print(f" - {result.name}: {result.description}") | |
| if result.failed_files: | |
| print(f" Failed files: {len(result.failed_files)}") | |
| print(f"\nBaseline files: {self.baseline_count}") | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser(description='PhotoRec Image Filter Test Suite') | |
| parser.add_argument('tests', nargs='*', help='Specific tests to run (e.g., test_filesize_min test_filesize_max). If none specified, runs all tests.') | |
| parser.add_argument('--debug', action='store_true', help='Enable debug mode (show PhotoRec commands and output)') | |
| parser.add_argument('--list', action='store_true', help='List all available tests') | |
| parser.add_argument('--disk-image', default=os.path.expanduser("~/disk_30gb.img"), help='Path to disk image') | |
| parser.add_argument('--output-dir', default=os.path.expanduser("~/outputs"), help='Output directory') | |
| parser.add_argument('--photorec-bin', default="./src/photorec", help='PhotoRec binary path') | |
| args = parser.parse_args() | |
| # Configuration | |
| DISK_IMAGE = args.disk_image | |
| OUTPUT_DIR = args.output_dir | |
| PHOTOREC_BIN = args.photorec_bin | |
| try: | |
| # Initialize test suite | |
| suite = PhotoRecTestSuite(DISK_IMAGE, OUTPUT_DIR, PHOTOREC_BIN, debug=args.debug) | |
| if args.list: | |
| # Show available tests | |
| ranges = {'p25_kb': 5, 'p50_kb': 10, 'p75_kb': 20, 'p50_width': 200, 'p50_height': 150, 'p75_width': 400, 'p75_height': 300, 'p50_pixels': 30000, 'p75_pixels': 100000} | |
| test_definitions = suite.get_all_test_definitions(ranges) | |
| print("Available tests:") | |
| for test_name, (args, description) in test_definitions.items(): | |
| print(f" {test_name}: {description}") | |
| return | |
| # Clean previous results | |
| suite.cleanup_outputs() | |
| start_time = time.time() | |
| if args.tests: | |
| # Run specific tests | |
| suite.run_specific_tests(args.tests) | |
| else: | |
| # Run all tests | |
| suite.run_all_tests() | |
| total_time = time.time() - start_time | |
| # Print summary | |
| suite.print_summary() | |
| print(f"\nTotal execution time: {total_time:.1f}s") | |
| except FileNotFoundError as e: | |
| print(f"Error: {e}") | |
| sys.exit(1) | |
| except KeyboardInterrupt: | |
| print("\nTest suite interrupted by user") | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f"Unexpected error: {e}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment