diff --git a/.gitignore b/.gitignore index 7c104ca..2dc40a9 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,10 @@ dmypy.json # idea *.idea +.vscode/ +*.swp +*.swo +*~ # inference_result inference_results* @@ -146,4 +150,56 @@ onnx_models* det_models* rec_models* *副本* -*测试数据* \ No newline at end of file +*测试数据* + +# Claude +.claude/* + +# Poetry +poetry.lock + +# Testing +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +coverage.xml +*.cover +.hypothesis/ +pytest_cache/ +.tox/ + +# Build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ +wheels/ +pip-wheel-metadata/ + +# Virtual environments +.env +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +virtualenv/ + +# IDE files +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace + +# OS files +.DS_Store +Thumbs.db +*.bak + +# Temporary files +*.tmp +*.temp +.tmp/ +.temp/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d3cf2fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,195 @@ +[tool.poetry] +name = "paddleocr2pytorch" +version = "0.1.0" +description = "PaddleOCR to PyTorch conversion and implementation" +authors = ["Your Name "] +readme = "README.md" +license = "Apache-2.0" +homepage = "https://github.com/frotms/PaddleOCR2Pytorch" +repository = "https://github.com/frotms/PaddleOCR2Pytorch" +documentation = "https://github.com/frotms/PaddleOCR2Pytorch" +keywords = ["ocr", "pytorch", "paddleocr", "text-detection", "text-recognition"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] +packages = [ + { include = "pytorchocr" }, + { include = "ptstructure" }, + { include = "converter" }, + { include = "misc" }, + { include = "tools" } +] + +[tool.poetry.dependencies] +python = "^3.8.1" +torch = ">=1.12.0" +torchvision = ">=0.13.0" +numpy = "^1.21.0" +opencv-python = "^4.5.0" +pillow = "^9.0.0" +pyyaml = "^6.0" +tqdm = "^4.64.0" +scipy = "^1.9.0" +shapely = "^2.0.0" +scikit-image = "^0.19.0" +pyclipper = "^1.3.0" +lmdb = "^1.3.0" +rapidfuzz = "^3.0.0" +openpyxl = "^3.0.0" +attrdict = "^2.0.0" +Polygon3 = "^3.0.0" +lanms-neo = "^1.0.0" +visualdl = "^2.5.0" +flask = "^2.2.0" +flask-cors = "^3.0.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.0" +pytest-xdist = "^3.3.0" +pytest-timeout = "^2.1.0" +black = "^23.3.0" +isort = "^5.12.0" +flake8 = "^6.0.0" +mypy = "^1.4.0" +pre-commit = "^3.3.0" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--cov=pytorchocr", + "--cov=ptstructure", + "--cov=converter", + "--cov=misc", + "--cov=tools", + "--cov-branch", + "--cov-report=term-missing:skip-covered", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=80", + "-vv", + "--tb=short", + "--maxfail=1", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", +] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[tool.coverage.run] +branch = true +source = ["pytorchocr", "ptstructure", "converter", "misc", "tools"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/site-packages/*", + "*/dist-packages/*", + "*/.venv/*", + "*/venv/*", + "*/.tox/*", + "*/.coverage/*", + "*/htmlcov/*", + "*/.pytest_cache/*", + "*/__init__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "def __str__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", + "@abstractmethod", + "@abc.abstractmethod", + "except ImportError:", + "except KeyError:", + "except AttributeError:", + "pass", +] +precision = 2 +show_missing = true +skip_covered = false +fail_under = 80 + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[tool.isort] +profile = "black" +line_length = 120 +skip_gitignore = true +force_single_line = false +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true + +[tool.black] +line-length = 120 +target-version = ['py38', 'py39', 'py310', 'py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist + | htmlcov +)/ +''' + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +ignore_missing_imports = true +exclude = [ + "tests/", + "build/", + "dist/", + ".venv/", + "venv/", +] \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..eb094d0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,181 @@ +""" +Shared pytest fixtures and configuration for all tests. +""" + +import os +import shutil +import tempfile +from pathlib import Path +from typing import Generator, Dict, Any + +import pytest +import torch +import numpy as np +from PIL import Image + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory for test files.""" + temp_path = tempfile.mkdtemp() + yield Path(temp_path) + shutil.rmtree(temp_path) + + +@pytest.fixture +def sample_image(temp_dir: Path) -> Path: + """Create a sample image for testing.""" + img_path = temp_dir / "test_image.jpg" + img = Image.new('RGB', (640, 480), color='white') + img.save(img_path) + return img_path + + +@pytest.fixture +def sample_numpy_image() -> np.ndarray: + """Create a sample numpy array image.""" + return np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) + + +@pytest.fixture +def sample_tensor_image() -> torch.Tensor: + """Create a sample torch tensor image.""" + return torch.rand(3, 480, 640) + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Create a mock configuration dictionary.""" + return { + "Architecture": { + "model_type": "det", + "algorithm": "DB", + "Transform": None, + "Backbone": { + "name": "MobileNetV3", + "scale": 0.5, + "model_name": "large", + }, + "Neck": { + "name": "DBFPN", + "out_channels": 256, + }, + "Head": { + "name": "DBHead", + "k": 50, + }, + }, + "Global": { + "use_gpu": False, + "epoch_num": 500, + "log_smooth_window": 20, + "print_batch_step": 10, + "save_model_dir": "./output/db_mv3/", + "save_epoch_step": 10, + "eval_batch_step": [0, 100], + "pretrained_model": None, + "checkpoints": None, + "save_inference_dir": None, + }, + } + + +@pytest.fixture +def mock_yaml_config(temp_dir: Path, mock_config: Dict[str, Any]) -> Path: + """Create a mock YAML configuration file.""" + import yaml + + config_path = temp_dir / "config.yml" + with open(config_path, 'w') as f: + yaml.dump(mock_config, f) + return config_path + + +@pytest.fixture +def mock_model_path(temp_dir: Path) -> Path: + """Create a mock model file path.""" + model_path = temp_dir / "model.pth" + torch.save({"state_dict": {}, "config": {}}, model_path) + return model_path + + +@pytest.fixture +def sample_text_file(temp_dir: Path) -> Path: + """Create a sample text file.""" + text_path = temp_dir / "sample.txt" + text_path.write_text("Sample text for testing\nLine 2\nLine 3") + return text_path + + +@pytest.fixture +def sample_dict_file(temp_dir: Path) -> Path: + """Create a sample dictionary file for character recognition.""" + dict_path = temp_dir / "dict.txt" + chars = ['a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4', ' '] + dict_path.write_text('\n'.join(chars)) + return dict_path + + +@pytest.fixture(autouse=True) +def reset_random_seeds(): + """Reset random seeds before each test for reproducibility.""" + np.random.seed(42) + torch.manual_seed(42) + if torch.cuda.is_available(): + torch.cuda.manual_seed(42) + torch.cuda.manual_seed_all(42) + + +@pytest.fixture +def gpu_available() -> bool: + """Check if GPU is available for testing.""" + return torch.cuda.is_available() + + +@pytest.fixture +def mock_db_config() -> Dict[str, Any]: + """Mock configuration for DB text detection.""" + return { + "thresh": 0.3, + "box_thresh": 0.6, + "max_candidates": 1000, + "unclip_ratio": 1.5, + "use_dilation": False, + "score_mode": "fast", + } + + +@pytest.fixture +def mock_rec_config() -> Dict[str, Any]: + """Mock configuration for text recognition.""" + return { + "character_dict_path": "./ppocr/utils/ppocr_keys_v1.txt", + "use_space_char": True, + "max_text_length": 25, + "limited_max_width": 1280, + "limited_min_width": 16, + } + + +@pytest.fixture +def cleanup_cuda(): + """Cleanup CUDA cache after tests.""" + yield + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + +def pytest_configure(config): + """Configure pytest with custom settings.""" + config.addinivalue_line( + "markers", "gpu: mark test as requiring GPU" + ) + config.addinivalue_line( + "markers", "slow: mark test as slow running" + ) + config.addinivalue_line( + "markers", "unit: mark test as unit test" + ) + config.addinivalue_line( + "markers", "integration: mark test as integration test" + ) \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_setup_validation.py b/tests/test_setup_validation.py new file mode 100644 index 0000000..eca3ae8 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,157 @@ +""" +Validation tests to ensure the testing infrastructure is properly set up. +""" + +import pytest +import sys +import os +from pathlib import Path + + +class TestSetupValidation: + """Test class to validate the testing infrastructure setup.""" + + @pytest.mark.unit + def test_pytest_installed(self): + """Test that pytest is properly installed.""" + assert "pytest" in sys.modules or True + + @pytest.mark.unit + def test_project_structure_exists(self): + """Test that the expected project structure exists.""" + project_root = Path(__file__).parent.parent + + # Check main package directories + assert (project_root / "pytorchocr").exists() + assert (project_root / "ptstructure").exists() + assert (project_root / "converter").exists() + assert (project_root / "misc").exists() + assert (project_root / "tools").exists() + + # Check test directories + assert (project_root / "tests").exists() + assert (project_root / "tests" / "unit").exists() + assert (project_root / "tests" / "integration").exists() + assert (project_root / "tests" / "conftest.py").exists() + + @pytest.mark.unit + def test_configuration_files_exist(self): + """Test that configuration files exist.""" + project_root = Path(__file__).parent.parent + + assert (project_root / "pyproject.toml").exists() + assert (project_root / ".gitignore").exists() + + @pytest.mark.unit + def test_fixtures_available(self, temp_dir, sample_image, mock_config): + """Test that pytest fixtures are working correctly.""" + assert temp_dir.exists() + assert temp_dir.is_dir() + + assert sample_image.exists() + assert sample_image.suffix == ".jpg" + + assert isinstance(mock_config, dict) + assert "Architecture" in mock_config + assert "Global" in mock_config + + @pytest.mark.unit + def test_coverage_configured(self): + """Test that coverage is properly configured.""" + try: + import coverage + assert True + except ImportError: + pytest.fail("Coverage module not installed") + + @pytest.mark.unit + def test_mock_configured(self): + """Test that pytest-mock is properly configured.""" + try: + from pytest_mock import MockerFixture + assert True + except ImportError: + pytest.fail("pytest-mock module not installed") + + @pytest.mark.unit + def test_markers_registered(self, pytestconfig): + """Test that custom markers are registered.""" + markers = pytestconfig.getini("markers") + marker_names = [line.split(":")[0].strip() for line in markers if line.strip()] + + assert "unit" in marker_names + assert "integration" in marker_names + assert "slow" in marker_names + + @pytest.mark.unit + def test_import_main_modules(self): + """Test that main project modules can be imported.""" + try: + import pytorchocr + import ptstructure + import converter + import misc + import tools + assert True + except ImportError as e: + pytest.skip(f"Module import failed (expected before installation): {e}") + + @pytest.mark.integration + def test_integration_marker(self): + """Test that integration marker works.""" + assert True + + @pytest.mark.slow + def test_slow_marker(self): + """Test that slow marker works.""" + import time + time.sleep(0.1) + assert True + + @pytest.mark.unit + @pytest.mark.parametrize("value,expected", [ + (1, 1), + (2, 2), + (3, 3), + ]) + def test_parametrize_works(self, value, expected): + """Test that parametrize decorator works.""" + assert value == expected + + @pytest.mark.unit + def test_mocker_fixture(self, mocker): + """Test that mocker fixture from pytest-mock works.""" + mock_func = mocker.Mock(return_value=42) + result = mock_func() + + assert result == 42 + mock_func.assert_called_once() + + @pytest.mark.unit + def test_numpy_available(self): + """Test that numpy is available for tests.""" + try: + import numpy as np + arr = np.array([1, 2, 3]) + assert len(arr) == 3 + except ImportError: + pytest.skip("NumPy not yet installed") + + @pytest.mark.unit + def test_torch_available(self): + """Test that PyTorch is available for tests.""" + try: + import torch + tensor = torch.tensor([1., 2., 3.]) + assert tensor.shape[0] == 3 + except ImportError: + pytest.skip("PyTorch not yet installed") + + @pytest.mark.unit + def test_temporary_file_handling(self, temp_dir): + """Test temporary file handling in tests.""" + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + + assert test_file.exists() + assert test_file.read_text() == "test content" \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29