2424
2525import json
2626import os
27+ import socket
2728import time
2829import unittest
2930from pathlib import Path
@@ -91,73 +92,58 @@ def requires_device(cls):
9192 return cls
9293
9394
94- # Minimum free RAM for server tests (server.py is ~44KB)
95- MIN_RAM_FOR_SERVER = 200000
96-
97-
9895class MpyServerTestCase (unittest .TestCase ):
9996 """Base class for MicroPython server tests"""
10097
10198 mpy = None
10299 conn = None
103100 esp32_ip = None
104101 server_running = False
105- _server_uploaded = False
102+ _mount_handler = None
106103
107104 @classmethod
108105 def setUpClass (cls ):
109106 import mpytool
107+ from mpytool .mpy_cross import MpyCross
110108
111109 cls .conn = mpytool .ConnSerial (port = PORT , baudrate = 115200 )
112110 cls .mpy = mpytool .Mpy (cls .conn )
113- cls .mpy .stop ()
114111
115- # Check RAM before uploading large server module
116- free_mem = cls .get_free_memory ()
117- if free_mem < MIN_RAM_FOR_SERVER :
118- raise unittest .SkipTest (
119- f"Not enough RAM for server: { free_mem } < { MIN_RAM_FOR_SERVER } " )
112+ # Soft reset to clear any previous state
113+ cls .mpy .stop ()
114+ try :
115+ cls .conn .write (b'\x03 \x03 \x04 ' ) # Ctrl-C twice + Ctrl-D
116+ time .sleep (2 )
117+ cls .conn .read_all ()
118+ except Exception :
119+ pass
120+ cls .mpy .stop ()
120121
121- # Upload server module once
122- if not cls ._server_uploaded :
123- cls ._upload_server ()
124- cls ._server_uploaded = True
122+ # Mount server module with mpy-cross compilation
123+ server_dir = Path (__file__ ).parent .parent / 'uhttp'
124+ mpy_cross = MpyCross ()
125+ mpy_cross .init (cls .mpy .platform ())
126+ cls ._mount_handler = cls .mpy .mount (
127+ str (server_dir ), mount_point = '/lib/uhttp' , mpy_cross = mpy_cross )
125128
126129 # Connect WiFi and get IP
127130 cls .esp32_ip = cls ._connect_wifi ()
128131
129- # Start server on ESP32
132+ # Start server and wait for it to be ready
130133 cls ._start_server ()
131-
132- @classmethod
133- def get_free_memory (cls ):
134- """Get free memory on device"""
135- result = cls .mpy .comm .exec_raw_paste (
136- "import gc; gc.collect(); print(gc.mem_free())" , timeout = 5 )
137- return int (result .strip ())
134+ cls ._wait_for_server ()
138135
139136 @classmethod
140137 def tearDownClass (cls ):
141- cls ._stop_server ()
138+ if cls .server_running :
139+ try :
140+ cls .mpy .stop ()
141+ except Exception :
142+ pass
143+ cls .server_running = False
142144 if cls .conn :
143145 cls .conn .close ()
144146
145- @classmethod
146- def _upload_server (cls ):
147- """Upload server.py to ESP32"""
148- server_file = Path (__file__ ).parent .parent / 'uhttp' / 'server.py'
149- # Create /lib/uhttp directory
150- try :
151- cls .mpy .mkdir ('/lib' )
152- except Exception :
153- pass
154- try :
155- cls .mpy .mkdir ('/lib/uhttp' )
156- except Exception :
157- pass
158- # Upload server.py
159- cls .mpy .put (server_file .read_bytes (), '/lib/uhttp/server.py' )
160-
161147 @classmethod
162148 def _connect_wifi (cls ):
163149 """Connect ESP32 to WiFi and return IP address"""
@@ -192,20 +178,30 @@ def _connect_wifi(cls):
192178
193179 @classmethod
194180 def _start_server (cls ):
195- """Start HTTP server on ESP32 (fire-and-forget)"""
181+ """Start HTTP server on ESP32"""
182+ # Verify import works (also ensures mount is ready)
183+ test_result = cls .mpy .comm .exec (
184+ "import sys; sys.path.insert(0, '/lib'); "
185+ "from uhttp.server import HttpServer; print('OK')" ,
186+ timeout = 10
187+ ).decode ('utf-8' )
188+ if 'OK' not in test_result :
189+ raise RuntimeError (f"Failed to import uhttp.server: { test_result } " )
190+
191+ # Start server (fire-and-forget)
196192 code = f"""
197193import sys
198194sys.path.insert(0, '/lib')
199-
200195from uhttp.server import HttpServer
201196
202197server = HttpServer(port={ ESP32_SERVER_PORT } )
203- print('SERVER_STARTED')
204198
205199while True:
206200 client = server.wait(timeout=1)
207201 if client:
208- if client.path == '/json':
202+ if client.path == '/health':
203+ client.respond({{'status': 'ok'}})
204+ elif client.path == '/json':
209205 client.respond({{'method': client.method, 'path': client.path}})
210206 elif client.path == '/echo':
211207 client.respond({{'data': client.data, 'method': client.method}})
@@ -218,37 +214,49 @@ def _start_server(cls):
218214 else:
219215 client.respond({{'path': client.path, 'method': client.method}})
220216"""
221- # Start server with timeout=0 (fire-and-forget)
222217 cls .mpy .comm .exec (code , timeout = 0 )
223218 cls .server_running = True
224- time .sleep (1 ) # Give server time to start
219+ time .sleep (0.5 )
225220
226221 @classmethod
227- def _stop_server (cls ):
228- """Stop HTTP server on ESP32 """
229- if cls . server_running :
222+ def _wait_for_server (cls , max_attempts = 10 ):
223+ """Wait for server to respond to health check """
224+ for _ in range ( max_attempts ) :
230225 try :
231- cls .mpy .stop ()
232- time .sleep (0.5 )
233- except Exception :
226+ sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
227+ sock .settimeout (2 )
228+ sock .connect ((cls .esp32_ip , ESP32_SERVER_PORT ))
229+ sock .sendall (b'GET /health HTTP/1.0\r \n Host: test\r \n \r \n ' )
230+ if b'200' in sock .recv (1024 ):
231+ sock .close ()
232+ return
233+ sock .close ()
234+ except (OSError , socket .timeout ):
234235 pass
235- cls .server_running = False
236+ time .sleep (1 )
237+ raise RuntimeError (
238+ f"Server not ready on { cls .esp32_ip } :{ ESP32_SERVER_PORT } " )
236239
237240
238241@requires_device
239242class TestHTTPServer (MpyServerTestCase ):
240243 """Test HTTP server basic functionality"""
241244
242- def _request (self , method , path , ** kwargs ):
245+ def _request (self , method , path , retries = 2 , ** kwargs ):
243246 """Make HTTP request to ESP32 server"""
244- from uhttp .client import HttpClient
247+ from uhttp .client import HttpClient , HttpConnectionError , HttpTimeoutError
245248 url = f"http://{ self .esp32_ip } :{ ESP32_SERVER_PORT } "
246- client = HttpClient (url )
247- try :
248- response = getattr (client , method .lower ())(path , ** kwargs ).wait ()
249- return response
250- finally :
251- client .close ()
249+ last_error = None
250+ for _ in range (retries ):
251+ client = HttpClient (url , timeout = 10 )
252+ try :
253+ return getattr (client , method .lower ())(path , ** kwargs ).wait ()
254+ except (HttpConnectionError , HttpTimeoutError ) as e :
255+ last_error = e
256+ time .sleep (0.5 )
257+ finally :
258+ client .close ()
259+ raise last_error
252260
253261 def test_get_request (self ):
254262 """Test basic GET request"""
0 commit comments