mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-04 20:13:29 +00:00
EclipseJDTLS.is_ignored_dirname hard-ignored directories named target/build/bin/out/classes/dist/lib. Several of these (dist, lib, classes, out) are valid Java package identifiers, so any source file whose package path contained one was dropped from directory traversal and became invisible to the symbol tools (find_symbol, get_symbols_overview, find_referencing_symbols) even when git check-ignore reported nothing. Per maintainer guidance in #1645, remove the override entirely: the base is_ignored_dirname already skips _ALWAYS_IGNORED_DIRS (VCS/venv/cache/IDE internals), and real build output is already excluded via .gitignore, so the hardcoded list was both redundant and harmful. Adds a regression test (no Java/JDTLS process required) that fails on main and asserts the formerly-hardcoded names are no longer ignored while _ALWAYS_IGNORED_DIRS still are.
647 lines
31 KiB
Python
647 lines
31 KiB
Python
"""
|
|
Unit tests for the offline (upstream) JDTLS resolution helpers in
|
|
``solidlsp.language_servers.eclipse_jdtls``.
|
|
|
|
These tests cover only the path/version/validation logic; they do not start
|
|
JDTLS and do not require Java to be installed. Subprocess interactions are
|
|
mocked.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import platform
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
_JAVA_EXE_NAME = "java.exe" if platform.system() == "Windows" else "java"
|
|
|
|
from solidlsp.language_servers.eclipse_jdtls import (
|
|
JDTLS_CONFIG_DIR_BY_PLATFORM,
|
|
JDTLS_MIN_JDK_VERSION,
|
|
EclipseJDTLS,
|
|
RuntimeDependencyPaths,
|
|
)
|
|
from solidlsp.ls_exceptions import SolidLSPException
|
|
from solidlsp.settings import SolidLSPSettings
|
|
|
|
|
|
@pytest.fixture
|
|
def custom_settings() -> SolidLSPSettings.CustomLSSettings:
|
|
"""Empty CustomLSSettings instance for tests that don't need any keys set."""
|
|
return SolidLSPSettings.CustomLSSettings({})
|
|
|
|
|
|
def _make_fake_jdtls_install(
|
|
root: Path, *, with_launcher: bool = True, with_native_fragments: bool = True, with_configs: bool = True
|
|
) -> Path:
|
|
"""
|
|
Builds a minimal fake upstream JDTLS layout under ``root``: ``plugins/`` with
|
|
a main equinox launcher jar (and optionally native fragments) and
|
|
``config_<platform>/`` directories. Returns ``root``.
|
|
"""
|
|
plugins = root / "plugins"
|
|
plugins.mkdir(parents=True, exist_ok=True)
|
|
if with_launcher:
|
|
(plugins / "org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar").touch()
|
|
if with_native_fragments:
|
|
(plugins / "org.eclipse.equinox.launcher.cocoa.macosx.aarch64_1.2.0.v20240329-1112.jar").touch()
|
|
(plugins / "org.eclipse.equinox.launcher.gtk.linux.x86_64_1.2.0.v20240329-1112.jar").touch()
|
|
(plugins / "org.eclipse.equinox.launcher.win32.win32.x86_64_1.2.0.v20240329-1112.jar").touch()
|
|
if with_configs:
|
|
for config_name in set(JDTLS_CONFIG_DIR_BY_PLATFORM.values()):
|
|
(root / config_name).mkdir(exist_ok=True)
|
|
return root
|
|
|
|
|
|
def _make_runtime_dependency_paths(root: Path) -> RuntimeDependencyPaths:
|
|
"""
|
|
Build minimal runtime dependency paths for initialize-parameter tests.
|
|
|
|
:param root: Root directory under which fake runtime files are created.
|
|
:return: Runtime paths sufficient for ``EclipseJDTLS._create_base_initialize_params``.
|
|
"""
|
|
jre_home = root / "jre-home"
|
|
jre_home.mkdir(parents=True)
|
|
jre_bin = jre_home / "bin"
|
|
jre_bin.mkdir()
|
|
jre_path = jre_bin / _JAVA_EXE_NAME
|
|
jre_path.touch()
|
|
|
|
gradle_path = root / "gradle"
|
|
gradle_path.mkdir()
|
|
|
|
launcher = root / "jdtls-launcher.jar"
|
|
launcher.touch()
|
|
config = root / "config"
|
|
config.mkdir()
|
|
lombok = root / "lombok.jar"
|
|
lombok.touch()
|
|
|
|
return RuntimeDependencyPaths(
|
|
jre_path=str(jre_path),
|
|
jre_home_path=str(jre_home),
|
|
jdtls_launcher_jar_path=str(launcher),
|
|
jdtls_readonly_config_path=str(config),
|
|
lombok_jar_path=str(lombok),
|
|
gradle_path=str(gradle_path),
|
|
)
|
|
|
|
|
|
def _make_uninitialized_jdtls(
|
|
repository_root: Path, custom_settings: dict[str, object], runtime_dependency_paths: RuntimeDependencyPaths
|
|
) -> EclipseJDTLS:
|
|
"""
|
|
Build an EclipseJDTLS instance without starting dependency setup or JDTLS.
|
|
|
|
:param repository_root: Repository root reported to initialization-parameter generation.
|
|
:param custom_settings: Java language-server settings to attach to the instance.
|
|
:param runtime_dependency_paths: Runtime paths to attach to the instance.
|
|
:return: Partially initialized server suitable for pure parameter-generation tests.
|
|
"""
|
|
server = object.__new__(EclipseJDTLS)
|
|
server.repository_root_path = str(repository_root)
|
|
server._custom_settings = SolidLSPSettings.CustomLSSettings(custom_settings)
|
|
server.runtime_dependency_paths = runtime_dependency_paths
|
|
return server
|
|
|
|
|
|
def _gradle_java_home(initialize_params: dict) -> str:
|
|
"""
|
|
Return the Gradle Java home from initialize parameters.
|
|
|
|
:param initialize_params: JDTLS initialize-parameter payload.
|
|
:return: Configured Gradle Java home.
|
|
"""
|
|
return initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"]
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Gradle Java-home initialize settings
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestGradleJavaHomeInitializeSettings:
|
|
def test_explicit_gradle_java_home_takes_precedence_over_java_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Verify explicit Gradle Java home wins over system ``JAVA_HOME``."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
|
explicit_gradle_jdk = tmp_path / "explicit-gradle-jdk"
|
|
explicit_gradle_jdk.mkdir()
|
|
env_jdk = tmp_path / "env-jdk"
|
|
env_jdk.mkdir()
|
|
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
|
|
|
|
server = _make_uninitialized_jdtls(
|
|
repo,
|
|
{"gradle_java_home": str(explicit_gradle_jdk), "use_system_java_home": True},
|
|
runtime_paths,
|
|
)
|
|
|
|
assert _gradle_java_home(server._create_base_initialize_params()) == str(explicit_gradle_jdk)
|
|
|
|
def test_uses_java_home_for_gradle_when_system_java_home_enabled(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Verify Gradle import uses system ``JAVA_HOME`` when the system setting is enabled."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
|
env_jdk = tmp_path / "env-jdk"
|
|
env_jdk.mkdir()
|
|
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
|
|
|
|
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": True}, runtime_paths)
|
|
|
|
assert _gradle_java_home(server._create_base_initialize_params()) == str(env_jdk)
|
|
|
|
def test_uses_bundled_runtime_for_gradle_when_system_java_home_disabled(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Verify Gradle import uses the bundled runtime when system Java home is disabled."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
|
env_jdk = tmp_path / "env-jdk"
|
|
env_jdk.mkdir()
|
|
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
|
|
|
|
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": False}, runtime_paths)
|
|
|
|
assert _gradle_java_home(server._create_base_initialize_params()) == runtime_paths.jre_path
|
|
|
|
def test_uses_bundled_runtime_for_gradle_when_requested_java_home_is_unset(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Verify Gradle import falls back to the bundled runtime when ``JAVA_HOME`` is unset."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
|
|
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": True}, runtime_paths)
|
|
|
|
assert _gradle_java_home(server._create_base_initialize_params()) == runtime_paths.jre_path
|
|
|
|
def test_missing_explicit_gradle_java_home_raises(self, tmp_path: Path) -> None:
|
|
"""Verify missing explicit Gradle Java home remains a configuration error."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
|
|
|
server = _make_uninitialized_jdtls(repo, {"gradle_java_home": str(tmp_path / "missing-jdk")}, runtime_paths)
|
|
|
|
with pytest.raises(FileNotFoundError, match="Gradle Java home not found"):
|
|
server._create_base_initialize_params()
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _resolve_launcher_jar
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolveLauncherJar:
|
|
def test_picks_main_launcher_excluding_native_fragments(self, tmp_path: Path) -> None:
|
|
plugins = tmp_path / "plugins"
|
|
plugins.mkdir()
|
|
main_jar = plugins / "org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar"
|
|
main_jar.touch()
|
|
# native fragments should NOT be picked
|
|
(plugins / "org.eclipse.equinox.launcher.cocoa.macosx.aarch64_1.2.0.v20240329-1112.jar").touch()
|
|
(plugins / "org.eclipse.equinox.launcher.gtk.linux.x86_64_1.2.0.v20240329-1112.jar").touch()
|
|
(plugins / "org.eclipse.equinox.launcher.win32.win32.x86_64_1.2.0.v20240329-1112.jar").touch()
|
|
|
|
result = EclipseJDTLS.DependencyProvider._resolve_launcher_jar(plugins)
|
|
assert result == main_jar
|
|
|
|
def test_picks_highest_version_when_multiple_main_launchers(self, tmp_path: Path) -> None:
|
|
plugins = tmp_path / "plugins"
|
|
plugins.mkdir()
|
|
(plugins / "org.eclipse.equinox.launcher_1.6.500.v20240916-1115.jar").touch()
|
|
newer = plugins / "org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar"
|
|
newer.touch()
|
|
(plugins / "org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar").touch()
|
|
|
|
result = EclipseJDTLS.DependencyProvider._resolve_launcher_jar(plugins)
|
|
assert result == newer, "Expected the lexicographically highest launcher version to be selected"
|
|
|
|
def test_raises_when_no_launcher_present(self, tmp_path: Path) -> None:
|
|
plugins = tmp_path / "plugins"
|
|
plugins.mkdir()
|
|
# only native fragments, no main launcher
|
|
(plugins / "org.eclipse.equinox.launcher.cocoa.macosx.aarch64_1.2.0.v20240329-1112.jar").touch()
|
|
|
|
with pytest.raises(SolidLSPException, match="No main Equinox launcher jar found"):
|
|
EclipseJDTLS.DependencyProvider._resolve_launcher_jar(plugins)
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _resolve_config_dir
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolveConfigDir:
|
|
@pytest.mark.parametrize(
|
|
"platform_id,expected_dir",
|
|
[
|
|
("osx-arm64", "config_mac_arm"),
|
|
("darwin-arm64", "config_mac_arm"),
|
|
("osx-x64", "config_mac"),
|
|
("linux-arm64", "config_linux_arm"),
|
|
("linux-x64", "config_linux"),
|
|
("win-x64", "config_win"),
|
|
],
|
|
)
|
|
def test_maps_platform_to_correct_config_dir(self, tmp_path: Path, platform_id: str, expected_dir: str) -> None:
|
|
_make_fake_jdtls_install(tmp_path)
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.PlatformUtils.get_platform_id") as mock_get_pid:
|
|
mock_get_pid.return_value.value = platform_id
|
|
result = EclipseJDTLS.DependencyProvider._resolve_config_dir(tmp_path)
|
|
assert result.name == expected_dir
|
|
assert result.is_dir()
|
|
|
|
def test_raises_when_config_dir_missing(self, tmp_path: Path) -> None:
|
|
# plugins/ exists but no config_<platform>/ for current OS
|
|
(tmp_path / "plugins").mkdir()
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.PlatformUtils.get_platform_id") as mock_get_pid:
|
|
mock_get_pid.return_value.value = "linux-x64"
|
|
with pytest.raises(SolidLSPException, match="Config directory .* not found"):
|
|
EclipseJDTLS.DependencyProvider._resolve_config_dir(tmp_path)
|
|
|
|
def test_raises_for_unsupported_platform(self, tmp_path: Path) -> None:
|
|
_make_fake_jdtls_install(tmp_path)
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.PlatformUtils.get_platform_id") as mock_get_pid:
|
|
mock_get_pid.return_value.value = "freebsd-riscv64"
|
|
with pytest.raises(SolidLSPException, match="Unsupported platform"):
|
|
EclipseJDTLS.DependencyProvider._resolve_config_dir(tmp_path)
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _inspect_java
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestInspectJava:
|
|
@staticmethod
|
|
def _fake_subprocess_result(stderr: str, stdout: str = "", returncode: int = 0):
|
|
"""Build a minimal CompletedProcess-like object."""
|
|
|
|
class _Result:
|
|
def __init__(self) -> None:
|
|
self.returncode = returncode
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
|
|
return _Result()
|
|
|
|
def test_parses_temurin_21_output(self) -> None:
|
|
# Real Temurin 21.0.10 output (java -XshowSettings:properties -version writes to stderr)
|
|
stderr = (
|
|
"Property settings:\n"
|
|
" java.home = /Users/me/Library/Java/JavaVirtualMachines/temurin-21.0.10/Contents/Home\n"
|
|
" java.version = 21.0.10\n"
|
|
'openjdk version "21.0.10" 2026-01-20 LTS\n'
|
|
"OpenJDK Runtime Environment Temurin-21.0.10+7 (build 21.0.10+7-LTS)\n"
|
|
)
|
|
with patch("subprocess.run", return_value=self._fake_subprocess_result(stderr)):
|
|
home, major = EclipseJDTLS.DependencyProvider._inspect_java("/usr/bin/java")
|
|
assert home == "/Users/me/Library/Java/JavaVirtualMachines/temurin-21.0.10/Contents/Home"
|
|
assert major == 21
|
|
|
|
def test_parses_openjdk_17_output(self) -> None:
|
|
stderr = 'Property settings:\n java.home = /usr/lib/jvm/java-17-openjdk-amd64\nopenjdk version "17.0.5" 2023-10-17\n'
|
|
with patch("subprocess.run", return_value=self._fake_subprocess_result(stderr)):
|
|
home, major = EclipseJDTLS.DependencyProvider._inspect_java("/usr/bin/java")
|
|
assert home == "/usr/lib/jvm/java-17-openjdk-amd64"
|
|
assert major == 17
|
|
|
|
def test_raises_when_java_home_property_missing(self) -> None:
|
|
stderr = 'java version "21.0.0"\n'
|
|
with patch("subprocess.run", return_value=self._fake_subprocess_result(stderr)):
|
|
with pytest.raises(SolidLSPException, match="Could not parse java.home"):
|
|
EclipseJDTLS.DependencyProvider._inspect_java("/usr/bin/fakejava")
|
|
|
|
def test_raises_when_version_string_missing(self) -> None:
|
|
stderr = " java.home = /opt/jdk\n"
|
|
with patch("subprocess.run", return_value=self._fake_subprocess_result(stderr)):
|
|
with pytest.raises(SolidLSPException, match="Could not parse Java version"):
|
|
EclipseJDTLS.DependencyProvider._inspect_java("/usr/bin/fakejava")
|
|
|
|
def test_raises_on_subprocess_error(self) -> None:
|
|
with patch("subprocess.run", side_effect=OSError("permission denied")):
|
|
with pytest.raises(SolidLSPException, match="Failed to run"):
|
|
EclipseJDTLS.DependencyProvider._inspect_java("/nonexistent/java")
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _resolve_system_jdk
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolveSystemJdk:
|
|
"""Verifies the priority chain: java_home setting > JAVA_HOME env > PATH."""
|
|
|
|
@staticmethod
|
|
def _make_jdk_layout(root: Path, java_exe_name: str = _JAVA_EXE_NAME) -> Path:
|
|
bin_dir = root / "bin"
|
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
(bin_dir / java_exe_name).touch()
|
|
return root
|
|
|
|
def _patch_inspect_java(self, real_home: str, major: int):
|
|
return patch.object(EclipseJDTLS.DependencyProvider, "_inspect_java", return_value=(real_home, major))
|
|
|
|
def test_uses_explicit_java_home_setting(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
explicit_jdk = self._make_jdk_layout(tmp_path / "explicit-jdk")
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
|
|
with self._patch_inspect_java(str(explicit_jdk), 21):
|
|
settings = SolidLSPSettings.CustomLSSettings({"java_home": str(explicit_jdk)})
|
|
home, java_path = EclipseJDTLS.DependencyProvider._resolve_system_jdk(settings)
|
|
|
|
assert Path(home) == explicit_jdk
|
|
assert Path(java_path).name == _JAVA_EXE_NAME
|
|
|
|
def test_falls_back_to_java_home_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
env_jdk = self._make_jdk_layout(tmp_path / "env-jdk")
|
|
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
|
|
|
|
with self._patch_inspect_java(str(env_jdk), 21):
|
|
settings = SolidLSPSettings.CustomLSSettings({})
|
|
home, _ = EclipseJDTLS.DependencyProvider._resolve_system_jdk(settings)
|
|
|
|
assert Path(home) == env_jdk
|
|
|
|
def test_falls_back_to_which_java(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
path_jdk = self._make_jdk_layout(tmp_path / "path-jdk")
|
|
java_path = path_jdk / "bin" / _JAVA_EXE_NAME
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.shutil.which", return_value=str(java_path)):
|
|
with self._patch_inspect_java(str(path_jdk), 21):
|
|
home, _ = EclipseJDTLS.DependencyProvider._resolve_system_jdk(custom_settings)
|
|
|
|
assert Path(home) == path_jdk
|
|
|
|
def test_raises_when_no_java_anywhere(
|
|
self, monkeypatch: pytest.MonkeyPatch, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.shutil.which", return_value=None):
|
|
with pytest.raises(SolidLSPException, match="Could not locate a Java installation"):
|
|
EclipseJDTLS.DependencyProvider._resolve_system_jdk(custom_settings)
|
|
|
|
def test_raises_for_invalid_explicit_java_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
# path exists but no bin/java
|
|
broken = tmp_path / "broken-jdk"
|
|
broken.mkdir()
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
|
|
settings = SolidLSPSettings.CustomLSSettings({"java_home": str(broken)})
|
|
with pytest.raises(SolidLSPException, match=r"java_home=.*invalid"):
|
|
EclipseJDTLS.DependencyProvider._resolve_system_jdk(settings)
|
|
|
|
def test_raises_for_too_old_jdk(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
old_jdk = self._make_jdk_layout(tmp_path / "jdk-17")
|
|
monkeypatch.setenv("JAVA_HOME", str(old_jdk))
|
|
|
|
with self._patch_inspect_java(str(old_jdk), 17):
|
|
settings = SolidLSPSettings.CustomLSSettings({})
|
|
with pytest.raises(SolidLSPException, match=f"requires JDK {JDTLS_MIN_JDK_VERSION}"):
|
|
EclipseJDTLS.DependencyProvider._resolve_system_jdk(settings)
|
|
|
|
def test_uses_real_jdk_home_when_locator_is_macos_stub(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
"""
|
|
Simulates the macOS /usr/bin/java stub: the locator points to /usr/bin/java but the JVM's
|
|
java.home is the actual JDK. The resolver should report the real JDK home.
|
|
"""
|
|
real_jdk = self._make_jdk_layout(tmp_path / "real-jdk-21")
|
|
macos_stub = tmp_path / "fake-usr" / "bin" / _JAVA_EXE_NAME
|
|
macos_stub.parent.mkdir(parents=True)
|
|
macos_stub.touch()
|
|
monkeypatch.delenv("JAVA_HOME", raising=False)
|
|
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.shutil.which", return_value=str(macos_stub)):
|
|
with self._patch_inspect_java(str(real_jdk), 21):
|
|
home, java_path = EclipseJDTLS.DependencyProvider._resolve_system_jdk(custom_settings)
|
|
|
|
# the resolver must trust the JVM's reported java.home, not parent.parent of the stub
|
|
assert Path(home) == real_jdk
|
|
# and prefer the real-home java executable over the stub
|
|
assert Path(java_path) == real_jdk / "bin" / _JAVA_EXE_NAME
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _setup_from_existing_install
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestSetupFromExistingInstall:
|
|
@pytest.fixture
|
|
def lombok_jar(self, tmp_path: Path) -> Path:
|
|
jar = tmp_path / "lombok-1.18.44.jar"
|
|
jar.touch()
|
|
return jar
|
|
|
|
@pytest.fixture
|
|
def jdtls_root(self, tmp_path: Path) -> Path:
|
|
return _make_fake_jdtls_install(tmp_path / "jdtls")
|
|
|
|
def _fake_jdk(self, tmp_path: Path) -> Path:
|
|
jdk = tmp_path / "jdk-21"
|
|
(jdk / "bin").mkdir(parents=True)
|
|
(jdk / "bin" / "java").touch()
|
|
return jdk
|
|
|
|
def test_happy_path_returns_runtime_paths_with_no_gradle_and_no_intellicode(
|
|
self, tmp_path: Path, jdtls_root: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
jdk = self._fake_jdk(tmp_path)
|
|
with patch("solidlsp.language_servers.eclipse_jdtls.PlatformUtils.get_platform_id") as mock_pid:
|
|
mock_pid.return_value.value = "linux-x64"
|
|
with patch.object(EclipseJDTLS.DependencyProvider, "_resolve_system_jdk", return_value=(str(jdk), str(jdk / "bin" / "java"))):
|
|
result = EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_root), str(lombok_jar), custom_settings)
|
|
|
|
assert result.gradle_path is None
|
|
assert result.intellicode_jar_path is None
|
|
assert result.intellisense_members_path is None
|
|
assert result.lombok_jar_path == str(lombok_jar)
|
|
assert result.jre_home_path == str(jdk)
|
|
assert Path(result.jdtls_launcher_jar_path).name.startswith("org.eclipse.equinox.launcher_")
|
|
assert Path(result.jdtls_readonly_config_path).name == "config_linux"
|
|
|
|
def test_raises_for_nonexistent_jdtls_path(
|
|
self, tmp_path: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
with pytest.raises(SolidLSPException, match="not an existing directory"):
|
|
EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(tmp_path / "does-not-exist"), str(lombok_jar), custom_settings)
|
|
|
|
def test_raises_when_plugins_dir_missing(
|
|
self, tmp_path: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
empty_root = tmp_path / "no-plugins"
|
|
empty_root.mkdir()
|
|
with pytest.raises(SolidLSPException, match="'plugins/' directory not found"):
|
|
EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(empty_root), str(lombok_jar), custom_settings)
|
|
|
|
def test_raises_when_lombok_jar_missing(
|
|
self, jdtls_root: Path, tmp_path: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
|
) -> None:
|
|
with pytest.raises(SolidLSPException, match="lombok_path .* does not exist"):
|
|
EclipseJDTLS.DependencyProvider._setup_from_existing_install(
|
|
str(jdtls_root), str(tmp_path / "no-such-lombok.jar"), custom_settings
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _setup_runtime_dependencies (mode-switch logic)
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestSetupRuntimeDependenciesModeSwitch:
|
|
"""Verifies the activation trigger: both jdtls_path and lombok_path => upstream mode."""
|
|
|
|
def test_both_set_invokes_upstream_mode(self) -> None:
|
|
settings = SolidLSPSettings.CustomLSSettings({"jdtls_path": "/x", "lombok_path": "/y"})
|
|
with patch.object(EclipseJDTLS.DependencyProvider, "_setup_from_existing_install", return_value="upstream-result") as mock_upstream:
|
|
result = EclipseJDTLS.DependencyProvider._setup_runtime_dependencies("/ignored", settings)
|
|
mock_upstream.assert_called_once_with("/x", "/y", settings)
|
|
assert result == "upstream-result"
|
|
|
|
def test_only_jdtls_path_set_raises(self) -> None:
|
|
settings = SolidLSPSettings.CustomLSSettings({"jdtls_path": "/x"})
|
|
with pytest.raises(SolidLSPException, match="must be set together"):
|
|
EclipseJDTLS.DependencyProvider._setup_runtime_dependencies("/ignored", settings)
|
|
|
|
def test_only_lombok_path_set_raises(self) -> None:
|
|
settings = SolidLSPSettings.CustomLSSettings({"lombok_path": "/y"})
|
|
with pytest.raises(SolidLSPException, match="must be set together"):
|
|
EclipseJDTLS.DependencyProvider._setup_runtime_dependencies("/ignored", settings)
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# _compute_workspace_hash
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestComputeWorkspaceHash:
|
|
"""
|
|
The launcher jar path is mixed into the hash so that switching JDTLS versions
|
|
(default vscode-java VSIX bump or upstream install change) lands in a separate
|
|
ws_dir and avoids stale OSGi configs from the previous version blocking startup.
|
|
|
|
Workspace-affecting Java settings are also mixed into the hash so stale Maven /
|
|
Gradle import caches are not silently reused after config changes.
|
|
|
|
Backwards-compatibility carve-out: legacy default-mode users on
|
|
INITIAL_VSCODE_JAVA_VERSION keep the original ``md5(repository_root_path)`` format,
|
|
as long as they have not opted into tracked workspace-affecting settings.
|
|
"""
|
|
|
|
REPO = "/home/me/projects/widgets"
|
|
DEFAULT_LAUNCHER = "/srv/serena/static/eclipse-jdtls-1.49.0/plugins/org.eclipse.equinox.launcher_1.7.100.jar"
|
|
UPSTREAM_LAUNCHER = "/opt/homebrew/Cellar/jdtls/1.50.0/libexec/plugins/org.eclipse.equinox.launcher_1.7.0.jar"
|
|
|
|
def _initial_settings(self) -> "SolidLSPSettings.CustomLSSettings":
|
|
from solidlsp.language_servers.eclipse_jdtls import INITIAL_VSCODE_JAVA_VERSION
|
|
|
|
return SolidLSPSettings.CustomLSSettings({"vscode_java_version": INITIAL_VSCODE_JAVA_VERSION})
|
|
|
|
def test_initial_default_mode_matches_pre_upstream_format(self) -> None:
|
|
"""Legacy carve-out: INITIAL default-mode hash MUST equal md5(repository_root_path)."""
|
|
import hashlib
|
|
|
|
expected = hashlib.md5(self.REPO.encode()).hexdigest()
|
|
result = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, self._initial_settings())
|
|
assert result == expected
|
|
|
|
def test_initial_default_mode_ignores_launcher_path(self) -> None:
|
|
"""Legacy INITIAL hash must not depend on launcher path (so legacy users keep cache)."""
|
|
s = self._initial_settings()
|
|
h1 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, s)
|
|
h2 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.UPSTREAM_LAUNCHER, s)
|
|
assert h1 == h2
|
|
|
|
def test_default_mode_non_initial_includes_launcher_path(self) -> None:
|
|
"""Default mode on a non-INITIAL version must mix in launcher path so version bumps re-init."""
|
|
empty_settings = SolidLSPSettings.CustomLSSettings({})
|
|
h1 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, empty_settings)
|
|
h2 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.UPSTREAM_LAUNCHER, empty_settings)
|
|
assert h1 != h2
|
|
|
|
def test_upstream_mode_includes_launcher_path(self) -> None:
|
|
"""When jdtls_path is set, different launcher paths must produce different hashes."""
|
|
settings = SolidLSPSettings.CustomLSSettings({"jdtls_path": "/opt/homebrew/Cellar/jdtls/1.50.0/libexec"})
|
|
h1 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, settings)
|
|
h2 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.UPSTREAM_LAUNCHER, settings)
|
|
assert h1 != h2
|
|
|
|
def test_initial_and_upstream_produce_different_hashes(self) -> None:
|
|
"""Same repo + same launcher path but INITIAL-default vs upstream → different ws_dir."""
|
|
initial_h = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.UPSTREAM_LAUNCHER, self._initial_settings())
|
|
upstream_h = EclipseJDTLS.DependencyProvider._compute_workspace_hash(
|
|
self.REPO,
|
|
self.UPSTREAM_LAUNCHER,
|
|
SolidLSPSettings.CustomLSSettings({"jdtls_path": "/opt/homebrew/Cellar/jdtls/1.50.0/libexec"}),
|
|
)
|
|
assert initial_h != upstream_h
|
|
|
|
def test_different_repo_paths_produce_different_hashes(self) -> None:
|
|
empty_settings = SolidLSPSettings.CustomLSSettings({})
|
|
h1 = EclipseJDTLS.DependencyProvider._compute_workspace_hash("/a/repo", self.DEFAULT_LAUNCHER, empty_settings)
|
|
h2 = EclipseJDTLS.DependencyProvider._compute_workspace_hash("/b/repo", self.DEFAULT_LAUNCHER, empty_settings)
|
|
assert h1 != h2
|
|
|
|
def test_workspace_affecting_settings_change_hash(self) -> None:
|
|
base_settings = SolidLSPSettings.CustomLSSettings({"maven_user_settings": "/tmp/maven-a.xml"})
|
|
changed_settings = SolidLSPSettings.CustomLSSettings({"maven_user_settings": "/tmp/maven-b.xml"})
|
|
|
|
h1 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, base_settings)
|
|
h2 = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, changed_settings)
|
|
|
|
assert h1 != h2
|
|
|
|
def test_legacy_initial_mode_still_invalidates_when_workspace_settings_change(self) -> None:
|
|
settings = SolidLSPSettings.CustomLSSettings(
|
|
{
|
|
"vscode_java_version": self._initial_settings().get("vscode_java_version"),
|
|
"gradle_user_home": "/tmp/gradle-home",
|
|
}
|
|
)
|
|
|
|
legacy_hash = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, self._initial_settings())
|
|
configured_hash = EclipseJDTLS.DependencyProvider._compute_workspace_hash(self.REPO, self.DEFAULT_LAUNCHER, settings)
|
|
|
|
assert configured_hash != legacy_hash
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# is_ignored_dirname (directory-traversal filter)
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestIsIgnoredDirname:
|
|
"""Regression for #1645: the Java language server must not hard-ignore directories whose names
|
|
collide with build-tool output (``lib``, ``dist``, ``classes``, ``out``, ...). Those are all
|
|
valid Java package identifiers, so ignoring them by name hid legitimate source from the symbol
|
|
tools even when ``git check-ignore`` reported nothing. Real build output is already filtered via
|
|
``.gitignore``, so directory traversal should only skip the language-agnostic
|
|
``_ALWAYS_IGNORED_DIRS`` (VCS/venv/cache/IDE internals) inherited from the base language server.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def jdtls(self) -> EclipseJDTLS:
|
|
# is_ignored_dirname only reads the class-level _ALWAYS_IGNORED_DIRS, so an uninitialized
|
|
# instance is sufficient here (no Java or JDTLS process required).
|
|
return object.__new__(EclipseJDTLS)
|
|
|
|
@pytest.mark.parametrize("dirname", ["lib", "dist", "classes", "out", "target", "build", "bin"])
|
|
def test_valid_package_dirnames_are_not_ignored(self, jdtls: EclipseJDTLS, dirname: str) -> None:
|
|
assert jdtls.is_ignored_dirname(dirname) is False
|
|
|
|
@pytest.mark.parametrize("dirname", [".git", ".venv", ".idea", ".serena", ".mypy_cache"])
|
|
def test_always_ignored_dirs_are_still_ignored(self, jdtls: EclipseJDTLS, dirname: str) -> None:
|
|
assert jdtls.is_ignored_dirname(dirname) is True
|