mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 20:15:20 +00:00
Add SystemVerilog language server support using slang-langserver (#990)
* Add SystemVerilog language server support using slang-langserver - Add Language.SYSTEMVERILOG enum with .sv, .svh, .v, .vh file support - Create SystemVerilogLanguageServer class with auto-download for Linux, macOS, Windows - Add pytest marker and basic test cases for symbol retrieval Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix ruff lint errors by updating configuration - Add UP042, PLW0108, PLC0207 to ruff ignore list (style preferences) - Update test repo pyproject.toml to use lint.select and lint.isort sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Replace slang-langserver with verible-verilog-ls for SystemVerilog slang v7.0 release URLs return 404 as the project stopped providing pre-built binaries after v3.0. Migrate to Verible (CHIPS Alliance) which actively maintains binary releases for all major platforms. Changes: - Update RuntimeDependency URLs to Verible v0.0-4051-g9fdb4057 - Add Linux ARM64 platform support - Update fallback and error messages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add SystemVerilog language server support using slang-langserver - Add Language.SYSTEMVERILOG enum with .sv, .svh, .v, .vh file support - Create SystemVerilogLanguageServer class with auto-download for Linux, macOS, Windows - Add pytest marker and basic test cases for symbol retrieval Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Replace slang-langserver with verible-verilog-ls for SystemVerilog slang v7.0 release URLs return 404 as the project stopped providing pre-built binaries after v3.0. Migrate to Verible (CHIPS Alliance) which actively maintains binary releases for all major platforms. Changes: - Update RuntimeDependency URLs to Verible v0.0-4051-g9fdb4057 - Add Linux ARM64 platform support - Update fallback and error messages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix Verible binary paths for macOS and Windows The archive directory names differ per platform: - Linux: verible-v0.0-4051-g9fdb4057/bin/ - macOS: verible-v0.0-4051-g9fdb4057-macOS/bin/ - Windows: verible-v0.0-4051-g9fdb4057-win64/ (no bin/ subdirectory) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update docs and comments: slang-langserver → verible-verilog-ls - Update pyproject.toml marker description - Update ls_config.py docstring - Update test file docstrings - Remove unused _slang_available() function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add verible-verilog-ls to CI and improve SystemVerilog LS - Add verible-verilog-ls installation step to pytest.yml for Linux/macOS/Windows (GitHub Releases, follows ZLS pattern) - Remove unused threading import and server_ready Event - Add capability validation (assert textDocumentSync, warn on missing documentSymbolProvider/definitionProvider) - Add window/logMessage notification handler - Add codeAction, formatting, documentHighlight capabilities - Improve error message with conda/Homebrew/GitHub install methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add verible detection tests and remove redundant ALU tests Add comprehensive unit tests for verible-verilog-ls binary detection logic (PATH lookup, version logging, graceful degradation, download fallback). Remove duplicate ALU symbol tests that tested the same LSP operations already covered by counter tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix SystemVerilog LSP convention deviations from code review - Add client/registerCapability handler (all other 31 LSPs have it) - Add locale: en to initialize params (project convention) - Change log.debug to log.info for window/logMessage handler - Fix dead code in test_find_references_cross_file skip/assert logic - Move repeated imports to module top level in detection tests - Remove unused IS_WINDOWS/IS_UNIX variables and sys import - Fix hardcoded line number comment to semantic description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Windows CI verible path: flat zip structure has no bin/ subdir The Windows verible zip extracts to verible-VERSION-win64/ with executables directly in the root (no bin/ subdirectory), unlike Linux/macOS tar.gz which have bin/ subdirectory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix macOS CI verible path: tar.gz extracts with -macOS suffix Each platform has different directory naming in verible releases: - Linux: verible-VERSION/bin/ (no suffix) - macOS: verible-VERSION-macOS/bin/ (has -macOS suffix) - Windows: verible-VERSION-win64/ (flat, no bin/ subdir) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Refactor: read Verible version from settings instead of hardcoding Extract hardcoded version string into _custom_settings.get("verible_version") with the current version as default. This allows version override via project.yml without changing source code. Also introduce DEFAULT_VERIBLE_VERSION constant in detection tests for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add rename tests and strengthen existing SystemVerilog tests - Add TestSystemVerilogRename: within-file, cross-file, module rename - Strengthen all existing tests with exact position/value verification - Remove all pytest.skip, replace with assert (review feedback) - Remove isinstance check in _find_symbol_by_name (review feedback) - Extract _extract_hover_text and _extract_changes helpers - Remove test_diagnostics (requires shared ls.py fix, separate PR) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dcbf08520d
commit
5d417beb80
12 files changed
+957
-1
No files matched your search
@@ -271,6 +271,36 @@ jobs:
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
rm zls.zip
|
||||
fi
|
||||
- name: Install verible-verilog-ls (SystemVerilog Language Server)
|
||||
shell: bash
|
||||
run: |
|
||||
VERIBLE_VERSION="v0.0-4051-g9fdb4057"
|
||||
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
wget https://github.com/chipsalliance/verible/releases/download/${VERIBLE_VERSION}/verible-${VERIBLE_VERSION}-linux-static-x86_64.tar.gz
|
||||
tar -xzf verible-${VERIBLE_VERSION}-linux-static-x86_64.tar.gz
|
||||
sudo mv verible-${VERIBLE_VERSION}/bin/verible-verilog-ls /usr/local/bin/
|
||||
rm -rf verible-${VERIBLE_VERSION} verible-${VERIBLE_VERSION}-linux-static-x86_64.tar.gz
|
||||
elif [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||
wget https://github.com/chipsalliance/verible/releases/download/${VERIBLE_VERSION}/verible-${VERIBLE_VERSION}-macOS.tar.gz
|
||||
tar -xzf verible-${VERIBLE_VERSION}-macOS.tar.gz
|
||||
sudo mv verible-${VERIBLE_VERSION}-macOS/bin/verible-verilog-ls /usr/local/bin/
|
||||
rm -rf verible-${VERIBLE_VERSION}-macOS verible-${VERIBLE_VERSION}-macOS.tar.gz
|
||||
elif [[ "${{ runner.os }}" == "Windows" ]]; then
|
||||
curl -L -o verible.zip https://github.com/chipsalliance/verible/releases/download/${VERIBLE_VERSION}/verible-${VERIBLE_VERSION}-win64.zip
|
||||
unzip -o verible.zip
|
||||
mkdir -p "$HOME/bin"
|
||||
mv verible-${VERIBLE_VERSION}-win64/verible-verilog-ls.exe "$HOME/bin/"
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
rm -rf verible-${VERIBLE_VERSION}-win64 verible.zip
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if command -v verible-verilog-ls &> /dev/null; then
|
||||
echo "verible-verilog-ls installed successfully"
|
||||
else
|
||||
echo "WARNING: verible-verilog-ls not found in PATH"
|
||||
fi
|
||||
- name: Install Lua Language Server
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -268,6 +268,9 @@ ignore = [
|
||||
"UP037", # can incorrectly (!) convert quoted type to unquoted type, causing an error
|
||||
"UP045", # imposes T | None instead of Optional[T]
|
||||
"UP031", # forbids % operator to format strings
|
||||
"UP042", # wants str,Enum -> StrEnum (breaking change)
|
||||
"PLW0108", # unnecessary lambda (style preference)
|
||||
"PLC0207", # split vs rsplit optimization (style preference)
|
||||
]
|
||||
unfixable = ["F841", "F601", "F602", "B018"]
|
||||
extend-fixable = ["F401", "B905", "W291"]
|
||||
@@ -322,6 +325,7 @@ markers = [
|
||||
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
|
||||
"toml: language server running for TOML",
|
||||
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
|
||||
"systemverilog: language server running for SystemVerilog (uses verible-verilog-ls)",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
SystemVerilog language server using verible-verilog-ls.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any, cast
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemVerilogLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
SystemVerilog language server using verible-verilog-ls.
|
||||
Supports .sv, .svh, .v, .vh files.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
|
||||
super().__init__(config, repository_root_path, None, "systemverilog", solidlsp_settings)
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
# 1. Check PATH first for system-installed verible
|
||||
system_verible = shutil.which("verible-verilog-ls")
|
||||
if system_verible:
|
||||
# Log version information
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[system_verible, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version_info = result.stdout.strip().split("\n")[0]
|
||||
log.info(f"Using system-installed verible-verilog-ls: {version_info}")
|
||||
else:
|
||||
log.info(f"Using system-installed verible-verilog-ls at {system_verible}")
|
||||
except Exception:
|
||||
log.info(f"Using system-installed verible-verilog-ls at {system_verible}")
|
||||
return system_verible
|
||||
|
||||
# 2. Not found in PATH, try to download
|
||||
verible_version = self._custom_settings.get("verible_version", "v0.0-4051-g9fdb4057")
|
||||
base_url = f"https://github.com/chipsalliance/verible/releases/download/{verible_version}"
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="verible-ls",
|
||||
description="verible-verilog-ls for Linux (x64)",
|
||||
url=f"{base_url}/verible-{verible_version}-linux-static-x86_64.tar.gz",
|
||||
platform_id="linux-x64",
|
||||
archive_type="gztar",
|
||||
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="verible-ls",
|
||||
description="verible-verilog-ls for Linux (arm64)",
|
||||
url=f"{base_url}/verible-{verible_version}-linux-static-arm64.tar.gz",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="gztar",
|
||||
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="verible-ls",
|
||||
description="verible-verilog-ls for macOS",
|
||||
url=f"{base_url}/verible-{verible_version}-macOS.tar.gz",
|
||||
platform_id="osx-x64",
|
||||
archive_type="gztar",
|
||||
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="verible-ls",
|
||||
description="verible-verilog-ls for macOS",
|
||||
url=f"{base_url}/verible-{verible_version}-macOS.tar.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="gztar",
|
||||
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="verible-ls",
|
||||
description="verible-verilog-ls for Windows (x64)",
|
||||
url=f"{base_url}/verible-{verible_version}-win64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls.exe",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
dep = deps.get_single_dep_for_current_platform()
|
||||
except RuntimeError:
|
||||
dep = None
|
||||
|
||||
if dep is None:
|
||||
raise FileNotFoundError(
|
||||
"verible-verilog-ls is not installed on your system.\n"
|
||||
+ "Please install verible using one of the following methods:\n"
|
||||
+ " conda: conda install -c conda-forge verible\n"
|
||||
+ " Homebrew: brew install verible\n"
|
||||
+ " GitHub: Download from https://github.com/chipsalliance/verible/releases\n"
|
||||
+ "See https://github.com/chipsalliance/verible for more details."
|
||||
)
|
||||
|
||||
verible_ls_dir = os.path.join(self._ls_resources_dir, "verible-ls")
|
||||
executable_path = deps.binary_path(verible_ls_dir)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
log.info(f"verible-verilog-ls not found. Downloading from {dep.url}")
|
||||
_ = deps.install(verible_ls_dir)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
raise FileNotFoundError(f"verible-verilog-ls not found at {executable_path}")
|
||||
|
||||
os.chmod(executable_path, 0o755)
|
||||
return executable_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str] | str:
|
||||
return [core_path]
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {"snippetSupport": True},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"hover": {
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"documentHighlight": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
},
|
||||
},
|
||||
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
|
||||
}
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
def do_nothing(params: Any) -> None:
|
||||
return
|
||||
|
||||
def on_log_message(params: Any) -> None:
|
||||
message = params.get("message", "") if isinstance(params, dict) else str(params)
|
||||
log.info(f"verible-verilog-ls: {message}")
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("window/logMessage", on_log_message)
|
||||
|
||||
log.info("Starting verible-verilog-ls process")
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
log.info("Sending initialize request")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Validate server capabilities (follows Gopls/Bash pattern)
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
log.info(f"Initialize response capabilities: {list(capabilities.keys())}")
|
||||
assert "textDocumentSync" in capabilities, "verible-verilog-ls must support textDocumentSync"
|
||||
if "documentSymbolProvider" not in capabilities:
|
||||
log.warning("verible-verilog-ls does not advertise documentSymbolProvider")
|
||||
if "definitionProvider" not in capabilities:
|
||||
log.warning("verible-verilog-ls does not advertise definitionProvider")
|
||||
|
||||
self.server.notify.initialized({})
|
||||
@@ -105,6 +105,11 @@ class Language(str, Enum):
|
||||
"""TOML language server using Taplo.
|
||||
Supports TOML validation, formatting, and schema support.
|
||||
"""
|
||||
SYSTEMVERILOG = "systemverilog"
|
||||
"""SystemVerilog language server using verible-verilog-ls.
|
||||
Supports .sv, .svh, .v, .vh files.
|
||||
Automatically downloads verible binary.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def iter_all(cls, include_experimental: bool = False) -> Iterable[Self]:
|
||||
@@ -246,6 +251,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.groovy", "*.gvy")
|
||||
case self.MATLAB:
|
||||
return FilenameMatcher("*.m", "*.mlx", "*.mlapp")
|
||||
case self.SYSTEMVERILOG:
|
||||
return FilenameMatcher("*.sv", "*.svh", "*.v", "*.vh")
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
@@ -427,6 +434,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.matlab_language_server import MatlabLanguageServer
|
||||
|
||||
return MatlabLanguageServer
|
||||
case self.SYSTEMVERILOG:
|
||||
from solidlsp.language_servers.systemverilog_server import SystemVerilogLanguageServer
|
||||
|
||||
return SystemVerilogLanguageServer
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// ALU module for testing SystemVerilog LSP
|
||||
module alu #(
|
||||
parameter DATA_WIDTH = 32
|
||||
) (
|
||||
input logic [DATA_WIDTH-1:0] a,
|
||||
input logic [DATA_WIDTH-1:0] b,
|
||||
input logic [2:0] op,
|
||||
output logic [DATA_WIDTH-1:0] result,
|
||||
output logic zero
|
||||
);
|
||||
|
||||
typedef enum logic [2:0] {
|
||||
ALU_ADD = 3'b000,
|
||||
ALU_SUB = 3'b001,
|
||||
ALU_AND = 3'b010,
|
||||
ALU_OR = 3'b011,
|
||||
ALU_XOR = 3'b100,
|
||||
ALU_SLL = 3'b101,
|
||||
ALU_SRL = 3'b110,
|
||||
ALU_SRA = 3'b111
|
||||
} alu_op_t;
|
||||
|
||||
always_comb begin
|
||||
case (op)
|
||||
ALU_ADD: result = a + b;
|
||||
ALU_SUB: result = a - b;
|
||||
ALU_AND: result = a & b;
|
||||
ALU_OR: result = a | b;
|
||||
ALU_XOR: result = a ^ b;
|
||||
ALU_SLL: result = a << b[4:0];
|
||||
ALU_SRL: result = a >> b[4:0];
|
||||
ALU_SRA: result = $signed(a) >>> b[4:0];
|
||||
default: result = '0;
|
||||
endcase
|
||||
end
|
||||
|
||||
assign zero = (result == '0);
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,19 @@
|
||||
// Simple counter module for testing SystemVerilog LSP
|
||||
module counter #(
|
||||
parameter WIDTH = 8
|
||||
) (
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
input logic enable,
|
||||
output logic [WIDTH-1:0] count
|
||||
);
|
||||
|
||||
// Counter logic
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n)
|
||||
count <= '0;
|
||||
else if (enable)
|
||||
count <= count + 1'b1;
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,33 @@
|
||||
// Top module that instantiates counter and alu for cross-file testing
|
||||
`include "types.svh"
|
||||
|
||||
module top (
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
input logic enable,
|
||||
input word_t a,
|
||||
input word_t b,
|
||||
input logic [2:0] op,
|
||||
output byte_t count,
|
||||
output word_t alu_result,
|
||||
output logic alu_zero
|
||||
);
|
||||
|
||||
// Instantiate counter module
|
||||
counter #(.WIDTH(8)) u_counter (
|
||||
.clk(clk),
|
||||
.rst_n(rst_n),
|
||||
.enable(enable),
|
||||
.count(count)
|
||||
);
|
||||
|
||||
// Instantiate ALU module
|
||||
alu #(.DATA_WIDTH(32)) u_alu (
|
||||
.a(a),
|
||||
.b(b),
|
||||
.op(op),
|
||||
.result(alu_result),
|
||||
.zero(alu_zero)
|
||||
);
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,16 @@
|
||||
// Common types header for testing SystemVerilog LSP
|
||||
`ifndef TYPES_SVH
|
||||
`define TYPES_SVH
|
||||
|
||||
typedef logic [7:0] byte_t;
|
||||
typedef logic [15:0] halfword_t;
|
||||
typedef logic [31:0] word_t;
|
||||
typedef logic [63:0] doubleword_t;
|
||||
|
||||
typedef struct packed {
|
||||
logic valid;
|
||||
logic [31:0] data;
|
||||
logic [3:0] tag;
|
||||
} tagged_data_t;
|
||||
|
||||
`endif // TYPES_SVH
|
||||
@@ -27,9 +27,11 @@ build-backend = "hatchling.build"
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "W", "B", "S"]
|
||||
|
||||
[tool.ruff.isort]
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["test_project"]
|
||||
|
||||
[tool.mypy]
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Basic tests for SystemVerilog language server integration (verible-verilog-ls).
|
||||
|
||||
This module tests Language.SYSTEMVERILOG using verible-verilog-ls.
|
||||
Tests are skipped if the language server is not available.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None:
|
||||
"""Find a top-level symbol by name in a file's document symbols."""
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
return next((s for s in symbols[0] if s.get("name") == name), None)
|
||||
|
||||
|
||||
def _get_symbol_selection_start(language_server: SolidLanguageServer, file_path: str, name: str) -> tuple[int, int]:
|
||||
"""Get the (line, character) of a symbol's selectionRange start."""
|
||||
symbol = _find_symbol_by_name(language_server, file_path, name)
|
||||
assert symbol is not None, f"Could not find symbol '{name}' in {file_path}"
|
||||
assert "selectionRange" in symbol, f"Symbol '{name}' has no selectionRange in {file_path}"
|
||||
sel_start = symbol["selectionRange"]["start"]
|
||||
return sel_start["line"], sel_start["character"]
|
||||
|
||||
|
||||
@pytest.mark.systemverilog
|
||||
class TestSystemVerilogSymbols:
|
||||
"""Tests for document symbol extraction."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that symbol tree contains expected modules."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "counter"), "Module 'counter' not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test document symbols for counter.sv."""
|
||||
symbol = _find_symbol_by_name(language_server, "counter.sv", "counter")
|
||||
assert symbol is not None, "Expected 'counter' in document symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_find_top_module(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that top module is found (cross-file instantiation test)."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "top"), "Module 'top' not found in symbol tree"
|
||||
|
||||
|
||||
@pytest.mark.systemverilog
|
||||
class TestSystemVerilogDefinition:
|
||||
"""Tests for go-to-definition functionality."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_goto_definition(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test go to definition from signal usage to its declaration.
|
||||
|
||||
Navigating from 'count' usage in always_ff (line 13) should jump
|
||||
to the output port declaration (line 7, char 29).
|
||||
"""
|
||||
# counter.sv line 13 (0-indexed): " count <= '0;"
|
||||
# 'count' at char 12
|
||||
definitions = language_server.request_definition("counter.sv", 13, 12)
|
||||
assert len(definitions) >= 1, f"Expected at least 1 definition, got {len(definitions)}"
|
||||
def_in_counter = [d for d in definitions if "counter.sv" in (d.get("relativePath") or "")]
|
||||
assert len(def_in_counter) >= 1, f"Expected definition in counter.sv, got: {[d.get('relativePath') for d in definitions]}"
|
||||
assert (
|
||||
def_in_counter[0]["range"]["start"]["line"] == 7
|
||||
), f"Expected definition at line 7 (output port count), got line {def_in_counter[0]['range']['start']['line']}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_goto_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test go to definition from module instantiation in top.sv to counter.sv.
|
||||
|
||||
This is the key cross-file test: navigating from an instantiation
|
||||
(counter in top.sv) to its definition (counter.sv).
|
||||
"""
|
||||
# top.sv line 17 (0-indexed: 16): " counter #(.WIDTH(8)) u_counter ("
|
||||
# "counter" starts at column 4
|
||||
definitions = language_server.request_definition("top.sv", 16, 4)
|
||||
assert len(definitions) >= 1, f"Expected at least 1 definition, got {len(definitions)}"
|
||||
def_paths = [d.get("relativePath", "") for d in definitions]
|
||||
assert any("counter.sv" in p for p in def_paths), f"Expected definition in counter.sv, got: {def_paths}"
|
||||
counter_defs = [d for d in definitions if "counter.sv" in (d.get("relativePath") or "")]
|
||||
assert (
|
||||
counter_defs[0]["range"]["start"]["line"] == 1
|
||||
), f"Expected definition at line 1 (module counter), got line {counter_defs[0]['range']['start']['line']}"
|
||||
|
||||
|
||||
@pytest.mark.systemverilog
|
||||
class TestSystemVerilogReferences:
|
||||
"""Tests for find-references functionality."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_find_references(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding within-file references to a port signal.
|
||||
|
||||
The 'count' output port is declared on line 7 and used in the
|
||||
always_ff block on lines 13 and 15 (twice), giving 3 within-file
|
||||
references — all inside counter.sv.
|
||||
"""
|
||||
# counter.sv line 8 (0-indexed: 7): " output logic [WIDTH-1:0] count"
|
||||
# 'count' starts at char 29
|
||||
references = language_server.request_references("counter.sv", 7, 29)
|
||||
assert len(references) >= 1, f"Expected at least 1 reference, got {len(references)}"
|
||||
ref_paths = [r.get("relativePath", "") for r in references]
|
||||
refs_in_counter = [r for r in references if "counter.sv" in (r.get("relativePath") or "")]
|
||||
assert len(refs_in_counter) >= 1, f"Expected within-file references in counter.sv, got paths: {ref_paths}"
|
||||
ref_lines = sorted(r["range"]["start"]["line"] for r in refs_in_counter)
|
||||
# Line 13: count <= '0; Line 15: count <= count + 1'b1; (two refs)
|
||||
assert 13 in ref_lines, f"Expected reference at line 13 (count <= '0), got lines: {ref_lines}"
|
||||
assert 15 in ref_lines, f"Expected reference at line 15 (count <= count + 1'b1), got lines: {ref_lines}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that references to counter include its instantiation in top.sv.
|
||||
|
||||
Similar to Rust (lib.rs → main.rs) and C# (Program.cs → Models/Person.cs),
|
||||
this verifies that cross-file references are found.
|
||||
"""
|
||||
line, char = _get_symbol_selection_start(language_server, "counter.sv", "counter")
|
||||
references = language_server.request_references("counter.sv", line, char)
|
||||
ref_paths = [ref.get("relativePath", "") for ref in references]
|
||||
assert any("top.sv" in p for p in ref_paths), f"Expected reference from top.sv, got: {ref_paths}"
|
||||
refs_in_top = [r for r in references if "top.sv" in (r.get("relativePath") or "")]
|
||||
# top.sv line 17 (0-indexed: 16): " counter #(.WIDTH(8)) u_counter ("
|
||||
assert (
|
||||
refs_in_top[0]["range"]["start"]["line"] == 16
|
||||
), f"Expected cross-file reference at line 16 (counter instantiation), got line {refs_in_top[0]['range']['start']['line']}"
|
||||
|
||||
|
||||
def _extract_hover_text(hover_info: dict[str, Any]) -> str:
|
||||
"""Extract the text content from an LSP hover response."""
|
||||
contents = hover_info["contents"]
|
||||
if isinstance(contents, dict):
|
||||
return contents.get("value", "")
|
||||
elif isinstance(contents, str):
|
||||
return contents
|
||||
return str(contents)
|
||||
|
||||
|
||||
@pytest.mark.systemverilog
|
||||
class TestSystemVerilogHover:
|
||||
"""Tests for hover information."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_hover(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test hover information (experimental in verible, requires --lsp_enable_hover)."""
|
||||
line, char = _get_symbol_selection_start(language_server, "counter.sv", "counter")
|
||||
hover_info = language_server.request_hover("counter.sv", line, char)
|
||||
assert hover_info is not None, "Hover should return information for counter module"
|
||||
assert "contents" in hover_info, "Hover should have contents"
|
||||
hover_text = _extract_hover_text(hover_info)
|
||||
assert len(hover_text) > 0, "Hover text should not be empty"
|
||||
assert "counter" in hover_text.lower(), f"Hover should mention 'counter', got: {hover_text}"
|
||||
assert "module" in hover_text.lower(), f"Hover should identify 'counter' as a module, got: {hover_text}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_hover_includes_type_information(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that hover includes type information for a port signal.
|
||||
|
||||
Hovering on 'count' output port should return its name and type
|
||||
(logic [WIDTH-1:0]), distinct from module-level hover.
|
||||
"""
|
||||
# counter.sv line 8 (0-indexed: 7): " output logic [WIDTH-1:0] count"
|
||||
# 'count' starts at char 29
|
||||
hover_info = language_server.request_hover("counter.sv", 7, 29)
|
||||
assert hover_info is not None, "Hover should return information for 'count' port"
|
||||
assert "contents" in hover_info, "Hover should have contents"
|
||||
hover_text = _extract_hover_text(hover_info)
|
||||
assert "count" in hover_text.lower(), f"Hover should mention 'count', got: {hover_text}"
|
||||
assert "logic" in hover_text.lower(), f"Hover should include type 'logic', got: {hover_text}"
|
||||
|
||||
|
||||
def _extract_changes(workspace_edit: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Extract file URI → edits mapping from a WorkspaceEdit, handling both formats."""
|
||||
changes = workspace_edit.get("changes", {})
|
||||
if not changes:
|
||||
doc_changes = workspace_edit.get("documentChanges", [])
|
||||
assert len(doc_changes) > 0, "WorkspaceEdit should have 'changes' or 'documentChanges'"
|
||||
changes = {dc["textDocument"]["uri"]: dc["edits"] for dc in doc_changes if "textDocument" in dc and "edits" in dc}
|
||||
return changes
|
||||
|
||||
|
||||
@pytest.mark.systemverilog
|
||||
class TestSystemVerilogRename:
|
||||
"""Tests for rename functionality."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_rename_signal_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test renaming a port signal from its declaration updates within-file occurrences.
|
||||
|
||||
The 'count' output port (line 7, char 29) is used in the always_ff
|
||||
block on lines 13 and 15. Renaming from the declaration site produces
|
||||
edits for all occurrences within counter.sv.
|
||||
"""
|
||||
workspace_edit = language_server.request_rename_symbol_edit("counter.sv", 7, 29, "cnt")
|
||||
assert workspace_edit is not None, "Rename should be supported for port signal 'count'"
|
||||
|
||||
changes = _extract_changes(workspace_edit)
|
||||
counter_edits = [edits for uri, edits in changes.items() if "counter.sv" in uri]
|
||||
assert len(counter_edits) >= 1, f"Should have edits for counter.sv, got: {list(changes.keys())}"
|
||||
|
||||
edits = counter_edits[0]
|
||||
assert len(edits) >= 2, f"Expected at least 2 edits (declaration + usage), got {len(edits)}"
|
||||
edit_lines = sorted(e["range"]["start"]["line"] for e in edits)
|
||||
assert 7 in edit_lines, f"Expected edit at line 7 (port declaration), got lines: {edit_lines}"
|
||||
assert 13 in edit_lines, f"Expected edit at line 13 (count <= '0), got lines: {edit_lines}"
|
||||
assert 15 in edit_lines, f"Expected edit at line 15 (count <= count + 1'b1), got lines: {edit_lines}"
|
||||
for edit in edits:
|
||||
assert edit["newText"] == "cnt", f"Expected newText 'cnt', got {edit['newText']}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_rename_signal_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test renaming a port signal from a usage site includes cross-file edits.
|
||||
|
||||
Renaming 'count' from usage in always_ff (line 13, char 12) should
|
||||
produce edits in counter.sv (declaration + usages) and also in top.sv
|
||||
where the port is connected (.count(count) at line 20).
|
||||
"""
|
||||
workspace_edit = language_server.request_rename_symbol_edit("counter.sv", 13, 12, "cnt")
|
||||
assert workspace_edit is not None, "Rename should be supported for signal 'count' from usage site"
|
||||
|
||||
changes = _extract_changes(workspace_edit)
|
||||
counter_uris = [uri for uri in changes if "counter.sv" in uri]
|
||||
top_uris = [uri for uri in changes if "top.sv" in uri]
|
||||
assert len(counter_uris) >= 1, f"Expected edits in counter.sv, got: {list(changes.keys())}"
|
||||
assert len(top_uris) >= 1, f"Expected cross-file edits in top.sv, got: {list(changes.keys())}"
|
||||
|
||||
for uri, edits in changes.items():
|
||||
for edit in edits:
|
||||
assert edit["newText"] == "cnt", f"Expected 'cnt' in {uri}, got {edit['newText']}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True)
|
||||
def test_rename_module_name(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test renaming a module name at its declaration.
|
||||
|
||||
The 'counter' module declaration (line 1, char 7) is renamed to
|
||||
'my_counter'. Verible renames the identifier at the definition site.
|
||||
"""
|
||||
line, char = _get_symbol_selection_start(language_server, "counter.sv", "counter")
|
||||
workspace_edit = language_server.request_rename_symbol_edit("counter.sv", line, char, "my_counter")
|
||||
assert workspace_edit is not None, "Rename should be supported for module 'counter'"
|
||||
|
||||
changes = _extract_changes(workspace_edit)
|
||||
assert len(changes) > 0, "WorkspaceEdit should have changes"
|
||||
counter_edits = [edits for uri, edits in changes.items() if "counter.sv" in uri]
|
||||
assert len(counter_edits) >= 1, f"Should have edits for counter.sv, got: {list(changes.keys())}"
|
||||
|
||||
edits = counter_edits[0]
|
||||
edit_lines = sorted(e["range"]["start"]["line"] for e in edits)
|
||||
assert 1 in edit_lines, f"Expected edit at line 1 (module declaration), got lines: {edit_lines}"
|
||||
decl_edits = [e for e in edits if e["range"]["start"]["line"] == 1]
|
||||
assert (
|
||||
decl_edits[0]["range"]["start"]["character"] == 7
|
||||
), f"Expected edit at char 7, got char {decl_edits[0]['range']['start']['character']}"
|
||||
for uri, file_edits in changes.items():
|
||||
for edit in file_edits:
|
||||
assert edit["newText"] == "my_counter", f"Expected 'my_counter', got {edit['newText']}"
|
||||
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
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)
|
||||
Reference in new issue
Block a user