# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import json import logging import sys from intermittent_failures import IntermittentFailuresFetcher from mach.decorators import Command, CommandArgument, SubCommand @Command( "intermittents", category="testing", description="Analyze intermittent test failures", ) def intermittents(command_context): """ Utility to analyze intermittent test failures in Firefox. """ # Print help text when no subcommand is provided print("usage: mach intermittents [options]") print() print("Analyze intermittent test failures in Firefox.") print() print("subcommands:") print(" list List the most frequent intermittent test failures") print() print("Run 'mach intermittents --help' for more information.") sys.exit(0) @SubCommand( "intermittents", "list", description="List the most frequent intermittent test failures", ) @CommandArgument( "--days", type=int, default=7, help="Number of days to look back for failures (default: 7)", ) @CommandArgument( "--threshold", type=int, default=30, help="Minimum number of failures to include (default: 30)", ) @CommandArgument( "--branch", default="trunk", help="Branch to query (default: trunk)", ) @CommandArgument( "--json", action="store_true", dest="json_output", help="Output results as JSON", ) @CommandArgument( "--verbose", action="store_true", help="Show additional details for each failure", ) @CommandArgument( "--all", action="store_true", help="Show all bugs (by default only single tracking bugs with test paths are shown)", ) def list_intermittents( command_context, days=7, threshold=30, branch="trunk", json_output=False, verbose=False, all=False, ): """List the most frequent intermittent test failures""" # Logging setup if not json_output: command_context.log( logging.INFO, "intermittents", {}, f"Fetching intermittent failures from the last {days} days with at least {threshold} occurrences...", ) fetcher = IntermittentFailuresFetcher( days=days, threshold=threshold, verbose=verbose and not json_output ) try: results = fetcher.get_failures(branch=branch) except Exception as e: command_context.log( logging.ERROR, "intermittents", {"error": str(e)}, "Error fetching failures: {error}", ) return 1 if not all: results = [ result for result in results if result.get("test_path") and "single tracking bug" in result["summary"] ] if not results: if not json_output: message = f"No bugs found with at least {threshold} failures in the last {days} days." if not all: 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." command_context.log( logging.INFO, "intermittents", {}, message, ) else: print(json.dumps([])) return 0 results.sort(key=lambda x: x["failure_count"], reverse=True) if json_output: print(json.dumps(results, indent=2)) else: command_context.log( logging.INFO, "intermittents", {"count": len(results), "threshold": threshold}, "Found {count} bugs with at least {threshold} failures:", ) print() for i, result in enumerate(results, 1): print(f"{i}. Bug {result['bug_id']}: {result['failure_count']} failures") if result.get("test_path"): print(f" Test: {result['test_path']}") print(f" Summary: {result['summary']}") print(f" Status: {result['status']}", end="") if result.get("resolution"): print(f" - {result['resolution']}") else: print() if result.get("creation_time"): created = result["creation_time"].split("T")[0] # Just the date part print(f" Created: {created}") if result.get("last_change_time"): updated = result["last_change_time"].split("T")[0] # Just the date part print(f" Last updated: {updated}") if result.get("comment_count") is not None: print(f" Comments: {result['comment_count']}") print( f" URL: https://bugzilla.mozilla.org/show_bug.cgi?id={result['bug_id']}" ) print() return 0