| # Copyright (c) 2009 Google Inc. All rights reserved. |
| # Copyright (c) 2009 Apple Inc. All rights reserved. |
| # Copyright (c) 2012 Intel Corporation. All rights reserved. |
| # Copyright (c) 2013 University of Szeged. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| from __future__ import print_function |
| import fnmatch |
| import logging |
| import re |
| |
| from datetime import datetime |
| from optparse import make_option |
| |
| from webkitpy.tool import steps |
| |
| from webkitpy.common.checkout.commitinfo import CommitInfo |
| from webkitpy.common.config.committers import CommitterList |
| import webkitpy.common.config.urls as config_urls |
| from webkitpy.common.net.buildbot import BuildBot |
| from webkitpy.common.net.bugzilla import Bugzilla |
| from webkitpy.common.net.regressionwindow import RegressionWindow |
| from webkitpy.common.system.crashlogs import CrashLogs |
| from webkitpy.common.system.user import User |
| from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand |
| from webkitpy.tool.grammar import pluralize |
| from webkitpy.tool.multicommandtool import Command |
| from webkitpy.layout_tests.models.test_expectations import TestExpectations |
| from webkitpy.port import platform_options, configuration_options |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class SuggestReviewers(AbstractSequencedCommand): |
| name = "suggest-reviewers" |
| help_text = "Suggest reviewers for a patch based on recent changes to the modified files." |
| steps = [ |
| steps.SuggestReviewers, |
| ] |
| |
| def _prepare_state(self, options, args, tool): |
| options.suggest_reviewers = True |
| |
| |
| class BugsToCommit(Command): |
| name = "bugs-to-commit" |
| help_text = "List bugs in the commit-queue" |
| |
| def execute(self, options, args, tool): |
| # FIXME: This command is poorly named. It's fetching the commit-queue list here. The name implies it's fetching pending-commit (all r+'d patches). |
| bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue() |
| for bug_id in bug_ids: |
| print("%s" % bug_id) |
| |
| |
| class PatchesInCommitQueue(Command): |
| name = "patches-in-commit-queue" |
| help_text = "List patches in the commit-queue" |
| |
| def execute(self, options, args, tool): |
| patches = tool.bugs.queries.fetch_patches_from_commit_queue() |
| _log.info("Patches in commit queue:") |
| for patch in patches: |
| print(patch.url()) |
| |
| |
| class PatchesToCommitQueue(Command): |
| name = "patches-to-commit-queue" |
| help_text = "Patches which should be added to the commit queue" |
| |
| def __init__(self): |
| options = [ |
| make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"), |
| ] |
| Command.__init__(self, options=options) |
| |
| @staticmethod |
| def _needs_commit_queue(patch): |
| if patch.commit_queue() == "+": # If it's already cq+, ignore the patch. |
| _log.info("%s already has cq=%s" % (patch.id(), patch.commit_queue())) |
| return False |
| |
| # We only need to worry about patches from contributers who are not yet committers. |
| committer_record = CommitterList().committer_by_email(patch.attacher_email()) |
| if committer_record: |
| _log.info("%s committer = %s" % (patch.id(), committer_record)) |
| return not committer_record |
| |
| def execute(self, options, args, tool): |
| patches = tool.bugs.queries.fetch_patches_from_pending_commit_list() |
| patches_needing_cq = filter(self._needs_commit_queue, patches) |
| if options.bugs: |
| bugs_needing_cq = map(lambda patch: patch.bug_id(), patches_needing_cq) |
| bugs_needing_cq = sorted(set(bugs_needing_cq)) |
| for bug_id in bugs_needing_cq: |
| print("%s" % tool.bugs.bug_url_for_bug_id(bug_id)) |
| else: |
| for patch in patches_needing_cq: |
| print("%s" % tool.bugs.attachment_url_for_id(patch.id(), action="edit")) |
| |
| |
| class PatchesToReview(Command): |
| name = "patches-to-review" |
| help_text = "List bugs which have attachments pending review" |
| |
| def __init__(self): |
| options = [ |
| make_option("--all", action="store_true", |
| help="Show all bugs regardless of who is on CC (it might take a while)"), |
| make_option("--include-cq-denied", action="store_true", |
| help="By default, r? patches with cq- are omitted unless this option is set"), |
| make_option("--cc-email", |
| help="Specifies the email on the CC field (defaults to your bugzilla login email)"), |
| ] |
| Command.__init__(self, options=options) |
| |
| def _print_report(self, report, cc_email, print_all): |
| if print_all: |
| print("Bugs with attachments pending review:") |
| else: |
| print("Bugs with attachments pending review that has %s in the CC list:" % cc_email) |
| |
| print("http://webkit.org/b/bugid Description (age in days)") |
| for row in report: |
| print("%s (%d)" % (row[1], row[0])) |
| |
| print("Total: %d" % len(report)) |
| |
| def _generate_report(self, bugs, include_cq_denied): |
| report = [] |
| |
| for bug in bugs: |
| patch = bug.unreviewed_patches()[-1] |
| |
| if not include_cq_denied and patch.commit_queue() == "-": |
| continue |
| |
| age_in_days = (datetime.today() - patch.attach_date()).days |
| report.append((age_in_days, "http://webkit.org/b/%-7s %s" % (bug.id(), bug.title()))) |
| |
| report.sort() |
| return report |
| |
| def execute(self, options, args, tool): |
| tool.bugs.authenticate() |
| |
| cc_email = options.cc_email |
| if not cc_email and not options.all: |
| cc_email = tool.bugs.username |
| |
| bugs = tool.bugs.queries.fetch_bugs_from_review_queue(cc_email=cc_email) |
| report = self._generate_report(bugs, options.include_cq_denied) |
| self._print_report(report, cc_email, options.all) |
| |
| |
| class WhatBroke(Command): |
| name = "what-broke" |
| help_text = "Print failing buildbots (%s) and what revisions broke them" % config_urls.buildbot_url |
| |
| def _print_builder_line(self, builder_name, max_name_width, status_message): |
| print("%s : %s" % (builder_name.ljust(max_name_width), status_message)) |
| |
| def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True): |
| builder = self._tool.buildbot.builder_with_name(builder_status["name"]) |
| red_build = builder.build(builder_status["build_number"]) |
| regression_window = builder.find_regression_window(red_build) |
| if not regression_window.failing_build(): |
| self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)") |
| return |
| if not regression_window.build_before_failure(): |
| self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % regression_window.failing_build().revision()) |
| return |
| |
| revisions = regression_window.revisions() |
| first_failure_message = "" |
| if (regression_window.failing_build() == builder.build(builder_status["build_number"])): |
| first_failure_message = " FIRST FAILURE, possibly a flaky test" |
| self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (revisions, first_failure_message)) |
| for revision in revisions: |
| commit_info = self._tool.checkout().commit_info_for_revision(revision) |
| if commit_info: |
| print(commit_info.blame_string(self._tool.bugs)) |
| else: |
| print("FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision) |
| |
| def execute(self, options, args, tool): |
| builder_statuses = tool.buildbot.builder_statuses() |
| longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses))) |
| failing_builders = 0 |
| for builder_status in builder_statuses: |
| # If the builder is green, print OK, exit. |
| if builder_status["is_green"]: |
| continue |
| self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name) |
| failing_builders += 1 |
| if failing_builders: |
| print("%s of %s are failing" % (failing_builders, pluralize(len(builder_statuses), "builder"))) |
| else: |
| print("All builders are passing!") |
| |
| |
| class ResultsFor(Command): |
| name = "results-for" |
| help_text = "Print a list of failures for the passed revision from bots on %s" % config_urls.buildbot_url |
| argument_names = "REVISION" |
| |
| def _print_layout_test_results(self, results): |
| if not results: |
| print(" No results.") |
| return |
| for title, files in results.parsed_results().items(): |
| print(" %s" % title) |
| for filename in files: |
| print(" %s" % filename) |
| |
| def execute(self, options, args, tool): |
| builders = self._tool.buildbot.builders() |
| for builder in builders: |
| print("%s:" % builder.name()) |
| build = builder.build_for_revision(args[0], allow_failed_lookups=True) |
| self._print_layout_test_results(build.layout_test_results()) |
| |
| |
| class FailureReason(Command): |
| name = "failure-reason" |
| help_text = "Lists revisions where individual test failures started at %s" % config_urls.buildbot_url |
| arguments_names = "[LAYOUT_TESTS]" |
| |
| def _blame_line_for_revision(self, revision): |
| try: |
| commit_info = self._tool.checkout().commit_info_for_revision(revision) |
| except Exception as e: |
| return "FAILED to fetch CommitInfo for r%s, exception: %s" % (revision, e) |
| if not commit_info: |
| return "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision |
| return commit_info.blame_string(self._tool.bugs) |
| |
| def _print_blame_information_for_transition(self, regression_window, failing_tests): |
| red_build = regression_window.failing_build() |
| print("SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests)) |
| print("Suspect revisions:") |
| for revision in regression_window.revisions(): |
| print(self._blame_line_for_revision(revision)) |
| |
| def _explain_failures_for_builder(self, builder, start_revision): |
| print("Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision)) |
| revision_to_test = start_revision |
| build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True) |
| layout_test_results = build.layout_test_results() |
| if not layout_test_results: |
| # FIXME: This could be made more user friendly. |
| print("Failed to load layout test results from %s; can't continue. (start revision = r%s)" % (build.results_url(), start_revision)) |
| return 1 |
| |
| results_to_explain = set(layout_test_results.failing_tests()) |
| last_build_with_results = build |
| print("Starting at %s" % revision_to_test) |
| while results_to_explain and not self._done_explaining(): |
| revision_to_test -= 1 |
| new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True) |
| if not new_build: |
| print("No build for %s" % revision_to_test) |
| continue |
| build = new_build |
| latest_results = build.layout_test_results() |
| if not latest_results: |
| print("No results build %s (r%s)" % (build._number, build.revision())) |
| continue |
| failures = set(latest_results.failing_tests()) |
| if len(failures) >= 500: |
| # FIXME: We may need to move this logic into the LayoutTestResults class. |
| # The buildbot stops runs after 500 failures so we don't have full results to work with here. |
| print("Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())) |
| continue |
| fixed_results = results_to_explain - failures |
| if not fixed_results: |
| print("No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures))) |
| last_build_with_results = build |
| continue |
| self.explained_failures.update(fixed_results) |
| regression_window = RegressionWindow(build, last_build_with_results) |
| self._print_blame_information_for_transition(regression_window, fixed_results) |
| last_build_with_results = build |
| results_to_explain -= fixed_results |
| if results_to_explain: |
| print("Failed to explain failures: %s" % results_to_explain) |
| return 1 |
| print("Explained all results for %s" % builder.name()) |
| return 0 |
| |
| def _builder_to_explain(self): |
| builder_statuses = self._tool.buildbot.builder_statuses() |
| red_statuses = [status for status in builder_statuses if not status["is_green"]] |
| print("%s failing" % (pluralize(len(red_statuses), "builder"))) |
| builder_choices = [status["name"] for status in red_statuses] |
| # We could offer an "All" choice here. |
| chosen_name = self._tool.user.prompt_with_list("Which builder to diagnose:", builder_choices) |
| # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object. |
| for status in red_statuses: |
| if status["name"] == chosen_name: |
| return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) |
| |
| def _done_explaining(self): |
| if not self.failures_to_explain: |
| return False |
| |
| return self.explained_failures.issuperset(self.failures_to_explain) |
| |
| def execute(self, options, args, tool): |
| (builder, latest_revision) = self._builder_to_explain() |
| start_revision = self._tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision |
| self.failures_to_explain = args |
| self.explained_failures = set() |
| if not start_revision: |
| print("Revision required.") |
| return 1 |
| return self._explain_failures_for_builder(builder, start_revision=int(start_revision)) |
| |
| |
| class FindFlakyTests(Command): |
| name = "find-flaky-tests" |
| help_text = "Lists tests that often fail for a single build at %s" % config_urls.buildbot_url |
| |
| def _find_failures(self, builder, revision): |
| build = builder.build_for_revision(revision, allow_failed_lookups=True) |
| if not build: |
| print("No build for %s" % revision) |
| return (None, None) |
| results = build.layout_test_results() |
| if not results: |
| print("No results build %s (r%s)" % (build._number, build.revision())) |
| return (None, None) |
| failures = set(results.failing_tests()) |
| if len(failures) >= 20: |
| # FIXME: We may need to move this logic into the LayoutTestResults class. |
| # The buildbot stops runs after 20 failures so we don't have full results to work with here. |
| print("Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())) |
| return (None, None) |
| return (build, failures) |
| |
| def _increment_statistics(self, flaky_tests, flaky_test_statistics): |
| for test in flaky_tests: |
| count = flaky_test_statistics.get(test, 0) |
| flaky_test_statistics[test] = count + 1 |
| |
| def _print_statistics(self, statistics): |
| print("=== Results ===") |
| print("Occurrences Test name") |
| for value, key in sorted([(value, key) for key, value in statistics.items()]): |
| print("%10d %s" % (value, key)) |
| |
| def _walk_backwards_from(self, builder, start_revision, limit): |
| flaky_test_statistics = {} |
| all_previous_failures = set([]) |
| one_time_previous_failures = set([]) |
| previous_build = None |
| for i in range(limit): |
| revision = start_revision - i |
| print("Analyzing %s ... " % revision, end=' ') |
| (build, failures) = self._find_failures(builder, revision) |
| if failures == None: |
| # Notice that we don't loop on the empty set! |
| continue |
| print("has %s failures" % len(failures)) |
| flaky_tests = one_time_previous_failures - failures |
| if flaky_tests: |
| print("Flaky tests: %s %s" % (sorted(flaky_tests), |
| previous_build.results_url())) |
| self._increment_statistics(flaky_tests, flaky_test_statistics) |
| one_time_previous_failures = failures - all_previous_failures |
| all_previous_failures = failures |
| previous_build = build |
| self._print_statistics(flaky_test_statistics) |
| |
| def _builder_to_analyze(self): |
| statuses = self._tool.buildbot.builder_statuses() |
| choices = [status["name"] for status in statuses] |
| chosen_name = self._tool.user.prompt_with_list("Which builder to analyze:", choices) |
| for status in statuses: |
| if status["name"] == chosen_name: |
| return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) |
| |
| def execute(self, options, args, tool): |
| (builder, latest_revision) = self._builder_to_analyze() |
| limit = self._tool.user.prompt("How many revisions to look through? [10000] ") or 10000 |
| return self._walk_backwards_from(builder, latest_revision, limit=int(limit)) |
| |
| |
| class TreeStatus(Command): |
| name = "tree-status" |
| help_text = "Print the status of the %s buildbots" % config_urls.buildbot_url |
| long_help = """Fetches build status from https://build.webkit.org/one_box_per_builder |
| and displayes the status of each builder.""" |
| |
| def execute(self, options, args, tool): |
| for builder in tool.buildbot.builder_statuses(): |
| status_string = "ok" if builder["is_green"] else "FAIL" |
| print("%s : %s" % (status_string.ljust(4), builder["name"])) |
| |
| |
| class CrashLog(Command): |
| name = "crash-log" |
| help_text = "Print the newest crash log for the given process" |
| long_help = """Finds the newest crash log matching the given process name |
| and PID and prints it to stdout.""" |
| argument_names = "PROCESS_NAME [PID]" |
| |
| def execute(self, options, args, tool): |
| default_port = tool.port_factory.get() |
| crash_logs = CrashLogs(tool, default_port.path_to_crash_logs()) |
| pid = None |
| if len(args) > 1: |
| pid = int(args[1]) |
| print(crash_logs.find_newest_log(args[0], pid)) |
| |
| |
| class PrintExpectations(Command): |
| name = 'print-expectations' |
| help_text = 'Print the expected result for the given test(s) on the given port(s)' |
| |
| def __init__(self): |
| options = [ |
| make_option('--all', action='store_true', default=False, |
| help='display the expectations for *all* tests'), |
| make_option('-x', '--exclude-keyword', action='append', default=[], |
| help='limit to tests not matching the given keyword (for example, "skip", "slow", or "crash". May specify multiple times'), |
| make_option('-i', '--include-keyword', action='append', default=[], |
| help='limit to tests with the given keyword (for example, "skip", "slow", or "crash". May specify multiple times'), |
| make_option('-f', '--full', action='store_true', default=False, |
| help='Print a full TestExpectations-style line for every match'), |
| make_option('--paths', action='store_true', default=False, |
| help='display the paths for all applicable expectation files'), |
| ] + platform_options(use_globs=True) |
| |
| Command.__init__(self, options=options) |
| self._expectation_models = {} |
| |
| def execute(self, options, args, tool): |
| if not options.paths and not args and not options.all: |
| print("You must either specify one or more test paths or --all.") |
| return |
| |
| if options.platform: |
| port_names = fnmatch.filter(tool.port_factory.all_port_names(), options.platform) |
| if not port_names: |
| default_port = tool.port_factory.get(options.platform) |
| if default_port: |
| port_names = [default_port.name()] |
| else: |
| print("No port names match '%s'" % options.platform) |
| return |
| else: |
| default_port = tool.port_factory.get(port_names[0]) |
| else: |
| default_port = tool.port_factory.get(options=options) |
| port_names = [default_port.name()] |
| |
| if options.paths: |
| files = default_port.expectations_files() |
| layout_tests_dir = default_port.layout_tests_dir() |
| for file in files: |
| if file.startswith(layout_tests_dir): |
| file = file.replace(layout_tests_dir, 'LayoutTests') |
| print(file) |
| return |
| |
| tests = set(default_port.tests(args)) |
| for port_name in port_names: |
| model = self._model(options, port_name, tests) |
| tests_to_print = self._filter_tests(options, model, tests) |
| lines = [model.get_expectation_line(test) for test in sorted(tests_to_print)] |
| if port_name != port_names[0]: |
| print() |
| print('\n'.join(self._format_lines(options, port_name, lines))) |
| |
| def _filter_tests(self, options, model, tests): |
| filtered_tests = set() |
| if options.include_keyword: |
| for keyword in options.include_keyword: |
| filtered_tests.update(model.get_test_set_for_keyword(keyword)) |
| else: |
| filtered_tests = tests |
| |
| for keyword in options.exclude_keyword: |
| filtered_tests.difference_update(model.get_test_set_for_keyword(keyword)) |
| return filtered_tests |
| |
| def _format_lines(self, options, port_name, lines): |
| output = [] |
| if lines: |
| include_modifiers = options.full |
| include_expectations = options.full or len(options.include_keyword) != 1 or len(options.exclude_keyword) |
| output.append("// For %s" % port_name) |
| for line in lines: |
| output.append("%s" % line.to_string(None, include_modifiers, include_expectations, include_comment=False)) |
| return output |
| |
| def _model(self, options, port_name, tests): |
| port = self._tool.port_factory.get(port_name, options) |
| expectations = TestExpectations(port, tests) |
| expectations.parse_all_expectations() |
| return expectations.model() |
| |
| |
| class PrintBaselines(Command): |
| name = 'print-baselines' |
| help_text = 'Prints the baseline locations for given test(s) on the given port(s)' |
| |
| def __init__(self): |
| options = [ |
| make_option('--all', action='store_true', default=False, |
| help='display the baselines for *all* tests'), |
| ] + platform_options(use_globs=True) |
| Command.__init__(self, options=options) |
| self._platform_regexp = re.compile('platform/([^\/]+)/(.+)') |
| |
| def execute(self, options, args, tool): |
| if not args and not options.all: |
| print("You must either specify one or more test paths or --all.") |
| return |
| |
| default_port = tool.port_factory.get() |
| if options.platform: |
| port_names = fnmatch.filter(tool.port_factory.all_port_names(), options.platform) |
| if not port_names: |
| print("No port names match '%s'" % options.platform) |
| else: |
| port_names = [default_port.name()] |
| |
| # FIXME: make real_tests() a public method. |
| tests = sorted(default_port._real_tests(args)) |
| |
| for port_name in port_names: |
| if port_name != port_names[0]: |
| print() |
| print("// For %s" % port_name) |
| port = tool.port_factory.get(port_name) |
| for test_name in tests: |
| self._print_baselines(options, port_name, test_name, port.expected_baselines_by_extension(test_name)) |
| |
| def _print_baselines(self, options, port_name, test_name, baselines): |
| for extension in sorted(baselines.keys()): |
| baseline_location = baselines[extension] |
| if baseline_location: |
| print(baseline_location) |
| |
| def _platform_for_path(self, relpath): |
| platform_matchobj = self._platform_regexp.match(relpath) |
| if platform_matchobj: |
| return platform_matchobj.group(1) |
| return None |
| |
| |
| class FindResolvedBugs(Command): |
| name = "find-resolved-bugs" |
| help_text = "Collect the RESOLVED bugs in the given TestExpectations file" |
| argument_names = "TEST_EXPECTATIONS_FILE" |
| |
| def execute(self, options, args, tool): |
| filename = args[0] |
| if not tool.filesystem.isfile(filename): |
| print("The given path is not a file, please pass a valid path.") |
| return |
| |
| ids = set() |
| inputfile = tool.filesystem.open_text_file_for_reading(filename) |
| for line in inputfile: |
| result = re.search("(https://bugs\.webkit\.org/show_bug\.cgi\?id=|webkit\.org/b/)([0-9]+)", line) |
| if result: |
| ids.add(result.group(2)) |
| inputfile.close() |
| |
| resolved_ids = set() |
| num_of_bugs = len(ids) |
| bugzilla = Bugzilla() |
| for i, bugid in enumerate(ids, start=1): |
| bug = bugzilla.fetch_bug(bugid) |
| print("Checking bug %s \t [%d/%d]" % (bugid, i, num_of_bugs)) |
| if not bug.is_open(): |
| resolved_ids.add(bugid) |
| |
| print("Resolved bugs in %s :" % (filename)) |
| for bugid in resolved_ids: |
| print("https://bugs.webkit.org/show_bug.cgi?id=%s" % (bugid)) |