diff --git a/.gitignore b/.gitignore index 511e2c4..cbbf15f 100644 --- a/.gitignore +++ b/.gitignore @@ -107,7 +107,7 @@ ipython_config.py # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# NOTE: We DO NOT ignore poetry.lock - it should be committed # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. @@ -160,4 +160,45 @@ dmypy.json .pytype/ # Cython debug symbols -cython_debug/ \ No newline at end of file +cython_debug/ + +# Claude settings +.claude/* + +# Testing artifacts +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +coverage.xml +*.cover +*.py,cover +pytest_cache/ +.tox/ +.nox/ + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ +.python-version + +# Build artifacts +build/ +dist/ +*.egg-info/ +*.egg +.eggs/ +wheels/ +*.whl + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2eb22c8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,134 @@ +[tool.poetry] +name = "cogagent" +version = "0.1.0" +description = "CogAgent: A Visual Language Model for GUI Agents" +authors = ["CogAgent Team"] +readme = "README.md" +packages = [{include = "app"}, {include = "inference"}, {include = "finetune"}] + +[tool.poetry.dependencies] +python = "^3.10" +transformers = ">=4.47.0" +torch = ">=2.5.1" +torchvision = ">=0.20.0" +huggingface-hub = ">=0.25.1" +sentencepiece = ">=0.2.0" +jinja2 = ">=3.1.4" +pydantic = ">=2.9.2" +timm = ">=1.0.9" +tiktoken = ">=0.8.0" +numpy = "1.26.4" +accelerate = ">=1.1.1" +sentence-transformers = ">=3.1.1" +gradio = ">=5.23.2" +openai = ">=1.70.0" +einops = ">=0.8.0" +pillow = ">=10.4.0" +sse-starlette = ">=2.1.3" +bitsandbytes = ">=0.43.2" +spaces = ">=0.31.1" +pyautogui = ">=0.9.54" +pyperclip = ">=1.9.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.0" + +[tool.poetry.group.finetune] +optional = true + +[tool.poetry.group.finetune.dependencies] +nltk = ">=3.9.1" +jieba = ">=0.42.1" +"ruamel.yaml" = ">=0.18.10" +datasets = "*" +peft = ">0.15.1" +rouge-chinese = ">=1.0.3" + +[tool.poetry.group.vllm] +optional = true + +[tool.poetry.group.vllm.dependencies] +vllm = ">=0.6.6" + +[tool.poetry.scripts] +test = "pytest" +tests = "pytest" + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--cov=app", + "--cov=inference", + "--cov=finetune", + "--cov-branch", + "--cov-report=term-missing:skip-covered", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=80", + "-v", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +console_output_style = "progress" +filterwarnings = [ + "error", + "ignore::UserWarning", + "ignore::DeprecationWarning", +] + +[tool.coverage.run] +source = ["app", "inference", "finetune"] +branch = true +parallel = true +omit = [ + "*/tests/*", + "*/test_*", + "*/__pycache__/*", + "*/venv/*", + "*/.venv/*", + "*/dist/*", + "*/build/*", + "*.egg-info/*", +] + +[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", +] +precision = 2 +show_missing = true +skip_covered = false +skip_empty = true +sort = "Cover" + +[tool.coverage.html] +directory = "htmlcov" +title = "CogAgent Coverage Report" + +[tool.coverage.xml] +output = "coverage.xml" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/run_validation.py b/run_validation.py new file mode 100644 index 0000000..b0042ed --- /dev/null +++ b/run_validation.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Simple validation script to check testing infrastructure setup. +This can be run without Poetry installation to verify the structure. +""" + +import os +import sys +from pathlib import Path + +def check_file_exists(filepath, description): + """Check if a file exists and print result.""" + exists = Path(filepath).exists() + status = "✓" if exists else "✗" + print(f"{status} {description}: {filepath}") + return exists + +def check_directory_exists(dirpath, description): + """Check if a directory exists and print result.""" + exists = Path(dirpath).exists() and Path(dirpath).is_dir() + status = "✓" if exists else "✗" + print(f"{status} {description}: {dirpath}") + return exists + +def check_file_contains(filepath, search_text, description): + """Check if a file contains specific text.""" + try: + content = Path(filepath).read_text() + contains = search_text in content + status = "✓" if contains else "✗" + print(f"{status} {description}") + return contains + except: + print(f"✗ {description} (file not readable)") + return False + +def main(): + """Run validation checks.""" + print("Testing Infrastructure Validation") + print("=" * 50) + + all_checks_passed = True + + # Check project structure + print("\n1. Project Structure:") + all_checks_passed &= check_file_exists("pyproject.toml", "Poetry configuration file") + all_checks_passed &= check_directory_exists("tests", "Tests directory") + all_checks_passed &= check_directory_exists("tests/unit", "Unit tests directory") + all_checks_passed &= check_directory_exists("tests/integration", "Integration tests directory") + all_checks_passed &= check_file_exists("tests/__init__.py", "Tests package init") + all_checks_passed &= check_file_exists("tests/unit/__init__.py", "Unit tests init") + all_checks_passed &= check_file_exists("tests/integration/__init__.py", "Integration tests init") + all_checks_passed &= check_file_exists("tests/conftest.py", "Pytest configuration") + all_checks_passed &= check_file_exists("tests/test_setup_validation.py", "Validation tests") + + # Check pyproject.toml contents + print("\n2. Poetry Configuration:") + all_checks_passed &= check_file_contains("pyproject.toml", "[tool.poetry]", "Poetry section") + all_checks_passed &= check_file_contains("pyproject.toml", "[tool.poetry.dependencies]", "Dependencies section") + all_checks_passed &= check_file_contains("pyproject.toml", "[tool.poetry.group.dev.dependencies]", "Dev dependencies") + all_checks_passed &= check_file_contains("pyproject.toml", "pytest", "Pytest dependency") + all_checks_passed &= check_file_contains("pyproject.toml", "pytest-cov", "Coverage dependency") + all_checks_passed &= check_file_contains("pyproject.toml", "pytest-mock", "Mock dependency") + + # Check pytest configuration + print("\n3. Pytest Configuration:") + all_checks_passed &= check_file_contains("pyproject.toml", "[tool.pytest.ini_options]", "Pytest config section") + all_checks_passed &= check_file_contains("pyproject.toml", "[tool.coverage", "Coverage config section") + all_checks_passed &= check_file_contains("pyproject.toml", 'test = "pytest"', "Test script command") + all_checks_passed &= check_file_contains("pyproject.toml", 'tests = "pytest"', "Tests script command") + + # Check test markers + print("\n4. Test Markers:") + all_checks_passed &= check_file_contains("pyproject.toml", '"unit: Unit tests"', "Unit test marker") + all_checks_passed &= check_file_contains("pyproject.toml", '"integration: Integration tests"', "Integration test marker") + all_checks_passed &= check_file_contains("pyproject.toml", '"slow: Slow running tests"', "Slow test marker") + + # Check fixtures + print("\n5. Test Fixtures:") + all_checks_passed &= check_file_contains("tests/conftest.py", "def temp_dir", "Temp directory fixture") + all_checks_passed &= check_file_contains("tests/conftest.py", "def mock_model_config", "Model config fixture") + all_checks_passed &= check_file_contains("tests/conftest.py", "def mock_tokenizer", "Tokenizer fixture") + + # Check .gitignore updates + print("\n6. Git Configuration:") + all_checks_passed &= check_file_contains(".gitignore", ".claude/*", "Claude settings ignored") + all_checks_passed &= check_file_contains(".gitignore", "# NOTE: We DO NOT ignore poetry.lock", "Poetry lock note") + + # Summary + print("\n" + "=" * 50) + if all_checks_passed: + print("✓ All validation checks passed!") + print("\nNext steps:") + print("1. Run: poetry install --with dev") + print("2. Run: poetry run test") + print("3. Run: poetry run tests") + return 0 + else: + print("✗ Some validation checks failed!") + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ 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..19ef760 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,161 @@ +import os +import sys +import tempfile +import shutil +from pathlib import Path +from typing import Generator, Dict, Any +import pytest +from unittest.mock import Mock, MagicMock + +# Add project root to Python path +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Provide a temporary directory that is cleaned up after the test.""" + temp_path = Path(tempfile.mkdtemp()) + yield temp_path + shutil.rmtree(temp_path) + + +@pytest.fixture +def temp_file(temp_dir: Path) -> Generator[Path, None, None]: + """Provide a temporary file within a temporary directory.""" + temp_file_path = temp_dir / "test_file.txt" + temp_file_path.write_text("Test content") + yield temp_file_path + + +@pytest.fixture +def mock_model_config() -> Dict[str, Any]: + """Provide a mock configuration for model testing.""" + return { + "model_name": "cogagent-test", + "model_path": "/path/to/model", + "device": "cpu", + "max_length": 2048, + "temperature": 0.7, + "top_p": 0.95, + "num_beams": 1, + } + + +@pytest.fixture +def mock_transformers_model() -> Mock: + """Provide a mock transformers model.""" + model = MagicMock() + model.generate.return_value = MagicMock() + model.config = MagicMock( + max_position_embeddings=2048, + hidden_size=768, + num_attention_heads=12, + ) + return model + + +@pytest.fixture +def mock_tokenizer() -> Mock: + """Provide a mock tokenizer.""" + tokenizer = MagicMock() + tokenizer.encode.return_value = [1, 2, 3, 4, 5] + tokenizer.decode.return_value = "Mock decoded text" + tokenizer.pad_token_id = 0 + tokenizer.eos_token_id = 1 + return tokenizer + + +@pytest.fixture +def mock_image_data() -> bytes: + """Provide mock image data for testing.""" + # Simple 1x1 PNG image + return ( + b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00' + b'\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r' + b'IDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x01\x00\x18\xdd\x8d\xb4' + b'\x00\x00\x00\x00IEND\xaeB`\x82' + ) + + +@pytest.fixture +def mock_openai_client() -> Mock: + """Provide a mock OpenAI client.""" + client = MagicMock() + response = MagicMock() + response.choices = [MagicMock(message=MagicMock(content="Mock response"))] + client.chat.completions.create.return_value = response + return client + + +@pytest.fixture +def mock_gradio_interface() -> Mock: + """Provide a mock Gradio interface.""" + interface = MagicMock() + interface.launch.return_value = None + return interface + + +@pytest.fixture +def env_setup(monkeypatch) -> None: + """Set up test environment variables.""" + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "-1") + monkeypatch.setenv("TRANSFORMERS_CACHE", "/tmp/test_cache") + monkeypatch.setenv("HF_HOME", "/tmp/test_hf_home") + + +@pytest.fixture +def mock_torch_cuda(monkeypatch) -> None: + """Mock torch CUDA availability for CPU testing.""" + import torch + monkeypatch.setattr(torch.cuda, "is_available", lambda: False) + monkeypatch.setattr(torch.cuda, "device_count", lambda: 0) + + +@pytest.fixture(autouse=True) +def cleanup_imports(): + """Clean up imports after each test to avoid side effects.""" + modules_to_remove = [ + mod for mod in sys.modules + if mod.startswith(('app.', 'inference.', 'finetune.')) + ] + yield + for mod in modules_to_remove: + sys.modules.pop(mod, None) + + +@pytest.fixture +def capture_logs(caplog): + """Capture and return logs for testing.""" + with caplog.at_level("DEBUG"): + yield caplog + + +@pytest.fixture +def mock_subprocess_run(monkeypatch) -> Mock: + """Mock subprocess.run for testing command execution.""" + mock_run = Mock() + mock_run.return_value.returncode = 0 + mock_run.return_value.stdout = "Success" + mock_run.return_value.stderr = "" + monkeypatch.setattr("subprocess.run", mock_run) + return mock_run + + +# Markers for test categorization +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line("markers", "unit: Unit tests") + config.addinivalue_line("markers", "integration: Integration tests") + config.addinivalue_line("markers", "slow: Slow running tests") + + +# Hooks for test execution +def pytest_collection_modifyitems(config, items): + """Modify test collection to add markers based on test location.""" + for item in items: + # Add markers based on test file location + if "unit" in str(item.fspath): + item.add_marker(pytest.mark.unit) + elif "integration" in str(item.fspath): + item.add_marker(pytest.mark.integration) \ 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..51cf6ae --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,102 @@ +import sys +import os +from pathlib import Path +import pytest + + +class TestSetupValidation: + """Validation tests to ensure the testing infrastructure is properly configured.""" + + def test_python_path_includes_project_root(self): + """Test that the project root is in the Python path.""" + project_root = Path(__file__).parent.parent + assert str(project_root) in sys.path, "Project root should be in Python path" + + def test_pytest_is_available(self): + """Test that pytest is properly installed.""" + import pytest + assert pytest.__version__, "pytest should be installed with a valid version" + + def test_coverage_is_available(self): + """Test that pytest-cov is properly installed.""" + import pytest_cov + assert pytest_cov.__version__, "pytest-cov should be installed" + + def test_mock_is_available(self): + """Test that pytest-mock is properly installed.""" + import pytest_mock + assert pytest_mock.__version__, "pytest-mock should be installed" + + def test_project_modules_are_importable(self): + """Test that project modules can be imported.""" + # These imports should work if the project structure is correct + modules_to_test = [ + "app", + "inference", + "finetune", + ] + + for module in modules_to_test: + try: + __import__(module) + except ImportError as e: + if "No module named" not in str(e): + # Module exists but has other import errors - that's OK for this test + pass + else: + pytest.fail(f"Module {module} should be importable") + + def test_fixtures_are_available(self, temp_dir, mock_model_config): + """Test that custom fixtures from conftest.py are available.""" + assert temp_dir.exists(), "temp_dir fixture should provide existing directory" + assert isinstance(mock_model_config, dict), "mock_model_config should be a dictionary" + assert "model_name" in mock_model_config, "mock_model_config should contain model_name" + + @pytest.mark.unit + def test_unit_marker_works(self): + """Test that the unit test marker is properly registered.""" + assert True, "Unit marker should work" + + @pytest.mark.integration + def test_integration_marker_works(self): + """Test that the integration test marker is properly registered.""" + assert True, "Integration marker should work" + + @pytest.mark.slow + def test_slow_marker_works(self): + """Test that the slow test marker is properly registered.""" + assert True, "Slow marker should work" + + def test_test_directories_exist(self): + """Test that all test directories are properly created.""" + test_root = Path(__file__).parent + assert test_root.exists(), "tests directory should exist" + assert (test_root / "unit").exists(), "tests/unit directory should exist" + assert (test_root / "integration").exists(), "tests/integration directory should exist" + assert (test_root / "conftest.py").exists(), "conftest.py should exist" + + def test_pyproject_toml_exists(self): + """Test that pyproject.toml is properly created.""" + project_root = Path(__file__).parent.parent + pyproject_path = project_root / "pyproject.toml" + assert pyproject_path.exists(), "pyproject.toml should exist" + + # Check that it contains expected sections + content = pyproject_path.read_text() + assert "[tool.poetry]" in content, "pyproject.toml should contain Poetry configuration" + assert "[tool.pytest.ini_options]" in content, "pyproject.toml should contain pytest configuration" + assert "[tool.coverage" in content, "pyproject.toml should contain coverage configuration" + + def test_poetry_scripts_configured(self): + """Test that Poetry scripts for running tests are configured.""" + project_root = Path(__file__).parent.parent + pyproject_path = project_root / "pyproject.toml" + content = pyproject_path.read_text() + + assert 'test = "pytest"' in content, "Poetry script 'test' should be configured" + assert 'tests = "pytest"' in content, "Poetry script 'tests' should be configured" + + +if __name__ == "__main__": + # This allows running the validation directly with python + pytest.main([__file__, "-v"]) \ 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