diff --git a/src/test/python_tests/conftest.py b/src/test/python_tests/conftest.py new file mode 100644 index 0000000..8f39e7b --- /dev/null +++ b/src/test/python_tests/conftest.py @@ -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 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..70f1931 --- /dev/null +++ b/src/test/python_tests/test_global_defaults.py @@ -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" diff --git a/src/test/python_tests/test_logging.py b/src/test/python_tests/test_logging.py new file mode 100644 index 0000000..6f94a01 --- /dev/null +++ b/src/test/python_tests/test_logging.py @@ -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()