#!/usr/bin/env vpython3 # Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. # # Use of this source code is governed by a BSD-style license # that can be found in the LICENSE file in the root of the source # tree. An additional intellectual property rights grant can be found # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. import glob import os import shutil import sys import tempfile import unittest import mock 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, FindAddedDeps, \ FindRemovedDeps, ChooseCQMode, GenerateCommitMessage, \ GetMatchingDepsEntries, ParseDepsDict, ParseLocalDepsFile, UpdateDepsFile, \ ChromiumRevisionUpdate TEST_DATA_VARS = { 'chromium_git': 'https://chromium.googlesource.com', 'chromium_revision': '1b9c098a08e40114e44b6c1ec33ddf95c40b901d', } DEPS_ENTRIES = { 'src/build': 'https://build.com', 'src/third_party/depot_tools': 'https://depottools.com', 'src/testing/gtest': 'https://gtest.com', 'src/testing/gmock': 'https://gmock.com', } BUILD_OLD_REV = '52f7afeca991d96d68cf0507e20dbdd5b845691f' BUILD_NEW_REV = 'HEAD' DEPOTTOOLS_OLD_REV = 'b9ae2ca9a55d9b754c313f4c9e9f0f3b804a5e44' DEPOTTOOLS_NEW_REV = '1206a353e40abb70d8454eb9af53db0ad10b713c' NO_CHROMIUM_REVISION_UPDATE = ChromiumRevisionUpdate('cafe', 'cafe') class TestError(Exception): pass class FakeCmd: def __init__(self): self.expectations = [] def AddExpectation(self, *args, **kwargs): returns = kwargs.pop('_returns', None) 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, 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) raise TestError(message) return exp_returns class NullCmd: """No-op mock when calls mustn't be checked. """ def __call__(self, *args, **kwargs): # Empty stdout and stderr. return None, None 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) 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() def tearDown(self): shutil.rmtree(self._output_dir, ignore_errors=True) self.assertEqual(self.fake.expectations, []) def testVarLookup(self): local_scope = {'foo': 'wrong', 'vars': {'foo': 'bar'}} lookup = roll_deps.VarLookup(local_scope) self.assertEqual(lookup('foo'), 'bar') def testUpdateDepsFile(self): new_rev = 'aaaaabbbbbcccccdddddeeeeefffff0000011111' current_rev = TEST_DATA_VARS['chromium_revision'] with open(self._new_cr_depsfile_android, 'rb') as deps_file: new_cr_contents = deps_file.read().decode('utf-8') UpdateDepsFile(self._webrtc_depsfile, ChromiumRevisionUpdate(current_rev, new_rev), [], new_cr_contents) with open(self._webrtc_depsfile, 'rb') as deps_file: deps_contents = deps_file.read().decode('utf-8') 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, 'rb') as deps_file: webrtc_contents = deps_file.read().decode('utf-8') with open(self._new_cr_depsfile_android, 'rb') as deps_file: new_cr_contents = deps_file.read().decode('utf-8') webrtc_deps = ParseDepsDict(webrtc_contents) new_cr_deps = ParseDepsDict(new_cr_contents) changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps) with mock.patch('roll_deps._RunCommand', NullCmd()): UpdateDepsFile(self._webrtc_depsfile_android, NO_CHROMIUM_REVISION_UPDATE, changed_deps, new_cr_contents) with open(self._webrtc_depsfile_android, 'rb') as deps_file: updated_contents = deps_file.read().decode('utf-8') return webrtc_contents, updated_contents def testUpdateAndroidGeneratedDeps(self): _, 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): 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): 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, 'rb') as deps_file: deps_contents = deps_file.read().decode('utf-8') local_scope = ParseDepsDict(deps_contents) vars_dict = local_scope['vars'] def AssertVar(variable_name): self.assertEqual(vars_dict[variable_name], TEST_DATA_VARS[variable_name]) AssertVar('chromium_git') AssertVar('chromium_revision') self.assertEqual(len(local_scope['deps']), 3) self.assertEqual(len(local_scope['deps_os']), 1) def testGetMatchingDepsEntriesReturnsPathInSimpleCase(self): entries = GetMatchingDepsEntries(DEPS_ENTRIES, 'src/testing/gtest') self.assertEqual(len(entries), 1) self.assertEqual(entries[0], DEPS_ENTRIES['src/testing/gtest']) def testGetMatchingDepsEntriesHandlesSimilarStartingPaths(self): entries = GetMatchingDepsEntries(DEPS_ENTRIES, 'src/testing') self.assertEqual(len(entries), 2) def testGetMatchingDepsEntriesHandlesTwoPathsWithIdenticalFirstParts(self): entries = GetMatchingDepsEntries(DEPS_ENTRIES, 'src/build') self.assertEqual(len(entries), 1) def testCalculateChangedDeps(self): webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile) new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile) with mock.patch('roll_deps._RunCommand', self.fake): _SetupGitLsRemoteCall( self.fake, 'https://chromium.googlesource.com/chromium/src/build', BUILD_NEW_REV) changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps) self.assertEqual(len(changed_deps), 4) self.assertEqual(changed_deps[0].path, 'fuchsia') self.assertEqual(changed_deps[0].current_version, 'version:10.20221201.3.1') self.assertEqual(changed_deps[0].new_version, 'version:11.20230207.1.1') self.assertEqual(changed_deps[1].path, 'src/build') self.assertEqual(changed_deps[1].current_rev, BUILD_OLD_REV) self.assertEqual(changed_deps[1].new_rev, BUILD_NEW_REV) self.assertEqual(changed_deps[2].path, 'src/buildtools/linux64') self.assertEqual(changed_deps[2].package, 'gn/gn/linux-amd64') self.assertEqual(changed_deps[2].current_version, 'git_revision:69ec4fca1fa69ddadae13f9e6b7507efa0675263') self.assertEqual(changed_deps[2].new_version, 'git_revision:new-revision') self.assertEqual(changed_deps[3].path, 'src/third_party/depot_tools') self.assertEqual(changed_deps[3].current_rev, DEPOTTOOLS_OLD_REV) self.assertEqual(changed_deps[3].new_rev, DEPOTTOOLS_NEW_REV) def testWithDistinctDeps(self): """Check CalculateChangedDeps 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.assertEqual(len(changed_deps), 1) self.assertEqual( changed_deps[0].path, 'src/third_party/android_deps/libs/android_arch_core_common') self.assertEqual( changed_deps[0].package, 'chromium/third_party/android_deps/libs/android_arch_core_common') self.assertEqual(changed_deps[0].current_version, 'version:0.9.0') self.assertEqual(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.assertEqual( added_android_paths, ['src/third_party/android_deps/libs/android_arch_lifecycle_common']) self.assertEqual(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.assertEqual( removed_android_paths, ['src/third_party/android_deps/libs/android_arch_lifecycle_runtime']) self.assertEqual(other_paths, []) def testMissingDepsIsDetected(self): """Check 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.assertEqual( other_paths, ['fuchsia', 'src/buildtools/linux64', 'src/third_party/depot_tools']) def testExpectedDepsIsNotReportedMissing(self): """Some deps musn't be seen as missing, even if absent from Chromium.""" webrtc_deps = ParseLocalDepsFile(self._webrtc_depsfile) new_cr_deps = ParseLocalDepsFile(self._new_cr_depsfile_android) removed_android_paths, other_paths = FindRemovedDeps( webrtc_deps, new_cr_deps) self.assertTrue('src/build' not in removed_android_paths) self.assertTrue('src/build' not in other_paths) 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) current_commit_pos = 'cafe' new_commit_pos = 'f00d' commit_msg = GenerateCommitMessage(NO_CHROMIUM_REVISION_UPDATE, current_commit_pos, new_commit_pos, changed_deps, 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): self.assertEqual(ChooseCQMode(True, 99, 500000, 500100), 0) def testDryRun(self): self.assertEqual(ChooseCQMode(False, 101, 500000, 500100), 1) def testSubmit(self): self.assertEqual(ChooseCQMode(False, 100, 500000, 500100), 2) class TestReadUrlContent(unittest.TestCase): def setUp(self): self.url = 'http://localhost+?format=TEXT' def testReadUrlContent(self): url_mock = mock.Mock() roll_deps.urllib.request.urlopen = url_mock roll_deps.ReadUrlContent(self.url) calls = [ mock.call('http://localhost+?format=TEXT'), mock.call().readlines(), mock.call().close() ] self.assertEqual(url_mock.mock_calls, calls) def testReadUrlContentError(self): roll_deps.logging = mock.Mock() readlines_mock = mock.Mock() readlines_mock.readlines = mock.Mock( side_effect=IOError('Connection error')) readlines_mock.close = mock.Mock() url_mock = mock.Mock(return_value=readlines_mock) roll_deps.urllib.request.urlopen = url_mock try: roll_deps.ReadUrlContent(self.url) except OSError: self.assertTrue(roll_deps.logging.exception.called) def _SetupGitLsRemoteCall(cmd_fake, url, revision): cmd = ['git', 'ls-remote', url, revision] cmd_fake.AddExpectation(cmd, _returns=(revision, None)) if __name__ == '__main__': unittest.main()