diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91c7cea --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +# Note: Do NOT ignore poetry.lock - it should be committed +dist/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Claude settings +.claude/* + +# Testing specific +test-results/ +.benchmarks/ + +# Temporary files +*.tmp +*.temp +*.bak + +# OS specific +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.lnk \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e737809 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# Project Context + +This file contains important context and notes for Claude Code. + +## Testing Commands +- Run tests: `poetry run test` or `poetry run tests` +- Run specific test: `poetry run pytest tests/path/to/test.py` +- Run with coverage: `poetry run pytest --cov` +- Run unit tests only: `poetry run pytest -m unit` +- Run integration tests only: `poetry run pytest -m integration` +- Run without slow tests: `poetry run pytest -m "not slow"` + +## Linting Commands +- To be determined after setup + +## Project Structure +- Tests are located in `tests/` directory +- Unit tests in `tests/unit/` +- Integration tests in `tests/integration/` +- Test configuration in `pyproject.toml` +- Shared fixtures in `tests/conftest.py` + +## Package Management +- Using Poetry for dependency management +- Install dependencies: `poetry install` +- Add new dependency: `poetry add ` +- Add dev dependency: `poetry add --group dev ` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0d8b13b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[tool.poetry] +name = "web-thinker" +version = "0.1.0" +description = "A project for web search and thinking capabilities" +authors = ["Your Name "] +readme = "README.md" +packages = [{include = "scripts"}, {include = "demo"}] + +[tool.poetry.dependencies] +python = "^3.9" +torch = "^2.5.0" +transformers = "^4.46.0" +sentencepiece = "^0.2.0" +# vllm = "0.6.4" # Commented out due to system dependencies +tqdm = "^4.67.0" +nltk = "^3.9.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +pytest-cov = "^5.0.0" +pytest-mock = "^3.14.0" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--verbose", + "--cov=scripts", + "--cov=demo", + "--cov-report=html", + "--cov-report=xml", + "--cov-report=term-missing", + # "--cov-fail-under=80", # Uncomment when actual tests are written +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", +] + +[tool.coverage.run] +source = ["scripts", "demo"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/test_*.py", + "*/*_test.py", + "*/conftest.py", + "*/setup.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if __name__ == .__main__.:", + "raise AssertionError", + "raise NotImplementedError", + "if TYPE_CHECKING:", +] +precision = 2 +show_missing = true +skip_covered = false + +[tool.coverage.html] +directory = "htmlcov" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..3a12369 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package initialization \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0f53782 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,230 @@ +"""Shared pytest fixtures and configuration for all tests.""" + +import json +import os +import shutil +import tempfile +from pathlib import Path +from typing import Dict, Any, Generator +from unittest.mock import Mock, MagicMock + +import pytest + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory that is cleaned up after the test.""" + temp_path = Path(tempfile.mkdtemp()) + yield temp_path + shutil.rmtree(temp_path, ignore_errors=True) + + +@pytest.fixture +def temp_file(temp_dir: Path) -> Generator[Path, None, None]: + """Create a temporary file in the temp directory.""" + temp_path = temp_dir / "test_file.txt" + temp_path.write_text("test content") + yield temp_path + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Provide a mock configuration dictionary.""" + return { + "api_key": "test_api_key", + "model": "test_model", + "temperature": 0.7, + "max_tokens": 1000, + "timeout": 30, + "base_url": "https://api.example.com", + "debug": True, + } + + +@pytest.fixture +def sample_json_data() -> Dict[str, Any]: + """Provide sample JSON data for testing.""" + return { + "id": "test_123", + "name": "Test Item", + "description": "A test item for unit testing", + "metadata": { + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "tags": ["test", "sample", "fixture"], + }, + "items": [ + {"id": 1, "value": "first"}, + {"id": 2, "value": "second"}, + {"id": 3, "value": "third"}, + ], + } + + +@pytest.fixture +def json_file(temp_dir: Path, sample_json_data: Dict[str, Any]) -> Path: + """Create a temporary JSON file with sample data.""" + json_path = temp_dir / "test_data.json" + with open(json_path, "w") as f: + json.dump(sample_json_data, f, indent=2) + return json_path + + +@pytest.fixture +def mock_api_client() -> Mock: + """Create a mock API client for testing.""" + client = Mock() + client.get = MagicMock(return_value={"status": "success", "data": []}) + client.post = MagicMock(return_value={"status": "created", "id": "new_123"}) + client.put = MagicMock(return_value={"status": "updated"}) + client.delete = MagicMock(return_value={"status": "deleted"}) + return client + + +@pytest.fixture +def mock_llm_response() -> Dict[str, Any]: + """Provide a mock LLM response structure.""" + return { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "This is a test response from the LLM.", + }, + "finish_reason": "stop", + }], + "usage": { + "prompt_tokens": 50, + "completion_tokens": 20, + "total_tokens": 70, + }, + } + + +@pytest.fixture +def mock_search_results() -> list[Dict[str, Any]]: + """Provide mock search results for testing.""" + return [ + { + "title": "First Result", + "url": "https://example.com/1", + "snippet": "This is the first search result", + "score": 0.95, + }, + { + "title": "Second Result", + "url": "https://example.com/2", + "snippet": "This is the second search result", + "score": 0.87, + }, + { + "title": "Third Result", + "url": "https://example.com/3", + "snippet": "This is the third search result", + "score": 0.73, + }, + ] + + +@pytest.fixture +def env_vars(monkeypatch) -> Dict[str, str]: + """Set and clean up environment variables for testing.""" + test_vars = { + "TEST_API_KEY": "test_key_12345", + "TEST_ENV": "testing", + "DEBUG": "true", + } + + for key, value in test_vars.items(): + monkeypatch.setenv(key, value) + + return test_vars + + +@pytest.fixture +def mock_file_system(temp_dir: Path) -> Dict[str, Path]: + """Create a mock file system structure for testing.""" + structure = { + "data": temp_dir / "data", + "outputs": temp_dir / "outputs", + "logs": temp_dir / "logs", + "cache": temp_dir / "cache", + } + + for dir_path in structure.values(): + dir_path.mkdir(parents=True, exist_ok=True) + + # Create some sample files + (structure["data"] / "sample.txt").write_text("Sample data content") + (structure["outputs"] / "result.json").write_text('{"result": "success"}') + + return structure + + +@pytest.fixture(autouse=True) +def reset_singletons(): + """Reset any singleton instances between tests.""" + # This is a placeholder for resetting global state + # Add any singleton reset logic here if needed + yield + + +@pytest.fixture +def capture_logs(caplog): + """Fixture to capture log messages during tests.""" + with caplog.at_level("DEBUG"): + yield caplog + + +@pytest.fixture +def benchmark_timer(): + """Simple timer fixture for performance testing.""" + import time + + class Timer: + def __init__(self): + self.start_time = None + self.end_time = None + + def start(self): + self.start_time = time.time() + + def stop(self): + self.end_time = time.time() + + @property + def elapsed(self): + if self.start_time and self.end_time: + return self.end_time - self.start_time + return None + + return Timer() + + +# Pytest configuration hooks + +def pytest_configure(config): + """Configure pytest with custom settings.""" + config.addinivalue_line( + "markers", "unit: Mark test as a unit test" + ) + config.addinivalue_line( + "markers", "integration: Mark test as an integration test" + ) + config.addinivalue_line( + "markers", "slow: Mark test as slow running" + ) + + +def pytest_collection_modifyitems(config, items): + """Modify test collection to add markers based on location.""" + for item in items: + # Add markers based on test 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..f73c813 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +# Integration tests package initialization \ No newline at end of file diff --git a/tests/test_infrastructure_validation.py b/tests/test_infrastructure_validation.py new file mode 100644 index 0000000..26f00d5 --- /dev/null +++ b/tests/test_infrastructure_validation.py @@ -0,0 +1,180 @@ +"""Validation tests to ensure the testing infrastructure is properly configured.""" + +import json +import os +from pathlib import Path +from unittest.mock import Mock + +import pytest + + +class TestInfrastructureValidation: + """Test class to validate that the testing infrastructure is working correctly.""" + + def test_pytest_is_working(self): + """Basic test to ensure pytest is running.""" + assert True + assert 1 + 1 == 2 + + def test_temp_dir_fixture(self, temp_dir): + """Test that the temp_dir fixture creates a directory.""" + assert temp_dir.exists() + assert temp_dir.is_dir() + + # Test we can create files in it + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + assert test_file.exists() + assert test_file.read_text() == "test content" + + def test_temp_file_fixture(self, temp_file): + """Test that the temp_file fixture creates a file.""" + assert temp_file.exists() + assert temp_file.is_file() + assert temp_file.read_text() == "test content" + + def test_mock_config_fixture(self, mock_config): + """Test that the mock_config fixture provides expected data.""" + assert isinstance(mock_config, dict) + assert "api_key" in mock_config + assert mock_config["api_key"] == "test_api_key" + assert mock_config["temperature"] == 0.7 + + def test_sample_json_data_fixture(self, sample_json_data): + """Test that the sample_json_data fixture provides valid JSON data.""" + assert isinstance(sample_json_data, dict) + assert sample_json_data["id"] == "test_123" + assert len(sample_json_data["items"]) == 3 + assert "metadata" in sample_json_data + + def test_json_file_fixture(self, json_file, sample_json_data): + """Test that the json_file fixture creates a valid JSON file.""" + assert json_file.exists() + assert json_file.suffix == ".json" + + # Read and validate the content + with open(json_file) as f: + loaded_data = json.load(f) + + assert loaded_data == sample_json_data + + def test_mock_api_client_fixture(self, mock_api_client): + """Test that the mock_api_client fixture works correctly.""" + assert isinstance(mock_api_client, Mock) + + # Test GET + response = mock_api_client.get("/test") + assert response["status"] == "success" + + # Test POST + response = mock_api_client.post("/test", data={"test": "data"}) + assert response["status"] == "created" + assert "id" in response + + def test_env_vars_fixture(self, env_vars): + """Test that the env_vars fixture sets environment variables.""" + assert os.environ.get("TEST_API_KEY") == "test_key_12345" + assert os.environ.get("TEST_ENV") == "testing" + assert os.environ.get("DEBUG") == "true" + + def test_mock_file_system_fixture(self, mock_file_system): + """Test that the mock_file_system fixture creates expected structure.""" + assert all(path.exists() for path in mock_file_system.values()) + assert (mock_file_system["data"] / "sample.txt").exists() + assert (mock_file_system["outputs"] / "result.json").exists() + + # Test reading a file + content = (mock_file_system["data"] / "sample.txt").read_text() + assert content == "Sample data content" + + @pytest.mark.unit + def test_unit_marker(self): + """Test that the unit marker is available.""" + assert True + + @pytest.mark.integration + def test_integration_marker(self): + """Test that the integration marker is available.""" + assert True + + @pytest.mark.slow + def test_slow_marker(self): + """Test that the slow marker is available.""" + import time + # Simulate a slow test + time.sleep(0.1) + assert True + + def test_pytest_mock_is_available(self, mocker): + """Test that pytest-mock is properly installed and working.""" + mock_func = mocker.Mock(return_value=42) + assert mock_func() == 42 + mock_func.assert_called_once() + + def test_capture_logs_fixture(self, capture_logs): + """Test that the capture_logs fixture works.""" + import logging + + logger = logging.getLogger(__name__) + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + + assert "Debug message" in capture_logs.text + assert "Info message" in capture_logs.text + assert "Warning message" in capture_logs.text + + def test_benchmark_timer_fixture(self, benchmark_timer): + """Test that the benchmark_timer fixture works.""" + import time + + benchmark_timer.start() + time.sleep(0.1) + benchmark_timer.stop() + + assert benchmark_timer.elapsed is not None + assert benchmark_timer.elapsed >= 0.1 + assert benchmark_timer.elapsed < 0.2 + + +class TestCoverageConfiguration: + """Tests to validate coverage configuration.""" + + def test_coverage_is_configured(self): + """Test that coverage reporting is properly configured.""" + # This test will help verify coverage is running + def sample_function(x, y): + if x > 0: + return x + y + else: + return x - y + + assert sample_function(5, 3) == 8 + assert sample_function(-5, 3) == -8 + + def test_multiple_branches(self): + """Test with multiple branches for coverage.""" + def complex_function(value): + if value < 0: + return "negative" + elif value == 0: + return "zero" + elif value < 10: + return "small" + else: + return "large" + + assert complex_function(-5) == "negative" + assert complex_function(0) == "zero" + assert complex_function(5) == "small" + assert complex_function(15) == "large" + + +def test_module_level_test(): + """Test that module-level tests are discovered.""" + assert True + + +if __name__ == "__main__": + # This allows running the test file directly + 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..b129a9a --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +# Unit tests package initialization \ No newline at end of file