Auto roller: improved tracking of android dependencies.

Until now, only revision changes were automatically rolled.
This CL detect new and removed dependencies in third_party/android_deps.

Change-Id: I4f83b7308be577115cc3ed57edd9881496428173
Bug: chromium:855108
Reviewed-on: https://webrtc-review.googlesource.com/100021
Commit-Queue: Patrik Höglund <phoglund@webrtc.org>
Reviewed-by: Patrik Höglund <phoglund@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#24773}
This commit is contained in:
Yves Gerey
2018-09-19 09:51:41 +02:00
committed by Commit Bot
parent 3a050c2ef5
commit 401afd51bb
6 changed files with 385 additions and 24 deletions

View File

@ -51,6 +51,13 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CHECKOUT_SRC_DIR = FindSrcDirPath()
CHECKOUT_ROOT_DIR = os.path.realpath(os.path.join(CHECKOUT_SRC_DIR, os.pardir))
# Copied from tools/android/roll/android_deps/.../BuildConfigGenerator.groovy.
ANDROID_DEPS_START = r'=== ANDROID_DEPS Generated Code Start ==='
ANDROID_DEPS_END = r'=== ANDROID_DEPS Generated Code End ==='
# Location of automically gathered android deps.
ANDROID_DEPS_PATH = 'src/third_party/android_deps/'
sys.path.append(os.path.join(CHECKOUT_SRC_DIR, 'build'))
import find_depot_tools
@ -96,11 +103,6 @@ def ParseLocalDepsFile(filename):
return ParseDepsDict(deps_content)
def ParseRemoteCrDepsFile(revision):
deps_content = ReadRemoteCrFile('DEPS', revision)
return ParseDepsDict(deps_content)
def ParseCommitPosition(commit_message):
for line in reversed(commit_message.splitlines()):
m = COMMIT_POSITION_RE.match(line.strip())
@ -254,6 +256,79 @@ def _FindChangedCipdPackages(path, old_pkgs, new_pkgs):
old_version, new_version)
def _FindNewDeps(old, new):
""" Gather dependencies only in |new| and return corresponding paths. """
old_entries = set(BuildDepsentryDict(old))
new_entries = set(BuildDepsentryDict(new))
return [path for path in new_entries - old_entries
if path not in DONT_AUTOROLL_THESE]
def FindAddedDeps(webrtc_deps, new_cr_deps):
"""
Calculate new deps entries of interest.
Ideally, that would mean: only appearing in chromium DEPS
but transitively used in WebRTC.
Since it's hard to compute, we restrict ourselves to a well defined subset:
deps sitting in |ANDROID_DEPS_PATH|.
Otherwise, assumes that's a Chromium-only dependency.
Args:
webrtc_deps: dict of deps as defined in the WebRTC DEPS file.
new_cr_deps: dict of deps as defined in the chromium DEPS file.
Caveat: Doesn't detect a new package in existing dep.
Returns:
A tuple consisting of:
A list of paths added dependencies sitting in |ANDROID_DEPS_PATH|.
A list of paths for other added dependencies.
"""
all_added_deps = _FindNewDeps(webrtc_deps, new_cr_deps)
generated_android_deps = [path for path in all_added_deps
if path.startswith(ANDROID_DEPS_PATH)]
other_deps = [path for path in all_added_deps
if path not in generated_android_deps]
return generated_android_deps, other_deps
def FindRemovedDeps(webrtc_deps, new_cr_deps):
"""
Calculate obsolete deps entries.
Ideally, that would mean: no more appearing in chromium DEPS
and not used in WebRTC.
Since it's hard to compute:
1/ We restrict ourselves to a well defined subset:
deps sitting in |ANDROID_DEPS_PATH|.
2/ We rely on existing behavior of CalculateChangeDeps.
I.e. Assumes non-CIPD dependencies are WebRTC-only, don't remove them.
Args:
webrtc_deps: dict of deps as defined in the WebRTC DEPS file.
new_cr_deps: dict of deps as defined in the chromium DEPS file.
Caveat: Doesn't detect a deleted package in existing dep.
Returns:
A tuple consisting of:
A list of paths of dependencies removed from |ANDROID_DEPS_PATH|.
A list of paths of unexpected disappearing dependencies.
"""
all_removed_deps = _FindNewDeps(new_cr_deps, webrtc_deps)
generated_android_deps = [path for path in all_removed_deps
if path.startswith(ANDROID_DEPS_PATH)]
# DepsEntry are assumed to be webrtc-only dependencies,
# and are handled in CalculatedChangedDeds.
other_deps = [path for path in all_removed_deps
if path not in generated_android_deps and
not isinstance(webrtc_deps['deps'][path], DepsEntry)]
return generated_android_deps, other_deps
def CalculateChangedDeps(webrtc_deps, new_cr_deps):
"""
Calculate changed deps entries based on entries defined in the WebRTC DEPS
@ -296,8 +371,9 @@ def CalculateChangedDeps(webrtc_deps, new_cr_deps):
'HEAD'])
new_rev = stdout.strip().split('\t')[0]
else:
raise RollError('WebRTC DEPS entry %s is missing from Chromium. Remove '
'it or add it to DONT_AUTOROLL_THESE.' % path)
# The dependency has been removed from chromium.
# This is handled by FindRemovedDeps.
continue
# Check if an update is necessary.
if webrtc_deps_entry.revision != new_rev:
@ -325,8 +401,10 @@ def CalculateChangedClang(new_cr_rev):
return ChangedDep(CLANG_UPDATE_SCRIPT_LOCAL_PATH, None, current_rev, new_rev)
def GenerateCommitMessage(rev_update, current_commit_pos,
new_commit_pos, changed_deps_list, clang_change):
def GenerateCommitMessage(rev_update, current_commit_pos, new_commit_pos,
changed_deps_list, clang_change=None,
added_deps_paths=None,
removed_deps_paths=None):
current_cr_rev = rev_update.current_chromium_rev[0:10]
new_cr_rev = rev_update.new_chromium_rev[0:10]
rev_interval = '%s..%s' % (current_cr_rev, new_cr_rev)
@ -337,9 +415,14 @@ def GenerateCommitMessage(rev_update, current_commit_pos,
'Change log: %s' % (CHROMIUM_LOG_TEMPLATE % rev_interval),
'Full diff: %s\n' % (CHROMIUM_COMMIT_TEMPLATE %
rev_interval)]
def Section(adjective, deps):
noun = 'dependency' if len(deps) == 1 else 'dependencies'
commit_msg.append('%s %s' % (adjective, noun))
tbr_authors = ''
if changed_deps_list:
commit_msg.append('Changed dependencies:')
Section('Changed', changed_deps_list)
for c in changed_deps_list:
if isinstance(c, ChangedCipdPackage):
@ -352,12 +435,23 @@ def GenerateCommitMessage(rev_update, current_commit_pos,
if 'libvpx' in c.path:
tbr_authors += 'marpan@webrtc.org, '
if added_deps_paths:
Section('Added', added_deps_paths)
commit_msg.extend('* %s' % p for p in added_deps_paths)
if removed_deps_paths:
Section('Removed', removed_deps_paths)
commit_msg.extend('* %s' % p for p in removed_deps_paths)
if any([changed_deps_list,
added_deps_paths,
removed_deps_paths]):
change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval, 'DEPS')
commit_msg.append('DEPS diff: %s\n' % change_url)
else:
commit_msg.append('No dependencies changed.')
if clang_change.current_rev != clang_change.new_rev:
if clang_change and clang_change.current_rev != clang_change.new_rev:
commit_msg.append('Clang version changed %s:%s' %
(clang_change.current_rev, clang_change.new_rev))
change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval,
@ -377,14 +471,30 @@ def GenerateCommitMessage(rev_update, current_commit_pos,
return '\n'.join(commit_msg)
def UpdateDepsFile(deps_filename, rev_update, changed_deps):
def UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content):
"""Update the DEPS file with the new revision."""
# Update the chromium_revision variable.
with open(deps_filename, 'rb') as deps_file:
deps_content = deps_file.read()
# Update the chromium_revision variable.
deps_content = deps_content.replace(rev_update.current_chromium_rev,
rev_update.new_chromium_rev)
# Add and remove dependencies. For now: only generated android deps.
# Since gclient cannot add or remove deps, we on the fact that
# these android deps are located in one place we can copy/paste.
deps_re = re.compile(ANDROID_DEPS_START + '.*' + ANDROID_DEPS_END,
re.DOTALL)
new_deps = deps_re.search(new_cr_content)
old_deps = deps_re.search(deps_content)
if not new_deps or not old_deps:
faulty = 'Chromium' if not new_deps else 'WebRTC'
raise RollError('Was expecting to find "%s" and "%s"\n'
'in %s DEPS'
% (ANDROID_DEPS_START, ANDROID_DEPS_END, faulty))
deps_content = deps_re.sub(new_deps.group(0), deps_content)
with open(deps_filename, 'wb') as deps_file:
deps_file.write(deps_content)
@ -537,6 +647,7 @@ def main():
deps_filename = os.path.join(CHECKOUT_SRC_DIR, 'DEPS')
webrtc_deps = ParseLocalDepsFile(deps_filename)
rev_update = GetRollRevisionRanges(opts, webrtc_deps)
current_commit_pos = ParseCommitPosition(
@ -544,16 +655,28 @@ def main():
new_commit_pos = ParseCommitPosition(
ReadRemoteCrCommit(rev_update.new_chromium_rev))
new_cr_deps = ParseRemoteCrDepsFile(rev_update.new_chromium_rev)
new_cr_content = ReadRemoteCrFile('DEPS', rev_update.new_chromium_rev)
new_cr_deps = ParseDepsDict(new_cr_content)
changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps)
# Discard other deps, assumed to be chromium-only dependencies.
new_generated_android_deps, _ = FindAddedDeps(webrtc_deps, new_cr_deps)
removed_generated_android_deps, other_deps = FindRemovedDeps(webrtc_deps,
new_cr_deps)
if other_deps:
raise RollError('WebRTC DEPS entries are missing from Chromium: %s. '
'Remove them or add them to DONT_AUTOROLL_THESE.' % other_deps)
clang_change = CalculateChangedClang(rev_update.new_chromium_rev)
commit_msg = GenerateCommitMessage(rev_update,
current_commit_pos, new_commit_pos,
changed_deps, clang_change)
changed_deps,
new_generated_android_deps,
removed_generated_android_deps,
clang_change)
logging.debug('Commit message:\n%s', commit_msg)
_CreateRollBranch(opts.dry_run)
UpdateDepsFile(deps_filename, rev_update, changed_deps)
if not opts.dry_run:
UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content)
if _IsTreeClean():
logging.info("No DEPS changes detected, skipping CL creation.")
else:

View File

@ -19,7 +19,8 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PARENT_DIR = os.path.join(SCRIPT_DIR, os.pardir)
sys.path.append(PARENT_DIR)
import roll_deps
from roll_deps import CalculateChangedDeps, ChooseCQMode, \
from roll_deps import CalculateChangedDeps, FindAddedDeps, \
FindRemovedDeps, ChooseCQMode, GenerateCommitMessage, \
GetMatchingDepsEntries, ParseDepsDict, ParseLocalDepsFile, UpdateDepsFile, \
ChromiumRevisionUpdate
@ -41,6 +42,8 @@ BUILD_NEW_REV = 'HEAD'
BUILDTOOLS_OLD_REV = '64e38f0cebdde27aa0cfb405f330063582f9ac76'
BUILDTOOLS_NEW_REV = '55ad626b08ef971fd82a62b7abb325359542952b'
NO_CHROMIUM_REVISION_UPDATE = ChromiumRevisionUpdate('cafe', 'cafe')
class TestError(Exception):
pass
@ -52,12 +55,15 @@ class FakeCmd(object):
def AddExpectation(self, *args, **kwargs):
returns = kwargs.pop('_returns', None)
self.expectations.append((args, kwargs, returns))
ignores = kwargs.pop('_ignores', [])
self.expectations.append((args, kwargs, returns, ignores))
def __call__(self, *args, **kwargs):
if not self.expectations:
raise TestError('Got unexpected\n%s\n%s' % (args, kwargs))
exp_args, exp_kwargs, exp_returns = self.expectations.pop(0)
exp_args, exp_kwargs, exp_returns, ignores = self.expectations.pop(0)
for item in ignores:
kwargs.pop(item, None)
if args != exp_args or kwargs != exp_kwargs:
message = 'Expected:\n args: %s\n kwargs: %s\n' % (exp_args, exp_kwargs)
message += 'Got:\n args: %s\n kwargs: %s\n' % (args, kwargs)
@ -65,14 +71,29 @@ class FakeCmd(object):
return exp_returns
class NullCmd(object):
""" No-op mock when calls mustn't be checked. """
def __call__(self, *args, **kwargs):
# Empty stdout and stderr.
return None, None
def MuteCommandMock():
setattr(roll_deps, '_RunCommand', NullCmd())
class TestRollChromiumRevision(unittest.TestCase):
def setUp(self):
self._output_dir = tempfile.mkdtemp()
test_data_dir = os.path.join(SCRIPT_DIR, 'testdata', 'roll_deps')
for test_file in glob.glob(os.path.join(test_data_dir, '*')):
shutil.copy(test_file, self._output_dir)
self._webrtc_depsfile = os.path.join(self._output_dir, 'DEPS')
self._new_cr_depsfile = os.path.join(self._output_dir, 'DEPS.chromium.new')
join = lambda f: os.path.join(self._output_dir, f)
self._webrtc_depsfile = join('DEPS')
self._new_cr_depsfile = join('DEPS.chromium.new')
self._webrtc_depsfile_android = join('DEPS.with_android_deps')
self._new_cr_depsfile_android = join('DEPS.chromium.with_android_deps')
self.fake = FakeCmd()
self.old_run_command = getattr(roll_deps, '_RunCommand')
@ -83,6 +104,7 @@ class TestRollChromiumRevision(unittest.TestCase):
self.assertEqual(self.fake.expectations, [])
setattr(roll_deps, '_RunCommand', self.old_run_command)
def testVarLookup(self):
local_scope = {'foo': 'wrong', 'vars': {'foo': 'bar'}}
lookup = roll_deps.VarLookup(local_scope)
@ -90,16 +112,64 @@ class TestRollChromiumRevision(unittest.TestCase):
def testUpdateDepsFile(self):
new_rev = 'aaaaabbbbbcccccdddddeeeeefffff0000011111'
current_rev = TEST_DATA_VARS['chromium_revision']
with open(self._new_cr_depsfile_android) as deps_file:
new_cr_contents = deps_file.read()
UpdateDepsFile(self._webrtc_depsfile,
ChromiumRevisionUpdate(current_rev, new_rev),
[])
[],
new_cr_contents)
with open(self._webrtc_depsfile) as deps_file:
deps_contents = deps_file.read()
self.assertTrue(new_rev in deps_contents,
'Failed to find %s in\n%s' % (new_rev, deps_contents))
def _UpdateDepsSetup(self):
with open(self._webrtc_depsfile_android) as deps_file:
webrtc_contents = deps_file.read()
with open(self._new_cr_depsfile_android) as deps_file:
new_cr_contents = deps_file.read()
webrtc_deps = ParseDepsDict(webrtc_contents)
new_cr_deps = ParseDepsDict(new_cr_contents)
changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps)
UpdateDepsFile(self._webrtc_depsfile_android,
NO_CHROMIUM_REVISION_UPDATE,
changed_deps,
new_cr_contents)
with open(self._webrtc_depsfile_android) as deps_file:
updated_contents = deps_file.read()
return webrtc_contents, updated_contents
def testUpdateAndroidGeneratedDeps(self):
MuteCommandMock()
_, updated_contents = self._UpdateDepsSetup()
changed = 'third_party/android_deps/libs/android_arch_core_common'
changed_version = '1.0.0-cr0'
self.assertTrue(changed in updated_contents)
self.assertTrue(changed_version in updated_contents)
def testAddAndroidGeneratedDeps(self):
MuteCommandMock()
webrtc_contents, updated_contents = self._UpdateDepsSetup()
added = 'third_party/android_deps/libs/android_arch_lifecycle_common'
self.assertFalse(added in webrtc_contents)
self.assertTrue(added in updated_contents)
def testRemoveAndroidGeneratedDeps(self):
MuteCommandMock()
webrtc_contents, updated_contents = self._UpdateDepsSetup()
removed = 'third_party/android_deps/libs/android_arch_lifecycle_runtime'
self.assertTrue(removed in webrtc_contents)
self.assertFalse(removed in updated_contents)
def testParseDepsDict(self):
with open(self._webrtc_depsfile) as deps_file:
deps_contents = deps_file.read()
@ -125,7 +195,7 @@ class TestRollChromiumRevision(unittest.TestCase):
def testGetMatchingDepsEntriesHandlesTwoPathsWithIdenticalFirstParts(self):
entries = GetMatchingDepsEntries(DEPS_ENTRIES, 'src/build')
self.assertEquals(len(entries), 1)
self.assertEquals(entries[0], DEPS_ENTRIES['src/build'])
def testCalculateChangedDeps(self):
_SetupGitLsRemoteCall(self.fake,
@ -147,6 +217,103 @@ class TestRollChromiumRevision(unittest.TestCase):
self.assertEquals(changed_deps[2].current_version, 'version:1.4.8-cr0')
self.assertEquals(changed_deps[2].new_version, 'version:1.10.0-cr0')
def testWithDistinctDeps(self):
""" Check CalculateChangedDeps still works when deps are added/removed. """
webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile_android)
new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android)
changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps)
self.assertEquals(len(changed_deps), 1)
self.assertEquals(
changed_deps[0].path,
'src/third_party/android_deps/libs/android_arch_core_common')
self.assertEquals(
changed_deps[0].package,
'chromium/third_party/android_deps/libs/android_arch_core_common')
self.assertEquals(changed_deps[0].current_version, 'version:0.9.0')
self.assertEquals(changed_deps[0].new_version, 'version:1.0.0-cr0')
def testFindAddedDeps(self):
webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile_android)
new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android)
added_android_paths, other_paths = FindAddedDeps(webrtc_deps, new_cr_deps)
self.assertEquals(
added_android_paths,
['src/third_party/android_deps/libs/android_arch_lifecycle_common'])
self.assertEquals(other_paths, [])
def testFindRemovedDeps(self):
webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile_android)
new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android)
removed_android_paths, other_paths = FindRemovedDeps(webrtc_deps,
new_cr_deps)
self.assertEquals(removed_android_paths,
['src/third_party/android_deps/libs/android_arch_lifecycle_runtime'])
self.assertEquals(other_paths, [])
def testMissingDepsIsDetected(self):
""" Check an error is reported when deps cannot be automatically removed."""
# The situation at test is the following:
# * A WebRTC DEPS entry is missing from Chromium.
# * The dependency isn't an android_deps (those are supported).
webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile)
new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android)
_, other_paths = FindRemovedDeps(webrtc_deps, new_cr_deps)
self.assertEquals(other_paths, ['src/build',
'src/third_party/xstream',
'src/buildtools'])
def _CommitMessageSetup(self):
webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile_android)
new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android)
changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps)
added_paths, _ = FindAddedDeps(webrtc_deps, new_cr_deps)
removed_paths, _ = FindRemovedDeps(webrtc_deps, new_cr_deps)
# We don't really care, but it's needed to construct the message.
self.fake.AddExpectation(['git', 'config', 'user.email'],
_returns=('nobody@nowhere.no', None),
_ignores=['working_dir'])
current_commit_pos = 'cafe'
new_commit_pos = 'f00d'
commit_msg = GenerateCommitMessage(NO_CHROMIUM_REVISION_UPDATE,
current_commit_pos, new_commit_pos,
changed_deps, None,
added_paths, removed_paths)
return [l.strip() for l in commit_msg.split('\n')]
def testChangedDepsInCommitMessage(self):
commit_lines = self._CommitMessageSetup()
changed = '* src/third_party/android_deps/libs/' \
'android_arch_core_common: version:0.9.0..version:1.0.0-cr0'
self.assertTrue(changed in commit_lines)
# Check it is in adequate section.
changed_line = commit_lines.index(changed)
self.assertTrue('Changed' in commit_lines[changed_line-1])
def testAddedDepsInCommitMessage(self):
commit_lines = self._CommitMessageSetup()
added = '* src/third_party/android_deps/libs/' \
'android_arch_lifecycle_common'
self.assertTrue(added in commit_lines)
# Check it is in adequate section.
added_line = commit_lines.index(added)
self.assertTrue('Added' in commit_lines[added_line-1])
def testRemovedDepsInCommitMessage(self):
commit_lines = self._CommitMessageSetup()
removed = '* src/third_party/android_deps/libs/' \
'android_arch_lifecycle_runtime'
self.assertTrue(removed in commit_lines)
# Check it is in adequate section.
removed_line = commit_lines.index(removed)
self.assertTrue('Removed' in commit_lines[removed_line-1])
class TestChooseCQMode(unittest.TestCase):
def testSkip(self):

View File

@ -25,6 +25,10 @@ deps = {
'condition': 'checkout_android',
'dep_type': 'cipd',
},
# Script expects to find these markers.
# === ANDROID_DEPS Generated Code Start ===
# === ANDROID_DEPS Generated Code End ===
}
deps_os = {

View File

@ -22,4 +22,8 @@ deps = {
'condition': 'checkout_android',
'dep_type': 'cipd',
},
# Script expects to find these markers.
# === ANDROID_DEPS Generated Code Start ===
# === ANDROID_DEPS Generated Code End ===
}

View File

@ -0,0 +1,30 @@
# DEPS file for unit tests.
deps = {
# === ANDROID_DEPS Generated Code Start ===
# Usually generated by //tools/android/roll/android_deps/fetch_all.py
'src/third_party/android_deps/libs/android_arch_core_common': {
'packages': [
{
'package': 'chromium/third_party/android_deps/libs/android_arch_core_common',
'version': 'version:1.0.0-cr0',
},
],
'condition': 'checkout_android',
'dep_type': 'cipd',
},
'src/third_party/android_deps/libs/android_arch_lifecycle_common': {
'packages': [
{
'package': 'chromium/third_party/android_deps/libs/android_arch_lifecycle_common',
'version': 'version:1.0.0-cr0',
},
],
'condition': 'checkout_android',
'dep_type': 'cipd',
},
# === ANDROID_DEPS Generated Code End ===
}

View File

@ -0,0 +1,33 @@
# DEPS file for unit tests.
deps = {
# === ANDROID_DEPS Generated Code Start ===
# Version must be updated.
'src/third_party/android_deps/libs/android_arch_core_common': {
'packages': [
{
'package': 'chromium/third_party/android_deps/libs/android_arch_core_common',
'version': 'version:0.9.0',
},
],
'condition': 'checkout_android',
'dep_type': 'cipd',
},
# Missing here: android_arch_lifecycle_common. Must be added automatically.
# Missing in ref DEPS, must be removed automatically.
'src/third_party/android_deps/libs/android_arch_lifecycle_runtime': {
'packages': [
{
'package': 'chromium/third_party/android_deps/libs/android_arch_lifecycle_runtime',
'version': 'version:1.0.0-cr0',
},
],
'condition': 'checkout_android',
'dep_type': 'cipd',
},
# === ANDROID_DEPS Generated Code End ===
}