|
|
| |
| """ |
| Comprehensive test script for data_utils.py |
| Tests the orchestration layer that combines transforms, url_dataset, and creates DataLoaders |
| Run from project root: python test_data_utils_comprehensive.py |
| """ |
|
|
| import sys |
| import os |
| from pathlib import Path |
| import torch |
| import numpy as np |
| from PIL import Image |
| import json |
| import tempfile |
| import traceback |
| import logging |
| from typing import Dict, List |
| from unittest.mock import patch, MagicMock |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') |
|
|
|
|
| class TestRunner: |
| """Test runner with results tracking""" |
|
|
| def __init__(self): |
| self.passed = 0 |
| self.failed = 0 |
| self.skipped = 0 |
| self.errors = [] |
|
|
| def run_test(self, test_name: str, test_func): |
| """Run a single test with error handling""" |
| print(f"\n{'=' * 60}") |
| print(f"Running: {test_name}") |
| print(f"{'=' * 60}") |
|
|
| try: |
| test_func() |
| self.passed += 1 |
| print(f"β
PASSED: {test_name}") |
| except FileNotFoundError as e: |
| self.skipped += 1 |
| print(f"β οΈ SKIPPED: {test_name} - {str(e)}") |
| except Exception as e: |
| self.failed += 1 |
| self.errors.append({ |
| 'test': test_name, |
| 'error': str(e), |
| 'traceback': traceback.format_exc() |
| }) |
| print(f"β FAILED: {test_name}") |
| print(f"Error: {str(e)}") |
| print(f"Traceback:\n{traceback.format_exc()}") |
|
|
| def print_summary(self): |
| """Print test summary""" |
| total = self.passed + self.failed + self.skipped |
| print(f"\n{'=' * 60}") |
| print(f"TEST SUMMARY") |
| print(f"{'=' * 60}") |
| print(f"Total tests: {total}") |
| print(f"β
Passed: {self.passed}") |
| print(f"β Failed: {self.failed}") |
| print(f"β οΈ Skipped: {self.skipped}") |
|
|
| if self.errors: |
| print(f"\nFailed tests:") |
| for error in self.errors: |
| print(f" - {error['test']}: {error['error']}") |
|
|
|
|
| def create_mock_model_configs(): |
| """Create mock model configs for testing""" |
| return { |
| 'efficientnet-b2': { |
| 'input_size': 260, |
| 'batch_size': 32, |
| }, |
| 'resnet50': { |
| 'input_size': 224, |
| 'batch_size': 32, |
| } |
| } |
|
|
|
|
| def create_mock_split_data(num_items=20, split_name="train"): |
| """Create mock dataset split data""" |
| decades = ['1960s', '1970s', '1980s', '1990s', '2000s'] |
| mock_data = [] |
|
|
| for i in range(num_items): |
| mock_data.append({ |
| "id": f"{split_name}_id_{i}", |
| "product_id": f"product_{i % 5}", |
| "name": f"Test Product {i}", |
| "decade": decades[i % 5], |
| "url": f"https://example.com/{split_name}_image_{i}.jpg", |
| "classification": f"furniture", |
| "makers": f"Test Maker {i % 3}", |
| "country": f"Country_{i % 3}" |
| }) |
|
|
| return mock_data |
|
|
|
|
| def create_test_environment(): |
| """Create a complete test environment with mock data""" |
| temp_dir = tempfile.mkdtemp() |
| temp_path = Path(temp_dir) |
|
|
| |
| data_dir = temp_path / "data" |
| splits_dir = data_dir / "splits" |
| cache_dir = data_dir / "cache" / "images" |
|
|
| splits_dir.mkdir(parents=True) |
| cache_dir.mkdir(parents=True) |
|
|
| |
| split_files = {} |
| for split_name in ["train", "val", "test"]: |
| split_data = create_mock_split_data(20 if split_name == "train" else 10, split_name) |
| split_file = splits_dir / f"{split_name}.json" |
|
|
| with open(split_file, 'w') as f: |
| json.dump(split_data, f, indent=2) |
|
|
| split_files[split_name] = split_file |
|
|
| return temp_path, data_dir, split_files, cache_dir |
|
|
|
|
| def test_imports(): |
| """Test that all required modules can be imported""" |
| print("Testing imports...") |
|
|
| try: |
| |
| current_dir = Path.cwd() |
| src_path = current_dir / "src" |
| if src_path.exists(): |
| sys.path.insert(0, str(current_dir)) |
|
|
| from src.data.data_utils import ( |
| create_data_loaders, create_weighted_sampler, analyze_dataset_splits, |
| create_test_loader, prepare_data_for_training, get_dataset_statistics |
| ) |
| from src.data.url_dataset import BaseDataset, URLDataset, CachedDataset |
| from src.data.transforms import get_transforms_for_model |
|
|
| print("β All data_utils imports successful") |
|
|
| return True |
|
|
| except ImportError as e: |
| print(f"β Import failed: {e}") |
| return False |
|
|
|
|
| def test_create_data_loaders(): |
| """Test create_data_loaders function""" |
| print("Testing create_data_loaders...") |
|
|
| |
| from src.data.data_utils import create_data_loaders |
| from torchvision import transforms |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| with patch('src.data.transforms.TRAINING_CONFIGS', create_mock_model_configs()): |
| |
| config = { |
| 'model_name': 'efficientnet-b2', |
| 'batch_size': 4, |
| 'num_workers': 0, |
| 'data_dir': str(data_dir), |
| 'use_cached': False, |
| 'use_class_weights': False, |
| 'use_weighted_sampling': False, |
| 'augmentation_level': 'medium', |
| 'max_download_retries': 1, |
| 'download_timeout': 5 |
| } |
|
|
| |
| mock_image = Image.fromarray(np.random.randint(0, 255, (200, 200, 3), dtype=np.uint8)) |
|
|
| def mock_load_image(self, item): |
| return mock_image |
|
|
| with patch('src.data.url_dataset.URLDataset._load_image', mock_load_image): |
| |
| train_loader, val_loader, class_weights, class_names = create_data_loaders( |
| config, |
| data_dir=data_dir, |
| use_subset=True, |
| subset_fraction=0.5 |
| ) |
|
|
| print(f"β Created DataLoaders successfully") |
| print(f" Train loader: {len(train_loader)} batches") |
| print(f" Val loader: {len(val_loader)} batches") |
| print(f" Class names: {class_names}") |
|
|
| |
| assert len(train_loader) > 0, "Train loader should have batches" |
| assert len(val_loader) > 0, "Val loader should have batches" |
| assert class_names == ['1960s', '1970s', '1980s', '1990s', '2000s'], "Wrong class names" |
| assert class_weights is None, "Class weights should be None when disabled" |
|
|
| |
| for batch_idx, batch_data in enumerate(train_loader): |
| if len(batch_data) == 3: |
| images, labels, metadata = batch_data |
| else: |
| images, labels = batch_data[0], batch_data[1] |
| metadata = None |
|
|
| print(f"β Loaded batch: images={images.shape}, labels={labels.shape}") |
|
|
| |
| assert images.shape[0] <= config['batch_size'], "Batch size exceeded" |
| assert images.shape[1] == 3, "Wrong number of channels" |
| assert labels.shape[0] == images.shape[0], "Label count mismatch" |
|
|
| break |
|
|
| finally: |
| |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_weighted_sampler(): |
| """Test create_weighted_sampler function""" |
| print("Testing create_weighted_sampler...") |
|
|
| from src.data.data_utils import create_weighted_sampler |
| from src.data.url_dataset import BaseDataset |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| imbalanced_data = [] |
| decades = ['1960s', '1970s', '1980s', '1990s', '2000s'] |
| |
| counts = [20, 5, 10, 3, 15] |
|
|
| item_id = 0 |
| for decade_idx, count in enumerate(counts): |
| for i in range(count): |
| imbalanced_data.append({ |
| "id": f"id_{item_id}", |
| "product_id": f"product_{item_id}", |
| "name": f"Product {item_id}", |
| "decade": decades[decade_idx], |
| "url": f"https://example.com/image_{item_id}.jpg", |
| "classification": "test", |
| "makers": "test", |
| "country": "test" |
| }) |
| item_id += 1 |
|
|
| |
| imbalanced_split = data_dir / "splits" / "imbalanced.json" |
| with open(imbalanced_split, 'w') as f: |
| json.dump(imbalanced_data, f) |
|
|
| |
| dataset = BaseDataset(str(imbalanced_split)) |
|
|
| print(f"β Created imbalanced dataset with {len(dataset)} samples") |
|
|
| |
| labels = dataset.get_labels() |
| from collections import Counter |
| original_counts = Counter(labels) |
| print(f" Original distribution: {dict(original_counts)}") |
|
|
| |
| sampler = create_weighted_sampler(dataset) |
|
|
| print(f"β Created weighted sampler with {len(sampler)} samples") |
|
|
| |
| sampled_indices = list(sampler)[:100] |
| sampled_labels = [labels[idx] for idx in sampled_indices] |
| sampled_counts = Counter(sampled_labels) |
|
|
| print(f" Sampled distribution: {dict(sampled_counts)}") |
|
|
| |
| original_ratio = max(original_counts.values()) / min(original_counts.values()) |
| sampled_ratio = max(sampled_counts.values()) / min(sampled_counts.values()) if min( |
| sampled_counts.values()) > 0 else float('inf') |
|
|
| print(f" Balance improvement: {original_ratio:.2f} -> {sampled_ratio:.2f}") |
|
|
| |
| assert sampled_ratio < original_ratio or sampled_ratio < 3.0, "Sampling should improve balance" |
|
|
| finally: |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_analyze_dataset_splits(): |
| """Test analyze_dataset_splits function""" |
| print("Testing analyze_dataset_splits...") |
|
|
| from src.data.data_utils import analyze_dataset_splits |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| analysis = analyze_dataset_splits(data_dir) |
|
|
| print(f"β Analysis completed for {len(analysis)} splits") |
|
|
| |
| expected_splits = ['train', 'val', 'test'] |
| for split_name in expected_splits: |
| assert split_name in analysis, f"Missing analysis for {split_name}" |
|
|
| split_stats = analysis[split_name] |
| print(f" {split_name}: {split_stats['total_images']} images, {split_stats['unique_products']} products") |
|
|
| |
| required_keys = ['total_images', 'unique_products', 'decades', 'classifications', 'countries'] |
| for key in required_keys: |
| assert key in split_stats, f"Missing key {key} in {split_name} analysis" |
|
|
| |
| decades_dist = split_stats['decades'] |
| assert len(decades_dist) == 5, f"Should have 5 decades, got {len(decades_dist)}" |
|
|
| |
| |
| if 'data_leakage' in analysis: |
| print(f" β οΈ Data leakage detected: {analysis['data_leakage']['train_val_overlap']} overlapping products") |
| else: |
| print(f" β No data leakage detected") |
|
|
| finally: |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_prepare_data_for_training(): |
| """Test prepare_data_for_training function""" |
| print("Testing prepare_data_for_training...") |
|
|
| from src.data.data_utils import prepare_data_for_training |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| config = { |
| 'data_dir': str(data_dir), |
| 'use_cached': False |
| } |
|
|
| |
| is_ready = prepare_data_for_training( |
| config, |
| download_if_missing=False, |
| verify_splits=False |
| ) |
|
|
| print(f"β Data preparation (no verification): {is_ready}") |
| assert is_ready == True, "Data preparation should succeed when verification disabled" |
|
|
| |
| is_ready_with_verification = prepare_data_for_training( |
| config, |
| download_if_missing=False, |
| verify_splits=True |
| ) |
|
|
| print(f"β Data preparation (with verification): {is_ready_with_verification}") |
| |
| assert is_ready_with_verification == False, "Data preparation should fail when data leakage detected" |
|
|
| |
| missing_split = data_dir / "splits" / "train.json" |
| missing_split.unlink() |
|
|
| is_ready_missing = prepare_data_for_training( |
| config, |
| download_if_missing=False, |
| verify_splits=False |
| ) |
|
|
| print(f"β Data preparation with missing files: {is_ready_missing}") |
| assert is_ready_missing == False, "Data preparation should fail with missing splits" |
|
|
| print("β All data preparation scenarios tested correctly") |
|
|
| finally: |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_get_dataset_statistics(): |
| """Test get_dataset_statistics function""" |
| print("Testing get_dataset_statistics...") |
|
|
| from src.data.data_utils import get_dataset_statistics |
| from src.data.url_dataset import BaseDataset |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| dataset = BaseDataset(str(split_files['train'])) |
|
|
| |
| stats = get_dataset_statistics(dataset) |
|
|
| print(f"β Generated statistics for dataset") |
|
|
| |
| required_keys = [ |
| 'total_samples', 'num_classes', 'class_names', |
| 'class_distribution', 'class_balance', 'product_stats', |
| 'top_classifications', 'top_countries' |
| ] |
|
|
| for key in required_keys: |
| assert key in stats, f"Missing statistics key: {key}" |
|
|
| print(f" Total samples: {stats['total_samples']}") |
| print(f" Number of classes: {stats['num_classes']}") |
| print(f" Class distribution: {stats['class_distribution']}") |
| print(f" Imbalance ratio: {stats['class_balance']['imbalance_ratio']:.2f}") |
| print(f" Unique products: {stats['product_stats']['unique_products']}") |
| print(f" Avg images per product: {stats['product_stats']['avg_images_per_product']:.2f}") |
|
|
| |
| assert stats['total_samples'] == len(dataset), "Wrong total samples count" |
| assert stats['num_classes'] == 5, "Wrong number of classes" |
| assert len(stats['class_names']) == 5, "Wrong number of class names" |
|
|
| finally: |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_create_test_loader(): |
| """Test create_test_loader function""" |
| print("Testing create_test_loader...") |
|
|
| from src.data.data_utils import create_test_loader |
|
|
| temp_path, data_dir, split_files, cache_dir = create_test_environment() |
|
|
| try: |
| |
| with patch('src.data.transforms.TRAINING_CONFIGS', create_mock_model_configs()): |
| config = { |
| 'model_name': 'resnet50', |
| 'batch_size': 4, |
| 'num_workers': 0, |
| 'data_dir': str(data_dir), |
| 'use_cached': False, |
| 'max_download_retries': 1, |
| 'download_timeout': 5 |
| } |
|
|
| |
| mock_image = Image.fromarray(np.random.randint(0, 255, (200, 200, 3), dtype=np.uint8)) |
|
|
| def mock_load_image(self, item): |
| return mock_image |
|
|
| with patch('src.data.url_dataset.URLDataset._load_image', mock_load_image): |
| test_loader, class_names = create_test_loader( |
| config, |
| data_dir=data_dir, |
| batch_size=6 |
| ) |
|
|
| print(f"β Created test loader with {len(test_loader)} batches") |
| print(f" Class names: {class_names}") |
|
|
| |
| assert len(test_loader) > 0, "Test loader should have batches" |
| assert class_names == ['1960s', '1970s', '1980s', '1990s', '2000s'], "Wrong class names" |
|
|
| |
| for batch_data in test_loader: |
| if len(batch_data) == 3: |
| images, labels, metadata = batch_data |
| else: |
| images, labels = batch_data[0], batch_data[1] |
|
|
| print(f"β Test batch: images={images.shape}, labels={labels.shape}") |
| assert images.shape[0] <= 6, "Batch size should respect config" |
| break |
|
|
| finally: |
| import shutil |
| shutil.rmtree(temp_path) |
|
|
|
|
| def test_integration_with_real_data(): |
| """Test integration with real project data if available""" |
| print("Testing integration with real data...") |
|
|
| |
| project_root = Path.cwd() |
| real_data_dir = project_root / "data" |
| real_splits_dir = real_data_dir / "splits" |
| real_train_split = real_splits_dir / "train.json" |
|
|
| if not real_train_split.exists(): |
| print("β οΈ Real data not found, skipping integration test") |
| raise FileNotFoundError("Real data not available") |
|
|
| from src.data.data_utils import create_data_loaders, analyze_dataset_splits |
|
|
| try: |
| |
| with patch('src.data.transforms.TRAINING_CONFIGS', create_mock_model_configs()): |
| print(f"β Found real data at {real_data_dir}") |
|
|
| |
| analysis = analyze_dataset_splits(real_data_dir) |
|
|
| for split_name, stats in analysis.items(): |
| if isinstance(stats, dict) and 'total_images' in stats: |
| print(f" {split_name}: {stats['total_images']} images") |
|
|
| |
| config = { |
| 'model_name': 'efficientnet-b2', |
| 'batch_size': 4, |
| 'num_workers': 0, |
| 'data_dir': str(real_data_dir), |
| 'use_cached': False, |
| 'use_class_weights': False, |
| 'use_weighted_sampling': False, |
| 'augmentation_level': 'light', |
| 'max_download_retries': 1, |
| 'download_timeout': 5 |
| } |
|
|
| |
| train_loader, val_loader, class_weights, class_names = create_data_loaders( |
| config, |
| data_dir=real_data_dir, |
| use_subset=True, |
| subset_fraction=0.001 |
| ) |
|
|
| print(f"β Real data integration successful") |
| print(f" Train batches: {len(train_loader)}") |
| print(f" Val batches: {len(val_loader)}") |
|
|
| except Exception as e: |
| print(f"β οΈ Real data integration test failed: {e}") |
| raise |
|
|
|
|
| def main(): |
| """Main test function""" |
| print("DATA_UTILS.PY COMPREHENSIVE TEST SUITE") |
| print("=" * 60) |
| print("Testing data orchestration and integration layer") |
|
|
| runner = TestRunner() |
|
|
| |
| if not test_imports(): |
| print("β Cannot proceed without successful imports") |
| return 1 |
|
|
| |
| tests = [ |
| ("Create Data Loaders", test_create_data_loaders), |
| ("Weighted Sampler", test_weighted_sampler), |
| ("Analyze Dataset Splits", test_analyze_dataset_splits), |
| ("Prepare Data for Training", test_prepare_data_for_training), |
| ("Get Dataset Statistics", test_get_dataset_statistics), |
| ("Create Test Loader", test_create_test_loader), |
| ("Integration with Real Data", test_integration_with_real_data), |
| ] |
|
|
| |
| for test_name, test_func in tests: |
| runner.run_test(test_name, test_func) |
|
|
| |
| runner.print_summary() |
|
|
| |
| success = runner.failed == 0 |
| if success: |
| print("\nπ ALL DATA_UTILS TESTS PASSED!") |
| print("β
The complete data pipeline is working correctly!") |
| print("\nYour data loading system is ready for:") |
| print(" π Model training") |
| print(" π Data analysis") |
| print(" π Production deployment") |
| else: |
| print("\nβ SOME TESTS FAILED!") |
| print("Check the issues above before proceeding to training") |
|
|
| return 0 if success else 1 |
|
|
|
|
| if __name__ == "__main__": |
| exit_code = main() |
| print(f"\nTest completed with exit code: {exit_code}") |