Skip to content

Commit 35924a6

Browse files
committed
fix: make test suite pass on Windows and Python 3.12+
- Fix event loop issues in ASGI tests under newer asgiref by wrapping communicator setup in an async coroutine run on the loop. - Fix mmap file locking (PermissionError: [WinError 32]) on Windows in multiprocess tests by closing open database handles before unlinking/removing directories. - Skip case-sensitive env var deprecation warning assertion on Windows where environment variables are case-insensitive. - Conditionally define the parser benchmark test to avoid failure when pytest-benchmark is not installed. Signed-off-by: MelvinjoseC <165891174+MelvinjoseC@users.noreply.github.com>
1 parent 7819d80 commit 35924a6

4 files changed

Lines changed: 72 additions & 22 deletions

File tree

prometheus_client/values.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
from .mmap_dict import mmap_key, MmapedDict
66

77

8+
_multi_process_cleanups = []
9+
10+
11+
def close_all_multiprocess_files():
12+
for cleanup in _multi_process_cleanups:
13+
cleanup()
14+
_multi_process_cleanups.clear()
15+
16+
817
class MutexValue:
918
"""A float protected by a mutex."""
1019

@@ -52,6 +61,13 @@ def MultiProcessValue(process_identifier=os.getpid):
5261
# This avoids the need to also have mutexes in __MmapDict.
5362
lock = Lock()
5463

64+
def cleanup():
65+
for f in files.values():
66+
f.close()
67+
files.clear()
68+
values.clear()
69+
_multi_process_cleanups.append(cleanup)
70+
5571
class MmapedValue:
5672
"""A float protected by a mutex backed by a per-process mmaped file."""
5773

@@ -122,6 +138,14 @@ def get_exemplar(self):
122138
# TODO: Implement exemplars for multiprocess mode.
123139
return None
124140

141+
@classmethod
142+
def close_all_files(cls):
143+
with lock:
144+
for f in files.values():
145+
f.close()
146+
files.clear()
147+
values.clear()
148+
125149
return MmapedValue
126150

127151

tests/test_asgi.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,32 @@ def setUp(self):
3232
# Setup ASGI scope
3333
self.scope = {}
3434
setup_testing_defaults(self.scope)
35+
self.loop = asyncio.new_event_loop()
36+
asyncio.set_event_loop(self.loop)
3537
self.communicator = None
3638

3739
def tearDown(self):
3840
if self.communicator:
39-
asyncio.new_event_loop().run_until_complete(
41+
self.loop.run_until_complete(
4042
self.communicator.wait()
4143
)
44+
self.loop.close()
4245

4346
def seed_app(self, app):
44-
self.communicator = ApplicationCommunicator(app, self.scope)
47+
async def _init():
48+
self.communicator = ApplicationCommunicator(app, self.scope)
49+
self.loop.run_until_complete(_init())
4550

4651
def send_input(self, payload):
47-
asyncio.new_event_loop().run_until_complete(
52+
self.loop.run_until_complete(
4853
self.communicator.send_input(payload)
4954
)
5055

5156
def send_default_request(self):
5257
self.send_input({"type": "http.request", "body": b""})
5358

5459
def get_output(self):
55-
output = asyncio.new_event_loop().run_until_complete(
60+
output = self.loop.run_until_complete(
5661
self.communicator.receive_output(0)
5762
)
5863
return output
@@ -148,9 +153,9 @@ def test_gzip(self):
148153
increments = 2
149154
self.increment_metrics(metric_name, help_text, increments)
150155
app = make_asgi_app(self.registry)
151-
self.seed_app(app)
152156
# Send input with gzip header.
153157
self.scope["headers"] = [(b"accept-encoding", b"gzip")]
158+
self.seed_app(app)
154159
self.send_input({"type": "http.request", "body": b""})
155160
# Assert outputs are compressed.
156161
outputs = self.get_all_output()
@@ -164,9 +169,9 @@ def test_gzip_disabled(self):
164169
self.increment_metrics(metric_name, help_text, increments)
165170
# Disable compression explicitly.
166171
app = make_asgi_app(self.registry, disable_compression=True)
167-
self.seed_app(app)
168172
# Send input with gzip header.
169173
self.scope["headers"] = [(b"accept-encoding", b"gzip")]
174+
self.seed_app(app)
170175
self.send_input({"type": "http.request", "body": b""})
171176
# Assert outputs are not compressed.
172177
outputs = self.get_all_output()
@@ -175,8 +180,8 @@ def test_gzip_disabled(self):
175180
def test_openmetrics_encoding(self):
176181
"""Response content type is application/openmetrics-text when appropriate Accept header is in request"""
177182
app = make_asgi_app(self.registry)
178-
self.seed_app(app)
179183
self.scope["headers"] = [(b"Accept", b"application/openmetrics-text; version=1.0.0")]
184+
self.seed_app(app)
180185
self.send_input({"type": "http.request", "body": b""})
181186

182187
content_type = self.get_response_header_value('Content-Type').split(";")[0]
@@ -204,8 +209,8 @@ def test_qs_parsing(self):
204209
self.increment_metrics(*m)
205210

206211
for i_1 in range(len(metrics)):
207-
self.seed_app(app)
208212
self.scope['query_string'] = f"name[]={metrics[i_1][0]}_total".encode("utf-8")
213+
self.seed_app(app)
209214
self.send_default_request()
210215

211216
outputs = self.get_all_output()
@@ -220,7 +225,7 @@ def test_qs_parsing(self):
220225

221226
self.assert_not_metrics(output, *metrics[i_2])
222227

223-
asyncio.new_event_loop().run_until_complete(
228+
self.loop.run_until_complete(
224229
self.communicator.wait()
225230
)
226231

@@ -237,8 +242,8 @@ def test_qs_parsing_multi(self):
237242
for m in metrics:
238243
self.increment_metrics(*m)
239244

240-
self.seed_app(app)
241245
self.scope['query_string'] = "&".join([f"name[]={m[0]}_total" for m in metrics[0:2]]).encode("utf-8")
246+
self.seed_app(app)
242247
self.send_default_request()
243248

244249
outputs = self.get_all_output()
@@ -249,6 +254,6 @@ def test_qs_parsing_multi(self):
249254
self.assert_metrics(output, *metrics[1])
250255
self.assert_not_metrics(output, *metrics[2])
251256

252-
asyncio.new_event_loop().run_until_complete(
257+
self.loop.run_until_complete(
253258
self.communicator.wait()
254259
)

tests/test_multiprocess.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,26 @@ def setUp(self):
2424
def tearDown(self):
2525
os.environ.pop('prometheus_multiproc_dir', None)
2626
os.environ.pop('PROMETHEUS_MULTIPROC_DIR', None)
27+
values.close_all_multiprocess_files()
2728
values.ValueClass = MutexValue
2829
shutil.rmtree(self.tempdir)
2930

3031
def test_deprecation_warning(self):
3132
os.environ['prometheus_multiproc_dir'] = self.tempdir
3233
with warnings.catch_warnings(record=True) as w:
34+
warnings.simplefilter("always")
3335
values.ValueClass = get_value_class()
3436
registry = CollectorRegistry()
3537
collector = MultiProcessCollector(registry)
3638
Counter('c', 'help', registry=None)
3739

3840
assert os.environ['PROMETHEUS_MULTIPROC_DIR'] == self.tempdir
39-
assert len(w) == 1
40-
assert issubclass(w[-1].category, DeprecationWarning)
41-
assert "PROMETHEUS_MULTIPROC_DIR" in str(w[-1].message)
41+
if os.name != 'nt':
42+
assert len(w) == 1
43+
assert issubclass(w[-1].category, DeprecationWarning)
44+
assert "PROMETHEUS_MULTIPROC_DIR" in str(w[-1].message)
45+
else:
46+
assert len(w) == 0
4247

4348
def test_mark_process_dead_respects_lowercase(self):
4449
os.environ['prometheus_multiproc_dir'] = self.tempdir
@@ -61,8 +66,9 @@ def _value_class(self):
6166

6267
def tearDown(self):
6368
del os.environ['PROMETHEUS_MULTIPROC_DIR']
64-
shutil.rmtree(self.tempdir)
69+
values.close_all_multiprocess_files()
6570
values.ValueClass = MutexValue
71+
shutil.rmtree(self.tempdir)
6672

6773
def test_counter_adds(self):
6874
c1 = Counter('c', 'help', registry=None)
@@ -119,6 +125,7 @@ def test_gauge_liveall(self):
119125
g2.set(2)
120126
self.assertEqual(1, self.registry.get_sample_value('g', {'pid': '123'}))
121127
self.assertEqual(2, self.registry.get_sample_value('g', {'pid': '456'}))
128+
values.close_all_multiprocess_files()
122129
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
123130
self.assertEqual(None, self.registry.get_sample_value('g', {'pid': '123'}))
124131
self.assertEqual(2, self.registry.get_sample_value('g', {'pid': '456'}))
@@ -140,6 +147,7 @@ def test_gauge_livemin(self):
140147
g1.set(1)
141148
g2.set(2)
142149
self.assertEqual(1, self.registry.get_sample_value('g'))
150+
values.close_all_multiprocess_files()
143151
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
144152
self.assertEqual(2, self.registry.get_sample_value('g'))
145153

@@ -160,6 +168,7 @@ def test_gauge_livemax(self):
160168
g1.set(2)
161169
g2.set(1)
162170
self.assertEqual(2, self.registry.get_sample_value('g'))
171+
values.close_all_multiprocess_files()
163172
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
164173
self.assertEqual(1, self.registry.get_sample_value('g'))
165174

@@ -171,6 +180,7 @@ def test_gauge_sum(self):
171180
g1.set(1)
172181
g2.set(2)
173182
self.assertEqual(3, self.registry.get_sample_value('g'))
183+
values.close_all_multiprocess_files()
174184
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
175185
self.assertEqual(3, self.registry.get_sample_value('g'))
176186

@@ -182,6 +192,7 @@ def test_gauge_livesum(self):
182192
g1.set(1)
183193
g2.set(2)
184194
self.assertEqual(3, self.registry.get_sample_value('g'))
195+
values.close_all_multiprocess_files()
185196
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
186197
self.assertEqual(2, self.registry.get_sample_value('g'))
187198

@@ -192,6 +203,7 @@ def test_gauge_mostrecent(self):
192203
g2.set(2)
193204
g1.set(1)
194205
self.assertEqual(1, self.registry.get_sample_value('g'))
206+
values.close_all_multiprocess_files()
195207
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
196208
self.assertEqual(1, self.registry.get_sample_value('g'))
197209

@@ -202,6 +214,7 @@ def test_gauge_livemostrecent(self):
202214
g2.set(2)
203215
g1.set(1)
204216
self.assertEqual(1, self.registry.get_sample_value('g'))
217+
values.close_all_multiprocess_files()
205218
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
206219
self.assertEqual(2, self.registry.get_sample_value('g'))
207220

@@ -626,6 +639,7 @@ def test_corruption_detected(self):
626639
list(self.d.read_all_values())
627640

628641
def tearDown(self):
642+
self.d.close()
629643
os.unlink(self.tempfile)
630644

631645

tests/test_parser.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,15 @@ def collect(self):
375375
self.assertEqual(text.encode('utf-8'), generate_latest(registry, ALLOWUTF8))
376376

377377

378-
def test_benchmark_text_string_to_metric_families(benchmark):
379-
text = """# HELP go_gc_duration_seconds A summary of the GC invocation durations.
378+
try:
379+
import pytest_benchmark
380+
HAS_BENCHMARK = True
381+
except ImportError:
382+
HAS_BENCHMARK = False
383+
384+
if HAS_BENCHMARK:
385+
def test_benchmark_text_string_to_metric_families(benchmark):
386+
text = """# HELP go_gc_duration_seconds A summary of the GC invocation durations.
380387
# TYPE go_gc_duration_seconds summary
381388
go_gc_duration_seconds{quantile="0"} 0.013300656000000001
382389
go_gc_duration_seconds{quantile="0.25"} 0.013638736
@@ -422,11 +429,11 @@ def test_benchmark_text_string_to_metric_families(benchmark):
422429
hist_sum 2
423430
"""
424431

425-
@benchmark
426-
def _():
427-
# We need to convert the generator to a full list in order to
428-
# accurately measure the time to yield everything.
429-
return list(text_string_to_metric_families(text))
432+
@benchmark
433+
def _():
434+
# We need to convert the generator to a full list in order to
435+
# accurately measure the time to yield everything.
436+
return list(text_string_to_metric_families(text))
430437

431438

432439
if __name__ == '__main__':

0 commit comments

Comments
 (0)