intermittents_mach_commands.py (5006B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import json 6 import logging 7 import sys 8 9 from intermittent_failures import IntermittentFailuresFetcher 10 from mach.decorators import Command, CommandArgument, SubCommand 11 12 13 @Command( 14 "intermittents", 15 category="testing", 16 description="Analyze intermittent test failures", 17 ) 18 def intermittents(command_context): 19 """ 20 Utility to analyze intermittent test failures in Firefox. 21 """ 22 # Print help text when no subcommand is provided 23 print("usage: mach intermittents <subcommand> [options]") 24 print() 25 print("Analyze intermittent test failures in Firefox.") 26 print() 27 print("subcommands:") 28 print(" list List the most frequent intermittent test failures") 29 print() 30 print("Run 'mach intermittents <subcommand> --help' for more information.") 31 sys.exit(0) 32 33 34 @SubCommand( 35 "intermittents", 36 "list", 37 description="List the most frequent intermittent test failures", 38 ) 39 @CommandArgument( 40 "--days", 41 type=int, 42 default=7, 43 help="Number of days to look back for failures (default: 7)", 44 ) 45 @CommandArgument( 46 "--threshold", 47 type=int, 48 default=30, 49 help="Minimum number of failures to include (default: 30)", 50 ) 51 @CommandArgument( 52 "--branch", 53 default="trunk", 54 help="Branch to query (default: trunk)", 55 ) 56 @CommandArgument( 57 "--json", 58 action="store_true", 59 dest="json_output", 60 help="Output results as JSON", 61 ) 62 @CommandArgument( 63 "--verbose", 64 action="store_true", 65 help="Show additional details for each failure", 66 ) 67 @CommandArgument( 68 "--all", 69 action="store_true", 70 help="Show all bugs (by default only single tracking bugs with test paths are shown)", 71 ) 72 def list_intermittents( 73 command_context, 74 days=7, 75 threshold=30, 76 branch="trunk", 77 json_output=False, 78 verbose=False, 79 all=False, 80 ): 81 """List the most frequent intermittent test failures""" 82 83 # Logging setup 84 if not json_output: 85 command_context.log( 86 logging.INFO, 87 "intermittents", 88 {}, 89 f"Fetching intermittent failures from the last {days} days with at least {threshold} occurrences...", 90 ) 91 92 fetcher = IntermittentFailuresFetcher( 93 days=days, threshold=threshold, verbose=verbose and not json_output 94 ) 95 96 try: 97 results = fetcher.get_failures(branch=branch) 98 except Exception as e: 99 command_context.log( 100 logging.ERROR, 101 "intermittents", 102 {"error": str(e)}, 103 "Error fetching failures: {error}", 104 ) 105 return 1 106 107 if not all: 108 results = [ 109 result 110 for result in results 111 if result.get("test_path") and "single tracking bug" in result["summary"] 112 ] 113 114 if not results: 115 if not json_output: 116 message = f"No bugs found with at least {threshold} failures in the last {days} days." 117 if not all: 118 message = f"No single tracking bugs with test paths found with at least {threshold} failures in the last {days} days. Use --all to see all bugs." 119 command_context.log( 120 logging.INFO, 121 "intermittents", 122 {}, 123 message, 124 ) 125 else: 126 print(json.dumps([])) 127 return 0 128 129 results.sort(key=lambda x: x["failure_count"], reverse=True) 130 131 if json_output: 132 print(json.dumps(results, indent=2)) 133 else: 134 command_context.log( 135 logging.INFO, 136 "intermittents", 137 {"count": len(results), "threshold": threshold}, 138 "Found {count} bugs with at least {threshold} failures:", 139 ) 140 print() 141 142 for i, result in enumerate(results, 1): 143 print(f"{i}. Bug {result['bug_id']}: {result['failure_count']} failures") 144 if result.get("test_path"): 145 print(f" Test: {result['test_path']}") 146 print(f" Summary: {result['summary']}") 147 print(f" Status: {result['status']}", end="") 148 if result.get("resolution"): 149 print(f" - {result['resolution']}") 150 else: 151 print() 152 if result.get("creation_time"): 153 created = result["creation_time"].split("T")[0] # Just the date part 154 print(f" Created: {created}") 155 if result.get("last_change_time"): 156 updated = result["last_change_time"].split("T")[0] # Just the date part 157 print(f" Last updated: {updated}") 158 if result.get("comment_count") is not None: 159 print(f" Comments: {result['comment_count']}") 160 print( 161 f" URL: https://bugzilla.mozilla.org/show_bug.cgi?id={result['bug_id']}" 162 ) 163 print() 164 165 return 0