"""
Tests for verible-verilog-ls detection logic.
These tests describe the expected behavior of SystemVerilogLanguageServer.DependencyProvider._get_or_install_core_dependency():
1. System PATH should be checked FIRST (prefers user-installed verible)
2. Runtime download should be fallback when not in PATH
3. Version information should be logged when available
4. Version check failures should be handled gracefully
5. Helpful error messages when verible is not available on unsupported platforms
WHY these tests matter:
- Users install verible via conda, Homebrew, system packages, or GitHub releases
- Detection failing means Serena is unusable for SystemVerilog, even when verible is correctly installed
- Without these tests, the detection logic can silently break for users with system installations
- Version logging helps debug compatibility issues
"""
import os
import shutil
import subprocess
import tempfile
from unittest.mock import MagicMock, Mock, patch
import pytest
from solidlsp.language_servers.systemverilog_server import SystemVerilogLanguageServer
from solidlsp.settings import SolidLSPSettings
DEFAULT_VERIBLE_VERSION = "v0.0-4051-g9fdb4057"
class TestVeribleVerilogLsDetection:
"""Unit tests for verible-verilog-ls binary detection logic."""
@pytest.mark.systemverilog
def test_detect_from_path_returns_system_verible(self):
"""
GIVEN verible-verilog-ls is in system PATH
WHEN _get_or_install_core_dependency is called
THEN it returns the system path without downloading
WHY: Users with system-installed verible (via conda, Homebrew, apt)
should use that version instead of downloading. This is faster and
respects user's environment management.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which") as mock_which:
mock_which.return_value = "/usr/local/bin/verible-verilog-ls"
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
returncode=0,
stdout="Verible v0.0-4051-g9fdb4057 (2024-01-01)\nCommit: 9fdb4057",
stderr="",
)
result = provider._get_or_install_core_dependency()
assert result == "/usr/local/bin/verible-verilog-ls"
mock_which.assert_called_once_with("verible-verilog-ls")
mock_run.assert_called_once()
assert mock_run.call_args[0][0] == ["/usr/local/bin/verible-verilog-ls", "--version"]
@pytest.mark.systemverilog
def test_detect_from_path_logs_version(self):
"""
GIVEN verible-verilog-ls is in PATH with version output
WHEN detected
THEN version info is logged
WHY: Version information helps debug compatibility issues.
Users and developers need to know which verible version is being used.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which", return_value="/usr/bin/verible-verilog-ls"):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="Verible v0.0-4051-g9fdb4057", stderr="")
with patch("solidlsp.language_servers.systemverilog_server.log") as mock_log:
result = provider._get_or_install_core_dependency()
# Verify version check was called
assert mock_run.call_args[0][0] == ["/usr/bin/verible-verilog-ls", "--version"]
# Verify version was logged
assert mock_log.info.called
log_message = mock_log.info.call_args[0][0]
assert "Verible v0.0-4051" in log_message
assert result == "/usr/bin/verible-verilog-ls"
@pytest.mark.systemverilog
def test_detect_from_path_handles_version_failure_gracefully(self):
"""
GIVEN verible-verilog-ls is in PATH but --version fails (returncode=1)
WHEN detected
THEN it still returns the system path (graceful degradation)
WHY: Some verible builds might not support --version or have different flags.
Detection should not fail just because version check fails - the binary
might still work fine for LSP operations.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which", return_value="/custom/bin/verible-verilog-ls"):
with patch("subprocess.run") as mock_run:
# Version check fails
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="Unknown option: --version")
result = provider._get_or_install_core_dependency()
# Should still return the path despite version check failure
assert result == "/custom/bin/verible-verilog-ls"
@pytest.mark.systemverilog
def test_detect_from_path_handles_version_timeout_gracefully(self):
"""
GIVEN verible-verilog-ls is in PATH but --version times out
WHEN detected
THEN it still returns the system path (graceful degradation)
WHY: Version check has a timeout to avoid hanging. If it times out,
we should still use the detected binary.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which", return_value="/opt/verible/bin/verible-verilog-ls"):
with patch("subprocess.run") as mock_run:
# Version check times out
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["verible-verilog-ls", "--version"], timeout=5)
result = provider._get_or_install_core_dependency()
# Should still return the path despite timeout
assert result == "/opt/verible/bin/verible-verilog-ls"
@pytest.mark.systemverilog
def test_error_message_when_not_found_anywhere(self):
"""
GIVEN verible is NOT in PATH AND platform is unsupported
WHEN _get_or_install_core_dependency is called
THEN raises FileNotFoundError with helpful installation instructions
WHY: Users need clear guidance on how to install verible when it's missing.
Error message should mention conda, Homebrew, and GitHub releases.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which", return_value=None):
# Mock RuntimeDependencyCollection to raise RuntimeError for unsupported platform
with patch("solidlsp.language_servers.systemverilog_server.RuntimeDependencyCollection") as mock_deps_class:
mock_deps = Mock()
mock_deps.get_single_dep_for_current_platform.side_effect = RuntimeError("Unsupported platform")
mock_deps_class.return_value = mock_deps
with pytest.raises(FileNotFoundError) as exc_info:
provider._get_or_install_core_dependency()
error_message = str(exc_info.value)
# Error should mention installation methods
assert "conda" in error_message.lower()
assert "Homebrew" in error_message or "brew" in error_message.lower()
assert "GitHub" in error_message or "github.com" in error_message.lower()
assert "verible" in error_message.lower()
@pytest.mark.systemverilog
def test_downloads_when_not_in_path(self):
"""
GIVEN verible is NOT in PATH AND platform IS supported AND binary exists after download
WHEN _get_or_install_core_dependency is called
THEN returns the downloaded executable path
WHY: When verible is not installed system-wide and platform is supported,
Serena should auto-download it. This enables zero-setup experience.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
expected_path = os.path.join(temp_dir, "verible-ls", f"verible-{DEFAULT_VERIBLE_VERSION}", "bin", "verible-verilog-ls")
with patch("shutil.which", return_value=None):
with patch("solidlsp.language_servers.systemverilog_server.RuntimeDependencyCollection") as mock_deps_class:
# Create mock dependency and collection
mock_dep = Mock()
mock_dep.url = "https://github.com/chipsalliance/verible/releases/download/v0.0-4051/verible.tar.gz"
mock_deps = Mock()
mock_deps.get_single_dep_for_current_platform.return_value = mock_dep
mock_deps.binary_path.return_value = expected_path
mock_deps.install.return_value = expected_path
mock_deps_class.return_value = mock_deps
with patch("os.path.exists") as mock_exists:
# Before download: binary doesn't exist yet → after download: binary exists
mock_exists.side_effect = [False, True]
with patch("os.chmod"):
result = provider._get_or_install_core_dependency()
assert result == expected_path
mock_deps.install.assert_called_once()
@pytest.mark.systemverilog
def test_detection_prefers_path_over_download(self):
"""
GIVEN verible is in PATH AND download would also work
WHEN _get_or_install_core_dependency is called
THEN PATH version is used (download never attempted)
WHY: System-installed verible should always take precedence.
This respects user's environment and avoids unnecessary downloads.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
with patch("shutil.which", return_value="/usr/bin/verible-verilog-ls"):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="Verible v0.0-4051", stderr="")
with patch("solidlsp.language_servers.systemverilog_server.RuntimeDependencyCollection") as mock_deps_class:
result = provider._get_or_install_core_dependency()
# RuntimeDependencyCollection should never be instantiated
mock_deps_class.assert_not_called()
assert result == "/usr/bin/verible-verilog-ls"
@pytest.mark.systemverilog
def test_download_fails_if_binary_not_found_after_install(self):
"""
GIVEN verible is NOT in PATH AND platform IS supported
WHEN download completes BUT binary still doesn't exist at expected path
THEN raises FileNotFoundError
WHY: If download/extraction fails silently, we should catch it and report clearly.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
expected_path = os.path.join(temp_dir, "verible-ls", f"verible-{DEFAULT_VERIBLE_VERSION}", "bin", "verible-verilog-ls")
with patch("shutil.which", return_value=None):
with patch("solidlsp.language_servers.systemverilog_server.RuntimeDependencyCollection") as mock_deps_class:
mock_dep = Mock()
mock_deps = Mock()
mock_deps.get_single_dep_for_current_platform.return_value = mock_dep
mock_deps.binary_path.return_value = expected_path
mock_deps.install.return_value = expected_path
mock_deps_class.return_value = mock_deps
# Binary never appears after install
with patch("os.path.exists", return_value=False):
with pytest.raises(FileNotFoundError) as exc_info:
provider._get_or_install_core_dependency()
error_message = str(exc_info.value)
assert "verible-verilog-ls not found" in error_message
assert expected_path in error_message
@pytest.mark.systemverilog
def test_uses_already_downloaded_binary_without_reinstalling(self):
"""
GIVEN verible is NOT in PATH AND platform IS supported
AND binary already exists at download location
WHEN _get_or_install_core_dependency is called
THEN returns existing path without downloading again
WHY: Avoid redundant downloads if verible was already downloaded in previous session.
This speeds up subsequent runs.
"""
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
expected_path = os.path.join(temp_dir, "verible-ls", f"verible-{DEFAULT_VERIBLE_VERSION}", "bin", "verible-verilog-ls")
with patch("shutil.which", return_value=None):
with patch("solidlsp.language_servers.systemverilog_server.RuntimeDependencyCollection") as mock_deps_class:
mock_dep = Mock()
mock_deps = Mock()
mock_deps.get_single_dep_for_current_platform.return_value = mock_dep
mock_deps.binary_path.return_value = expected_path
mock_deps_class.return_value = mock_deps
# Binary already exists
with patch("os.path.exists", return_value=True):
with patch("os.chmod"):
result = provider._get_or_install_core_dependency()
# Should NOT call install since binary already exists
mock_deps.install.assert_not_called()
assert result == expected_path
class TestVeribleVerilogLsDetectionIntegration:
"""
Integration tests that verify detection works on the current system.
These tests are skipped if verible-verilog-ls is not installed.
"""
@pytest.mark.systemverilog
def test_integration_finds_installed_verible(self):
"""
GIVEN verible-verilog-ls is installed on this system (via any method)
WHEN _get_or_install_core_dependency is called
THEN it returns a valid executable path
This test verifies the detection logic works end-to-end on the current system.
"""
# Skip if verible-verilog-ls is not installed
if not shutil.which("verible-verilog-ls"):
pytest.skip("verible-verilog-ls not installed on this system")
with tempfile.TemporaryDirectory() as temp_dir:
custom_settings = SolidLSPSettings.CustomLSSettings({})
provider = SystemVerilogLanguageServer.DependencyProvider(custom_settings, temp_dir)
result = provider._get_or_install_core_dependency()
assert result is not None
assert os.path.isfile(result)
assert os.access(result, os.X_OK)