fix(dart): wait for analyzer readiness signal before returning from start()

DartLanguageServer registered handlers for $/analyzerStatus and
experimental/serverStatus but both were no-ops, so start() returned as
soon as initialized was sent instead of waiting for either signal, unlike
every other backend (pyright, basedpyright, rust-analyzer, clojure-lsp,
jedi, omnisharp, clangd) that receives the same notifications. A request
issued right after activation could return before the workspace scan
finished.

Gate on a threading.Event set by either notification, bounded by 60s,
mirroring pyright_server.py's analysis_complete pattern.
This commit is contained in:
Amir Fathi authored and Dr. Dominik Jain committed 2026-09-03 13:02:50 +02:00
1 parent 813fd98f4f
commit e1322a3b99
3 files changed
+137 -2

No files matched your search

+104
View File
@@ -1,6 +1,7 @@
"""Regression test for Dart analyzer-status notification handling."""
import logging
import threading
from pathlib import Path
from typing import Any
from unittest.mock import patch
@@ -61,6 +62,8 @@ def test_analyzer_status_notification_is_handled_after_startup(
"_create_language_server_interface",
return_value=server_interface,
),
# No readiness signal is ever sent here, so keep the fallback wait short.
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 0.05),
):
server = DartLanguageServer(
LanguageServerConfig(ls_id=LanguageServerId.DART),
@@ -73,3 +76,104 @@ def test_analyzer_status_notification_is_handled_after_startup(
server_interface.receive_notification("$/analyzerStatus", {"isAnalyzing": True})
assert "Unhandled method '$/analyzerStatus'" not in caplog.messages
@pytest.mark.parametrize(
("method", "params"),
[
("experimental/serverStatus", {"quiescent": True}),
("$/analyzerStatus", {"isAnalyzing": False}),
],
)
def test_start_waits_for_analysis_readiness_signal(
tmp_path: Path,
method: str,
params: dict[str, Any],
) -> None:
"""`start()` must block until the Dart analysis server reports it has finished its
initial workspace scan, instead of returning as soon as `initialized` is sent.
"""
settings = SolidLSPSettings(
solidlsp_dir=str(tmp_path / "global"),
project_data_path=str(tmp_path / "project"),
ls_specific_settings={LanguageServerId.DART: {}},
)
server_interface = _FakeLanguageServerInterface()
with (
patch.object(
DartLanguageServer,
"_setup_runtime_dependencies",
return_value=str(tmp_path / "dart-sdk"),
),
patch.object(
DartLanguageServer,
"_create_language_server_interface",
return_value=server_interface,
),
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 2.0, create=True),
):
server = DartLanguageServer(
LanguageServerConfig(ls_id=LanguageServerId.DART),
str(tmp_path),
settings,
)
start_thread = threading.Thread(target=server.start)
start_thread.start()
try:
# No readiness signal has been sent yet: start() must still be blocked.
start_thread.join(timeout=0.3)
assert start_thread.is_alive(), "start() returned before any readiness signal was received"
server_interface.receive_notification(method, params)
start_thread.join(timeout=2.0)
assert not start_thread.is_alive(), f"start() did not resume after {method} {params}"
finally:
if start_thread.is_alive():
server_interface.receive_notification("experimental/serverStatus", {"quiescent": True})
start_thread.join(timeout=2.0)
def test_start_proceeds_after_readiness_timeout_if_no_signal_arrives(
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""If neither readiness notification ever arrives, start() must still return once
`_TIMEOUT_FOR_INITIAL_ANALYSIS` elapses, rather than blocking forever.
"""
settings = SolidLSPSettings(
solidlsp_dir=str(tmp_path / "global"),
project_data_path=str(tmp_path / "project"),
ls_specific_settings={LanguageServerId.DART: {}},
)
server_interface = _FakeLanguageServerInterface()
with (
patch.object(
DartLanguageServer,
"_setup_runtime_dependencies",
return_value=str(tmp_path / "dart-sdk"),
),
patch.object(
DartLanguageServer,
"_create_language_server_interface",
return_value=server_interface,
),
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 0.2, create=True),
):
server = DartLanguageServer(
LanguageServerConfig(ls_id=LanguageServerId.DART),
str(tmp_path),
settings,
)
start_thread = threading.Thread(target=server.start)
with caplog.at_level(logging.WARNING):
start_thread.start()
start_thread.join(timeout=2.0)
assert not start_thread.is_alive(), "start() never returned despite the readiness timeout elapsing"
assert any("Timeout waiting for dart-language-server analysis completion" in m for m in caplog.messages)
assert server.analysis_complete.is_set()