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:
Hyun-Gyu (Ethan) KimandClaude Opus 4.6 authored and GitHub committed 2026-02-22 21:08:32 +01:00
1 parent dcbf08520d
commit 5d417beb80
12 files changed
+957 -1

No files matched your search

+30
View File
@@ -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: |
+4
View File
@@ -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({})
+11
View File
@@ -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)