-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathtest_server_logging.py
More file actions
432 lines (354 loc) · 19.7 KB
/
Copy pathtest_server_logging.py
File metadata and controls
432 lines (354 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
"""Tests for UserInjectionFilter, UserRoutingFileHandler, and resolve_port in
Middleware/common/server_startup.py (imported without server.py's module-level
application initialization)."""
import logging
import os
from unittest.mock import patch
import pytest
from Middleware.common import instance_global_variables
class TestUserInjectionFilter:
"""Tests for the UserInjectionFilter logging filter."""
@pytest.fixture(autouse=True)
def reset_request_user(self):
yield
instance_global_variables.clear_request_user()
def _make_record(self):
return logging.LogRecord(
name="test", level=logging.INFO, pathname="", lineno=0,
msg="test message", args=(), exc_info=None,
)
def test_defaults_to_system(self):
"""When no request user is set, wilmer_user defaults to 'system'."""
from Middleware.common.server_startup import UserInjectionFilter
f = UserInjectionFilter()
record = self._make_record()
instance_global_variables.clear_request_user()
result = f.filter(record)
assert result is True
assert record.wilmer_user == "system"
def test_uses_request_user(self):
"""When a request user is set, wilmer_user is that user."""
from Middleware.common.server_startup import UserInjectionFilter
f = UserInjectionFilter()
record = self._make_record()
instance_global_variables.set_request_user("alice")
result = f.filter(record)
assert result is True
assert record.wilmer_user == "alice"
class TestUserRoutingFileHandler:
"""Tests for the UserRoutingFileHandler logging handler."""
def _make_record(self, user="system"):
record = logging.LogRecord(
name="test", level=logging.INFO, pathname="", lineno=0,
msg="test message", args=(), exc_info=None,
)
record.wilmer_user = user
return record
def test_creates_system_log(self, tmp_path):
"""System log is created at {log_dir}/wilmerai.log and receives the record."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(message)s"))
try:
handler.emit(self._make_record("system"))
log_path = os.path.join(str(tmp_path), "wilmerai.log")
assert os.path.isfile(log_path)
with open(log_path) as f:
assert "test message" in f.read()
finally:
handler.close()
def test_creates_user_subdirectory_log(self, tmp_path):
"""User log is created at {log_dir}/{user}/wilmerai.log and receives the record."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(message)s"))
try:
handler.emit(self._make_record("alice"))
expected = os.path.join(str(tmp_path), "alice", "wilmerai.log")
assert os.path.isfile(expected)
with open(expected) as f:
assert "test message" in f.read()
finally:
handler.close()
def test_routes_to_correct_file(self, tmp_path):
"""Records for different users go to different files."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(wilmer_user)s: %(message)s"))
try:
record_alice = self._make_record("alice")
record_alice.msg = "alice message"
record_bob = self._make_record("bob")
record_bob.msg = "bob message"
handler.emit(record_alice)
handler.emit(record_bob)
alice_log = os.path.join(str(tmp_path), "alice", "wilmerai.log")
bob_log = os.path.join(str(tmp_path), "bob", "wilmerai.log")
# The "user: " prefix proves the parent handler's formatter was
# propagated to the lazily created per-user handlers; the bare
# message would appear even with no formatter at all.
with open(alice_log) as f:
content = f.read()
assert "alice: alice message" in content
assert "bob message" not in content
with open(bob_log) as f:
content = f.read()
assert "bob: bob message" in content
assert "alice message" not in content
finally:
handler.close()
def test_same_user_reuses_cached_handler(self, tmp_path):
"""Repeated records for one user reuse a single cached file handler
(no handler-per-record leak) and append to the same file."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(message)s"))
try:
first = self._make_record("alice")
first.msg = "first message"
second = self._make_record("alice")
second.msg = "second message"
handler.emit(first)
handler.emit(second)
assert len(handler._handlers) == 1
with open(os.path.join(str(tmp_path), "alice", "wilmerai.log")) as f:
content = f.read()
assert "first message" in content
assert "second message" in content
finally:
handler.close()
def test_close_cleans_up_handlers(self, tmp_path):
"""close() cleans up all internal handlers."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(message)s"))
handler.emit(self._make_record("alice"))
assert len(handler._handlers) == 1
handler.close()
assert len(handler._handlers) == 0
def test_missing_wilmer_user_defaults_to_system(self, tmp_path):
"""Records without wilmer_user attribute go to system log."""
from Middleware.common.server_startup import UserRoutingFileHandler
handler = UserRoutingFileHandler(str(tmp_path))
handler.setFormatter(logging.Formatter("%(message)s"))
try:
record = logging.LogRecord(
name="test", level=logging.INFO, pathname="", lineno=0,
msg="no user", args=(), exc_info=None,
)
# Don't set wilmer_user attribute
handler.emit(record)
assert os.path.isfile(os.path.join(str(tmp_path), "wilmerai.log"))
finally:
handler.close()
class TestResolvePort:
"""Tests for the resolve_port() function."""
@pytest.fixture(autouse=True)
def reset_globals(self):
"""Save and restore PORT and USERS around each test."""
orig_port = instance_global_variables.PORT
orig_users = instance_global_variables.USERS
yield
instance_global_variables.PORT = orig_port
instance_global_variables.USERS = orig_users
def test_cli_port_takes_precedence_single_user(self):
"""--port flag overrides user config in single-user mode."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = 9999
instance_global_variables.USERS = ["alice"]
assert resolve_port() == 9999
def test_cli_port_takes_precedence_multi_user(self):
"""--port flag overrides the default in multi-user mode."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = 8080
instance_global_variables.USERS = ["alice", "bob"]
assert resolve_port() == 8080
def test_multi_user_defaults_to_5050(self, capsys):
"""Multi-user mode without --port defaults to 5050 and warns on stderr."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = ["alice", "bob"]
assert resolve_port() == 5050
captured = capsys.readouterr()
assert "ignored in multi-user mode" in captured.err
assert "5050" in captured.err
def test_single_user_reads_from_config(self):
"""Single-user mode without --port reads from user config."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for", return_value={"port": 7777}):
assert resolve_port() == 7777
def test_single_user_config_missing_port_key_defaults_to_5000(self):
"""A readable user config with no 'port' key yields the documented 5000 default."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for", return_value={}):
assert resolve_port() == 5000
def test_single_user_config_error_falls_back_to_5000(self):
"""If user config can't be read, fall back to 5000."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for", side_effect=FileNotFoundError("no config")):
assert resolve_port() == 5000
def test_no_users_reads_from_config(self):
"""No --User arg reads port from _current-user.json config."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = None
with patch("Middleware.common.server_startup.config_utils.get_application_port", return_value=6060):
assert resolve_port() == 6060
def test_no_users_none_application_port_falls_back_to_5000(self):
"""get_application_port() returning None (config present, port unset)
falls through the `or 5000` branch to the documented default."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = None
with patch("Middleware.common.server_startup.config_utils.get_application_port", return_value=None):
assert resolve_port() == 5000
def test_no_users_config_error_falls_back_to_5000(self):
"""If no --User and config fails, fall back to 5000."""
from Middleware.common.server_startup import resolve_port
instance_global_variables.PORT = None
instance_global_variables.USERS = None
with patch("Middleware.common.server_startup.config_utils.get_application_port", side_effect=FileNotFoundError("no config")):
assert resolve_port() == 5000
class TestResolveFileLogging:
"""Tests for the resolve_file_logging() precedence table."""
@pytest.fixture(autouse=True)
def reset_globals(self):
"""Save and restore FILE_LOGGING and USERS around each test."""
orig_file_logging = instance_global_variables.FILE_LOGGING
orig_users = instance_global_variables.USERS
yield
instance_global_variables.FILE_LOGGING = orig_file_logging
instance_global_variables.USERS = orig_users
def test_cli_flag_true_wins_over_config(self):
"""--file-logging beats a config that says off; config is never read."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = True
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for") as mock_cfg:
assert resolve_file_logging() is True
mock_cfg.assert_not_called()
def test_cli_flag_false_wins_over_config_true(self):
"""An explicit False flag is not 'absent': it overrides a config that
enables file logging. This pins the tri-state None/False distinction."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = False
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for",
return_value={"useFileLogging": True}) as mock_cfg:
assert resolve_file_logging() is False
mock_cfg.assert_not_called()
def test_single_user_reads_config_true(self):
"""Absent flag + single user: the user's useFileLogging setting decides."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for",
return_value={"useFileLogging": True}):
assert resolve_file_logging() is True
def test_single_user_config_missing_key_defaults_off(self):
"""A readable config with no useFileLogging key means off."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for", return_value={}):
assert resolve_file_logging() is False
def test_single_user_config_error_defaults_off(self):
"""A config read failure degrades to off instead of blocking startup."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = ["alice"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for",
side_effect=FileNotFoundError("no config")):
assert resolve_file_logging() is False
def test_no_users_legacy_reads_current_user_config(self):
"""Absent flag + no --User: legacy mode reads _current-user.json's config."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = None
with patch("Middleware.common.server_startup.config_utils.get_user_config",
return_value={"useFileLogging": True}):
assert resolve_file_logging() is True
def test_no_users_config_error_defaults_off(self):
"""Legacy-mode config failure degrades to off."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = None
with patch("Middleware.common.server_startup.config_utils.get_user_config",
side_effect=FileNotFoundError("no config")):
assert resolve_file_logging() is False
def test_multi_user_defaults_off_without_consulting_configs(self):
"""Absent flag + multiple users: off, and per-user configs are never read
(a config-enabled user must not force file logging on a shared instance)."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = None
instance_global_variables.USERS = ["alice", "bob"]
with patch("Middleware.common.server_startup.config_utils.get_user_config_for") as mock_for, \
patch("Middleware.common.server_startup.config_utils.get_user_config") as mock_legacy:
assert resolve_file_logging() is False
mock_for.assert_not_called()
mock_legacy.assert_not_called()
def test_multi_user_cli_flag_enables(self):
"""--file-logging turns file logging on in multi-user mode."""
from Middleware.common.server_startup import resolve_file_logging
instance_global_variables.FILE_LOGGING = True
instance_global_variables.USERS = ["alice", "bob"]
assert resolve_file_logging() is True
class TestResolveLoggingDirectory:
"""Tests for the resolve_logging_directory_user_token() <user> token handling."""
@pytest.fixture(autouse=True)
def reset_globals(self):
"""Save and restore LOGGING_DIRECTORY and USERS around each test."""
orig_dir = instance_global_variables.LOGGING_DIRECTORY
orig_users = instance_global_variables.USERS
yield
instance_global_variables.LOGGING_DIRECTORY = orig_dir
instance_global_variables.USERS = orig_users
def test_single_user_replaces_token(self):
"""Single-user mode replaces <user> with the configured username."""
from Middleware.common.launch_arguments import resolve_logging_directory_user_token
instance_global_variables.USERS = ["alice"]
instance_global_variables.LOGGING_DIRECTORY = os.path.join("logs", "<user>", "app")
resolve_logging_directory_user_token()
assert instance_global_variables.LOGGING_DIRECTORY == os.path.join("logs", "alice", "app")
def test_multi_user_strips_token_and_warns(self, capsys):
"""Multi-user mode strips the token (and trailing separators) and warns on stderr."""
from Middleware.common.launch_arguments import resolve_logging_directory_user_token
instance_global_variables.USERS = ["alice", "bob"]
instance_global_variables.LOGGING_DIRECTORY = os.path.join("logs", "<user>")
resolve_logging_directory_user_token()
assert instance_global_variables.LOGGING_DIRECTORY == "logs"
captured = capsys.readouterr()
assert "not supported in multi-user mode" in captured.err
def test_no_users_resolves_via_current_username(self):
"""With USERS unset (legacy _current-user.json mode), the token resolves
through get_current_username() rather than staying a literal <user>."""
from Middleware.common.launch_arguments import resolve_logging_directory_user_token
instance_global_variables.USERS = None
instance_global_variables.LOGGING_DIRECTORY = os.path.join("logs", "<user>")
with patch("Middleware.utilities.config_utils.get_current_username", return_value="legacy-user"):
resolve_logging_directory_user_token()
assert instance_global_variables.LOGGING_DIRECTORY == os.path.join("logs", "legacy-user")
def test_no_users_username_failure_strips_token_and_warns(self, capsys):
"""If the legacy username lookup fails, the token is stripped with a warning
instead of creating a literal '<user>' directory."""
from Middleware.common.launch_arguments import resolve_logging_directory_user_token
instance_global_variables.USERS = None
instance_global_variables.LOGGING_DIRECTORY = os.path.join("logs", "<user>")
with patch("Middleware.utilities.config_utils.get_current_username", side_effect=FileNotFoundError("no config")):
resolve_logging_directory_user_token()
assert instance_global_variables.LOGGING_DIRECTORY == "logs"
assert "could not resolve the <user> token" in capsys.readouterr().err
def test_no_token_is_noop(self, capsys):
"""Without a <user> token, the directory is untouched and nothing is printed."""
from Middleware.common.launch_arguments import resolve_logging_directory_user_token
instance_global_variables.USERS = ["alice", "bob"]
instance_global_variables.LOGGING_DIRECTORY = "plain-logs"
resolve_logging_directory_user_token()
assert instance_global_variables.LOGGING_DIRECTORY == "plain-logs"
assert capsys.readouterr().err == ""