11---
22name : e2e
33description : >
4- Write, run, and debug end-to-end tests for rtp2httpd. Use this skill whenever the user wants to
5- create new e2e test cases, add tests to existing test files, run the e2e test suite, or diagnose
6- failing tests. Also activate when the user mentions pytest, test_*.py files under tools/e2e/,
7- mock servers (RTSP/HTTP/FCC/STUN), multicast testing, or the run.sh test runner.
4+ Write, run, and debug end-to-end tests for rtp2httpd. ALWAYS use this skill when the user:
5+ (1) wants to write new e2e/integration tests or add test cases to existing test files,
6+ (2) asks to run tests (e.g. "跑测试", "run tests", "run pytest", mentions run.sh or uv),
7+ (3) needs to debug failing or hanging tests (timeout, assertion errors, import errors),
8+ (4) mentions ANY file under tools/e2e/ (test_*.py, conftest.py, helpers/*, run.sh),
9+ (5) mentions mock servers (MockRTSP*, MockHTTP*, MockFCC*, MockSTUN*), R2HProcess, or test fixtures,
10+ (6) asks about test infrastructure (markers, fixtures, scope, multicast setup, port allocation),
11+ (7) mentions "端到端测试", "e2e test", "integration test" in the context of rtp2httpd.
12+ This skill contains the complete helper API reference, test patterns, and conventions — without it
13+ the agent must read many files to discover what the skill provides instantly.
814argument-hint : " [run|write|debug] [optional test file or keyword]"
915---
1016
@@ -39,9 +45,6 @@ All commands run from the project root. The `run.sh` wrapper handles uv/pytest i
3945
4046# Dry run (list tests without running)
4147./tools/e2e/run.sh --co
42-
43- # Direct pytest (from tools/ dir)
44- cd tools && uv run --extra test pytest e2e/test_m3u.py -v
4548```
4649
4750** Prerequisite** — the binary must be built first:
@@ -55,39 +58,34 @@ Python deps are managed via uv (`tools/pyproject.toml`, extra `test`).
5558## Project Layout
5659
5760``` text
58- tools/
59- ├── pyproject.toml # pytest config, Python deps
60- ├── e2e/
61- │ ├── run.sh # Test runner script
62- │ ├── conftest.py # Shared fixtures and markers
63- │ ├── test_m3u.py # M3U playlist tests
64- │ ├── test_multicast.py # RTP multicast streaming
65- │ ├── test_rtsp_*.py # RTSP proxy (transport, seek, stun, misc, content_base)
66- │ ├── test_http_proxy*.py # HTTP proxy (basic, seek, m3u_rewrite)
67- │ ├── test_fcc.py # Fast Channel Change
68- │ ├── test_config.py # Config parsing
69- │ ├── test_auth.py # Authentication
70- │ ├── test_error.py # Error handling
71- │ ├── test_epg.py # EPG
72- │ ├── test_pages.py # Static pages
73- │ ├── test_zerocopy.py # Zero-copy streaming
74- │ └── helpers/
75- │ ├── __init__.py # Re-exports all helpers
76- │ ├── constants.py # BINARY_PATH, LOOPBACK_IF, MCAST_ADDR
77- │ ├── ports.py # find_free_port(), wait_for_port()
78- │ ├── http.py # http_get(), http_request(), stream_get()
79- │ ├── rtp.py # make_rtp_packet(), MulticastSender
80- │ ├── r2h_process.py # R2HProcess wrapper
81- │ ├── mock_rtsp.py # MockRTSPServer variants
82- │ ├── mock_http.py # MockHTTPUpstream variants
83- │ ├── mock_fcc.py # MockFCCServer
84- │ └── mock_stun.py # MockSTUNServer
61+ tools/e2e/
62+ ├── run.sh # Test runner script
63+ ├── conftest.py # Shared fixtures and markers
64+ ├── test_m3u.py # M3U playlist tests
65+ ├── test_multicast.py # RTP multicast streaming
66+ ├── test_rtsp_*.py # RTSP proxy (transport, seek, stun, misc, content_base)
67+ ├── test_http_proxy*.py # HTTP proxy (basic, seek, m3u_rewrite)
68+ ├── test_fcc.py # Fast Channel Change
69+ ├── test_config.py # Config parsing
70+ ├── test_auth.py # Authentication
71+ ├── test_error.py # Error handling
72+ ├── test_epg.py / test_pages.py / test_zerocopy.py
73+ └── helpers/
74+ ├── __init__.py # Re-exports all helpers (must update when adding new ones)
75+ ├── constants.py # BINARY_PATH, LOOPBACK_IF, MCAST_ADDR, FIXTURES_DIR
76+ ├── ports.py # find_free_port(), wait_for_port()
77+ ├── http.py # http_get(), http_request(), stream_get()
78+ ├── rtp.py # make_rtp_packet(), MulticastSender
79+ ├── r2h_process.py # R2HProcess wrapper
80+ ├── mock_rtsp.py # MockRTSPServer variants
81+ ├── mock_http.py # MockHTTPUpstream variants
82+ ├── mock_fcc.py # MockFCCServer
83+ └── mock_stun.py # MockSTUNServer
8584```
8685
8786## Writing Tests
8887
8988Before writing new tests, read the relevant existing test file and helpers to match conventions.
90- For the full API of each helper, read the source files listed above.
9189
9290### Imports
9391
@@ -141,13 +139,6 @@ from helpers import (
141139- ` MulticastSender(addr=MCAST_ADDR, port=0, pps=200, ts_per_rtp=7, ...) ` — sends RTP multicast on loopback
142140- ` make_rtp_packet(seq, timestamp, ssrc=0x12345678, payload_type=33, payload=None) `
143141
144- ** Constants** :
145-
146- - ` BINARY_PATH ` — ` PROJECT_ROOT / "build" / "rtp2httpd" `
147- - ` LOOPBACK_IF ` — ` "lo" ` on Linux, ` "lo0" ` on macOS
148- - ` MCAST_ADDR ` — ` "239.255.0.1" `
149- - ` FIXTURES_DIR ` — ` PROJECT_ROOT / "tools" / "fixtures" `
150-
151142### Shared Fixtures (conftest.py)
152143
153144- ` r2h_binary ` (session) — Path to binary, skips if missing
@@ -181,158 +172,71 @@ across multiple tests whenever possible to cut test setup overhead significantly
1811722 . Tests need different configs or args?
182173 → Only then start a per-test R2HProcess on demand.
183174
184- ** Scope guidelines:**
185-
186- | Scope | When to use |
187- | :--------- | :----------------------------------------------------------------- |
188- | ` module ` | All tests in the file share the same r2h config (best performance) |
189- | ` class ` | A group of tests in one class share the same config |
190- | ` function ` | Last resort — tests need incompatible configs or startup args |
191-
192175### Test Patterns
193176
194- ** Pattern 1 — Shared R2HProcess via module fixture (preferred)**
177+ ** Pattern 1 — Module-scoped shared R2HProcess (preferred)**
195178
196- When multiple tests can share the same rtp2httpd config, start it once for the whole file.
197- This is the default approach — only deviate when tests genuinely need different startup parameters.
179+ The default approach. Start r2h once for the whole file, all tests share it:
198180
199181``` python
200182@pytest.fixture (scope = " module" )
201183def shared_r2h (r2h_binary ):
202- """ Single rtp2httpd instance shared by all tests in this module."""
203184 port = find_free_port()
204185 config = f """ \
205186[global]
206187verbosity = 4
207-
208188[bind]
209189* { port}
210-
211190[services]
212191#EXTM3U
213192#EXTINF:-1,Channel One
214193rtp://239.0.0.1:1234
215- #EXTINF:-1,Channel Two
216- rtp://239.0.0.2:1234
217194"""
218195 r2h = R2HProcess(r2h_binary, port, config_content = config)
219196 r2h.start()
220197 yield r2h
221198 r2h.stop()
222199
223-
224200class TestPlaylist :
225201 def test_playlist_served (self , shared_r2h ):
226202 status, _, body = http_get(" 127.0.0.1" , shared_r2h.port, " /playlist.m3u" )
227203 assert status == 200
228- assert b " Channel One" in body
229-
230- def test_playlist_etag (self , shared_r2h ):
231- _, hdrs, _ = http_get(" 127.0.0.1" , shared_r2h.port, " /playlist.m3u" )
232- assert hdrs.get(" ETag" )
233-
234- def test_unknown_service_404 (self , shared_r2h ):
235- status, _, _ = http_get(" 127.0.0.1" , shared_r2h.port, " /NonExistent" )
236- assert status == 404
237204```
238205
239- ** Pattern 2 — Shared R2HProcess via class fixture**
206+ Use ` scope="class" ` when different classes in the same file need different configs.
207+
208+ ** Pattern 2 — Per-test R2HProcess (only when needed)**
240209
241- When different classes in the same file need different configs :
210+ When the config is unique to one test and can't be shared. Use ` try/finally ` :
242211
243212``` python
244- class TestFeatureA :
245- @pytest.fixture (scope = " class" )
246- def r2h_feature_a (self , r2h_binary ):
247- port = find_free_port()
248- config = f """ \
213+ def test_custom_max_clients (self , r2h_binary ):
214+ port = find_free_port()
215+ config = f """ \
249216[global]
250217verbosity = 4
218+ maxclients = 1
251219[bind]
252220* { port}
253- [services]
254- #EXTM3U
255- #EXTINF:-1,A Channel
256- rtp://239.0.0.1:1234
257221"""
258- r2h = R2HProcess(r2h_binary, port, config_content = config)
222+ r2h = R2HProcess(r2h_binary, port, config_content = config)
223+ try :
259224 r2h.start()
260- yield r2h
261- r2h.stop()
262-
263- def test_a_one (self , r2h_feature_a ):
264- status, _, _ = http_get(" 127.0.0.1" , r2h_feature_a.port, " /playlist.m3u" )
265- assert status == 200
266-
267- def test_a_two (self , r2h_feature_a ):
268- status, _, _ = http_get(" 127.0.0.1" , r2h_feature_a.port, " /A%20Channel" )
225+ status, _, _ = http_get(" 127.0.0.1" , port, " /playlist.m3u" )
269226 assert status == 200
227+ finally :
228+ r2h.stop()
270229```
271230
272- ** Pattern 3 — Per-test R2HProcess (on demand, only when needed )**
231+ ** Pattern 3 — With mock servers (RTSP / HTTP / multicast )**
273232
274- Only create a fresh R2HProcess per test when the config or startup args differ from other tests
275- and cannot be shared. Use ` try/finally ` for cleanup:
276-
277- ``` python
278- class TestSpecialConfig :
279- def test_custom_max_clients (self , r2h_binary ):
280- """ This test needs a unique maxclients value, cannot share."""
281- port = find_free_port()
282- config = f """ \
283- [global]
284- verbosity = 4
285- maxclients = 1
286- [bind]
287- * { port}
288- """
289- r2h = R2HProcess(r2h_binary, port, config_content = config)
290- try :
291- r2h.start()
292- status, _, _ = http_get(" 127.0.0.1" , port, " /playlist.m3u" )
293- assert status == 200
294- finally :
295- r2h.stop()
296- ```
297-
298- ** Pattern 4 — Multicast streaming (module-scoped R2HProcess)**
299-
300- ``` python
301- pytestmark = pytest.mark.multicast
302-
303- @pytest.fixture (scope = " module" )
304- def multicast_r2h (r2h_binary ):
305- port = find_free_port()
306- r2h = R2HProcess(r2h_binary, port, extra_args = [" -v" , " 4" , " -m" , " 100" , " -r" , LOOPBACK_IF ])
307- r2h.start()
308- yield r2h
309- r2h.stop()
310-
311- class TestStreaming :
312- def test_rtp_stream (self , multicast_r2h ):
313- mcast_port = find_free_udp_port()
314- sender = MulticastSender(addr = MCAST_ADDR , port = mcast_port, pps = 200 )
315- sender.start()
316- try :
317- status, _, body = stream_get(
318- " 127.0.0.1" , multicast_r2h.port,
319- f " /rtp/ { MCAST_ADDR } : { mcast_port} " ,
320- read_bytes = 4096 , timeout = 10.0 ,
321- )
322- assert status == 200
323- assert len (body) > 0
324- finally :
325- sender.stop()
326- ```
327-
328- ** Pattern 5 — RTSP proxy (module-scoped R2HProcess + mock)**
233+ Module-scoped fixture that starts both mock + r2h, yields both, stops both:
329234
330235``` python
331236pytestmark = pytest.mark.rtsp
332237
333238@pytest.fixture (scope = " module" )
334239def rtsp_env (r2h_binary ):
335- """ Shared RTSP mock + r2h for the whole module."""
336240 mock = MockRTSPServer()
337241 mock.start()
338242 port = find_free_port()
@@ -351,52 +255,14 @@ rtsp://127.0.0.1:{mock.port}/live
351255 yield r2h, mock
352256 r2h.stop()
353257 mock.stop()
354-
355- class TestRTSPProxy :
356- def test_rtsp_stream (self , rtsp_env ):
357- r2h, mock = rtsp_env
358- status, _, body = stream_get(" 127.0.0.1" , r2h.port, " /RTSP%20Ch" , read_bytes = 4096 )
359- assert status == 200
360258```
361259
362- ** Pattern 6 — HTTP proxy (module-scoped)**
363-
364- ``` python
365- pytestmark = pytest.mark.http_proxy
366-
367- @pytest.fixture (scope = " module" )
368- def http_proxy_env (r2h_binary ):
369- upstream = MockHTTPUpstream(routes = {
370- " /stream" : {" status" : 200 , " body" : b " data" , " headers" : {" Content-Type" : " video/mp2t" }},
371- })
372- upstream.start()
373- port = find_free_port()
374- config = f """ \
375- [global]
376- verbosity = 4
377- [bind]
378- * { port}
379- [services]
380- #EXTM3U
381- #EXTINF:-1,HTTP Ch
382- http://127.0.0.1: { upstream.port} /stream
383- """
384- r2h = R2HProcess(r2h_binary, port, config_content = config)
385- r2h.start()
386- yield r2h, upstream
387- r2h.stop()
388- upstream.stop()
389-
390- class TestHTTPProxy :
391- def test_proxy_passthrough (self , http_proxy_env ):
392- r2h, upstream = http_proxy_env
393- status, _, body = http_get(" 127.0.0.1" , r2h.port, " /HTTP%20Ch" )
394- assert status == 200
395- ```
260+ Same pattern works for HTTP proxy (with ` MockHTTPUpstream ` ) and multicast (with ` MulticastSender `
261+ and ` extra_args=["-v", "4", "-m", "100", "-r", LOOPBACK_IF] ` instead of config_content).
396262
397263### Conventions
398264
399- - ** Reuse R2HProcess** : Default to ` scope="module" ` fixtures. Only create per-test instances when startup parameters are incompatible. Starting fewer processes = faster test suite .
265+ - ** Reuse R2HProcess** : Default to ` scope="module" ` fixtures. Starting fewer processes = faster tests .
400266- ** File naming** : ` test_<feature>.py ` in ` tools/e2e/ `
401267- ** Class grouping** : Group related tests in classes (` TestFeatureSubArea ` )
402268- ** Test naming** : ` test_<what>_<expected_behavior> ` (e.g. ` test_etag_present ` , ` test_if_none_match_304 ` )
@@ -405,12 +271,18 @@ class TestHTTPProxy:
405271- ** URL encoding** : Use ` %20 ` for spaces in service name URLs (e.g. ` /Test%20Service ` )
406272- ** Config format** : INI-style with ` [global] ` , ` [bind] ` , ` [services] ` sections
407273- ** Module docstrings** : Each test file starts with a docstring describing what it covers
408- - ** Async waits** : Use ` time.sleep() ` sparingly for inherently async operations (e.g. external M3U fetch)
274+
275+ ### Gotchas
276+
277+ - ** Adding new helpers** : New mock servers or helpers must be added to ` helpers/__init__.py ` re-exports (both the ` from .module import ` line AND the ` __all__ ` list), otherwise ` from helpers import NewThing ` fails with ImportError.
278+ - ** ` MockRTSPServer ` already supports ` custom_sdp ` ** : Before creating a new mock subclass for custom SDP, check if ` MockRTSPServer(..., custom_sdp="...") ` already does what you need.
279+ - ** External M3U fetch is async** : After starting r2h with ` -M http://... ` or ` -M file://... ` , add ` time.sleep(0.5) ` before assertions to let the async curl fetch complete.
280+ - ** Concurrent connection tests** : To test connection limits (maxclients), use raw sockets to hold connections open while attempting new ones via ` stream_get() ` .
281+ - ** RTSP mock inspection** : Use ` mock_rtsp.requests_received ` (list of method names) and ` mock_rtsp.requests_detailed ` to verify handshake sequences.
282+ - ** HTTP mock inspection** : Use ` upstream.requests_log ` to check request details.
409283
410284### Debugging Tips
411285
412- - Run a single test sequentially with verbose output: ` ./tools/e2e/run.sh -p 1 -k "test_name" -x `
413- - Check if the binary is built: ` ls -la build/rtp2httpd `
414- - If a test hangs, it's usually a ` stream_get() ` timeout too short or the multicast sender not reaching the process
415- - For RTSP tests, inspect ` mock_rtsp.requests_received ` and ` mock_rtsp.requests_detailed `
416- - For HTTP proxy tests, inspect ` upstream.requests_log ` for request details
286+ - Run single test with verbose output: ` ./tools/e2e/run.sh -p 1 -k "test_name" -x `
287+ - Check binary is built: ` ls -la build/rtp2httpd `
288+ - Test hangs? Usually ` stream_get() ` timeout too short or multicast sender not reaching the process
0 commit comments