Skip to content

Commit 0f7ace5

Browse files
authored
Gracefully handle OSError in change_cwd (#264)
* Gracefully handle OSError in change_cwd; log inaccessible directory with error text * Add tests
1 parent 1e3b00b commit 0f7ace5

2 files changed

Lines changed: 85 additions & 3 deletions

File tree

bundled/tool/lsp_utils.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import contextlib
77
import io
8+
import logging
89
import os
910
import os.path
1011
import runpy
@@ -102,9 +103,21 @@ def redirect_io(stream: str, new_stream):
102103
@contextlib.contextmanager
103104
def change_cwd(new_cwd):
104105
"""Change working directory before running code."""
105-
os.chdir(new_cwd)
106-
yield
107-
os.chdir(SERVER_CWD)
106+
try:
107+
os.chdir(new_cwd)
108+
except OSError as e:
109+
logging.warning(
110+
"Failed to change directory to %r, running in %r instead: %s",
111+
new_cwd,
112+
SERVER_CWD,
113+
e,
114+
)
115+
yield
116+
return
117+
try:
118+
yield
119+
finally:
120+
os.chdir(SERVER_CWD)
108121

109122

110123
def _run_module(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Unit tests for the change_cwd() context manager in lsp_utils."""
4+
5+
import logging
6+
import os
7+
import pathlib
8+
import sys
9+
from unittest.mock import patch
10+
11+
# Ensure bundled libs and tool are importable.
12+
_PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent.parent
13+
sys.path.insert(0, os.fsdecode(_PROJECT_ROOT / "bundled" / "libs"))
14+
sys.path.insert(0, os.fsdecode(_PROJECT_ROOT / "bundled" / "tool"))
15+
16+
import lsp_utils
17+
18+
19+
def test_change_cwd_happy_path(tmp_path):
20+
"""change_cwd switches to the requested directory and restores SERVER_CWD after."""
21+
original_cwd = os.getcwd()
22+
target = str(tmp_path)
23+
24+
with lsp_utils.change_cwd(target):
25+
inside_cwd = os.getcwd()
26+
27+
assert os.path.normcase(inside_cwd) == os.path.normcase(target)
28+
# After the context manager exits the working directory is restored.
29+
assert os.path.normcase(os.getcwd()) == os.path.normcase(lsp_utils.SERVER_CWD)
30+
31+
# Restore for other tests.
32+
os.chdir(original_cwd)
33+
34+
35+
def test_change_cwd_permission_error_does_not_crash(caplog):
36+
"""When os.chdir raises PermissionError the body still runs, cwd is unchanged, and a warning is logged."""
37+
original_cwd = os.getcwd()
38+
body_executed = False
39+
40+
with patch("lsp_utils.os.chdir", side_effect=PermissionError("Access denied")):
41+
with caplog.at_level(logging.WARNING):
42+
with lsp_utils.change_cwd("/restricted/path"):
43+
body_executed = True
44+
# The working directory must not have changed.
45+
assert os.path.normcase(os.getcwd()) == os.path.normcase(original_cwd)
46+
47+
assert body_executed
48+
# cwd is still the original after the context manager exits.
49+
assert os.path.normcase(os.getcwd()) == os.path.normcase(original_cwd)
50+
# A warning must have been emitted mentioning the inaccessible path and the error.
51+
assert any("/restricted/path" in r.message for r in caplog.records)
52+
assert any("Access denied" in r.message for r in caplog.records)
53+
54+
55+
def test_change_cwd_oserror_does_not_crash(caplog):
56+
"""When os.chdir raises an arbitrary OSError the body still runs and a warning is logged."""
57+
original_cwd = os.getcwd()
58+
body_executed = False
59+
60+
with patch("lsp_utils.os.chdir", side_effect=OSError("Some OS error")):
61+
with caplog.at_level(logging.WARNING):
62+
with lsp_utils.change_cwd("/inaccessible"):
63+
body_executed = True
64+
assert os.path.normcase(os.getcwd()) == os.path.normcase(original_cwd)
65+
66+
assert body_executed
67+
assert os.path.normcase(os.getcwd()) == os.path.normcase(original_cwd)
68+
assert any("/inaccessible" in r.message for r in caplog.records)
69+
assert any("Some OS error" in r.message for r in caplog.records)

0 commit comments

Comments
 (0)