1
1
"""Tests for Jupyter Server metrics functionality."""
2
2
3
+ import socket
3
4
import time
4
5
from unittest .mock import patch
5
6
10
11
from jupyter_server .serverapp import ServerApp
11
12
12
13
14
+ def find_available_port (start_port = 9090 , max_attempts = 10 ):
15
+ """Find an available port starting from start_port."""
16
+ for i in range (max_attempts ):
17
+ port = start_port + i
18
+ try :
19
+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
20
+ s .bind (('localhost' , port ))
21
+ return port
22
+ except OSError :
23
+ continue
24
+ raise RuntimeError (f"Could not find available port starting from { start_port } " )
25
+
26
+
27
+ def wait_for_server (url , timeout = 10 , interval = 0.1 ):
28
+ """Wait for a server to be ready to accept connections."""
29
+ start_time = time .time ()
30
+ while time .time () - start_time < timeout :
31
+ try :
32
+ response = requests .get (url , timeout = 1 )
33
+ return response
34
+ except (requests .exceptions .ConnectionError , requests .exceptions .Timeout ):
35
+ time .sleep (interval )
36
+ raise TimeoutError (f"Server at { url } not ready after { timeout } seconds" )
37
+
38
+
13
39
@pytest .fixture (autouse = True )
14
40
def cleanup_metrics_servers ():
15
41
"""Ensure metrics servers are cleaned up after each test."""
16
42
yield
17
43
# Give any remaining threads time to clean up
18
- time .sleep (0.2 )
44
+ time .sleep (0.3 )
19
45
20
46
21
47
@pytest .fixture
22
48
def metrics_server_app ():
23
49
"""Create a server app with metrics enabled on a specific port."""
24
50
# Use a unique port for this test
25
- port = 9090
51
+ port = find_available_port ( 9090 )
26
52
# Override the environment variable for this test
27
53
with patch .dict ("os.environ" , {"JUPYTER_SERVER_METRICS_PORT" : str (port )}):
28
54
app = ServerApp ()
@@ -33,143 +59,143 @@ def metrics_server_app():
33
59
34
60
35
61
@pytest .fixture
36
- def metrics_server (metrics_server_app ):
37
- """Start a metrics server for testing."""
38
- server = start_metrics_server (metrics_server_app , 9090 )
39
- # Give the server time to start
40
- time .sleep (0.1 )
62
+ def standalone_metrics_server ():
63
+ """Create a standalone metrics server for testing."""
64
+ port = find_available_port (9091 )
65
+ server = PrometheusMetricsServer (port = port )
66
+ server .start ()
67
+ # Wait for server to be ready
68
+ time .sleep (0.5 )
41
69
yield server
42
- # Cleanup
43
- if hasattr (server , "stop" ):
44
- server .stop ()
45
- # Give time for cleanup
46
- time .sleep (0.2 )
70
+ server .stop ()
47
71
48
72
49
- def test_metrics_server_starts (metrics_server ):
50
- """Test that the metrics server starts successfully."""
51
- assert metrics_server is not None
52
- assert hasattr (metrics_server , "port" )
53
- assert metrics_server .port == 9090
54
-
55
-
56
- def test_metrics_endpoint_accessible (metrics_server ):
57
- """Test that the metrics endpoint is accessible."""
58
- response = requests .get (f"http://localhost:{ metrics_server .port } /metrics" )
73
+ def test_metrics_server_startup (standalone_metrics_server ):
74
+ """Test that metrics server starts correctly."""
75
+ assert standalone_metrics_server .port is not None
76
+ assert standalone_metrics_server .port > 0
77
+
78
+ # Test that metrics endpoint is accessible
79
+ response = wait_for_server (f"http://localhost:{ standalone_metrics_server .port } /metrics" )
59
80
assert response .status_code == 200
60
- assert "jupyter_server" in response .text
61
-
62
-
63
- def test_metrics_contains_kernel_metrics (metrics_server ):
64
- """Test that kernel metrics are present."""
65
- response = requests .get (f"http://localhost:{ metrics_server .port } /metrics" )
66
- assert response .status_code == 200
67
- content = response .text
68
- assert "jupyter_kernel_currently_running_total" in content
69
-
70
-
71
- def test_metrics_contains_server_info (metrics_server ):
72
- """Test that server info metrics are present."""
73
- response = requests .get (f"http://localhost:{ metrics_server .port } /metrics" )
74
- assert response .status_code == 200
75
- content = response .text
76
- assert "jupyter_server_info" in content
81
+ assert "jupyter_server_info" in response .text
77
82
78
83
79
84
def test_metrics_server_with_authentication ():
80
85
"""Test metrics server with authentication enabled."""
81
- app = ServerApp ()
82
- app .metrics_port = 9091
83
- app .authenticate_prometheus = True
84
- app .initialize ([])
85
- app .identity_provider .token = "test_token"
86
-
87
- server = start_metrics_server (app , 9091 )
88
- time .sleep (0.1 )
89
-
90
- try :
91
- # Without token should fail
92
- response = requests .get (f"http://localhost:{ server .port } /metrics" )
93
- assert response .status_code == 401
94
-
95
- # With token should succeed
96
- response = requests .get (f"http://localhost:{ server .port } /metrics?token=test_token" )
97
- assert response .status_code == 200
98
- finally :
99
- if hasattr (server , "stop" ):
100
- server .stop ()
101
- time .sleep (0.2 )
102
-
103
-
104
- def test_metrics_server_port_conflict_handling ():
86
+ port = find_available_port (9092 )
87
+
88
+ # Create a server app with authentication
89
+ with patch .dict ("os.environ" , {"JUPYTER_SERVER_METRICS_PORT" : str (port )}):
90
+ app = ServerApp ()
91
+ app .metrics_port = port
92
+ app .authenticate_prometheus = True
93
+ app .initialize ([])
94
+
95
+ # Start the app
96
+ app .start_app ()
97
+
98
+ # Wait for both servers to be ready
99
+ time .sleep (1.0 )
100
+
101
+ try :
102
+ # Get the token
103
+ token = app .identity_provider .token
104
+
105
+ # Test metrics endpoint with token
106
+ response = wait_for_server (
107
+ f"http://localhost:{ port } /metrics?token={ token } " ,
108
+ timeout = 5
109
+ )
110
+ assert response .status_code == 200
111
+ assert "jupyter_server_info" in response .text
112
+
113
+ # Test without token should fail
114
+ try :
115
+ response = requests .get (f"http://localhost:{ port } /metrics" , timeout = 2 )
116
+ assert response .status_code == 403
117
+ except requests .exceptions .ConnectionError :
118
+ # Server might not be ready yet, which is also acceptable
119
+ pass
120
+
121
+ finally :
122
+ app .stop ()
123
+
124
+
125
+ def test_metrics_server_without_authentication ():
126
+ """Test metrics server without authentication."""
127
+ port = find_available_port (9093 )
128
+
129
+ # Create a server app without authentication
130
+ with patch .dict ("os.environ" , {"JUPYTER_SERVER_METRICS_PORT" : str (port )}):
131
+ app = ServerApp ()
132
+ app .metrics_port = port
133
+ app .authenticate_prometheus = False
134
+ app .initialize ([])
135
+
136
+ # Start the app
137
+ app .start_app ()
138
+
139
+ # Wait for both servers to be ready
140
+ time .sleep (1.0 )
141
+
142
+ try :
143
+ # Test metrics endpoint without token should work
144
+ response = wait_for_server (
145
+ f"http://localhost:{ port } /metrics" ,
146
+ timeout = 5
147
+ )
148
+ assert response .status_code == 200
149
+ assert "jupyter_server_info" in response .text
150
+
151
+ finally :
152
+ app .stop ()
153
+
154
+
155
+ def test_metrics_server_port_conflict ():
105
156
"""Test that metrics server handles port conflicts gracefully."""
106
- app = ServerApp ()
107
- app .metrics_port = 9092
108
- app .initialize ([])
109
- server2 = None
110
- # Start first server
111
- server1 = start_metrics_server (app , 9092 )
112
- time .sleep (0.1 )
113
-
114
- try :
115
- # Try to start second server on same port
116
- server2 = start_metrics_server (app , 9092 )
117
- time .sleep (0.1 )
118
-
119
- # One of them should have failed to start or used a different port
120
- if server2 is not None and hasattr (server2 , "port" ):
121
- assert server2 .port != 9092 or server1 .port != 9092
122
- finally :
123
- if hasattr (server1 , "stop" ):
124
- server1 .stop ()
125
- time .sleep (0.2 )
126
- if server2 is not None and hasattr (server2 , "stop" ):
127
- server2 .stop ()
128
- time .sleep (0.2 )
129
-
130
-
131
- def test_metrics_server_disabled_when_port_zero ():
132
- """Test that metrics server is not started when port is 0."""
157
+ # Use a port that's likely to be in use
158
+ port = 8888 # Default Jupyter port
159
+
160
+ # Create a server app that should fail to start metrics server
161
+ with patch .dict ("os.environ" , {"JUPYTER_SERVER_METRICS_PORT" : str (port )}):
162
+ app = ServerApp ()
163
+ app .metrics_port = port
164
+ app .initialize ([])
165
+
166
+ # Start the app - should not crash
167
+ app .start_app ()
168
+
169
+ try :
170
+ # The app should still be running even if metrics server failed
171
+ assert app .http_server is not None
172
+
173
+ finally :
174
+ app .stop ()
175
+
176
+
177
+ def test_metrics_server_disabled ():
178
+ """Test that metrics server is disabled when port is 0."""
133
179
with patch .dict ("os.environ" , {"JUPYTER_SERVER_METRICS_PORT" : "0" }):
134
180
app = ServerApp ()
135
181
app .metrics_port = 0
136
182
app .initialize ([])
137
-
138
- # Should not start metrics server
139
- assert not hasattr (app , "metrics_server" ) or app .metrics_server is None
140
-
141
-
142
- def test_metrics_url_logging_with_separate_server ():
143
- """Test that metrics URL is logged correctly with separate server."""
144
- app = ServerApp ()
145
- app .metrics_port = 9093
146
- app .authenticate_prometheus = True
147
- app .initialize ([])
148
- app .identity_provider .token = "test_token"
149
-
150
- # Start metrics server
151
- server = start_metrics_server (app , 9093 )
152
- time .sleep (0.1 )
153
-
154
- try :
155
- # The URL should include the separate port
156
- expected_url = "http://localhost:9093/metrics?token=test_token"
157
- # This is a basic test - in practice you'd capture the log output
158
- assert server .port == 9093
159
- finally :
160
- if hasattr (server , "stop" ):
161
- server .stop ()
162
- time .sleep (0.2 )
163
-
164
-
165
- def test_metrics_url_logging_with_main_server ():
166
- """Test that metrics URL is logged correctly when using main server."""
167
- app = ServerApp ()
168
- app .metrics_port = 0 # Disable separate server
169
- app .authenticate_prometheus = True
170
- app .initialize ([])
171
- app .identity_provider .token = "test_token"
172
-
173
- # Should use main server's /metrics endpoint
174
- # This would be tested by checking the log output in practice
175
- assert app .metrics_port == 0
183
+
184
+ # Start the app
185
+ app .start_app ()
186
+
187
+ # Wait for server to be ready
188
+ time .sleep (0.5 )
189
+
190
+ try :
191
+ # Metrics should be available on main server
192
+ token = app .identity_provider .token
193
+ response = wait_for_server (
194
+ f"http://localhost:{ app .port } /metrics?token={ token } " ,
195
+ timeout = 5
196
+ )
197
+ assert response .status_code == 200
198
+ assert "jupyter_server_info" in response .text
199
+
200
+ finally :
201
+ app .stop ()
0 commit comments