Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/
dist/
*.egg
*.egg-info/
.coverage
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,18 @@ extra_checks = true
# Error on missing imports (don't allow implicit Any)
disallow_any_unimported = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
disallow_incomplete_defs = false
disallow_untyped_decorators = false

[tool.coverage.run]
omit = [
"solc_select/__main__.py",
"solc_select/solc_select.py",
"solc_select/utils.py"
]

[tool.uv]
python-preference = "only-managed"
15 changes: 7 additions & 8 deletions solc_select/services/artifact_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,16 @@ def _download_artifact(self, artifact: SolcArtifact) -> None:
response = self.session.get(artifact.download_url, stream=True)
response.raise_for_status()

with open(artifact.file_path, "w+b", opener=partial(os.open, mode=0o664)) as f:
try:
try:
with open(artifact.file_path, "w+b", opener=partial(os.open, mode=0o664)) as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
except KeyboardInterrupt:
if artifact.file_path.exists():
artifact.file_path.unlink(missing_ok=True)
raise

self.verify_checksum(artifact, f)
self.verify_checksum(artifact, f)
except KeyboardInterrupt:
if artifact.file_path.exists():
artifact.file_path.unlink(missing_ok=True)
raise

def download_and_install(self, version: SolcVersion, silent: bool = False) -> bool:
"""Download and install a Solidity compiler version."""
Expand Down
71 changes: 59 additions & 12 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,72 @@ This directory contains the pytest-based test suite for solc-select.

## Test Structure

- `conftest.py` - Pytest configuration and fixtures for test isolation
The test suite is organized into two main categories:

### Integration Tests (`integration/`)
- End-to-end tests that verify actual CLI behavior
- Real HTTP requests, filesystem operations, and binary execution
- Platform-specific validation
- `test_compiler_versions.py` - Tests for different Solidity compiler versions
- `test_platform_specific.py` - Platform-specific boundary tests
- `test_upgrade.py` - Tests for upgrade functionality
- `test_network_isolation.py` - Verifies offline execution after installation
- `test_version_verification.py` - Version and checksum verification

### Unit Tests (`unit/`)
- Fast, isolated tests with mocked dependencies
- Service layer business logic
- Repository matching algorithms
- Checksum verification and parallel downloads
- Platform detection and emulation handling
- Organized by layer:
- `services/` - Service layer tests (VersionManager, ArtifactManager, etc.)
- `infrastructure/` - Infrastructure layer tests (FilesystemManager, HTTP client)
- `models/` - Domain model tests (VersionRange, SolcArtifact, etc.)

## Running Tests

### Install test dependencies

```bash
# Install with all development dependencies (testing + linting)
pip install -e ".[dev]"
uv pip install -e ".[dev]"
```

### Run all tests

```bash
pytest
# Run all tests (both unit and integration)
uv run pytest tests/

# Run only unit tests (fast)
uv run pytest tests/unit/ -v

# Run only integration tests (slower, requires network)
uv run pytest tests/integration/ -v
```

### Run specific test files

```bash
pytest tests/test_compiler_versions.py
pytest tests/test_platform_specific.py
pytest tests/test_upgrade.py
# Unit tests
uv run pytest tests/unit/services/test_version_manager.py
uv run pytest tests/unit/services/test_artifact_manager.py

# Integration tests
uv run pytest tests/integration/test_compiler_versions.py
uv run pytest tests/integration/test_platform_specific.py
uv run pytest tests/integration/test_upgrade.py
```

### Check test coverage

```bash
# Coverage for unit tests
uv run pytest tests/unit/ --cov=solc_select --cov-report=term-missing

# Coverage for all tests
uv run pytest tests/ --cov=solc_select --cov-report=html
```

### Run platform-specific tests
Expand All @@ -56,16 +96,23 @@ pytest -n auto

## Test Fixtures

The test suite uses several fixtures to ensure proper isolation:
The test suite uses different fixtures depending on the test type:

### Integration Test Fixtures (`integration/conftest.py`)
- `isolated_solc_data` - Creates isolated solc-select data environment using VIRTUAL_ENV
- `isolated_python_env` - Creates completely isolated Python environment for install/uninstall tests
- `test_contracts_dir` - Path to test Solidity contracts in `tests/solidity_tests/`

### Helper Functions

- `run_command` - Executes shell commands for tests using `isolated_solc_data`
- `run_in_venv` - Executes commands in isolated virtual environments
- Helper functions:
- `run_command` - Executes shell commands for tests using `isolated_solc_data`
- `run_in_venv` - Executes commands in isolated virtual environments

### Unit Test Fixtures (`unit/conftest.py`)
- `mock_session` - Mock requests.Session for HTTP calls
- `mock_filesystem` - Mock FilesystemManager
- `mock_platform` - Mock Platform (linux-amd64 by default)
- `mock_repository` - Mock SolcRepository with common version set
- `temp_artifacts_dir` - Temporary artifacts directory for testing
- `temp_solc_select_dir` - Temporary solc-select directory with isolated paths

## Test Organization

Expand Down
Empty file added tests/integration/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion tests/conftest.py → tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def isolated_python_env(tmp_path: Path) -> Generator[dict[str, Any], None, None]
@pytest.fixture(scope="session")
def test_contracts_dir() -> Path:
"""Path to test Solidity contracts."""
return Path(__file__).parent / "solidity_tests"
return Path(__file__).parent.parent / "solidity_tests"


# Platform markers for conditional test execution
Expand Down
4 changes: 2 additions & 2 deletions tests/test_upgrade.py → tests/integration/test_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_upgrade_preserves_versions(self, isolated_python_env: Any) -> None:
and verifies everything is preserved.
"""
venv = isolated_python_env
project_root = Path(__file__).parent.parent
project_root = Path(__file__).parent.parent.parent

# Install release version from PyPI
run_in_venv(venv, 'pip install "solc-select>=1.0"', check=True)
Expand Down Expand Up @@ -70,7 +70,7 @@ def test_upgrade_preserves_versions(self, isolated_python_env: Any) -> None:

def test_cache_already_installed(self, isolated_python_env: Any) -> None:
venv = isolated_python_env
project_root = Path(__file__).parent.parent
project_root = Path(__file__).parent.parent.parent

# Install development version
run_in_venv(venv, f"pip install -e {project_root}", check=True)
Expand Down
File renamed without changes.
Empty file added tests/unit/__init__.py
Empty file.
80 changes: 80 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Fixtures for unit tests with mocking."""

from unittest.mock import Mock

import pytest
import requests

from solc_select.infrastructure.filesystem import FilesystemManager
from solc_select.models.platforms import Platform
from solc_select.models.versions import SolcVersion
from solc_select.repositories import SolcRepository


@pytest.fixture
def mock_session():
"""Mock requests.Session for HTTP calls."""
return Mock(spec=requests.Session)


@pytest.fixture
def mock_filesystem():
"""Mock FilesystemManager."""
return Mock(spec=FilesystemManager)


@pytest.fixture
def mock_platform():
"""Mock Platform (linux-amd64 by default)."""
return Platform("linux", "amd64")


@pytest.fixture
def mock_repository():
"""Mock SolcRepository with common version set."""
mock = Mock(spec=SolcRepository)
mock.available_versions = {
SolcVersion("0.8.19"): "solc-linux-amd64-v0.8.19+commit.abc123",
SolcVersion("0.8.20"): "solc-linux-amd64-v0.8.20+commit.def456",
SolcVersion("0.8.21"): "solc-linux-amd64-v0.8.21+commit.ghi789",
}
mock.latest_version = SolcVersion("0.8.21")
return mock


@pytest.fixture
def sample_versions():
"""Sample version list for testing."""
return [
SolcVersion("0.8.19"),
SolcVersion("0.8.20"),
SolcVersion("0.8.21"),
]


@pytest.fixture
def temp_artifacts_dir(tmp_path, monkeypatch):
"""Temporary artifacts directory for testing filesystem operations."""
artifacts_dir = tmp_path / "artifacts"
artifacts_dir.mkdir()
monkeypatch.setattr("solc_select.constants.ARTIFACTS_DIR", artifacts_dir)
return artifacts_dir


@pytest.fixture
def temp_solc_select_dir(tmp_path, monkeypatch):
"""Temporary solc-select directory for testing."""
solc_select_dir = tmp_path / ".solc-select"
solc_select_dir.mkdir()
artifacts_dir = solc_select_dir / "artifacts"
artifacts_dir.mkdir()

# Patch constants module
monkeypatch.setattr("solc_select.constants.SOLC_SELECT_DIR", solc_select_dir)
monkeypatch.setattr("solc_select.constants.ARTIFACTS_DIR", artifacts_dir)

# Patch where constants are imported in filesystem module
monkeypatch.setattr("solc_select.infrastructure.filesystem.ARTIFACTS_DIR", artifacts_dir)
monkeypatch.setattr("solc_select.infrastructure.filesystem.SOLC_SELECT_DIR", solc_select_dir)

return solc_select_dir
Empty file.
Loading