From 2a1546a7457bf035ac753eef8b7469f0bd151532 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 31 Mar 2026 19:43:51 -0700 Subject: [PATCH 1/5] test: add coverage for _get_global_defaults() ignorePatterns (refs #458, relates to #327) Verify that _get_global_defaults() reads ignorePatterns from GLOBAL_SETTINGS instead of always returning an empty list. Also covers showNotifications and importStrategy global settings fallback paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_global_defaults.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/test/python_tests/test_global_defaults.py diff --git a/src/test/python_tests/test_global_defaults.py b/src/test/python_tests/test_global_defaults.py new file mode 100644 index 0000000..3e6f6db --- /dev/null +++ b/src/test/python_tests/test_global_defaults.py @@ -0,0 +1,181 @@ +# 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. +""" + +import pathlib +import sys +import types + + +# --------------------------------------------------------------------------- +# Stub out bundled LSP dependencies so lsp_server can be imported without the +# full VS Code extension environment. +# --------------------------------------------------------------------------- +def _setup_mocks(): + 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 + + 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: + def __init__(self, *args, **kwargs): + pass + + 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) + + mock_lsp_utils = types.ModuleType("lsp_utils") + mock_lsp_utils.normalize_path = lambda p, **kw: str(p) + mock_lsp_utils.is_stdlib_file = lambda p: False + mock_lsp_utils.is_match = lambda patterns, path: False + mock_lsp_utils.is_current_interpreter = lambda i: True + mock_lsp_utils.RunResult = type("RunResult", (), {}) + mock_lsp_utils.substitute_attr = None + + class _QuickFixError(Exception): + pass + + mock_lsp_utils.QuickFixRegistrationError = _QuickFixError + + mock_jsonrpc = types.ModuleType("lsp_jsonrpc") + mock_jsonrpc.shutdown_json_rpc = lambda: None + + 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), + ("lsp_jsonrpc", mock_jsonrpc), + ("lsp_utils", mock_lsp_utils), + ]: + if _mod_name not in sys.modules: + sys.modules[_mod_name] = _mod + + tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool") + if tool_dir not in sys.path: + sys.path.insert(0, tool_dir) + + +_setup_mocks() + +import lsp_server # noqa: E402 + + +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" From aca2d23c13ed4fb6b0f14252dd3a39925ba6dc36 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 31 Mar 2026 20:15:15 -0700 Subject: [PATCH 2/5] test: add coverage for Pygls 2 logging API and notification logic (refs #458, relates to #367) Verify that log_to_output, log_error, log_warning, and log_always use the Pygls 2 window_log_message/window_show_message APIs. Tests cover all LS_SHOW_NOTIFICATION gating levels (off, onError, onWarning, always) to ensure notification popups are shown at the correct thresholds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_logging.py | 291 ++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 src/test/python_tests/test_logging.py diff --git a/src/test/python_tests/test_logging.py b/src/test/python_tests/test_logging.py new file mode 100644 index 0000000..e55c88b --- /dev/null +++ b/src/test/python_tests/test_logging.py @@ -0,0 +1,291 @@ +# 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. +""" + +import os +import pathlib +import sys +import types +from unittest.mock import MagicMock, patch + + +# --------------------------------------------------------------------------- +# Stub out bundled LSP dependencies so lsp_server can be imported without the +# full VS Code extension environment. +# --------------------------------------------------------------------------- +def _setup_mocks(): + 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 + + 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: + 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) + + mock_lsp_utils = types.ModuleType("lsp_utils") + mock_lsp_utils.normalize_path = lambda p, **kw: str(p) + mock_lsp_utils.is_stdlib_file = lambda p: False + mock_lsp_utils.is_match = lambda patterns, path: False + mock_lsp_utils.is_current_interpreter = lambda i: True + mock_lsp_utils.RunResult = type("RunResult", (), {}) + mock_lsp_utils.substitute_attr = None + + class _QuickFixError(Exception): + pass + + mock_lsp_utils.QuickFixRegistrationError = _QuickFixError + + mock_jsonrpc = types.ModuleType("lsp_jsonrpc") + mock_jsonrpc.shutdown_json_rpc = lambda: None + + 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), + ("lsp_jsonrpc", mock_jsonrpc), + ("lsp_utils", mock_lsp_utils), + ]: + if _mod_name not in sys.modules: + sys.modules[_mod_name] = _mod + + tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool") + if tool_dir not in sys.path: + sys.path.insert(0, tool_dir) + + +_setup_mocks() + +import lsp_server # noqa: E402 + + +def _patch_lsp_server(): + """Replace LSP_SERVER logging methods with mocks and return them.""" + log_mock = MagicMock() + show_mock = MagicMock() + lsp_server.LSP_SERVER.window_log_message = log_mock + lsp_server.LSP_SERVER.window_show_message = show_mock + return log_mock, show_mock + + +# --------------------------------------------------------------------------- +# log_to_output +# --------------------------------------------------------------------------- +def test_log_to_output_calls_window_log_message(): + """log_to_output uses the Pygls 2 window_log_message API.""" + log_mock, show_mock = _patch_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(): + """log_error always calls window_log_message regardless of notification setting.""" + log_mock, show_mock = _patch_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(): + """log_error shows a notification popup when LS_SHOW_NOTIFICATION=onError.""" + log_mock, show_mock = _patch_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(): + """log_error shows a notification popup when LS_SHOW_NOTIFICATION=always.""" + log_mock, show_mock = _patch_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(): + """log_warning does not show notification when LS_SHOW_NOTIFICATION=off.""" + log_mock, show_mock = _patch_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(): + """log_warning does not show notification when LS_SHOW_NOTIFICATION=onError.""" + log_mock, show_mock = _patch_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(): + """log_warning shows notification when LS_SHOW_NOTIFICATION=onWarning.""" + log_mock, show_mock = _patch_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(): + """log_warning shows notification when LS_SHOW_NOTIFICATION=always.""" + log_mock, show_mock = _patch_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(): + """log_always does not show notification when LS_SHOW_NOTIFICATION=off.""" + log_mock, show_mock = _patch_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(): + """log_always does not show notification when LS_SHOW_NOTIFICATION=onError.""" + log_mock, show_mock = _patch_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(): + """log_always does not show notification when LS_SHOW_NOTIFICATION=onWarning.""" + log_mock, show_mock = _patch_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(): + """log_always shows notification only when LS_SHOW_NOTIFICATION=always.""" + log_mock, show_mock = _patch_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() From e1a658ca76ec6a16450744f26badf3e159288d5c Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Apr 2026 11:30:19 -0700 Subject: [PATCH 3/5] fix: address review comments - extract shared mocks, fix state leaks - Extract duplicated _setup_mocks() into conftest.py with session teardown - Unify _FlexClass (kwargs-storing variant) to eliminate subtle divergence - Replace _patch_lsp_server() with patched_lsp_server fixture using patch.object for automatic restoration (fixes state leak between tests) - Use real lsp_utils/lsp_jsonrpc from bundled/tool instead of partial mocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/conftest.py | 177 ++++++++++++++++ src/test/python_tests/test_global_defaults.py | 131 +----------- src/test/python_tests/test_logging.py | 191 +++--------------- 3 files changed, 210 insertions(+), 289 deletions(-) create mode 100644 src/test/python_tests/conftest.py diff --git a/src/test/python_tests/conftest.py b/src/test/python_tests/conftest.py new file mode 100644 index 0000000..ea1f683 --- /dev/null +++ b/src/test/python_tests/conftest.py @@ -0,0 +1,177 @@ +# 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 + +import pytest +from unittest.mock import patch + + +# --------------------------------------------------------------------------- +# 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 diff --git a/src/test/python_tests/test_global_defaults.py b/src/test/python_tests/test_global_defaults.py index 3e6f6db..70f1931 100644 --- a/src/test/python_tests/test_global_defaults.py +++ b/src/test/python_tests/test_global_defaults.py @@ -4,136 +4,11 @@ Covers the fix from PR #327 where ignorePatterns was always returning [] instead of reading from GLOBAL_SETTINGS. -""" - -import pathlib -import sys -import types - - -# --------------------------------------------------------------------------- -# Stub out bundled LSP dependencies so lsp_server can be imported without the -# full VS Code extension environment. -# --------------------------------------------------------------------------- -def _setup_mocks(): - 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 - - 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: - def __init__(self, *args, **kwargs): - pass - - 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) - - mock_lsp_utils = types.ModuleType("lsp_utils") - mock_lsp_utils.normalize_path = lambda p, **kw: str(p) - mock_lsp_utils.is_stdlib_file = lambda p: False - mock_lsp_utils.is_match = lambda patterns, path: False - mock_lsp_utils.is_current_interpreter = lambda i: True - mock_lsp_utils.RunResult = type("RunResult", (), {}) - mock_lsp_utils.substitute_attr = None - - class _QuickFixError(Exception): - pass - - mock_lsp_utils.QuickFixRegistrationError = _QuickFixError - - mock_jsonrpc = types.ModuleType("lsp_jsonrpc") - mock_jsonrpc.shutdown_json_rpc = lambda: None - - 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), - ("lsp_jsonrpc", mock_jsonrpc), - ("lsp_utils", mock_lsp_utils), - ]: - if _mod_name not in sys.modules: - sys.modules[_mod_name] = _mod - - tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool") - if tool_dir not in sys.path: - sys.path.insert(0, tool_dir) - - -_setup_mocks() +Mock setup is provided by conftest.py (setup_lsp_mocks). +""" -import lsp_server # noqa: E402 +import lsp_server def _with_global_settings(overrides, fn): diff --git a/src/test/python_tests/test_logging.py b/src/test/python_tests/test_logging.py index e55c88b..6f94a01 100644 --- a/src/test/python_tests/test_logging.py +++ b/src/test/python_tests/test_logging.py @@ -5,155 +5,24 @@ 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 -import pathlib -import sys -import types -from unittest.mock import MagicMock, patch - +from unittest.mock import patch -# --------------------------------------------------------------------------- -# Stub out bundled LSP dependencies so lsp_server can be imported without the -# full VS Code extension environment. -# --------------------------------------------------------------------------- -def _setup_mocks(): - 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 - - 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: - 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) - - mock_lsp_utils = types.ModuleType("lsp_utils") - mock_lsp_utils.normalize_path = lambda p, **kw: str(p) - mock_lsp_utils.is_stdlib_file = lambda p: False - mock_lsp_utils.is_match = lambda patterns, path: False - mock_lsp_utils.is_current_interpreter = lambda i: True - mock_lsp_utils.RunResult = type("RunResult", (), {}) - mock_lsp_utils.substitute_attr = None - - class _QuickFixError(Exception): - pass - - mock_lsp_utils.QuickFixRegistrationError = _QuickFixError - - mock_jsonrpc = types.ModuleType("lsp_jsonrpc") - mock_jsonrpc.shutdown_json_rpc = lambda: None - - 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), - ("lsp_jsonrpc", mock_jsonrpc), - ("lsp_utils", mock_lsp_utils), - ]: - if _mod_name not in sys.modules: - sys.modules[_mod_name] = _mod - - tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool") - if tool_dir not in sys.path: - sys.path.insert(0, tool_dir) - - -_setup_mocks() - -import lsp_server # noqa: E402 - - -def _patch_lsp_server(): - """Replace LSP_SERVER logging methods with mocks and return them.""" - log_mock = MagicMock() - show_mock = MagicMock() - lsp_server.LSP_SERVER.window_log_message = log_mock - lsp_server.LSP_SERVER.window_show_message = show_mock - return log_mock, show_mock +import lsp_server # --------------------------------------------------------------------------- # log_to_output # --------------------------------------------------------------------------- -def test_log_to_output_calls_window_log_message(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server lsp_server.log_to_output("hello") @@ -164,9 +33,9 @@ def test_log_to_output_calls_window_log_message(): # --------------------------------------------------------------------------- # log_error # --------------------------------------------------------------------------- -def test_log_error_always_logs(): +def test_log_error_always_logs(patched_lsp_server): """log_error always calls window_log_message regardless of notification setting.""" - log_mock, show_mock = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}): lsp_server.log_error("error occurred") @@ -175,9 +44,9 @@ def test_log_error_always_logs(): show_mock.assert_not_called() -def test_log_error_shows_notification_on_error(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}): lsp_server.log_error("error occurred") @@ -186,9 +55,9 @@ def test_log_error_shows_notification_on_error(): show_mock.assert_called_once() -def test_log_error_shows_notification_on_always(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}): lsp_server.log_error("error occurred") @@ -200,9 +69,9 @@ def test_log_error_shows_notification_on_always(): # --------------------------------------------------------------------------- # log_warning # --------------------------------------------------------------------------- -def test_log_warning_no_notification_when_off(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}): lsp_server.log_warning("warning message") @@ -211,9 +80,9 @@ def test_log_warning_no_notification_when_off(): show_mock.assert_not_called() -def test_log_warning_no_notification_on_error_only(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}): lsp_server.log_warning("warning message") @@ -222,9 +91,9 @@ def test_log_warning_no_notification_on_error_only(): show_mock.assert_not_called() -def test_log_warning_shows_notification_on_warning(): +def test_log_warning_shows_notification_on_warning(patched_lsp_server): """log_warning shows notification when LS_SHOW_NOTIFICATION=onWarning.""" - log_mock, show_mock = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onWarning"}): lsp_server.log_warning("warning message") @@ -233,9 +102,9 @@ def test_log_warning_shows_notification_on_warning(): show_mock.assert_called_once() -def test_log_warning_shows_notification_on_always(): +def test_log_warning_shows_notification_on_always(patched_lsp_server): """log_warning shows notification when LS_SHOW_NOTIFICATION=always.""" - log_mock, show_mock = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}): lsp_server.log_warning("warning message") @@ -247,9 +116,9 @@ def test_log_warning_shows_notification_on_always(): # --------------------------------------------------------------------------- # log_always # --------------------------------------------------------------------------- -def test_log_always_no_notification_when_off(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "off"}): lsp_server.log_always("info message") @@ -258,9 +127,9 @@ def test_log_always_no_notification_when_off(): show_mock.assert_not_called() -def test_log_always_no_notification_on_error(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onError"}): lsp_server.log_always("info message") @@ -269,9 +138,9 @@ def test_log_always_no_notification_on_error(): show_mock.assert_not_called() -def test_log_always_no_notification_on_warning(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "onWarning"}): lsp_server.log_always("info message") @@ -280,9 +149,9 @@ def test_log_always_no_notification_on_warning(): show_mock.assert_not_called() -def test_log_always_shows_notification_on_always(): +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 = _patch_lsp_server() + log_mock, show_mock = patched_lsp_server with patch.dict(os.environ, {"LS_SHOW_NOTIFICATION": "always"}): lsp_server.log_always("info message") From 8bf552e8ca88a0a2dfd8b9bb28d63ea814eb7a9d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Apr 2026 11:38:01 -0700 Subject: [PATCH 4/5] style: fix black formatting in conftest.py --- src/test/python_tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/python_tests/conftest.py b/src/test/python_tests/conftest.py index ea1f683..83f1d14 100644 --- a/src/test/python_tests/conftest.py +++ b/src/test/python_tests/conftest.py @@ -14,7 +14,6 @@ import pytest from unittest.mock import patch - # --------------------------------------------------------------------------- # Module-level mock injection # --------------------------------------------------------------------------- From 240ce7c516b366e3da259d561232667d5347d9a8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Apr 2026 11:44:19 -0700 Subject: [PATCH 5/5] style: fix isort import ordering in conftest.py --- src/test/python_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/python_tests/conftest.py b/src/test/python_tests/conftest.py index 83f1d14..8f39e7b 100644 --- a/src/test/python_tests/conftest.py +++ b/src/test/python_tests/conftest.py @@ -10,9 +10,9 @@ import pathlib import sys import types +from unittest.mock import patch import pytest -from unittest.mock import patch # --------------------------------------------------------------------------- # Module-level mock injection