synapse-hub 2.0.0 → 2.1.0

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.
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
2
3
  """
3
4
  Enterprise Flaky Test Detector & Quarantine Sentinel
4
5
  ---------------------------------------------------
5
6
  Executes a target test scenario N times (default: 5) or parses historical JUnit XML
6
- reports to calculate the Flakiness Index, detect state flips, and recommend quarantine isolation.
7
+ reports to calculate the Flakiness Index, detect state flips, query persistent SQLite telemetry,
8
+ and automatically isolate unstable tests via source-code @quarantine annotation.
7
9
 
8
- Usage:
9
- python tools/flaky_detector.py --repeat 5 --cmd "python -m unittest tests/test_tools.py"
10
- python tools/flaky_detector.py --reports "reports/**/junit*.xml"
10
+ Features:
11
+ 1. Multi-Run Repeat Execution & Live State-Flip Detection
12
+ 2. Multi-Day Historical Telemetry Query from memory_cache.db
13
+ 3. Automatic Source Code Isolation (--isolate) across Groovy, TS, Python test scripts
14
+ 4. Quarantine Board JSON & Allure/CI metadata export
11
15
  """
12
16
 
13
17
  import os
18
+ import re
14
19
  import sys
15
20
  import glob
16
21
  import json
@@ -19,6 +24,7 @@ import argparse
19
24
  import subprocess
20
25
  import xml.etree.ElementTree as ET
21
26
  from datetime import datetime
27
+ from typing import Dict, Any, List, Optional, Tuple
22
28
 
23
29
  # Enforce UTF-8 stdout encoding for Windows console compatibility
24
30
  if hasattr(sys.stdout, 'reconfigure'):
@@ -28,9 +34,19 @@ if hasattr(sys.stdout, 'reconfigure'):
28
34
  except Exception:
29
35
  pass
30
36
 
31
- REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
37
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
38
+ REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
32
39
  REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
33
40
 
41
+ if SCRIPT_DIR not in sys.path:
42
+ sys.path.insert(0, SCRIPT_DIR)
43
+
44
+ try:
45
+ from cache_resolver import record_test_telemetry, get_flaky_telemetry
46
+ except ImportError:
47
+ record_test_telemetry = None
48
+ get_flaky_telemetry = None
49
+
34
50
  GREEN = "\033[92m"
35
51
  RED = "\033[91m"
36
52
  YELLOW = "\033[93m"
@@ -39,7 +55,7 @@ BOLD = "\033[1m"
39
55
  RESET = "\033[0m"
40
56
 
41
57
 
42
- def run_command(cmd, timeout=300):
58
+ def run_command(cmd: str, timeout: int = 300) -> Tuple[int, str, str, float]:
43
59
  """Executes a shell command and returns (returncode, stdout, stderr, elapsed_ms)."""
44
60
  start = time.time()
45
61
  try:
@@ -58,7 +74,7 @@ def run_command(cmd, timeout=300):
58
74
  return -1, "", "Execution timed out", elapsed_ms
59
75
 
60
76
 
61
- def calculate_flakiness(runs):
77
+ def calculate_flakiness(runs: List[bool]) -> Dict[str, Any]:
62
78
  """
63
79
  Given a list of boolean statuses [True, False, True, ...],
64
80
  calculates pass rate, flip count, and flakiness tier.
@@ -71,6 +87,7 @@ def calculate_flakiness(runs):
71
87
  "failures": 0,
72
88
  "pass_rate": 0.0,
73
89
  "flips": 0,
90
+ "flaky_ratio": 0.0,
74
91
  "status": "NO_DATA",
75
92
  "quarantine": False
76
93
  }
@@ -85,6 +102,8 @@ def calculate_flakiness(runs):
85
102
  if runs[i] != runs[i + 1]:
86
103
  flips += 1
87
104
 
105
+ flaky_ratio = round((flips / max(1, total - 1)), 3) if total > 1 else (0.0 if failures == 0 else 1.0)
106
+
88
107
  if pass_rate == 100.0:
89
108
  status = "STABLE"
90
109
  quarantine = False
@@ -93,7 +112,7 @@ def calculate_flakiness(runs):
93
112
  quarantine = False
94
113
  elif pass_rate >= 80.0:
95
114
  status = "MILD_FLAKE"
96
- quarantine = (flips >= 2)
115
+ quarantine = (flips >= 2 or flaky_ratio >= 0.15)
97
116
  else:
98
117
  status = "SEVERE_FLAKE"
99
118
  quarantine = True
@@ -104,14 +123,15 @@ def calculate_flakiness(runs):
104
123
  "failures": failures,
105
124
  "pass_rate": pass_rate,
106
125
  "flips": flips,
126
+ "flaky_ratio": flaky_ratio,
107
127
  "status": status,
108
128
  "quarantine": quarantine
109
129
  }
110
130
 
111
131
 
112
- def parse_junit_reports(glob_pattern):
132
+ def parse_junit_reports(glob_pattern: str) -> Dict[str, List[bool]]:
113
133
  """Parses JUnit XML report files and aggregates pass/fail counts by test case."""
114
- test_results = {}
134
+ test_results: Dict[str, List[bool]] = {}
115
135
  files = glob.glob(glob_pattern, recursive=True)
116
136
  if not files:
117
137
  print(f"⚠️ No JUnit XML reports found matching: {glob_pattern}")
@@ -134,12 +154,14 @@ def parse_junit_reports(glob_pattern):
134
154
  return test_results
135
155
 
136
156
 
137
- def benchmark_live_command(cmd, iterations=5):
157
+ def benchmark_live_command(cmd: str, iterations: int = 5, test_id: Optional[str] = None) -> Dict[str, Any]:
138
158
  """Executes a live test command N times to observe flakiness in real time."""
159
+ effective_id = test_id or f"CMD_{abs(hash(cmd)) % 100000}"
139
160
  print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
140
161
  print(f"{BOLD}{CYAN} ENTERPRISE FLAKY TEST SENTINEL (Multi-Run Verification) {RESET}")
141
162
  print(f"{BOLD}{CYAN}======================================================================{RESET}")
142
163
  print(f" Target Command : {cmd}")
164
+ print(f" Test Identifier : {effective_id}")
143
165
  print(f" Repetition Count : {iterations} consecutive iterations")
144
166
  print(f" Timestamp : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
145
167
 
@@ -152,10 +174,26 @@ def benchmark_live_command(cmd, iterations=5):
152
174
  is_pass = (code == 0)
153
175
  runs.append(is_pass)
154
176
  latencies.append(ms)
177
+
178
+ # Record to SQLite historical telemetry
179
+ if record_test_telemetry:
180
+ try:
181
+ record_test_telemetry(
182
+ test_id=effective_id,
183
+ status="PASS" if is_pass else "FAIL",
184
+ is_retry=1 if i > 1 else 0,
185
+ duration_ms=ms,
186
+ error_signature="NON_ZERO_EXIT" if not is_pass else "",
187
+ context=cmd
188
+ )
189
+ except Exception:
190
+ pass
191
+
155
192
  status_str = f"{GREEN}PASS{RESET}" if is_pass else f"{RED}FAIL (Exit {code}){RESET}"
156
193
  print(f"{status_str} ({ms} ms)")
157
194
 
158
195
  stats = calculate_flakiness(runs)
196
+ stats["test_id"] = effective_id
159
197
  stats["latencies_ms"] = latencies
160
198
  stats["avg_latency_ms"] = round(sum(latencies) / max(1, len(latencies)), 2)
161
199
  stats["command"] = cmd
@@ -166,9 +204,10 @@ def benchmark_live_command(cmd, iterations=5):
166
204
  print(f"{BOLD}======================================================================{RESET}")
167
205
  print(f" Pass Rate : {stats['pass_rate']}% ({stats['passes']}/{stats['total']} passes)")
168
206
  print(f" Status Flips : {stats['flips']} transitions")
207
+ print(f" Flaky Ratio : {stats['flaky_ratio'] * 100:.1f}%")
169
208
  print(f" Average Runtime : {stats['avg_latency_ms']} ms")
170
209
  print(f" Stability Status : {stats['status']}")
171
-
210
+
172
211
  if stats["quarantine"]:
173
212
  print(f" Recommendation : {RED}{BOLD}[QUARANTINE REQUIRED]{RESET} Tag with @quarantine immediately!")
174
213
  else:
@@ -185,22 +224,146 @@ def benchmark_live_command(cmd, iterations=5):
185
224
  return stats
186
225
 
187
226
 
227
+ def auto_quarantine_test_file(test_id: str, target_dir: str = ".") -> bool:
228
+ """
229
+ Searches repository test files and automatically annotates the flaky test method
230
+ with @quarantine or @tag("quarantine") or test.fixme().
231
+ """
232
+ clean_id = test_id.split("#")[-1].strip()
233
+ annotated = False
234
+
235
+ for root, _, files in os.walk(target_dir):
236
+ # Skip node_modules and dot folders
237
+ if any(p in root for p in ("node_modules", ".git", "__pycache__", "reports")):
238
+ continue
239
+
240
+ for f in files:
241
+ ext = os.path.splitext(f)[1].lower()
242
+ if ext not in (".groovy", ".ts", ".js", ".py"):
243
+ continue
244
+
245
+ file_path = os.path.join(root, f)
246
+ try:
247
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as fp:
248
+ content = fp.read()
249
+
250
+ # If test_id matches file or method inside file
251
+ if clean_id in content or f in test_id:
252
+ lines = content.splitlines(keepends=True)
253
+ new_lines = []
254
+ file_modified = False
255
+
256
+ for idx, line in enumerate(lines):
257
+ # Detect test declaration
258
+ if clean_id in line and not line.strip().startswith("//"):
259
+ # Check if already quarantined
260
+ if idx > 0 and "@quarantine" in lines[idx - 1]:
261
+ continue
262
+
263
+ # Inject quarantine tag based on language
264
+ indent = line[:len(line) - len(line.lstrip())]
265
+ if ext == ".groovy":
266
+ tag = f"{indent}// @quarantine - Auto-quarantined due to flakiness > 15%\n{indent}@Tag('quarantine')\n"
267
+ elif ext in (".ts", ".js"):
268
+ tag = f"{indent}// @quarantine - Auto-quarantined due to flakiness > 15%\n"
269
+ if "test(" in line:
270
+ line = line.replace("test(", "test.fixme(")
271
+ elif ext == ".py":
272
+ tag = f"{indent}@pytest.mark.quarantine\n"
273
+ else:
274
+ tag = f"{indent}// @quarantine\n"
275
+
276
+ new_lines.append(tag)
277
+ new_lines.append(line)
278
+ file_modified = True
279
+ annotated = True
280
+ else:
281
+ new_lines.append(line)
282
+
283
+ if file_modified:
284
+ with open(file_path, "w", encoding="utf-8") as fp:
285
+ fp.writelines(new_lines)
286
+ print(f"🔒 [QUARANTINE APPLIED] Injected quarantine annotation into: {file_path}")
287
+ except Exception as e:
288
+ print(f"⚠️ Could not patch {file_path}: {e}")
289
+
290
+ return annotated
291
+
292
+
188
293
  def main():
189
294
  parser = argparse.ArgumentParser(description="Enterprise Flaky Test Detector & Quarantine Sentinel")
190
295
  parser.add_argument("--cmd", "-c", default=None, help="Test command to execute repeatedly")
191
296
  parser.add_argument("--repeat", "-r", type=int, default=5, help="Number of repetitions (default: 5)")
297
+ parser.add_argument("--test-id", default=None, help="Custom identifier for the test scenario")
192
298
  parser.add_argument("--reports", default=None, help="Glob pattern for JUnit XML reports to analyze")
299
+ parser.add_argument("--history", action="store_true", help="Query 30-day historical flakiness from SQLite telemetry")
300
+ parser.add_argument("--days", type=int, default=30, help="Lookback window in days (default: 30)")
301
+ parser.add_argument("--threshold", type=float, default=0.15, help="Flaky ratio quarantine threshold (default: 0.15)")
302
+ parser.add_argument("--isolate", action="store_true", help="Automatically inject @quarantine tag into source files")
303
+ parser.add_argument("--target-dir", default=".", help="Root directory of test files to patch")
304
+ parser.add_argument("--json", action="store_true", help="Output results in JSON format")
305
+
193
306
  args = parser.parse_args()
194
307
 
195
- if args.cmd:
196
- stats = benchmark_live_command(args.cmd, iterations=args.repeat)
308
+ if args.history:
309
+ if not get_flaky_telemetry:
310
+ print("❌ SQLite cache resolver not available.")
311
+ sys.exit(1)
312
+
313
+ candidates = get_flaky_telemetry(days=args.days, threshold=args.threshold)
314
+ if args.json:
315
+ print(json.dumps(candidates, indent=2))
316
+ else:
317
+ print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
318
+ print(f"{BOLD}{CYAN} HISTORICAL FLAKINESS TELEMETRY ({args.days}-DAY WINDOW) {RESET}")
319
+ print(f"{BOLD}{CYAN}======================================================================{RESET}")
320
+ print(f" Quarantine Threshold : {args.threshold * 100:.1f}% flaky ratio")
321
+ print(f" Total Candidates : {len(candidates)}\n")
322
+
323
+ quarantined_count = 0
324
+ for c in candidates:
325
+ is_q = c["quarantine_recommended"]
326
+ tag = f"{RED}[QUARANTINE]{RESET}" if is_q else f"{GREEN}[STABLE]{RESET}"
327
+ print(f" • {tag} {BOLD}{c['test_id']}{RESET}")
328
+ print(f" Runs: {c['total_runs']} | Passes: {c['pass_count']} | Fails: {c['fail_count']} | Retry Passes: {c['retry_pass_count']}")
329
+ print(f" Flaky Ratio: {c['flaky_ratio'] * 100:.1f}% | Avg Latency: {c['avg_duration_ms']} ms\n")
330
+
331
+ if is_q:
332
+ quarantined_count += 1
333
+ if args.isolate:
334
+ auto_quarantine_test_file(c["test_id"], target_dir=args.target_dir)
335
+
336
+ print(f"======================================================================")
337
+ print(f"Quarantine Recommendations: {quarantined_count}/{len(candidates)}")
338
+ sys.exit(0)
339
+
340
+ elif args.cmd:
341
+ stats = benchmark_live_command(args.cmd, iterations=args.repeat, test_id=args.test_id)
342
+ if stats["quarantine"] and args.isolate:
343
+ auto_quarantine_test_file(stats["test_id"], target_dir=args.target_dir)
197
344
  sys.exit(1 if stats["quarantine"] else 0)
345
+
198
346
  elif args.reports:
199
347
  results = parse_junit_reports(args.reports)
200
348
  quarantine_candidates = []
201
349
  for test_id, runs in results.items():
202
350
  st = calculate_flakiness(runs)
203
351
  st["test_id"] = test_id
352
+
353
+ # Ingest into SQLite telemetry
354
+ if record_test_telemetry:
355
+ for idx, is_p in enumerate(runs):
356
+ try:
357
+ record_test_telemetry(
358
+ test_id=test_id,
359
+ status="PASS" if is_p else "FAIL",
360
+ is_retry=1 if idx > 0 else 0,
361
+ duration_ms=0.0,
362
+ context=args.reports
363
+ )
364
+ except Exception:
365
+ pass
366
+
204
367
  if st["quarantine"]:
205
368
  quarantine_candidates.append(st)
206
369
 
@@ -208,11 +371,14 @@ def main():
208
371
  print(f"Quarantine Candidates Detected: {len(quarantine_candidates)}")
209
372
  for qc in quarantine_candidates:
210
373
  print(f" - {RED}{qc['test_id']}{RESET}: Pass rate {qc['pass_rate']}%, Flips: {qc['flips']}")
374
+ if args.isolate:
375
+ auto_quarantine_test_file(qc["test_id"], target_dir=args.target_dir)
211
376
 
212
377
  report_file = os.path.join(REPORTS_DIR, "quarantine_board.json")
213
378
  with open(report_file, "w", encoding="utf-8") as fp:
214
379
  json.dump(quarantine_candidates, fp, indent=2)
215
380
  sys.exit(1 if quarantine_candidates else 0)
381
+
216
382
  else:
217
383
  parser.print_help()
218
384
  sys.exit(0)