gabriel / musehub public
test_clones_stress.py python
191 lines 6.8 KB
Raw
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor ⚠ breaking 20 days ago
1 """Tier 4 — Stress tests for the clone browser (issue #17).
2
3 Verifies correctness and response-time bounds under large data volumes.
4 Timing bounds are measured with ``time.perf_counter`` — no pytest-benchmark
5 dependency. All thresholds are conservative multiples of observed median
6 times so they remain stable under CI load.
7
8 Cases:
9 S01 List page renders correctly with 500 clusters — response < 500ms
10 S02 Detail page renders correctly with 300-member cluster — response < 300ms
11 S03 Dashboard clones card query is fast with 10 000 total clusters — < 500ms
12 S04 Tier filter over 500+500 clusters returns correct subset — < 300ms
13 S05 File breakdown with 50 distinct files in one cluster — no truncation
14 """
15 from __future__ import annotations
16
17 import json
18 import time
19
20 import pytest
21 import pytest_asyncio
22 from httpx import AsyncClient
23 from sqlalchemy.dialects.postgresql import insert as pg_insert
24 from sqlalchemy.ext.asyncio import AsyncSession
25
26 from musehub.db.musehub_intel_models import MusehubIntelClones
27 from tests.factories import create_repo
28 from muse.core.types import long_id
29
30 _REF = long_id("a" * 64)
31
32
33 async def _bulk_insert(
34 session: AsyncSession,
35 repo_id: str,
36 n: int,
37 tier: str = "exact",
38 member_count: int = 2,
39 members_json: str | None = None,
40 ) -> None:
41 """Insert N cluster rows using executemany for speed."""
42 if members_json is None:
43 members_json = json.dumps([
44 {"address": f"src/a.py::fn{i}", "kind": "function",
45 "language": "Python", "body_hash": long_id("a" * 64),
46 "signature_id": long_id("b" * 64), "content_id": long_id("a" * 64)}
47 for i in range(member_count)
48 ])
49
50 rows = [
51 {
52 "repo_id": repo_id,
53 "cluster_hash": long_id(f"{tier[0]}{str(i).zfill(63)}"),
54 "tier": tier,
55 "member_count": member_count,
56 "members_json": members_json,
57 "ref": _REF,
58 }
59 for i in range(n)
60 ]
61 # Insert in batches of 500 to stay within asyncpg parameter limit.
62 batch = 500
63 for start in range(0, len(rows), batch):
64 await session.execute(
65 pg_insert(MusehubIntelClones)
66 .values(rows[start : start + batch])
67 .on_conflict_do_nothing()
68 )
69 await session.commit()
70
71
72 @pytest_asyncio.fixture
73 async def repo_500(db_session: AsyncSession) -> MusehubRepo:
74 r = await create_repo(db_session, owner="stressuser", slug="stress-500")
75 await _bulk_insert(db_session, str(r.repo_id), 300, tier="exact")
76 await _bulk_insert(db_session, str(r.repo_id), 200, tier="near")
77 return r
78
79
80 @pytest_asyncio.fixture
81 async def repo_big_cluster(db_session: AsyncSession) -> tuple[MusehubRepo, str]:
82 """One cluster with 300 members spread across 50 files."""
83 r = await create_repo(db_session, owner="stressuser", slug="stress-big")
84 members = [
85 {
86 "address": f"src/module_{i // 6}.py::fn_{i}",
87 "kind": "function",
88 "language": "Python",
89 "body_hash": long_id("a" * 64),
90 "signature_id": long_id("b" * 64),
91 "content_id": long_id("a" * 64),
92 }
93 for i in range(300)
94 ]
95 h = long_id("b" * 64)
96 await db_session.execute(
97 pg_insert(MusehubIntelClones)
98 .values(
99 repo_id=str(r.repo_id),
100 cluster_hash=h,
101 tier="near",
102 member_count=300,
103 members_json=json.dumps(members),
104 ref=_REF,
105 )
106 .on_conflict_do_nothing()
107 )
108 await db_session.commit()
109 return r, h
110
111
112 @pytest_asyncio.fixture
113 async def repo_10k(db_session: AsyncSession) -> MusehubRepo:
114 r = await create_repo(db_session, owner="stressuser", slug="stress-10k")
115 await _bulk_insert(db_session, str(r.repo_id), 5000, tier="exact")
116 await _bulk_insert(db_session, str(r.repo_id), 5000, tier="near")
117 return r
118
119
120 class TestClonesStress:
121
122 @pytest.mark.asyncio
123 async def test_S01_list_500_clusters_under_500ms(
124 self, client: AsyncClient, repo_500: MusehubRepo
125 ) -> None:
126 """500-cluster list page responds within 500ms."""
127 t0 = time.perf_counter()
128 r = await client.get("/stressuser/stress-500/intel/clones")
129 elapsed = time.perf_counter() - t0
130
131 assert r.status_code == 200
132 assert b"cl-row" in r.content
133 assert elapsed < 0.5, f"List page too slow: {elapsed:.3f}s"
134
135 @pytest.mark.asyncio
136 async def test_S02_detail_300_members_under_300ms(
137 self, client: AsyncClient, repo_big_cluster: tuple[MusehubRepo, str]
138 ) -> None:
139 """300-member cluster detail page responds within 300ms."""
140 repo, h = repo_big_cluster
141 t0 = time.perf_counter()
142 r = await client.get(
143 f"/stressuser/stress-big/intel/clones/detail?cluster={h}"
144 )
145 elapsed = time.perf_counter() - t0
146
147 assert r.status_code == 200
148 assert b"cl-member-row" in r.content
149 assert elapsed < 0.3, f"Detail page too slow: {elapsed:.3f}s"
150
151 @pytest.mark.asyncio
152 async def test_S03_dashboard_10k_clusters_under_500ms(
153 self, client: AsyncClient, repo_10k: MusehubRepo
154 ) -> None:
155 """Dashboard clones card with 10 000 rows responds within 500ms."""
156 t0 = time.perf_counter()
157 r = await client.get("/stressuser/stress-10k/intel")
158 elapsed = time.perf_counter() - t0
159
160 assert r.status_code == 200
161 assert b"CLONES" in r.content
162 assert elapsed < 0.5, f"Dashboard too slow with 10k rows: {elapsed:.3f}s"
163
164 @pytest.mark.asyncio
165 async def test_S04_tier_filter_500_each_under_300ms(
166 self, client: AsyncClient, repo_500: MusehubRepo
167 ) -> None:
168 """Tier filter over 500 exact + 200 near responds within 300ms."""
169 t0 = time.perf_counter()
170 r = await client.get("/stressuser/stress-500/intel/clones?tier=exact")
171 elapsed = time.perf_counter() - t0
172
173 assert r.status_code == 200
174 assert b"cl-badge--exact" in r.content
175 assert b"cl-badge--near" not in r.content
176 assert elapsed < 0.3, f"Tier filter too slow: {elapsed:.3f}s"
177
178 @pytest.mark.asyncio
179 async def test_S05_file_breakdown_50_files_no_truncation(
180 self, client: AsyncClient, repo_big_cluster: tuple[MusehubRepo, str]
181 ) -> None:
182 """300-member cluster spanning 50 files shows the full file breakdown."""
183 repo, h = repo_big_cluster
184 r = await client.get(
185 f"/stressuser/stress-big/intel/clones/detail?cluster={h}"
186 )
187 assert r.status_code == 200
188 body = r.text
189 # 300 members / 6 per file = 50 distinct files — all should appear
190 file_rows = body.count("cl-file-row")
191 assert file_rows == 50, f"Expected 50 file rows, got {file_rows}"
File History 1 commit
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 20 days ago