blob: 35793149c3080998dce280b86ddda3ba1fe609af [file] [log] [blame]
# 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(),
)