Files
oraios--serena/test/solidlsp/java/test_jdtls_path_resolution.py
wehub-resource-sync c6af9e284a
Tests / catch-all (windows-latest) (push) Has been cancelled
Tests / jvm (macos-latest) (push) Has been cancelled
Tests / jvm (ubuntu-latest) (push) Has been cancelled
Tests / jvm (windows-latest) (push) Has been cancelled
Tests / native (macos-latest) (push) Has been cancelled
Tests / native (ubuntu-latest) (push) Has been cancelled
Tests / native (windows-latest) (push) Has been cancelled
Tests / niche (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (macos-latest) (push) Has been cancelled
Tests / other-langs (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (windows-latest) (push) Has been cancelled
Tests / catch-all (macos-latest) (push) Has been cancelled
Tests / catch-all (ubuntu-latest) (push) Has been cancelled
Docs Build / build (push) Has been cancelled
Docs Build / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Build and Push Docker Images / build-and-push (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:38 +08:00

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