flakeiq 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import sqlite3
4
+ import os
5
+ import sys
6
+ import argparse
7
+ from pathlib import Path
8
+ from datetime import datetime, timedelta
9
+ from http.server import HTTPServer, BaseHTTPRequestHandler
10
+ from urllib.parse import urlparse
11
+
12
+ DB_PATH = os.environ.get("FLAKE_DB", "flake.db")
13
+ PORT = int(os.environ.get("FLAKE_PORT", "8080"))
14
+ STATIC_DIR = Path(__file__).parent / "web" / "static"
15
+
16
+
17
+ class Handler(BaseHTTPRequestHandler):
18
+ def do_GET(self):
19
+ parsed = urlparse(self.path)
20
+ path = parsed.path
21
+ if path in ("/", "/index.html"):
22
+ self._serve_file("index.html", "text/html")
23
+ return
24
+ if path.startswith("/static/"):
25
+ fname = path[len("/static/"):]
26
+ content_types = {".html": "text/html", ".css": "text/css", ".js": "application/javascript"}
27
+ ext = Path(fname).suffix
28
+ ct = content_types.get(ext, "application/octet-stream")
29
+ self._serve_file(fname, ct)
30
+ return
31
+ handlers = {
32
+ "/api/stats": self.api_stats,
33
+ "/api/flake-rate": self.api_flake_rate,
34
+ "/api/breakdown": self.api_breakdown,
35
+ "/api/heatmap": self.api_heatmap,
36
+ "/api/top-flakes": self.api_top_flakes,
37
+ "/api/devices": self.api_devices,
38
+ "/api/by-action": self.api_by_action,
39
+ "/api/by-platform": self.api_by_platform,
40
+ "/api/volume": self.api_volume,
41
+ "/api/duration-dist": self.api_duration_dist,
42
+ "/api/classification-trend": self.api_classification_trend,
43
+ "/api/sessions": self.api_sessions,
44
+ "/api/latest-session": self.api_latest_session,
45
+ }
46
+ h = handlers.get(path)
47
+ if h:
48
+ h()
49
+ else:
50
+ self.send_response(404)
51
+ self.end_headers()
52
+
53
+ def _serve_file(self, filename, content_type):
54
+ filepath = STATIC_DIR / filename
55
+ if not filepath.exists():
56
+ self.send_response(404)
57
+ self.end_headers()
58
+ return
59
+ self.send_response(200)
60
+ self.send_header("Content-Type", f"{content_type}; charset=utf-8")
61
+ self.send_header("Cache-Control", "no-cache")
62
+ self.end_headers()
63
+ self.wfile.write(filepath.read_bytes())
64
+
65
+ def _json(self, data):
66
+ self.send_response(200)
67
+ self.send_header("Content-Type", "application/json")
68
+ self.send_header("Cache-Control", "no-cache")
69
+ self.send_header("Access-Control-Allow-Origin", "*")
70
+ self.end_headers()
71
+ self.wfile.write(json.dumps(data).encode())
72
+
73
+ def _query(self, sql, params=()):
74
+ db = sqlite3.connect(DB_PATH)
75
+ db.row_factory = sqlite3.Row
76
+ try:
77
+ return [dict(r) for r in db.execute(sql, params).fetchall()]
78
+ finally:
79
+ db.close()
80
+
81
+ def _scalar(self, sql, params=()):
82
+ rows = self._query(sql, params)
83
+ if rows:
84
+ return list(rows[0].values())[0]
85
+ return 0
86
+
87
+ def api_stats(self):
88
+ rows = self._query("""
89
+ SELECT
90
+ COUNT(*) as total_runs,
91
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count,
92
+ SUM(CASE WHEN classification IS NOT NULL THEN 1 ELSE 0 END) as classified_count,
93
+ SUM(CASE WHEN classification = 'REAL_BUG' THEN 1 ELSE 0 END) as real_bug_count
94
+ FROM test_runs
95
+ """)
96
+ s = rows[0]
97
+ s["flake_rate"] = round(s["fail_count"] / s["total_runs"] * 100, 1) if s["total_runs"] else 0
98
+ avg_dur = self._scalar("SELECT CAST(ROUND(AVG(duration_ms / 1000.0)) AS INTEGER) FROM test_runs")
99
+ s["avg_duration"] = avg_dur or 0
100
+ dr = self._query("SELECT MIN(DATE(run_at)) as first, MAX(DATE(run_at)) as last FROM test_runs")
101
+ if dr and dr[0]["first"] and dr[0]["last"]:
102
+ s["date_range"] = f"{dr[0]['first']} to {dr[0]['last']}"
103
+ else:
104
+ s["date_range"] = ""
105
+ self._json(s)
106
+
107
+ def api_flake_rate(self):
108
+ rows = self._query("""
109
+ SELECT DATE(run_at) as day,
110
+ ROUND(SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as rate
111
+ FROM test_runs
112
+ GROUP BY day ORDER BY day LIMIT 60
113
+ """)
114
+ self._json(rows)
115
+
116
+ def api_breakdown(self):
117
+ rows = self._query("""
118
+ SELECT COALESCE(classification, 'UNCLASSIFIED') as classification, COUNT(*) as count
119
+ FROM test_runs WHERE result != 'passed'
120
+ GROUP BY classification ORDER BY count DESC
121
+ """)
122
+ self._json(rows)
123
+
124
+ def api_heatmap(self):
125
+ rows = self._query("""
126
+ SELECT DATE(run_at) as day, screen_name as screen,
127
+ ROUND(SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as rate
128
+ FROM test_runs
129
+ WHERE screen_name IS NOT NULL AND screen_name != ''
130
+ GROUP BY day, screen
131
+ HAVING COUNT(*) >= 2
132
+ ORDER BY day, screen
133
+ """)
134
+ self._json(rows)
135
+
136
+ def api_top_flakes(self):
137
+ rows = self._query("""
138
+ SELECT test_name, platform, screen_name, COUNT(*) as total,
139
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count,
140
+ (SELECT classification FROM test_runs t2
141
+ WHERE t2.test_name = t1.test_name AND t2.result != 'passed'
142
+ GROUP BY classification ORDER BY COUNT(*) DESC LIMIT 1
143
+ ) as common_classification
144
+ FROM test_runs t1
145
+ GROUP BY test_name, platform
146
+ HAVING total >= 2
147
+ ORDER BY fail_count * 1.0 / total DESC
148
+ LIMIT 20
149
+ """)
150
+ self._json(rows)
151
+
152
+ def api_devices(self):
153
+ rows = self._query("""
154
+ SELECT device_id, platform, COUNT(*) as total,
155
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count,
156
+ (SELECT classification FROM test_runs t2
157
+ WHERE t2.device_id = t1.device_id AND t2.result != 'passed'
158
+ GROUP BY classification ORDER BY COUNT(*) DESC LIMIT 1
159
+ ) as common_classification
160
+ FROM test_runs t1
161
+ GROUP BY device_id
162
+ HAVING total >= 2
163
+ ORDER BY fail_count * 1.0 / total DESC
164
+ """)
165
+ self._json(rows)
166
+
167
+ def api_by_action(self):
168
+ rows = self._query("""
169
+ SELECT action_type,
170
+ COUNT(*) as total,
171
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count,
172
+ ROUND(SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as rate
173
+ FROM test_runs
174
+ WHERE action_type IS NOT NULL AND action_type != ''
175
+ GROUP BY action_type
176
+ ORDER BY rate DESC
177
+ """)
178
+ self._json(rows)
179
+
180
+ def api_by_platform(self):
181
+ rows = self._query("""
182
+ SELECT platform,
183
+ COUNT(*) as total,
184
+ SUM(CASE WHEN result = 'passed' THEN 1 ELSE 0 END) as pass_count,
185
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count,
186
+ ROUND(SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as fail_rate
187
+ FROM test_runs
188
+ WHERE platform IS NOT NULL AND platform != ''
189
+ GROUP BY platform
190
+ ORDER BY fail_rate DESC
191
+ """)
192
+ self._json(rows)
193
+
194
+ def api_volume(self):
195
+ rows = self._query("""
196
+ SELECT DATE(run_at) as day,
197
+ SUM(CASE WHEN result = 'passed' THEN 1 ELSE 0 END) as pass_count,
198
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as fail_count
199
+ FROM test_runs
200
+ GROUP BY day ORDER BY day
201
+ """)
202
+ self._json(rows)
203
+
204
+ def api_duration_dist(self):
205
+ rows = self._query("""
206
+ SELECT
207
+ CASE
208
+ WHEN duration_ms < 5000 THEN '0-5s'
209
+ WHEN duration_ms < 15000 THEN '5-15s'
210
+ WHEN duration_ms < 30000 THEN '15-30s'
211
+ WHEN duration_ms < 60000 THEN '30-60s'
212
+ WHEN duration_ms < 120000 THEN '60-120s'
213
+ ELSE '120s+'
214
+ END as bucket,
215
+ COUNT(*) as count
216
+ FROM test_runs
217
+ WHERE result != 'passed'
218
+ GROUP BY bucket
219
+ ORDER BY MIN(duration_ms)
220
+ """)
221
+ self._json(rows)
222
+
223
+ def api_classification_trend(self):
224
+ rows = self._query("""
225
+ SELECT DATE(run_at) as day, classification, COUNT(*) as count
226
+ FROM test_runs
227
+ WHERE result != 'passed' AND classification IS NOT NULL
228
+ GROUP BY day, classification
229
+ ORDER BY day
230
+ """)
231
+ self._json(rows)
232
+
233
+ def api_sessions(self):
234
+ rows = self._query("""
235
+ SELECT session_id, platform, device_id,
236
+ COUNT(*) as total_runs,
237
+ SUM(CASE WHEN result = 'passed' THEN 1 ELSE 0 END) as passed,
238
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as failed,
239
+ CAST(ROUND(AVG(duration_ms / 1000.0)) AS INTEGER) as avg_duration_s,
240
+ MIN(run_at) as first_run,
241
+ MAX(run_at) as last_run
242
+ FROM test_runs
243
+ WHERE session_id IS NOT NULL AND session_id != ''
244
+ GROUP BY session_id
245
+ ORDER BY last_run DESC
246
+ LIMIT 20
247
+ """)
248
+ self._json(rows)
249
+
250
+ def api_latest_session(self):
251
+ session = self._query("""
252
+ SELECT session_id, platform, device_id,
253
+ COUNT(*) as total_runs,
254
+ SUM(CASE WHEN result = 'passed' THEN 1 ELSE 0 END) as passed,
255
+ SUM(CASE WHEN result != 'passed' THEN 1 ELSE 0 END) as failed,
256
+ CAST(ROUND(AVG(duration_ms / 1000.0)) AS INTEGER) as avg_duration_s
257
+ FROM test_runs
258
+ WHERE session_id IS NOT NULL AND session_id != ''
259
+ GROUP BY session_id
260
+ ORDER BY MAX(run_at) DESC
261
+ LIMIT 1
262
+ """)
263
+ if not session:
264
+ self._json({"total_runs": 0, "passed": 0, "failed": 0})
265
+ return
266
+ s = session[0]
267
+ tests = self._query("""
268
+ SELECT test_name, result, duration_ms, platform, screen_name, classification
269
+ FROM test_runs
270
+ WHERE session_id = ?
271
+ ORDER BY run_at
272
+ """, (s["session_id"],))
273
+ s["tests"] = tests
274
+ self._json(s)
275
+
276
+ def log_message(self, format, *args):
277
+ pass
278
+
279
+
280
+ def main():
281
+ global DB_PATH, PORT
282
+ parser = argparse.ArgumentParser(description="FlakeIQ dashboard")
283
+ parser.add_argument("--db", default=None, help="SQLite database path")
284
+ parser.add_argument("--port", type=int, default=8080, help="HTTP port")
285
+ parser.add_argument("--host", default="127.0.0.1", help="Bind address")
286
+ parser.add_argument("--seed", action="store_true", help="Use the seed database (flake-seed.db) instead of real data")
287
+ parser.add_argument("--open", action="store_true", help="Open browser on start")
288
+ args = parser.parse_args()
289
+ if args.seed:
290
+ seed_path = os.path.join(os.path.dirname(__file__), "flake-seed.db")
291
+ if os.path.exists(seed_path):
292
+ DB_PATH = seed_path
293
+ print(f"Using seed database: {seed_path}")
294
+ else:
295
+ print("Seed database not found. Run 'python3 seed.py' first.")
296
+ sys.exit(1)
297
+ elif args.db:
298
+ DB_PATH = args.db
299
+ PORT = args.port
300
+ os.environ["FLAKE_DB"] = DB_PATH
301
+
302
+ if not os.path.exists(DB_PATH):
303
+ print(f"Database not found: {DB_PATH}")
304
+ print("Run 'python3 classify.py flake-results.jsonl' or 'python3 seed.py' first.")
305
+ sys.exit(1)
306
+
307
+ server = HTTPServer((args.host, PORT), Handler)
308
+ url = f"http://{args.host}:{PORT}"
309
+ print(f"FlakeIQ dashboard at {url}")
310
+ if args.open:
311
+ import webbrowser
312
+ webbrowser.open(url)
313
+ try:
314
+ server.serve_forever()
315
+ except KeyboardInterrupt:
316
+ print("\nShutting down.")
317
+ server.server_close()
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
@@ -0,0 +1,2 @@
1
+ bcrypt==4.2.0
2
+ python-jose[cryptography]==3.3.0
package/python/seed.py ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ import sqlite3
3
+ import json
4
+ import os
5
+ import random
6
+ import sys
7
+ from datetime import datetime, timedelta
8
+
9
+ DB_PATH = os.environ.get("FLAKE_DB", "flake.db")
10
+ random.seed(42)
11
+
12
+ SCREENS = ["login", "profile", "form", "list", "alert", "animation", "calendar", "gesture", "media", "signature", "home"]
13
+ DEVICES = [
14
+ ("Medium_Phone_API_36.0", "android"),
15
+ ("emulator-5554", "android"),
16
+ ("iPhone_16_Pro_iOS_18", "ios"),
17
+ ("iPhone_15_iOS_17", "ios"),
18
+ ]
19
+
20
+ TESTS = [
21
+ # (test_file, test_name, screen)
22
+ ("tests/example.spec.ts", "login flow - fill form and submit", "login"),
23
+ ("tests/example.spec.ts", "login flow - invalid credentials shows error", "login"),
24
+ ("tests/profile.spec.ts", "edit profile - change name and save", "profile"),
25
+ ("tests/profile.spec.ts", "toggle notification switches", "profile"),
26
+ ("tests/profile.spec.ts", "logout from profile screen", "profile"),
27
+ ("tests/forms.spec.ts", "fill all form fields and submit", "form"),
28
+ ("tests/forms.spec.ts", "form validation shows errors", "form"),
29
+ ("tests/lists.spec.ts", "scroll through list items", "list"),
30
+ ("tests/lists.spec.ts", "pull to refresh list", "list"),
31
+ ("tests/alerts.spec.ts", "show and dismiss alert dialog", "alert"),
32
+ ("tests/alerts.spec.ts", "alert with multiple buttons", "alert"),
33
+ ("tests/animation.spec.ts", "animated element appears", "animation"),
34
+ ("tests/calendar.spec.ts", "select date from calendar", "calendar"),
35
+ ("tests/gestures.spec.ts", "swipe between tabs", "gesture"),
36
+ ("tests/media.spec.ts", "pick image from gallery", "media"),
37
+ ("tests/signature.spec.ts", "draw and save signature", "signature"),
38
+ ("tests/home.spec.ts", "home screen loads correctly", "home"),
39
+ ]
40
+
41
+ ACTION_TEMPLATES = {
42
+ "login": [
43
+ ["tap(#emailInput)", "fill(#emailInput)", "tap(#passwordInput)", "fill(#passwordInput)", "tap(#loginButton)"],
44
+ ["tap(#emailInput)", "fill(#emailInput)", "tap(#passwordInput)", "fill(#passwordInput)", "tap(#loginButton)", "waitFor(#homeScreen)"],
45
+ ["tap(#emailInput)", "fill(#emailInput)", "tap(#passwordInput)", "fill(#passwordInput)", "tap(#loginButton)", "waitForText(Invalid)"],
46
+ ],
47
+ "profile": [
48
+ ["tap(#navProfile)", "waitFor(#profileScreen)", "tap(#editButton)", "fill(#nameInput)", "tap(#saveButton)", "waitForText(Profile updated)"],
49
+ ["tap(#navProfile)", "waitFor(#profileScreen)", "swipe('up')", "tap(#notificationsSwitch)", "waitForText(Saved)"],
50
+ ["tap(#navProfile)", "waitFor(#profileScreen)", "swipe('up')", "tap(#logoutButton)", "waitFor(#loginScreen)"],
51
+ ],
52
+ "form": [
53
+ ["tap(#navFormControls)", "waitFor(#formScreen)", "fill(#textInput)", "fill(#emailField)", "fill(#phoneField)", "tap(#submitButton)"],
54
+ ["tap(#navFormControls)", "waitFor(#formScreen)", "tap(#submitButton)", "waitForText(required)"],
55
+ ],
56
+ "list": [
57
+ ["tap(#navListsDemo)", "waitFor(#listScreen)", "swipe('up')", "swipe('up')", "tap(#listItem_5)"],
58
+ ["tap(#navListsDemo)", "waitFor(#listScreen)", "swipe('down')", "waitForText(Item 1)"],
59
+ ],
60
+ "alert": [
61
+ ["tap(#navAlertsDemo)", "waitFor(#alertScreen)", "tap(#showAlertButton)", "waitForText(OK)", "tap(#okButton)"],
62
+ ["tap(#navAlertsDemo)", "waitFor(#alertScreen)", "tap(#multiButtonAlert)", "tap(#cancelButton)"],
63
+ ],
64
+ "animation": [
65
+ ["tap(#navAnimationDemo)", "waitFor(#animationScreen)", "tap(#startAnimation)", "waitForText(Animation complete)"],
66
+ ],
67
+ "calendar": [
68
+ ["tap(#navCalendarDemo)", "waitFor(#calendarScreen)", "tap(#datePicker)", "tap(#day15)", "tap(#confirmButton)"],
69
+ ],
70
+ "gesture": [
71
+ ["tap(#navGesturesDemo)", "waitFor(#gestureScreen)", "swipe('left')", "waitForText(Tab 2)"],
72
+ ],
73
+ "media": [
74
+ ["tap(#navMediaDemo)", "waitFor(#mediaScreen)", "tap(#pickImageButton)", "waitForText(Image selected)"],
75
+ ],
76
+ "signature": [
77
+ ["tap(#navSignatureDemo)", "waitFor(#signatureScreen)", "draw(#canvas)", "tap(#saveSignature)", "waitForText(Signature saved)"],
78
+ ],
79
+ "home": [
80
+ ["waitFor(#homeScreen)", "waitForText(Welcome)", "verify card list visible"],
81
+ ],
82
+ }
83
+
84
+ ERROR_TEMPLATES = {
85
+ "passed": (None, None),
86
+ "timedOut": [
87
+ ("actionTimeout 20000ms exceeded", "TIMEOUT_FLAKE", "action timed out waiting for element to appear, likely slow render"),
88
+ ("Timeout 30000ms exceeded while waiting for #element", "TIMEOUT_FLAKE", "element took longer than expected to become visible"),
89
+ ("Test timeout of 120000ms exceeded", "TIMEOUT_FLAKE", "full test timeout exceeded, possible memory pressure slow-down"),
90
+ ],
91
+ "failed": [
92
+ ("ExpectError: Expected 1, but received 0", "LOCATOR_FLAKE", "element not found in tree, race condition on navigation"),
93
+ ("ExpectError: Expected value to be truthy, but received false", "REAL_BUG", "UI state did not match expected value, possible regression"),
94
+ ("Error: element is not visible", "LOCATOR_FLAKE", "element found but not interactable, viewport calculation mismatch"),
95
+ ("Error: Method not found", "DEVICE_FLAKE", "device connection lost during action, RPC failure"),
96
+ ("Error: Could not verify foreground activity", "DEVICE_FLAKE", "ANR dialog or system overlay blocking the app"),
97
+ ("Error: fill failed - element detached from DOM", "LOCATOR_FLAKE", "stale element reference after navigation"),
98
+ ("ExpectError: Expected 'Profile updated' but received 'Error saving'", "REAL_BUG", "app returned error state instead of success"),
99
+ ],
100
+ }
101
+
102
+
103
+ def generate_last_actions(screen: str, result: str) -> list[str]:
104
+ templates = ACTION_TEMPLATES.get(screen, [["tap(#generic)", "waitFor(#generic)"]])
105
+ base = random.choice(templates)
106
+ if result == "passed":
107
+ return base
108
+ # For failures, return a prefix of actions (the last few before crash)
109
+ cut = random.randint(1, len(base))
110
+ return base[:cut]
111
+
112
+
113
+ def generate_run_at(base_date: datetime, day_offset: int) -> str:
114
+ day = base_date + timedelta(days=day_offset)
115
+ hour = random.randint(7, 22)
116
+ minute = random.randint(0, 59)
117
+ second = random.randint(0, 59)
118
+ return day.replace(hour=hour, minute=minute, second=second).isoformat()
119
+
120
+
121
+ def generate_failure_rate(screen: str, platform: str) -> float:
122
+ rates = {
123
+ "login": {"android": 0.15, "ios": 0.05},
124
+ "profile": {"android": 0.12, "ios": 0.04},
125
+ "form": {"android": 0.08, "ios": 0.03},
126
+ "list": {"android": 0.20, "ios": 0.06},
127
+ "alert": {"android": 0.10, "ios": 0.04},
128
+ "animation": {"android": 0.18, "ios": 0.07},
129
+ "calendar": {"android": 0.08, "ios": 0.03},
130
+ "gesture": {"android": 0.22, "ios": 0.08},
131
+ "media": {"android": 0.25, "ios": 0.10},
132
+ "signature": {"android": 0.15, "ios": 0.06},
133
+ "home": {"android": 0.05, "ios": 0.02},
134
+ }
135
+ return rates.get(screen, {}).get(platform, 0.10)
136
+
137
+
138
+ def seed_database(db_path: str, num_days: int = 30, runs_per_day: int = 5):
139
+ db = sqlite3.connect(db_path)
140
+ db.executescript("""
141
+ DROP TABLE IF EXISTS test_runs;
142
+ CREATE TABLE test_runs (
143
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
144
+ test_file TEXT,
145
+ test_name TEXT,
146
+ platform TEXT,
147
+ device_id TEXT,
148
+ duration_ms INTEGER,
149
+ result TEXT,
150
+ error_message TEXT,
151
+ last_actions TEXT,
152
+ classification TEXT,
153
+ classification_reason TEXT,
154
+ screen_name TEXT,
155
+ action_type TEXT,
156
+ session_id TEXT,
157
+ run_at TEXT
158
+ );
159
+ CREATE INDEX IF NOT EXISTS idx_run_at ON test_runs(run_at);
160
+ CREATE INDEX IF NOT EXISTS idx_screen ON test_runs(screen_name);
161
+ CREATE INDEX IF NOT EXISTS idx_result ON test_runs(result);
162
+ CREATE INDEX IF NOT EXISTS idx_platform ON test_runs(platform);
163
+ """)
164
+ cursor = db.cursor()
165
+
166
+ base_date = datetime.now() - timedelta(days=num_days)
167
+ total = 0
168
+ passed = 0
169
+ failed = 0
170
+
171
+ for day in range(num_days):
172
+ # run all test files on each day, some multiple times
173
+ for test_file, test_name, screen in TESTS:
174
+ for platform_id in [0, 1]: # 0=android, 1=ios
175
+ device_id, platform = DEVICES[platform_id]
176
+ failure_rate = generate_failure_rate(screen, platform)
177
+
178
+ for run_num in range(runs_per_day):
179
+ # Determine result
180
+ is_flake = random.random() < failure_rate
181
+
182
+ if is_flake:
183
+ # For flakes, sometimes retry succeeds
184
+ is_retry = random.random() < 0.4
185
+ if is_retry:
186
+ # retry passed after initial failure
187
+ result = "passed"
188
+ error_msg = None
189
+ classification = None
190
+ classification_reason = None
191
+ passed += 1
192
+ else:
193
+ result = random.choice(["timedOut", "failed"])
194
+ failed += 1
195
+ else:
196
+ result = "passed"
197
+ error_msg = None
198
+ classification = None
199
+ classification_reason = None
200
+ passed += 1
201
+
202
+ # Determine duration
203
+ if result == "timedOut":
204
+ duration_ms = random.randint(60000, 180000)
205
+ elif result == "failed":
206
+ duration_ms = random.randint(10000, 55000)
207
+ else:
208
+ duration_ms = random.randint(3000, 25000)
209
+
210
+ # Error message and classification
211
+ if result == "passed":
212
+ error_msg = ""
213
+ classification = None
214
+ classification_reason = None
215
+ last_actions = generate_last_actions(screen, "passed")
216
+ else:
217
+ error_opts = ERROR_TEMPLATES[result]
218
+ error_msg, classification, classification_reason = random.choice(error_opts)
219
+ last_actions = generate_last_actions(screen, "failed")
220
+
221
+ # Determine action_type from last actions
222
+ action_type = "other"
223
+ for action in reversed(last_actions):
224
+ al = action.lower()
225
+ if "fill" in al: action_type = "fill"; break
226
+ if "tap" in al or "click" in al: action_type = "tap"; break
227
+ if "swipe" in al or "scroll" in al: action_type = "swipe"; break
228
+ if "scrollintoview" in al: action_type = "scrollIntoView"; break
229
+ if "press" in al: action_type = "press"; break
230
+
231
+ cursor.execute(
232
+ """INSERT INTO test_runs
233
+ (test_file, test_name, platform, device_id, duration_ms, result,
234
+ error_message, last_actions, classification, classification_reason,
235
+ screen_name, action_type, session_id, run_at)
236
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
237
+ (
238
+ test_file, test_name, platform, device_id,
239
+ duration_ms, result, error_msg,
240
+ json.dumps(last_actions), classification,
241
+ classification_reason, screen, action_type,
242
+ f"{device_id}_{day}",
243
+ generate_run_at(base_date, day),
244
+ ),
245
+ )
246
+ total += 1
247
+
248
+ db.commit()
249
+ db.close()
250
+ print(f"Seeded {total} test runs ({passed} passed, {failed} failed)")
251
+ print(f"Overall pass rate: {passed/total*100:.1f}%")
252
+
253
+
254
+ if __name__ == "__main__":
255
+ path = sys.argv[1] if len(sys.argv) > 1 else DB_PATH
256
+ seed_database(path)