import re from collections import defaultdict def evaluate_feature_retries(log_file, max_retries=3): """ Evaluate feature retries based on Behave's debug log. Rules: - A feature is executed multiple times by BrowserStack test orchestration. - Every "END with status=" event is treated as one run. - A feature fails the build only if: * It has failed >= max_retries times, AND * The final attempt also failed ("still failing"). """ # Match EXACT Behave timestamp pattern: 2025-12-01 19:30:12,887 timestamp_re = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}") # Capture lines like: # [after_feature] END Android Basic Flow with status=Status.failed after_feature_re = re.compile( r"\[after_feature\]\s+END\s+(.*?)\s+with status=Status\.(\w+)", re.IGNORECASE ) feature_runs = defaultdict(lambda: { "runs": [], "attempts": 0, "final_result": None }) try: with open(log_file, "r") as f: for raw_line in f: line = raw_line.strip() # ❗ Skip BrowserStack SDK injected lines (no timestamp) if not timestamp_re.match(line): continue # Parse after_feature result m = after_feature_re.search(line) if m: feature_name = m.group(1).strip() result = m.group(2).lower() # passed / failed feature_runs[feature_name]["runs"].append(result) feature_runs[feature_name]["attempts"] += 1 except FileNotFoundError: # Return build not OK and empty summary if log file is missing return False, {} # Prepare FINAL summary summary = {} build_ok = True for feature, data in feature_runs.items(): runs = data["runs"] # last status determines final_result final_status = runs[-1] if runs else "unknown" data["final_result"] = final_status data["max_retries"] = max_retries # If the feature was retried max_retries times and STILL failing failed_attempts = runs.count("failed") + runs.count("error") if (failed_attempts >= max_retries and final_status in ("failed", "error")): build_ok = False summary[feature] = data return build_ok, summary