Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions src/test/python_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Shared test fixtures for lsp_server unit tests.

Provides mock LSP dependencies so that ``import lsp_server`` succeeds
without the full VS Code extension environment, and exposes reusable
fixtures for patching the LSP_SERVER singleton.
"""

import pathlib
import sys
import types
from unittest.mock import patch

import pytest

# ---------------------------------------------------------------------------
# Module-level mock injection
# ---------------------------------------------------------------------------
_INJECTED_MODULES = []
_INJECTED_PATH = None


def setup_lsp_mocks():
"""Inject mock LSP dependencies into ``sys.modules`` and ``sys.path``.

Tracks what is injected so :func:`teardown_lsp_mocks` can undo it.
"""
global _INJECTED_PATH

class _MockLS:
def __init__(self, *args, **kwargs):
pass

def feature(self, *args, **kwargs):
return lambda f: f

def command(self, *args, **kwargs):
return lambda f: f

# Pygls 1 API (kept for backward-compat with older test files)
def show_message_log(self, *args, **kwargs):
pass

def show_message(self, *args, **kwargs):
pass

# Pygls 2 API
def window_log_message(self, *args, **kwargs):
pass

def window_show_message(self, *args, **kwargs):
pass

mock_lsp_server_mod = types.ModuleType("pygls.lsp.server")
mock_lsp_server_mod.LanguageServer = _MockLS

_Doc = type("Document", (), {"path": None})
mock_workspace = types.ModuleType("pygls.workspace")
mock_workspace.Document = _Doc
mock_workspace.TextDocument = _Doc

mock_uris = types.ModuleType("pygls.uris")
mock_uris.from_fs_path = lambda p: "file://" + p

mock_lsp = types.ModuleType("lsprotocol.types")
for _name in [
"TEXT_DOCUMENT_DID_OPEN",
"TEXT_DOCUMENT_DID_SAVE",
"TEXT_DOCUMENT_DID_CLOSE",
"TEXT_DOCUMENT_CODE_ACTION",
"INITIALIZE",
"EXIT",
"SHUTDOWN",
"NOTEBOOK_DOCUMENT_DID_OPEN",
"NOTEBOOK_DOCUMENT_DID_CHANGE",
"NOTEBOOK_DOCUMENT_DID_SAVE",
"NOTEBOOK_DOCUMENT_DID_CLOSE",
]:
setattr(mock_lsp, _name, _name)

mock_lsp.CodeActionKind = types.SimpleNamespace(QuickFix="quickfix")

class _FlexClass:
"""Accepts arbitrary positional/keyword args (stores kwargs for inspection)."""

def __init__(self, *args, **kwargs):
self._kwargs = kwargs

for _name in [
"Diagnostic",
"DiagnosticSeverity",
"DidCloseTextDocumentParams",
"DidOpenTextDocumentParams",
"DidSaveTextDocumentParams",
"DidChangeNotebookDocumentParams",
"DidCloseNotebookDocumentParams",
"DidOpenNotebookDocumentParams",
"DidSaveNotebookDocumentParams",
"InitializeParams",
"NotebookCellKind",
"NotebookCellLanguage",
"NotebookDocumentFilterWithNotebook",
"NotebookDocumentSyncOptions",
"Position",
"Range",
"TextEdit",
"CodeAction",
"CodeActionOptions",
"Command",
"WorkspaceEdit",
"TextDocumentEdit",
"OptionalVersionedTextDocumentIdentifier",
"LogMessageParams",
"ShowMessageParams",
"PublishDiagnosticsParams",
]:
setattr(mock_lsp, _name, _FlexClass)
mock_lsp.MessageType = types.SimpleNamespace(Log=4, Error=1, Warning=2, Info=3)

# lsp_utils and lsp_jsonrpc live in bundled/tool and only use stdlib —
# let the real modules be imported so other tests (e.g. test_stdlib_detection)
# are not broken by a partial mock.

for _mod_name, _mod in [
("pygls", types.ModuleType("pygls")),
("pygls.lsp", types.ModuleType("pygls.lsp")),
("pygls.lsp.server", mock_lsp_server_mod),
("pygls.workspace", mock_workspace),
("pygls.uris", mock_uris),
("lsprotocol", types.ModuleType("lsprotocol")),
("lsprotocol.types", mock_lsp),
]:
if _mod_name not in sys.modules:
sys.modules[_mod_name] = _mod
_INJECTED_MODULES.append(_mod_name)

tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool")
if tool_dir not in sys.path:
sys.path.insert(0, tool_dir)
_INJECTED_PATH = tool_dir


# Run at import time so test modules can ``import lsp_server`` at the top level.
setup_lsp_mocks()


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session", autouse=True)
def _lsp_mock_teardown():
"""Remove injected mock modules and sys.path entries after the session."""
yield
for mod_name in _INJECTED_MODULES:
sys.modules.pop(mod_name, None)
_INJECTED_MODULES.clear()
if _INJECTED_PATH and _INJECTED_PATH in sys.path:
sys.path.remove(_INJECTED_PATH)


@pytest.fixture()
def patched_lsp_server():
"""Patch ``LSP_SERVER.window_log_message`` and ``window_show_message``
with ``MagicMock`` instances that are automatically restored after the test.
"""
import lsp_server

with patch.object(
lsp_server.LSP_SERVER, "window_log_message"
) as log_mock, patch.object(
lsp_server.LSP_SERVER, "window_show_message"
) as show_mock:
yield log_mock, show_mock
56 changes: 56 additions & 0 deletions src/test/python_tests/test_global_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Unit tests for _get_global_defaults() in lsp_server.

Covers the fix from PR #327 where ignorePatterns was always returning []
instead of reading from GLOBAL_SETTINGS.

Mock setup is provided by conftest.py (setup_lsp_mocks).
"""

import lsp_server


def _with_global_settings(overrides, fn):
"""Run fn with GLOBAL_SETTINGS temporarily set to overrides."""
original = lsp_server.GLOBAL_SETTINGS.copy()
try:
lsp_server.GLOBAL_SETTINGS.clear()
lsp_server.GLOBAL_SETTINGS.update(overrides)
return fn()
finally:
lsp_server.GLOBAL_SETTINGS.clear()
lsp_server.GLOBAL_SETTINGS.update(original)


def test_ignore_patterns_read_from_global_settings():
"""_get_global_defaults() returns ignorePatterns from GLOBAL_SETTINGS."""
result = _with_global_settings(
{"ignorePatterns": ["**/vendor/**", "**/.tox/**"]},
lsp_server._get_global_defaults,
)
assert result["ignorePatterns"] == ["**/vendor/**", "**/.tox/**"]


def test_ignore_patterns_defaults_to_empty_list():
"""_get_global_defaults() returns [] when GLOBAL_SETTINGS has no ignorePatterns."""
result = _with_global_settings({}, lsp_server._get_global_defaults)
assert result["ignorePatterns"] == []


def test_show_notifications_read_from_global_settings():
"""_get_global_defaults() returns showNotifications from GLOBAL_SETTINGS."""
result = _with_global_settings(
{"showNotifications": "always"},
lsp_server._get_global_defaults,
)
assert result["showNotifications"] == "always"


def test_import_strategy_read_from_global_settings():
"""_get_global_defaults() returns importStrategy from GLOBAL_SETTINGS."""
result = _with_global_settings(
{"importStrategy": "fromEnvironment"},
lsp_server._get_global_defaults,
)
assert result["importStrategy"] == "fromEnvironment"
160 changes: 160 additions & 0 deletions src/test/python_tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Unit tests for the logging/notification helpers in lsp_server.

Covers the Pygls 2 migration (PR #367) which changed logging calls from
show_message_log/show_message to window_log_message/window_show_message
with parameter objects, and verifies the LS_SHOW_NOTIFICATION gating logic.

Mock setup is provided by conftest.py (setup_lsp_mocks).
LSP_SERVER patching uses the ``patched_lsp_server`` fixture which restores
originals automatically via ``unittest.mock.patch.object``.
"""

import os
from unittest.mock import patch

import lsp_server


# ---------------------------------------------------------------------------
# log_to_output
# ---------------------------------------------------------------------------
def test_log_to_output_calls_window_log_message(patched_lsp_server):
"""log_to_output uses the Pygls 2 window_log_message API."""
log_mock, show_mock = patched_lsp_server

lsp_server.log_to_output("hello")

log_mock.assert_called_once()
show_mock.assert_not_called()


# ---------------------------------------------------------------------------
# log_error
# ---------------------------------------------------------------------------
def test_log_error_always_logs(patched_lsp_server):
"""log_error always calls window_log_message regardless of notification setting."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}):
lsp_server.log_error("error occurred")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_error_shows_notification_on_error(patched_lsp_server):
"""log_error shows a notification popup when LS_SHOW_NOTIFICATION=onError."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}):
lsp_server.log_error("error occurred")

log_mock.assert_called_once()
show_mock.assert_called_once()


def test_log_error_shows_notification_on_always(patched_lsp_server):
"""log_error shows a notification popup when LS_SHOW_NOTIFICATION=always."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}):
lsp_server.log_error("error occurred")

log_mock.assert_called_once()
show_mock.assert_called_once()


# ---------------------------------------------------------------------------
# log_warning
# ---------------------------------------------------------------------------
def test_log_warning_no_notification_when_off(patched_lsp_server):
"""log_warning does not show notification when LS_SHOW_NOTIFICATION=off."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}):
lsp_server.log_warning("warning message")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_warning_no_notification_on_error_only(patched_lsp_server):
"""log_warning does not show notification when LS_SHOW_NOTIFICATION=onError."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}):
lsp_server.log_warning("warning message")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_warning_shows_notification_on_warning(patched_lsp_server):
"""log_warning shows notification when LS_SHOW_NOTIFICATION=onWarning."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onWarning"}):
lsp_server.log_warning("warning message")

log_mock.assert_called_once()
show_mock.assert_called_once()


def test_log_warning_shows_notification_on_always(patched_lsp_server):
"""log_warning shows notification when LS_SHOW_NOTIFICATION=always."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}):
lsp_server.log_warning("warning message")

log_mock.assert_called_once()
show_mock.assert_called_once()


# ---------------------------------------------------------------------------
# log_always
# ---------------------------------------------------------------------------
def test_log_always_no_notification_when_off(patched_lsp_server):
"""log_always does not show notification when LS_SHOW_NOTIFICATION=off."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}):
lsp_server.log_always("info message")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_always_no_notification_on_error(patched_lsp_server):
"""log_always does not show notification when LS_SHOW_NOTIFICATION=onError."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}):
lsp_server.log_always("info message")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_always_no_notification_on_warning(patched_lsp_server):
"""log_always does not show notification when LS_SHOW_NOTIFICATION=onWarning."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onWarning"}):
lsp_server.log_always("info message")

log_mock.assert_called_once()
show_mock.assert_not_called()


def test_log_always_shows_notification_on_always(patched_lsp_server):
"""log_always shows notification only when LS_SHOW_NOTIFICATION=always."""
log_mock, show_mock = patched_lsp_server

with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}):
lsp_server.log_always("info message")

log_mock.assert_called_once()
show_mock.assert_called_once()
Loading