Skip to content

Commit 5c19880

Browse files
stackiaclaude
andcommitted
refactor(skill): trim e2e skill and improve trigger description
Reduce skill from 417 to 266 lines (-36%) by consolidating 6 test patterns into 3 and removing redundant examples. Add Gotchas section with domain-specific knowledge (helpers re-export mechanism, custom_sdp, async M3U fetch, concurrent connection testing). Expand description with explicit numbered trigger categories for better skill activation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c3049bb commit 5c19880

1 file changed

Lines changed: 65 additions & 193 deletions

File tree

‎.claude/skills/e2e/SKILL.md‎

Lines changed: 65 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
---
22
name: e2e
33
description: >
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.
814
argument-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

8988
Before 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
181172
2. 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")
201183
def 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]
206187
verbosity = 4
207-
208188
[bind]
209189
* {port}
210-
211190
[services]
212191
#EXTM3U
213192
#EXTINF:-1,Channel One
214193
rtp://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-
224200
class 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]
250217
verbosity = 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
331236
pytestmark = pytest.mark.rtsp
332237

333238
@pytest.fixture(scope="module")
334239
def 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

Comments
 (0)