-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_scopedstats.py
More file actions
493 lines (354 loc) · 14.1 KB
/
test_scopedstats.py
File metadata and controls
493 lines (354 loc) · 14.1 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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
import pytest
import time
from scopedstats import Recorder, incr, timer
def test_basic_increment():
# Test new Recorder API pattern
recorder = Recorder()
with recorder.record():
incr("test_key")
incr("test_key", amount=5)
result = recorder.get_result()
assert result["test_key"] == 6
assert "total_recording_duration" in result
assert isinstance(result["total_recording_duration"], float)
def test_increment_with_tags():
# Test new Recorder API with tag filtering
recorder = Recorder()
with recorder.record():
incr("requests", tags={"status": "success"})
incr("requests", tags={"status": "error"}, amount=2)
incr("requests", tags={"status": "success"}, amount=3)
result = recorder.get_result()
assert result["requests"] == 6
assert "total_recording_duration" in result
assert isinstance(result["total_recording_duration"], float)
success_only = recorder.get_result(tag_filter={"status": "success"})
assert success_only == {"requests": 4}
error_only = recorder.get_result(tag_filter={"status": "error"})
assert error_only == {"requests": 2}
def test_multiple_keys():
recorder = Recorder()
with recorder.record():
incr("key1", amount=10)
incr("key2", amount=20)
incr("key1", amount=5)
result = recorder.get_stats()
assert result["key1"] == 15
assert result["key2"] == 20
assert "total_recording_duration" in result
def test_nested_contexts():
stats1 = Recorder()
stats2 = Recorder()
with stats1.record():
incr("shared_key", amount=1)
with stats2.record():
incr("shared_key", amount=2)
incr("shared_key", amount=4)
# With the new stack-based approach, nested stats bubble up to the outer context
result1 = stats1.get_stats()
assert result1["shared_key"] == 7
assert "total_recording_duration" in result1
# stats2 gets its data when its context exits, which includes only what happened within it
result2 = stats2.get_stats()
assert result2["shared_key"] == 2
assert "total_recording_duration" in result2
def test_no_active_context():
incr("key1", amount=100)
stats = Recorder()
result = stats.get_stats()
assert result == {}
def test_complex_tag_filtering():
stats = Recorder()
with stats.record():
incr("api_calls", tags={"method": "GET", "status": "200"}, amount=10)
incr("api_calls", tags={"method": "POST", "status": "200"}, amount=5)
incr("api_calls", tags={"method": "GET", "status": "404"}, amount=2)
incr("api_calls", tags={"method": "POST", "status": "500"}, amount=1)
all_calls = stats.get_stats()
assert all_calls["api_calls"] == 18
assert "total_recording_duration" in all_calls
success_calls = stats.get_stats(tag_filter={"status": "200"})
assert success_calls == {"api_calls": 15}
get_calls = stats.get_stats(tag_filter={"method": "GET"})
assert get_calls == {"api_calls": 12}
get_success = stats.get_stats(tag_filter={"method": "GET", "status": "200"})
assert get_success == {"api_calls": 10}
def test_empty_stats():
stats = Recorder()
result = stats.get_stats()
assert result == {}
filtered = stats.get_stats(tag_filter={"nonexistent": "tag"})
assert filtered == {}
def test_timer_decorator_default_key():
recorder = Recorder()
@timer
def slow_function():
time.sleep(0.01)
return "result"
with recorder.record():
result = slow_function()
slow_function()
assert result == "result"
stats_data = recorder.get_stats()
expected_key_base = "calls.test_timer_decorator_default_key.<locals>.slow_function"
assert f"{expected_key_base}.count" in stats_data
assert f"{expected_key_base}.total_dur" in stats_data
assert stats_data[f"{expected_key_base}.count"] == 2
assert stats_data[f"{expected_key_base}.total_dur"] >= 0.02
# Check total_recording_duration exists and is reasonable
assert "total_recording_duration" in stats_data
assert stats_data["total_recording_duration"] >= 0.02
def test_timer_decorator_custom_key():
recorder = Recorder()
@timer(key="custom_timer")
def fast_function():
return 42
with recorder.record():
result = fast_function()
fast_function()
fast_function()
assert result == 42
stats_data = recorder.get_stats()
assert stats_data["custom_timer.count"] == 3
assert "custom_timer.total_dur" in stats_data
def test_timer_decorator_with_tags():
recorder = Recorder()
@timer(key="api_call", tags={"service": "user"})
def api_call():
time.sleep(0.005)
return "data"
with recorder.record():
api_call()
api_call()
stats_data = recorder.get_stats()
assert stats_data["api_call.count"] == 2
service_stats = recorder.get_stats(tag_filter={"service": "user"})
assert service_stats["api_call.count"] == 2
assert service_stats["api_call.total_dur"] >= 0.01
def test_timer_decorator_standalone():
"""Test @timer (without parentheses) works."""
recorder = Recorder()
@timer
def standalone_function():
time.sleep(0.001)
return "standalone"
with recorder.record():
result = standalone_function()
assert result == "standalone"
stats_data = recorder.get_stats()
expected_key_base = (
"calls.test_timer_decorator_standalone.<locals>.standalone_function"
)
assert stats_data[f"{expected_key_base}.count"] == 1
assert stats_data[f"{expected_key_base}.total_dur"] >= 0.001
def test_timer_decorator_no_active_context():
@timer()
def no_context_function():
return "no stats"
result = no_context_function()
assert result == "no stats"
stats = Recorder()
assert stats.get_stats() == {}
def test_timer_decorator_exception_handling():
stats = Recorder()
@timer(key="error_prone")
def failing_function():
time.sleep(0.005)
raise ValueError("Test error")
with stats.record():
with pytest.raises(ValueError):
failing_function()
stats_data = stats.get_stats()
assert stats_data["error_prone.count"] == 1
assert stats_data["error_prone.total_dur"] >= 0.005
def test_timer_nested_contexts():
stats1 = Recorder()
stats2 = Recorder()
@timer(key="nested_timer")
def nested_function():
time.sleep(0.005)
return "nested"
with stats1.record():
nested_function() # count=1 in collector1
with stats2.record():
nested_function() # count=1 in collector2, then merges to collector1
nested_function() # count=1 more in collector1
# stats1 gets all 3 calls (1 + 1 from nested + 1 more)
assert stats1.get_stats()["nested_timer.count"] == 3
# stats2 gets only the call that happened within its context
assert stats2.get_stats()["nested_timer.count"] == 1
def test_timer_decorator_variations():
stats = Recorder()
@timer(key="manual_timer")
def manual_operation():
time.sleep(0.01)
@timer(key="tagged_timer", tags={"operation": "test"})
def tagged_operation():
time.sleep(0.005)
with stats.record():
manual_operation()
tagged_operation()
stats_data = stats.get_stats()
assert stats_data["manual_timer.count"] == 1
assert stats_data["manual_timer.total_dur"] >= 0.01
assert stats_data["tagged_timer.count"] == 1
assert stats_data["tagged_timer.total_dur"] >= 0.005
tagged_only = stats.get_stats(tag_filter={"operation": "test"})
assert "manual_timer.count" not in tagged_only
assert tagged_only["tagged_timer.count"] == 1
def test_timer_decorator_no_active_stats():
@timer(key="no_stats_timer")
def no_stats_operation():
time.sleep(0.001)
# Call without any active Recorder context
no_stats_operation()
stats = Recorder()
assert stats.get_stats() == {}
def test_timer_decorator_exception_handling_duplicate():
stats = Recorder()
@timer(key="exception_timer")
def exception_operation():
time.sleep(0.005)
raise ValueError("Test exception")
with stats.record():
with pytest.raises(ValueError):
exception_operation()
stats_data = stats.get_stats()
assert stats_data["exception_timer.count"] == 1
assert stats_data["exception_timer.total_dur"] >= 0.005
def test_timer_decorator_nested_calls():
stats = Recorder()
@timer(key="inner_timer")
def inner_operation():
time.sleep(0.005)
@timer(key="outer_timer")
def outer_operation():
time.sleep(0.005)
inner_operation() # Nested timer call
with stats.record():
outer_operation()
stats_data = stats.get_stats()
assert stats_data["outer_timer.count"] == 1
assert stats_data["inner_timer.count"] == 1
assert stats_data["outer_timer.total_dur"] >= 0.01 # Includes inner time
assert stats_data["inner_timer.total_dur"] >= 0.005
def test_timer_perfect_type_preservation():
def typed_function(x: int, y: str = "default") -> tuple[int, str]:
return (x * 2, y.upper())
@timer(key="typed_func")
def decorated_typed_function(x: int, y: str = "default") -> tuple[int, str]:
return (x * 2, y.upper())
stats = Recorder()
with stats.record():
result1 = decorated_typed_function(5)
result2 = decorated_typed_function(10, "hello")
assert result1 == (10, "DEFAULT")
assert result2 == (20, "HELLO")
stats_data = stats.get_stats()
assert stats_data["typed_func.count"] == 2
def test_concurrent_access_same_statsdata():
import concurrent.futures
stats = Recorder()
def worker(worker_id: int) -> None:
with stats.record():
for i in range(1000):
incr(f"worker_{worker_id}", amount=1)
incr("shared_counter", amount=1)
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(worker, i) for i in range(4)]
for future in futures:
future.result()
results = stats.get_stats()
assert results["shared_counter"] == 4000
for i in range(4):
assert results[f"worker_{i}"] == 1000
def test_stack_based_isolation():
"""Test that the new stack-based approach provides proper isolation."""
stats_outer = Recorder()
stats_middle = Recorder()
stats_inner = Recorder()
with stats_outer.record():
incr("level", amount=1) # Goes to outer collector
with stats_middle.record():
incr("level", amount=2) # Goes to middle collector
with stats_inner.record():
incr("level", amount=3) # Goes to inner collector
# At this point inner exits: inner gets 3, middle collector gets 3 added
incr("level", amount=4) # Goes to middle collector (now has 2+3+4=9)
# At this point middle exits: middle gets 9, outer collector gets 9 added
incr("level", amount=5) # Goes to outer collector (now has 1+9+5=15)
# At this point outer exits: outer gets 15
# Each Recorder contains exactly what was collected within its direct scope
inner_result = stats_inner.get_stats()
assert inner_result["level"] == 3
assert "total_recording_duration" in inner_result
middle_result = stats_middle.get_stats()
assert middle_result["level"] == 9 # 2 + 3 + 4
assert "total_recording_duration" in middle_result
outer_result = stats_outer.get_stats()
assert outer_result["level"] == 15 # 1 + 9 + 5
assert "total_recording_duration" in outer_result
def test_no_cross_contamination():
"""Test that separate Recorder instances don't interfere."""
stats_a = Recorder()
stats_b = Recorder()
with stats_a.record():
incr("counter_a", amount=10)
with stats_b.record():
incr("counter_b", amount=20)
result_a = stats_a.get_stats()
assert result_a["counter_a"] == 10
assert "total_recording_duration" in result_a
result_b = stats_b.get_stats()
assert result_b["counter_b"] == 20
assert "total_recording_duration" in result_b
def test_recorder_require_recording_flag():
"""Test the require_recording flag in get_result."""
recorder = Recorder()
# Should work fine without recording when require_recording=False (default)
result = recorder.get_result()
assert result == {}
# Should raise ValueError when require_recording=True and no recording occurred
with pytest.raises(ValueError, match="No recording has occurred"):
recorder.get_result(require_recording=True)
# After recording, should work with require_recording=True
with recorder.record():
incr("test_key", amount=42)
result = recorder.get_result(require_recording=True)
assert result["test_key"] == 42
assert "total_recording_duration" in result
def test_recorder_new_api_pattern():
"""Test the new recommended API pattern."""
import scopedstats
# New recommended pattern
recorder = scopedstats.Recorder()
with recorder.record():
scopedstats.incr("operations", amount=5)
@scopedstats.timer(key="work")
def do_work():
time.sleep(0.001)
do_work()
result = recorder.get_result()
assert result["operations"] == 5
assert result["work.count"] == 1
assert result["work.total_dur"] >= 0.001
# Check total_recording_duration is included
assert "total_recording_duration" in result
assert isinstance(result["total_recording_duration"], float)
assert result["total_recording_duration"] >= 0.001
def test_backward_compatibility_statsdata():
"""Test that Recorder alias still works with new pattern."""
# Recorder alias should work with new pattern
stats = Recorder()
with stats.record():
incr("new_key", amount=10)
incr("new_key", amount=5)
result = stats.get_stats()
assert result["new_key"] == 15
assert "total_recording_duration" in result
# Also test get_result method
result = stats.get_result()
assert result["new_key"] == 15
assert "total_recording_duration" in result
if __name__ == "__main__":
pytest.main([__file__])