blob: 266ea42307a791248201911145483598dfff3bfe [file]
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from recipe_engine import post_process
from dataclasses import dataclass
from recipe_engine.recipe_api import RecipeScriptApi
from recipe_engine.recipe_test_api import RecipeTestApi
from RECIPE_MODULES.build import chromium, chromium_checkout
from RECIPE_MODULES.depot_tools import gclient, gerrit, tryserver
from RECIPE_MODULES.recipe_engine import (
buildbucket,
context,
file,
json,
path,
platform,
raw_io,
step,
tricium,
)
@dataclass
class DEPS(RecipeScriptApi):
buildbucket: buildbucket.API
chromium: chromium.API
chromium_checkout: chromium_checkout.API
context: context.API
file: file.API
gclient: gclient.API
gerrit: gerrit.API
json: json.API
path: path.API
platform: platform.API
raw_io: raw_io.API
step: step.API
tricium: tricium.API
tryserver: tryserver.API
@dataclass
class TEST_DEPS(RecipeTestApi):
chromium: chromium.TEST_API
chromium_checkout: chromium_checkout.TEST_API
file: file.TEST_API
json: json.TEST_API
path: path.TEST_API
platform: platform.TEST_API
raw_io: raw_io.TEST_API
step: step.TEST_API
class _ChangeDetails:
def __init__(self, api: DEPS):
changes = api.gerrit.get_changes(
'https://%s' % api.tryserver.gerrit_change.host,
query_params=[('change', str(api.tryserver.gerrit_change.change))],
o_params=[
'CURRENT_REVISION',
'ALL_COMMITS',
'DETAILED_LABELS',
'DETAILED_ACCOUNTS',
],
limit=1,
)
if not changes:
raise api.step.InfraFailure(
'Error querying for CL details: host:%r change:%r; patchset:%r'
% (
api.tryserver.gerrit_change.host,
api.tryserver.gerrit_change.change,
api.tryserver.gerrit_change.patchset,
)
)
change = changes[0]
# Skip reverted CLs
self.should_skip_linting = change['subject'].startswith('Revert')
self.cc = []
if 'reviewers' in change:
if 'REVIEWER' in change['reviewers']:
for reviewer in change['reviewers']['REVIEWER']:
# Check 'email' here since some account does not have an email address
if 'email' in reviewer:
self.cc.append(reviewer['email'])
if 'CC' in change['reviewers']:
for reviewer in change['reviewers']['CC']:
# Check 'email' here since some account does not have an email address
if 'email' in reviewer:
self.cc.append(reviewer['email'])
def _RunUntracedMemberAnalyzer(api: DEPS, src_dir, affected):
with api.step.nest('untraced_member'):
target = 'UntracedMember'
for path in affected:
contents = api.file.read_text('read_file', path).splitlines()
for line, text in enumerate(contents):
pos = text.find(target)
if not pos == -1:
with api.step.nest('generate_tricium_comment'):
category = 'Oilpan'
oilpan_email = 'oilpan-reviews@chromium.org'
message = (
'Please CC {0} if you are adding new UntracedMember.'.format(
oilpan_email
)
)
src_dir = api.chromium_checkout.source_dir
relpath = api.path.relpath(path, start=src_dir)
api.tricium.add_comment(
category,
message,
relpath,
start_line=(line + 1),
end_line=(line + 1),
start_char=pos,
end_char=(pos + len(target)),
)
# Providing just one comment is enough to get author's attention.
return
def RunSteps(api: DEPS):
assert api.tryserver.is_tryserver
api.buildbucket.hide_current_build_in_gerrit()
change = _ChangeDetails(api)
if change.should_skip_linting:
return
oilpan_email = 'oilpan-reviews@chromium.org'
if oilpan_email in change.cc:
api.step.empty('already_in_cc', step_text='{oilpan_email} is already CCed')
return
with api.chromium.chromium_layout():
api.gclient.set_config('chromium')
api.chromium.set_config('chromium')
# Do not rebase the patch, so that the Tricium analyzer observes the correct
# line numbers. Otherwise, line numbers would be relative to origin/main,
# which may be synced to include changes subsequent to the actual patch.
api.chromium_checkout.ensure_checkout(gerrit_no_rebase_patch_ref=True)
src_dir = api.chromium_checkout.source_dir
with api.context(cwd=src_dir):
src_file_suffixes = {'.cc', '.cpp', '.cxx', '.c', '.h', '.hpp'}
affected = [
src_dir / f
for f in api.chromium_checkout.get_files_affected_by_patch()
if api.path.exists(src_dir / f)
and api.path.splitext(f)[1] in src_file_suffixes
]
if not affected:
api.step.empty(
'no_cc_files_changed', step_text='No C/C++ files changed'
)
return
with api.step.nest('oilpan_analyzer'):
_RunUntracedMemberAnalyzer(api, src_dir, affected)
api.tricium.write_comments()
def GenTests(api: TEST_DEPS):
def build_with_patch(
affected_files, cc, fake_file_content='', is_revert=False
):
subject = 'Revert foo' if is_revert else 'foo'
subject += '\nTriciumTest'
test_data = sum(
[
api.chromium.try_build(),
api.platform('linux', 64),
api.override_step_data(
'gerrit changes',
api.json.output(
[
{
'subject': subject,
'reviewers': {
'REVIEWER': [
{
"_account_id": 1227909,
'name': 'Yuki Yamada',
'email': 'yukiy@chromium.org',
}
],
'CC': cc,
},
}
]
),
),
],
api.empty_test_data(),
)
if affected_files:
test_data += api.path.exists(
*[
api.path.cache_dir.joinpath('builder', 'src', x)
for x in affected_files
]
)
test_data += api.step_data(
'git diff to analyze patch',
api.raw_io.stream_output('\n'.join(affected_files)),
)
if fake_file_content:
test_data += api.step_data(
'oilpan_analyzer.untraced_member.read_file',
api.file.read_text(fake_file_content),
)
return test_data
yield api.test(
'infra_failure',
build_with_patch(affected_files=[], cc=[]),
api.override_step_data('gerrit changes', api.json.output([])),
api.post_process(
post_process.DoesNotRun, 'oilpan_analyzer.untraced_member'
),
api.expect_status('INFRA_FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'skip_reverted_cl',
build_with_patch(affected_files=[], cc=[], is_revert=True),
api.post_process(
post_process.DoesNotRun, 'oilpan_analyzer.untraced_member'
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'already_in_cc',
build_with_patch(
affected_files=[], cc=[{'email': 'oilpan-reviews@chromium.org'}]
),
api.post_process(
post_process.DoesNotRun, 'oilpan_analyzer.untraced_member'
),
api.post_process(post_process.StepSuccess, 'already_in_cc'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_files',
build_with_patch(affected_files=[], cc=[]),
api.post_process(
post_process.DoesNotRun, 'oilpan_analyzer.untraced_member'
),
api.post_process(post_process.StepSuccess, 'no_cc_files_changed'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_cc_files',
build_with_patch(affected_files=['path/to/some/file.txt'], cc=[]),
api.post_process(
post_process.DoesNotRun, 'oilpan_analyzer.untraced_member'
),
api.post_process(post_process.StepSuccess, 'no_cc_files_changed'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'not_adding_untraced_member',
build_with_patch(
affected_files=['path/to/some/file.cc'],
cc=[],
fake_file_content='aaa\nbbb',
),
api.post_process(post_process.MustRun, 'oilpan_analyzer.untraced_member'),
api.post_process(
post_process.DoesNotRun,
'oilpan_analyzer.untraced_member.generate_tricium_comment',
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'adding_untraced_member_without_oilpan_reviews',
build_with_patch(
affected_files=['add_untraced_member.cc'],
cc=[],
fake_file_content='aaa\nbbb\nUntracedMember',
),
api.post_process(
post_process.MustRun,
'oilpan_analyzer.untraced_member.generate_tricium_comment',
),
api.post_process(post_process.DropExpectation),
)