| # Copyright 2024 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Comment on changes that pass all rollers or are stuck. |
| |
| Check status of the listed rollers, and if the last unrolled change is now |
| rolled, comment on it (and any other newly rolled changes) that it has rolled |
| into N repositories. |
| |
| If no new changes have rolled, check to see if the rollers have been stuck for |
| longer than a certain threshold. If so, comment on the change. |
| |
| If there's been a manual roll, there will be a delay in commenting on the |
| original change until a subsequent automated roll happens. However, there |
| shouldn't be a warning because subsequent rolls will pass if they don't need to |
| roll anything. |
| """ |
| |
| from __future__ import annotations |
| |
| from collections.abc import Mapping |
| import datetime |
| import json |
| import re |
| from typing import TYPE_CHECKING, TypedDict |
| |
| from google.protobuf import json_format |
| from PB.go.chromium.org.luci.buildbucket.proto import ( |
| build as build_pb, |
| common as common_pb, |
| ) |
| from PB.recipes.pigweed.roll_commenter import Builder, InputProperties |
| from PB.recipe_engine import result as result_pb |
| from recipe_engine import post_process |
| |
| if TYPE_CHECKING: # pragma: no cover |
| from typing import Any, Generator |
| from recipe_engine import config_types, recipe_api, recipe_test_api |
| |
| DEPS = [ |
| 'fuchsia/builder_state', |
| 'fuchsia/builder_status', |
| 'fuchsia/gerrit', |
| 'fuchsia/git', |
| 'pigweed/checkout', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/context', |
| 'recipe_engine/defer', |
| 'recipe_engine/json', |
| 'recipe_engine/properties', |
| 'recipe_engine/step', |
| 'recipe_engine/time', |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| |
| def _get_current_revision( |
| api: recipe_api.RecipeScriptApi, |
| remote: str, |
| builds: Sequence[build_pb.Build], |
| ) -> str | None: |
| for build in builds: |
| if build.status != common_pb.SUCCESS: |
| continue |
| |
| input_props = json_format.MessageToDict(build.input.properties) or {} |
| er_input = input_props.get('checkout_options', {}).get( |
| 'equivalent_remotes', [] |
| ) |
| equivalent_remotes = api.checkout.load_equivalent_remotes(er_input) |
| |
| output_props = json_format.MessageToDict(build.output.properties) or {} |
| |
| for key, value in output_props.items(): |
| if not isinstance(value, Mapping): |
| continue # pragma: no cover |
| |
| if 'remote' in value and 'new' in value: |
| if api.checkout.remotes_equivalent( |
| equivalent_remotes, |
| remote, |
| value['remote'], |
| ): |
| return value['new'] |
| |
| return None |
| |
| |
| class State(TypedDict): |
| revisions: dict[str, str] |
| last_successful_commit: str | None |
| |
| |
| def commit40(n: int) -> str: |
| assert isinstance(n, int) |
| return ('c' + f'{n:06d}' * 7)[0:40] |
| |
| |
| def commit7(n: int) -> str: |
| return commit40(n)[0:7] |
| |
| |
| def RunSteps( |
| api: recipe_api.RecipeScriptApi, |
| props: InputProperties, |
| ) -> result_pb.RawResult | None: |
| """Run Pigweed presubmit checks.""" |
| checkout = api.checkout(props.checkout_options) |
| |
| def _link( |
| roller: str, |
| build: build_pb.Build, |
| suppress: bool | None = None, |
| ) -> str: |
| if suppress is None: |
| suppress = props.suppress_roller_details |
| if suppress: |
| url = api.buildbucket.build_url(build_id=build.id) |
| return f'[{build.id}]({url})' |
| url = api.buildbucket.builder_url(build=build) |
| return f'[{roller}]({url})' |
| |
| state = api.builder_state.fetch_previous_state() |
| state.setdefault('last_successful_commit', None) |
| state.setdefault('revisions', {}) |
| state.setdefault('warned', []) |
| current_revision: dict[str, str] = {} # Roller -> rolled revision. |
| |
| for roller in props.rollers: |
| builder_key = f'{roller.project}/{roller.bucket}/{roller.builder}' |
| |
| current_revision[builder_key] = None |
| |
| # Only copy things from the prior builder state if it's a builder we're |
| # still tracking. Ignore other entries in state. |
| if builder_key in state['revisions']: |
| current_revision[builder_key] = state['revisions'][builder_key] |
| |
| state['revisions'] = current_revision |
| api.builder_state.save(state) |
| |
| builder_status: dict[str, api.builder_status.BuilderStatus] = {} |
| skipped_rollers: list[str] = [] |
| |
| for roller in props.rollers: |
| assert roller.project |
| assert roller.bucket |
| assert roller.builder |
| builder_key = f'{roller.project}/{roller.bucket}/{roller.builder}' |
| with api.step.nest(builder_key): |
| api.time.sleep(0.3) # Don't DoS buildbucket. |
| |
| builder_status[builder_key] = api.builder_status.retrieve( |
| project=roller.project, |
| bucket=roller.bucket, |
| builder=roller.builder, |
| include_incomplete=False, |
| max_age=datetime.timedelta(days=10), |
| n=30, |
| assume_existence=True, |
| ) |
| |
| new_revision = _get_current_revision( |
| api, |
| checkout.options.remote, |
| builder_status[builder_key].builds, |
| ) |
| if new_revision and new_revision != current_revision[builder_key]: |
| pres = api.step.empty('setting').presentation |
| pres.step_summary_text = new_revision |
| current_revision[builder_key] = new_revision |
| api.builder_state.save(state) |
| |
| # Remove entries if the roller hasn't recently passed. |
| if new_revision is None and builder_key in current_revision: |
| api.step.empty('removing') |
| del current_revision[builder_key] |
| api.builder_state.save(state) |
| |
| if builder_key not in current_revision: |
| pres = api.step.empty('skipping').presentation |
| pres.step_summary_text = ( |
| 'this roller has not recently passed, ignoring it' |
| ) |
| skipped_rollers.append(builder_key) |
| |
| def skipped_summary(suppress: bool | None = None): |
| if suppress is None: |
| suppress = props.suppress_roller_details |
| |
| if not skipped_rollers: |
| return '' |
| |
| parts = [ |
| '', |
| f'Ignored {len(skipped_rollers)} broken ' |
| f'roller{"" if len(skipped_rollers) == 1 else "s"}:' |
| '', |
| ] |
| for key in skipped_rollers: |
| builds: list[build_pb.Build] = [] |
| if key in builder_status: |
| builds = builder_status[key].builds |
| |
| if builds: |
| parts.append(f'* {_link(key, builds[0], suppress=suppress)}') |
| else: |
| if suppress: |
| parts.append(f'* [redacted]') |
| else: |
| project, bucket, builder = key.split('/') |
| link = api.buildbucket.builder_url( |
| project=project, |
| bucket=bucket, |
| builder=builder, |
| ) |
| parts.append(f'* [{key}]({link})') |
| |
| return '\n'.join(parts) |
| |
| commit_age: dict[str, int] = {} |
| revisions: list[str] = [] |
| with api.context(cwd=checkout.root): |
| head = api.git.get_hash() |
| for i, line in enumerate( |
| api.git.log( |
| depth=1000, |
| fmt="format:%H", |
| test_data='\n'.join(commit40(x) for x in range(1000)), |
| ).stdout.splitlines() |
| ): |
| line = line.strip() |
| revisions.append(line) |
| commit_age[line] = i |
| |
| # Ignore new rollers (None values) until they start passing. |
| new_successful_commit = revisions[ |
| max(commit_age[x] for x in current_revision.values() if x) |
| ] |
| |
| pres = api.step.empty('previous successful commit').presentation |
| pres.step_summary_text = state['last_successful_commit'] |
| |
| pres = api.step.empty('new successful commit').presentation |
| pres.step_summary_text = new_successful_commit |
| |
| # If this is the first time this builder is being run, just update the saved |
| # value and don't comment on any changes. Future commits will get comments. |
| # We don't want to comment on really old changes just because we're |
| # initializing this builder. |
| if state['last_successful_commit'] is None: |
| state['last_successful_commit'] = new_successful_commit |
| api.builder_state.save(state) |
| api.step.empty('initial run') |
| return result_pb.RawResult( |
| summary_markdown='Initial run, not doing anything.', |
| status=common_pb.SUCCESS, |
| ) |
| |
| # If rolls have moved forward since the last run, comment on the newly |
| # rolled changes. |
| if new_successful_commit != state['last_successful_commit']: |
| newly_rolled: list[str] = [] |
| for i in range( |
| commit_age[state['last_successful_commit']] - 1, |
| commit_age[new_successful_commit] - 1, |
| -1, |
| ): |
| newly_rolled.append(revisions[i]) |
| |
| pres = api.step.empty('newly_rolled').presentation |
| pres.step_summary_text = repr(newly_rolled) |
| |
| # This is awkward and not useful in MILO but useful in recipe testing. |
| with api.step.nest('moved forward'): |
| api.step.empty( |
| f'{len(newly_rolled)} ' |
| f'commit{"" if len(newly_rolled) == 1 else "s"}' |
| ) |
| |
| change_links: list[tuple[str, str]] = [] |
| with api.defer.context() as defer: |
| for revision in newly_rolled: |
| with api.step.nest(revision[0:7]) as pres: |
| host = api.gerrit.host_from_remote_url( |
| props.checkout_options.remote, |
| ) |
| number_info = api.checkout.number_for_hash(host, revision) |
| if not number_info: # pragma: no cover |
| pres.step_summary_text = 'number not found' |
| continue |
| number = str(number_info['_number']) |
| |
| link = f'https://{host}/{number}' |
| change_links.append((number, link)) |
| pres.links['gerrit'] = link |
| |
| current_rolls = { |
| k: v for k, v in current_revision.items() if v |
| } |
| |
| summary = ( |
| f'[{api.buildbucket.build.builder.builder}]' |
| f'({api.buildbucket.build_url()}): ' |
| f'Successfully rolled into {len(current_rolls)} ' |
| f'project{"" if len(current_rolls) == 1 else "s"}' |
| f'{skipped_summary()}' |
| ) |
| |
| if props.dry_run: |
| pres.step_summary_text = 'dry run, not commenting' |
| comment = api.step.empty('comment').presentation |
| comment.step_summary_text = summary |
| |
| else: |
| notify = 'NONE' |
| if revision in state['warned']: |
| notify = 'OWNER_REVIEWERS' |
| |
| result = defer( |
| api.gerrit.set_review, |
| name='comment', |
| change_id=number, |
| host=host, |
| message=summary, |
| notify=notify, |
| ) |
| |
| if result.is_ok(): |
| pres.step_summary_text = 'successfully commented' |
| comment = result.result().presentation |
| comment.step_summary_text = summary |
| else: # pragma: no cover |
| pres.step_summary_text = 'failed to comment' |
| |
| if revision in state['warned']: |
| state['warned'].remove(revision) |
| |
| state['last_successful_commit'] = new_successful_commit |
| api.builder_state.save(state) |
| |
| # For simplicity, if we moved forward at all on commits, don't comment |
| # on changes about not rolling. If it's stuck the next run will comment. |
| dry_run_part = ' (dry-run)' if props.dry_run else '' |
| summary = [ |
| ( |
| f'Commented on {len(change_links)} newly rolled ' |
| f'commit{"" if len(change_links) == 1 else "s"}{dry_run_part}:' |
| ), |
| '', |
| ] |
| summary.extend(f'* [{number}]({link})' for number, link in change_links) |
| summary.append(skipped_summary(suppress=False)) |
| |
| return result_pb.RawResult( |
| summary_markdown='\n'.join(summary), |
| status=common_pb.SUCCESS, |
| ) |
| |
| # Warn for anything that hasn't rolled in 15 hours. Most rollers are |
| # configured to run at least every 12 hours and the go/pw-rerunner will |
| # retrigger most failures, so there should be multiple attempts within 15 |
| # hours. |
| # |
| # Don't warn for anything more than a week old. (Ok, a week before the |
| # warning threshold.) This should only come up if the roller is passing but |
| # not moving forward, like if all recent changes are to the top-level |
| # MODULE.bazel file which doesn't result in rolls. |
| current_time = api.time.utcnow() |
| delta = datetime.timedelta(hours=props.hours_before_warning or 15) |
| warning_time = current_time - delta |
| ignore_time = warning_time - datetime.timedelta(days=7) |
| |
| pres = api.step.empty('current time').presentation |
| pres.step_summary_text = repr(current_time) |
| |
| pres = api.step.empty('warning time').presentation |
| pres.step_summary_text = repr(warning_time) |
| |
| pres = api.step.empty('ignore time').presentation |
| pres.step_summary_text = repr(ignore_time) |
| |
| # Make the first two commits this checks in testing be beyond the threshold |
| # but all remaining commits are within the threshold. (See also where |
| # test_submit_time is incremented by 40 minutes in the loop.) |
| test_submit_time = warning_time - datetime.timedelta(hours=1) |
| |
| unrolled: list[str] = [] |
| for i in reversed(range(0, commit_age[new_successful_commit])): |
| unrolled.append(revisions[i]) |
| |
| if set(unrolled) <= set(skipped_rollers): |
| with api.step.nest('nothing to roll'): |
| # If we've rolled everything, we no longer need to keep track of any |
| # changes we've already posted warn comments on. Also, we don't |
| # clear out the warned list perfectly. This should prevent it from |
| # growing unbounded. |
| state['warned'] = [] |
| api.builder_state.save(state) |
| |
| summary = ['Nothing to roll', skipped_summary(suppress=False)] |
| |
| return result_pb.RawResult( |
| summary_markdown='\n'.join(summary), |
| status=common_pb.SUCCESS, |
| ) |
| |
| with api.step.nest('potentially stuck'): |
| behind = ( |
| f'{len(unrolled)} commit{"" if len(unrolled) == 1 else "s"} behind' |
| ) |
| api.step.empty(behind) |
| |
| trailing_rollers: list[str] = [] |
| with api.step.nest('trailing rollers'): |
| for builder_key, revision in current_revision.items(): |
| if revision == new_successful_commit: |
| trailing_rollers.append(builder_key) |
| api.step.empty(builder_key) |
| |
| trailing_failing_rollers: list[str] = [] |
| with api.step.nest('trailing failing rollers'): |
| for builder_key in trailing_rollers: |
| # Don't warn about a roller that's behind but passing. The next |
| # roll will pass in which case we're good, or fail, and then we |
| # can decide whether to warn or not. |
| if api.builder_status.is_passing(builder_status[builder_key]): |
| continue |
| trailing_failing_rollers.append(builder_key) |
| api.step.empty(builder_key) |
| |
| if not trailing_failing_rollers: |
| api.step.empty('no trailing failing rollers') |
| summary = [f'{behind}, but rollers are passing, waiting on:', ''] |
| for roller in trailing_rollers: |
| build = builder_status[roller].builds[0] |
| summary.append(f'* {_link(roller, build, suppress=False)}') |
| summary.append(skipped_summary(suppress=False)) |
| |
| return result_pb.RawResult( |
| summary_markdown='\n'.join(summary), |
| status=common_pb.SUCCESS, |
| ) |
| |
| num_failing = len(trailing_failing_rollers) |
| comment = [ |
| f'{api.buildbucket.build.builder.builder}:', |
| f'{num_failing} roller{"" if num_failing == 1 else "s"} failing:', |
| '', |
| ] |
| for roller in trailing_failing_rollers: |
| build = builder_status[builder_key].builds[0] |
| comment.append(f'* {_link(roller, build)}') |
| comment.append(skipped_summary()) |
| |
| change_links: list[tuple[str, str]] = [] |
| already_warned = 0 |
| with api.defer.context() as defer: |
| for revision in unrolled: |
| with api.step.nest(revision[0:7]) as pres: |
| if revision in state['warned']: |
| with api.step.nest('ignoring'): |
| api.step.empty('already warned') |
| pres.step_summary_text = 'already warned, ignoring' |
| already_warned += 1 |
| continue |
| |
| host = api.gerrit.host_from_remote_url( |
| props.checkout_options.remote, |
| ) |
| number_info = api.checkout.number_for_hash(host, revision) |
| if not number_info: # pragma: no cover |
| pres.step_summary_text = 'number not found' |
| continue |
| number = str(number_info['_number']) |
| |
| link = f'https://{host}/{number}' |
| pres.links['gerrit'] = link |
| |
| details = api.gerrit.change_details( |
| 'details', |
| change_id=number, |
| host=host, |
| test_data=api.json.test_api.output( |
| { |
| 'submitted': test_submit_time.strftime( |
| '%Y-%m-%d %H:%M:%S.000000000', |
| ) |
| } |
| ), |
| ).json.output |
| |
| test_submit_time += datetime.timedelta(minutes=40) |
| |
| submit_time = datetime.datetime.strptime( |
| details['submitted'], |
| '%Y-%m-%d %H:%M:%S.000000000', |
| ) |
| |
| pres2 = api.step.empty('submit time').presentation |
| pres2.step_summary_text = repr(submit_time) |
| |
| if submit_time < ignore_time: # pragma: no cover |
| pres.step_summary_text = 'too old, ignoring' |
| with api.step.nest('ignoring'): |
| api.step.empty('too old') |
| continue |
| |
| if submit_time > warning_time: |
| pres.step_summary_text = 'too recent, ignoring' |
| with api.step.nest('ignoring'): |
| api.step.empty('too recent') |
| continue |
| |
| state['warned'].append(revision) |
| change_links.append((number, link)) |
| api.builder_state.save(state) |
| |
| joined_comment = '\n'.join(comment) |
| |
| if props.dry_run: |
| pres.step_summary_text = 'dry run, not warning' |
| warning = api.step.empty('warning').presentation |
| warning.step_summary_text = joined_comment |
| |
| else: |
| result = defer( |
| api.gerrit.set_review, |
| name='warning', |
| change_id=number, |
| host=host, |
| message=joined_comment, |
| notify='OWNER_REVIEWERS', |
| ) |
| |
| if result.is_ok(): |
| pres.step_summary_text = 'successfully warned' |
| result.result().presentation.step_summary_text = ( |
| joined_comment |
| ) |
| else: # pragma: no cover |
| pres.step_summary_text = ( |
| f'failed to warn\n\n{joined_comment}' |
| ) |
| |
| summary = ['Failing rollers:', ''] |
| for roller in trailing_failing_rollers: |
| build = builder_status[roller].builds[0] |
| summary.append(f'* {_link(roller, build, suppress=False)}') |
| summary.append('') |
| |
| if already_warned: |
| summary.append( |
| f'Already warned on {already_warned} stuck ' |
| f'commit{"" if already_warned == 1 else "s"}.' |
| ) |
| |
| if not change_links: |
| # The way the tests are structured it's hard to cover this—there's |
| # always one or two commits that are old enough to get comments. |
| |
| summary.append( # pragma: no cover |
| 'All unwarned commits are too recent to be warned.' |
| ) |
| |
| else: |
| dry_run_part = ' (dry-run)' if props.dry_run else '' |
| summary.append( |
| f'Warned on {len(change_links)} stuck ' |
| f'commit{"" if len(change_links) == 1 else "s"}{dry_run_part}:' |
| ) |
| summary.append('') |
| summary.extend(f'* [{number}]({link})' for number, link in change_links) |
| |
| summary.append(skipped_summary(suppress=False)) |
| |
| return result_pb.RawResult( |
| summary_markdown='\n'.join(summary), |
| status=common_pb.SUCCESS, |
| ) |
| |
| |
| def GenTests(api) -> Generator[recipe_test_api.TestData, None, None]: |
| """Create tests.""" |
| |
| def checkout_options(): |
| return api.properties(checkout_options=api.checkout.git_options()) |
| |
| def full_builder_name(name): |
| parts = name.split('/') |
| if len(parts) == 1: |
| parts = ['roll', *parts] |
| if len(parts) == 2: |
| parts = ['project', *parts] |
| assert len(parts) == 3, repr(parts) |
| return '/'.join(parts) |
| |
| def roller_props( |
| *rollers: Sequence[str], |
| ): |
| props = InputProperties() |
| props.checkout_options.CopyFrom(api.checkout.git_options()) |
| |
| for roller in rollers: |
| project, bucket, builder = full_builder_name(roller).split('/') |
| props.rollers.append( |
| Builder(project=project, bucket=bucket, builder=builder) |
| ) |
| |
| return api.properties(props) |
| |
| def drop_checkout(): |
| return api.post_process( |
| post_process.DropExpectation, |
| 'checkout pigweed', |
| ) |
| |
| def ran(x): |
| return api.post_process(post_process.MustRun, x) |
| |
| def did_not_run(x): |
| return api.post_process(post_process.DoesNotRun, x) |
| |
| def skipping(builder: str): |
| return ran(f'{full_builder_name(builder)}.skipping') |
| |
| def commented(n: int): |
| return ran(f'{commit7(n)}.comment') |
| |
| def not_commented(n: int): |
| return did_not_run(f'{commit7(n)}.comment') |
| |
| def warned(n: int): |
| return ran(f'{commit7(n)}.warning') |
| |
| def not_warned(n: int): |
| return did_not_run(f'{commit7(n)}.warning') |
| |
| def moved_forward(n: int | None = None): |
| result = ran(f'moved forward') |
| if n: |
| result += ran(f'moved forward.{n} commit{"" if n == 1 else "s"}') |
| result += did_not_run('potentially stuck') |
| return result |
| |
| def nothing_to_roll(): |
| return ran('nothing to roll') |
| |
| def stuck(n: int | None = None): |
| result = ran('potentially stuck') |
| if n: |
| result += ran( |
| f'potentially stuck.{n} commit{"" if n == 1 else "s"} behind' |
| ) |
| result += did_not_run('moved forward') |
| return result |
| |
| def state_in(last: int, warned: Sequence[int] = (), **rollers: int): |
| state = { |
| 'last_successful_commit': commit40(last), |
| 'revisions': {}, |
| 'warned': [commit40(x) for x in warned], |
| } |
| for key, value in rollers.items(): |
| builder_key = f'project/roll/{key.replace("_", "-")}' |
| state['revisions'][builder_key] = commit40(value) |
| return api.builder_state(state) |
| |
| def state_out(last: int, warned: Sequence[int] = (), **rollers: int): |
| result = api.post_process( |
| post_process.PropertyMatchesCallable, |
| 'state', |
| lambda x: json.loads(x)['last_successful_commit'] == commit40(last), |
| ) |
| |
| for warn in warned: |
| result += api.post_process( |
| post_process.PropertyMatchesCallable, |
| 'state', |
| lambda x: commit40(warn) in json.loads(x)['warned'], |
| ) |
| |
| result += api.post_process( |
| post_process.PropertyMatchesCallable, |
| 'state', |
| lambda x: len(json.loads(x)['warned']) == len(warned), |
| ) |
| |
| for key, value in rollers.items(): |
| builder_key = full_builder_name(key.replace("_", "-")) |
| result += api.post_process( |
| post_process.PropertyMatchesCallable, |
| 'state', |
| lambda x: json.loads(x)['revisions'][builder_key] |
| == commit40(value), |
| ) |
| |
| result += api.post_process( |
| post_process.PropertyMatchesCallable, |
| 'state', |
| lambda x: len(json.loads(x)['revisions']) == len(rollers), |
| ) |
| |
| return result |
| |
| def passed(*, n: int, **kwargs): |
| build = api.builder_status.passed(**kwargs) |
| build.output.properties['pigweed'] = { |
| 'remote': api.checkout.pigweed_repo, |
| 'new': commit40(n), |
| } |
| return build |
| |
| def failed(*, n: int, **kwargs): |
| build = api.builder_status.failure(**kwargs) |
| build.output.properties['pigweed'] = { |
| 'remote': api.checkout.pigweed_repo, |
| 'new': commit40(n), |
| } |
| return build |
| |
| def results(name: str, *builds: build_pb.Build): |
| return api.buildbucket.simulated_search_results( |
| list(builds), |
| step_name=f'project/roll/{name}.buildbucket.search', |
| ) |
| |
| yield api.test( |
| 'initial_run', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.checkout.ci_test_data(), |
| # No state_in(). |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=11)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller', failed(id=88007, n=4), failed(id=88008, n=7)), |
| skipping('fail-roller'), |
| not_commented(9), |
| not_commented(10), |
| not_commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| state_out( |
| last=11, |
| warned=(), |
| foo_roller=11, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'one_successful_comment', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller'), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(11,), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=11)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| not_commented(9), |
| not_commented(10), |
| commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| moved_forward(1), |
| state_out( |
| last=11, |
| warned=(), |
| foo_roller=11, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'two_successful_comments', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(11,), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=10)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller'), |
| skipping('fail-roller'), |
| not_commented(9), |
| commented(10), |
| commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| moved_forward(2), |
| state_out( |
| last=10, |
| warned=(), |
| foo_roller=10, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'one_successful_comment_dry_run_suppress', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.properties(dry_run=True, suppress_roller_details=True), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=11)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller'), |
| skipping('fail-roller'), |
| not_commented(9), |
| not_commented(10), |
| commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| moved_forward(1), |
| state_out( |
| last=11, |
| warned=(), |
| foo_roller=11, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'no_forward_progress', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=12)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller', failed(id=88007, n=4), failed(id=88008, n=7)), |
| skipping('fail-roller'), |
| not_commented(9), |
| not_commented(10), |
| not_commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| warned(10), |
| warned(11), |
| not_warned(12), |
| not_warned(13), |
| state_out( |
| last=12, |
| warned=(10, 11), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| stuck(12), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'no_forward_progress_dry_run_already_warned_suppress', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.properties(dry_run=True, suppress_roller_details=True), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(10,), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=12)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller', failed(id=88007, n=4), failed(id=88008, n=7)), |
| skipping('fail-roller'), |
| not_commented(9), |
| not_commented(10), |
| not_commented(11), |
| not_commented(12), |
| not_commented(13), |
| warned(9), |
| not_warned(10), |
| warned(11), |
| not_warned(12), |
| not_warned(13), |
| stuck(12), |
| state_out( |
| last=12, |
| warned=(9, 10, 11), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'new_roller', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'new-roller'), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', failed(id=88001, n=8), passed(id=88002, n=11)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('new-roller'), |
| skipping('new-roller'), |
| not_commented(9), |
| not_commented(10), |
| commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| state_out( |
| last=11, |
| warned=(), |
| foo_roller=11, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'no_forward_progress_but_passing_suppress', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.properties(dry_run=True, suppress_roller_details=True), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=12, |
| warned=(), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| fail_roller=12, |
| ), |
| results('foo-roller', passed(id=88002, n=12)), |
| results('bar-roller', failed(id=88003, n=3), passed(id=88004, n=5)), |
| results('baz-roller', failed(id=88005, n=4), passed(id=88006, n=7)), |
| results('fail-roller', failed(id=88007, n=4), failed(id=88008, n=7)), |
| skipping('fail-roller'), |
| not_commented(9), |
| not_commented(10), |
| not_commented(11), |
| not_commented(12), |
| not_commented(13), |
| not_warned(9), |
| not_warned(10), |
| not_warned(11), |
| not_warned(12), |
| not_warned(13), |
| stuck(12), |
| state_out( |
| last=12, |
| warned=(), |
| foo_roller=12, |
| bar_roller=5, |
| baz_roller=7, |
| ), |
| drop_checkout(), |
| ) |
| |
| yield api.test( |
| 'nothing_to_roll', |
| checkout_options(), |
| roller_props('foo-roller', 'bar-roller', 'baz-roller', 'fail-roller'), |
| api.properties(dry_run=True, suppress_roller_details=True), |
| api.checkout.ci_test_data(), |
| state_in( |
| last=0, |
| warned=(), |
| foo_roller=0, |
| bar_roller=0, |
| baz_roller=0, |
| fail_roller=12, |
| ), |
| results('foo-roller', passed(id=88002, n=0)), |
| results('bar-roller', passed(id=88004, n=0)), |
| results('baz-roller', passed(id=88006, n=0)), |
| results('fail-roller', failed(id=88007, n=4), failed(id=88008, n=7)), |
| skipping('fail-roller'), |
| not_commented(0), |
| not_commented(1), |
| not_commented(1), |
| not_commented(2), |
| not_commented(3), |
| not_warned(0), |
| not_warned(1), |
| not_warned(1), |
| not_warned(2), |
| not_warned(3), |
| nothing_to_roll(), |
| state_out( |
| last=0, |
| warned=(), |
| foo_roller=0, |
| bar_roller=0, |
| baz_roller=0, |
| ), |
| drop_checkout(), |
| ) |