# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import functools
import io
import json
import os
import re
import subprocess
import sys
import unittest
from unittest.mock import ANY, Mock, MagicMock, mock_open, patch, call
bisect_builds = __import__('bisect-builds')
if 'NO_MOCK_SERVER' not in os.environ:
maybe_patch = patch
else:
# SetupEnvironment for gsutil to connect to real server.
options, _ = bisect_builds.ParseCommandLine(['-a', 'linux64'])
bisect_builds.SetupEnvironment(options)
bisect_builds.SetupAndroidEnvironment()
# Mock object that always wraps for the spec.
# This will pass the call through and ignore the return_value and side_effect.
class WrappedMock(MagicMock):
def __init__(self,
spec=None,
return_value=None,
side_effect=None,
*args,
**kwargs):
wraps = kwargs.pop('wraps', spec)
super().__init__(spec, *args, **kwargs, wraps=wraps)
maybe_patch = functools.partial(patch, spec=True, new_callable=WrappedMock)
maybe_patch.object = functools.partial(patch.object,
spec=True,
new_callable=WrappedMock)
class BisectTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
if sys.version_info[:2] <= (3, 8):
return
# Patch the name pattern for pkgutil to accept "bisect-builds" as module
# name.
dotted_words = r'(?!\d)([\w-]+)(\.(?!\d)(\w+))*'
name_pattern = re.compile(
f'^(?P<pkg>{dotted_words})'
f'(?P<cln>:(?P<obj>{dotted_words})?)?$', re.UNICODE)
cls.name_pattern_patcher = patch('pkgutil._NAME_PATTERN', name_pattern)
cls.name_pattern_patcher.start()
@classmethod
def tearDownClass(cls):
if sys.version_info[:2] <= (3, 8):
return
cls.name_pattern_patcher.stop()
class BisectTest(BisectTestCase):
max_rev = 10000
def bisect(self, good_rev, bad_rev, evaluate, num_runs=1):
options, args = bisect_builds.ParseCommandLine([
'-a', 'linux64', '-g', good_rev, '-b', bad_rev, '--times',
str(num_runs)
])
archive_build = bisect_builds.create_archive_build(options)
(minrev, maxrev) = bisect_builds.Bisect(archive_build=archive_build,
evaluate=evaluate,
try_args=args)
return (minrev, maxrev)
@patch('bisect-builds.DownloadJob._fetch')
@patch('bisect-builds.ArchiveBuild.run_revision', return_value=(0, '', ''))
@patch('bisect-builds.SnapshotBuild._get_rev_list',
return_value=range(max_rev))
def testBisectConsistentAnswer(self, mock_get_rev_list, mock_run_revision,
mock_fetch):
self.assertEqual(self.bisect(1000, 100, lambda *args: 'g'), (100, 101))
self.assertEqual(self.bisect(100, 1000, lambda *args: 'b'), (100, 101))
self.assertEqual(self.bisect(2000, 200, lambda *args: 'b'), (1999, 2000))
self.assertEqual(self.bisect(200, 2000, lambda *args: 'g'), (1999, 2000))
class DownloadJobTest(BisectTestCase):
@patch('bisect-builds.gsutil_download')
def test_fetch_gsutil(self, mock_gsutil_download):
fetch = bisect_builds.DownloadJob('gs://some-file.zip', 123)
fetch.start()
fetch.wait_for()
mock_gsutil_download.assert_called_once()
@patch('urllib.request.urlretrieve')
def test_fetch_http(self, mock_urlretrieve):
fetch = bisect_builds.DownloadJob('http://some-file.zip', 123)
fetch.start()
fetch.wait_for()
mock_urlretrieve.assert_called_once()
@patch('tempfile.mkstemp', return_value=(321, 'some-file.zip'))
@patch('urllib.request.urlretrieve')
@patch('os.close')
@patch('os.unlink')
def test_should_del(self, mock_unlink, mock_close, mock_urlretrieve,
mock_mkstemp):
fetch = bisect_builds.DownloadJob('http://some-file.zip', 123)
fetch.start().wait_for()
fetch.stop()
mock_mkstemp.assert_called_once()
mock_close.assert_called_once()
mock_urlretrieve.assert_called_once()
mock_unlink.assert_called_with('some-file.zip')
@patch('urllib.request.urlretrieve')
def test_stop_wait_for_should_be_able_to_reenter(self, mock_urlretrieve):
fetch = bisect_builds.DownloadJob('http://some-file.zip', 123)
fetch.start()
fetch.wait_for()
fetch.wait_for()
fetch.stop()
fetch.stop()
@patch('tempfile.mkstemp',
side_effect=[(321, 'some-file.apks'), (123, 'file2.apk')])
@patch('bisect-builds.gsutil_download')
@patch('os.close')
@patch('os.unlink')
def test_should_support_multiple_files(self, mock_unlink, mock_close,
mock_gsutil, mock_mkstemp):
urls = {
'trichrome':
('gs://chrome-unsigned/android-B0urB0N/129.0.6626.0/high-arm_64/'
'TrichromeChromeGoogle6432Stable.apks'),
'trichrome_library':
('gs://chrome-unsigned/android-B0urB0N/129.0.6626.0/high-arm_64/'
'TrichromeLibraryGoogle6432Stable.apk'),
}
fetch = bisect_builds.DownloadJob(urls, 123)
result = fetch.start().wait_for()
fetch.stop()
self.assertDictEqual(result, {
'trichrome': 'some-file.apks',
'trichrome_library': 'file2.apk',
})
self.assertEqual(mock_mkstemp.call_count, 2)
self.assertEqual(mock_close.call_count, 2)
self.assertEqual(mock_unlink.call_count, 2)
self.assertEqual(mock_gsutil.call_count, 2)
class ArchiveBuildTest(BisectTestCase):
def setUp(self):
self.patcher = patch.multiple(
bisect_builds.ArchiveBuild,
__abstractmethods__=set(),
build_type='release',
_get_rev_list=Mock(return_value=list(map(str, range(10)))),
_rev_list_cache_key='abc')
self.patcher.start()
def tearDown(self):
self.patcher.stop()
def create_build(self, args=None):
if args is None:
args = ['-a', 'linux64', '-g', '0', '-b', '9']
options, args = bisect_builds.ParseCommandLine(args)
return bisect_builds.ArchiveBuild(options)
def test_cache_should_not_work_if_not_enabled(self):
build = self.create_build()
self.assertFalse(build.use_local_cache)
with patch('builtins.open') as m:
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
bisect_builds.ArchiveBuild._get_rev_list.assert_called_once()
m.assert_not_called()
def test_cache_should_save_and_load(self):
build = self.create_build(
['-a', 'linux64', '-g', '0', '-b', '9', '--use-local-cache'])
self.assertTrue(build.use_local_cache)
# Load the non-existent cache and write to it.
cached_data = []
# The cache file would be opened 3 times:
# 1. read by _load_rev_list_cache
# 2. read by _save_rev_list_cache for existing cache
# 3. write by _save_rev_list_cache
write_mock = MagicMock()
write_mock.__enter__().write.side_effect = lambda d: cached_data.append(d)
with patch('builtins.open',
side_effect=[FileNotFoundError, FileNotFoundError, write_mock]):
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
bisect_builds.ArchiveBuild._get_rev_list.assert_called_once()
cached_json = json.loads(''.join(cached_data))
self.assertDictEqual(cached_json, {'abc': [str(x) for x in range(10)]})
# Load cache with cached data.
build = self.create_build(
['-a', 'linux64', '-g', '0', '-b', '9', '--use-local-cache'])
bisect_builds.ArchiveBuild._get_rev_list.reset_mock()
with patch('builtins.open', mock_open(read_data=''.join(cached_data))):
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
bisect_builds.ArchiveBuild._get_rev_list.assert_not_called()
@patch.object(bisect_builds.ArchiveBuild, '_load_rev_list_cache')
@patch.object(bisect_builds.ArchiveBuild, '_save_rev_list_cache')
@patch.object(bisect_builds.ArchiveBuild,
'_get_rev_list',
return_value=[str(x) for x in range(10)])
def test_should_request_partial_rev_list(self, mock_get_rev_list,
mock_save_rev_list_cache,
mock_load_rev_list_cache):
build = self.create_build()
# missing latest
mock_load_rev_list_cache.return_value = [str(x) for x in range(5)]
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
mock_get_rev_list.assert_called_with('4', '9')
# missing old and latest
mock_load_rev_list_cache.return_value = [str(x) for x in range(1, 5)]
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
mock_get_rev_list.assert_called_with('0', '9')
# missing old
mock_load_rev_list_cache.return_value = [str(x) for x in range(3, 10)]
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
mock_get_rev_list.assert_called_with('0', '3')
# no intersect
mock_load_rev_list_cache.return_value = ['c', 'd', 'e']
self.assertEqual(build.get_rev_list(), [str(x) for x in range(10)])
mock_save_rev_list_cache.assert_called_with([str(x) for x in range(10)] +
['c', 'd', 'e'])
mock_get_rev_list.assert_called_with('0', 'c')
@patch.object(bisect_builds.ArchiveBuild, '_get_rev_list', return_value=[])
def test_should_raise_error_when_no_rev_list(self, mock_get_rev_list):
build = self.create_build()
with self.assertRaises(bisect_builds.BisectException):
build.get_rev_list()
mock_get_rev_list.assert_any_call('0', '9')
mock_get_rev_list.assert_any_call()
@unittest.skipIf('NO_MOCK_SERVER' not in os.environ,
'The test is to ensure NO_MOCK_SERVER working correctly')
@maybe_patch('bisect-builds.GetRevisionFromVersion', return_value=123)
def test_no_mock(self, mock_GetRevisionFromVersion):
self.assertEqual(bisect_builds.GetRevisionFromVersion('127.0.6533.74'),
1313161)
mock_GetRevisionFromVersion.assert_called()
@patch('bisect-builds.ArchiveBuild._install_revision')
@patch('bisect-builds.ArchiveBuild._launch_revision',
return_value=(1, '', ''))
def test_run_revision_should_return_early(self, mock_launch_revision,
mock_install_revision):
build = self.create_build()
build.run_revision('', [])
mock_launch_revision.assert_called_once()
@patch('bisect-builds.ArchiveBuild._install_revision')
@patch('bisect-builds.ArchiveBuild._launch_revision',
return_value=(0, '', ''))
def test_run_revision_should_do_all_runs(self, mock_launch_revision,
mock_install_revision):
build = self.create_build(
['-a', 'linux64', '-g', '0', '-b', '9', '--time', '10'])
build.run_revision('', [])
self.assertEqual(mock_launch_revision.call_count, 10)
@patch('bisect-builds.UnzipFilenameToDir')
@patch('glob.glob', return_value=['temp-dir/linux64/chrome'])
@patch('os.path.abspath', return_value='/tmp/temp-dir/linux64/chrome')
def test_install_revision_should_unzip_and_search_executable(
self, mock_abspath, mock_glob, mock_UnzipFilenameToDir):
build = self.create_build()
self.assertEqual(build._install_revision('some-file.zip', 'temp-dir'),
'/tmp/temp-dir/linux64/chrome')
mock_UnzipFilenameToDir.assert_called_once_with('some-file.zip', 'temp-dir')
mock_glob.assert_called_once_with('temp-dir/*/chrome')
mock_abspath.assert_called_once_with('temp-dir/linux64/chrome')
@patch('subprocess.Popen', spec=subprocess.Popen)
def test_launch_revision_should_run_command(self, mock_Popen):
mock_Popen.return_value.communicate.return_value = ('', '')
mock_Popen.return_value.returncode = 0
build = self.create_build()
build._launch_revision('temp-dir', 'temp-dir/linux64/chrome', [])
mock_Popen.assert_called_once_with(
['temp-dir/linux64/chrome', '--user-data-dir=profile'],
cwd='temp-dir',
bufsize=-1,
stdout=ANY,
stderr=ANY)
class ReleaseBuildTest(BisectTestCase):
def test_should_look_up_path_context(self):
options, args = bisect_builds.ParseCommandLine(
['-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88'])
self.assertEqual(options.archive, 'linux64')
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.ReleaseBuild)
self.assertEqual(build.binary_name, 'chrome')
self.assertEqual(build.listing_platform_dir, 'linux64/')
self.assertEqual(build.archive_name, 'chrome-linux64.zip')
self.assertEqual(build.archive_extract_dir, 'chrome-linux64')
@maybe_patch(
'bisect-builds.GsutilList',
return_value=[
'gs://chrome-unsigned/desktop-5c0tCh/%s/linux64/chrome-linux64.zip' %
x for x in ['127.0.6533.74', '127.0.6533.75', '127.0.6533.76']
])
def test_get_rev_list(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine(
['-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.76'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.ReleaseBuild)
self.assertEqual(build.get_rev_list(),
['127.0.6533.74', '127.0.6533.75', '127.0.6533.76'])
mock_GsutilList.assert_any_call('gs://chrome-unsigned/desktop-5c0tCh')
mock_GsutilList.assert_any_call(*[
'gs://chrome-unsigned/desktop-5c0tCh/%s/linux64/chrome-linux64.zip' % x
for x in ['127.0.6533.74', '127.0.6533.75', '127.0.6533.76']
],
ignore_fail=True)
self.assertEqual(mock_GsutilList.call_count, 2)
@patch('bisect-builds.GsutilList',
return_value=['127.0.6533.74', '127.0.6533.75', '127.0.6533.76'])
def test_should_save_and_load_cache(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.77',
'--use-local-cache'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.ReleaseBuild)
# Load the non-existent cache and write to it.
cached_data = []
write_mock = MagicMock()
write_mock.__enter__().write.side_effect = lambda d: cached_data.append(d)
with patch('builtins.open',
side_effect=[FileNotFoundError, FileNotFoundError, write_mock]):
self.assertEqual(build.get_rev_list(),
['127.0.6533.74', '127.0.6533.75', '127.0.6533.76'])
mock_GsutilList.assert_called()
cached_json = json.loads(''.join(cached_data))
self.assertDictEqual(
cached_json, {
build._rev_list_cache_key:
['127.0.6533.74', '127.0.6533.75', '127.0.6533.76']
})
# Load cache with cached data and merge with new data
mock_GsutilList.return_value = ['127.0.6533.76', '127.0.6533.77']
build = bisect_builds.create_archive_build(options)
with patch('builtins.open', mock_open(read_data=''.join(cached_data))):
self.assertEqual(
build.get_rev_list(),
['127.0.6533.74', '127.0.6533.75', '127.0.6533.76', '127.0.6533.77'])
print(mock_GsutilList.call_args)
mock_GsutilList.assert_any_call(
'gs://chrome-unsigned/desktop-5c0tCh'
'/127.0.6533.76/linux64/chrome-linux64.zip',
'gs://chrome-unsigned/desktop-5c0tCh'
'/127.0.6533.77/linux64/chrome-linux64.zip',
ignore_fail=True)
class ArchiveBuildWithCommitPositionTest(BisectTestCase):
def setUp(self):
patch.multiple(bisect_builds.ArchiveBuildWithCommitPosition,
__abstractmethods__=set(),
build_type='release').start()
@maybe_patch('bisect-builds.GetRevisionFromVersion', return_value=1313161)
@maybe_patch('bisect-builds.GetChromiumRevision', return_value=999999999)
def test_should_convert_revision_as_commit_position(
self, mock_GetChromiumRevision, mock_GetRevisionFromVersion):
options, args = bisect_builds.ParseCommandLine(
['-a', 'linux64', '-g', '127.0.6533.74'])
build = bisect_builds.ArchiveBuildWithCommitPosition(options)
self.assertEqual(build.good_revision, 1313161)
self.assertEqual(build.bad_revision, 999999999)
mock_GetRevisionFromVersion.assert_called_once_with('127.0.6533.74')
mock_GetChromiumRevision.assert_called()
class OfficialBuildTest(BisectTestCase):
def test_should_lookup_path_context(self):
options, args = bisect_builds.ParseCommandLine(
['-o', '-a', 'linux64', '-g', '0', '-b', '10'])
self.assertEqual(options.archive, 'linux64')
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.OfficialBuild)
self.assertEqual(build.binary_name, 'chrome')
self.assertEqual(build.listing_platform_dir, 'linux-builder-perf/')
self.assertEqual(build.archive_name, 'chrome-perf-linux.zip')
self.assertEqual(build.archive_extract_dir, 'full-build-linux')
@maybe_patch('bisect-builds.GsutilList',
return_value=[
'full-build-linux_%d.zip' % x
for x in range(1313161, 1313164)
])
def test_get_rev_list(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine(
['-o', '-a', 'linux64', '-g', '1313161', '-b', '1313163'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.OfficialBuild)
self.assertEqual(build.get_rev_list(), list(range(1313161, 1313164)))
mock_GsutilList.assert_called_once_with(
'gs://chrome-test-builds/official-by-commit/linux-builder-perf/')
class SnapshotBuildTest(BisectTestCase):
def test_should_lookup_path_context(self):
options, args = bisect_builds.ParseCommandLine(
['-a', 'linux64', '-g', '0', '-b', '10'])
self.assertEqual(options.archive, 'linux64')
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.SnapshotBuild)
self.assertEqual(build.binary_name, 'chrome')
self.assertEqual(build.listing_platform_dir, 'Linux_x64/')
self.assertEqual(build.archive_name, 'chrome-linux.zip')
self.assertEqual(build.archive_extract_dir, 'chrome-linux')
CommonDataXMLContent = '''<?xml version='1.0' encoding='UTF-8'?>
<ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
<Name>chromium-browser-snapshots</Name>
<Prefix>Linux_x64/</Prefix>
<Marker></Marker>
<NextMarker></NextMarker>
<Delimiter>/</Delimiter>
<IsTruncated>true</IsTruncated>
<CommonPrefixes>
<Prefix>Linux_x64/1313161/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>Linux_x64/1313163/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>Linux_x64/1313185/</Prefix>
</CommonPrefixes>
</ListBucketResult>
'''
@maybe_patch('urllib.request.urlopen',
return_value=io.StringIO(CommonDataXMLContent))
@patch('bisect-builds.GetChromiumRevision', return_value=1313185)
def test_get_rev_list(self, mock_GetChromiumRevision, mock_urlopen):
options, args = bisect_builds.ParseCommandLine(
['-a', 'linux64', '-g', '1313161', '-b', '1313185'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.SnapshotBuild)
rev_list = build.get_rev_list()
mock_urlopen.assert_any_call(
'http://commondatastorage.googleapis.com/chromium-browser-snapshots/'
'?delimiter=/&prefix=Linux_x64/&marker=Linux_x64/1313161')
self.assertEqual(mock_urlopen.call_count, 1)
self.assertEqual(rev_list, [1313161, 1313163, 1313185])
@patch('bisect-builds.SnapshotBuild._fetch_and_parse',
return_value=([int(s)
for s in sorted([str(x) for x in range(1, 11)])], None))
def test_get_rev_list_should_start_from_a_marker(self, mock_fetch_and_parse):
options, args = bisect_builds.ParseCommandLine(
['-a', 'linux64', '-g', '0', '-b', '9'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.SnapshotBuild)
rev_list = build._get_rev_list(0, 9)
self.assertEqual(rev_list, list(range(1, 10)))
mock_fetch_and_parse.assert_called_once_with(
'http://commondatastorage.googleapis.com/chromium-browser-snapshots/'
'?delimiter=/&prefix=Linux_x64/&marker=Linux_x64/0')
mock_fetch_and_parse.reset_mock()
rev_list = build._get_rev_list(1, 9)
self.assertEqual(rev_list, list(range(1, 10)))
mock_fetch_and_parse.assert_called_once_with(
'http://commondatastorage.googleapis.com/chromium-browser-snapshots/'
'?delimiter=/&prefix=Linux_x64/&marker=Linux_x64/1')
@patch('bisect-builds.SnapshotBuild._fetch_and_parse',
return_value=([int(s)
for s in sorted([str(x) for x in range(1, 11)])], None))
def test_get_rev_list_should_scan_all_pages(self, mock_fetch_and_parse):
options, args = bisect_builds.ParseCommandLine(
['-a', 'linux64', '-g', '3', '-b', '11'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.SnapshotBuild)
rev_list = build._get_rev_list(0, 11)
self.assertEqual(sorted(rev_list), list(range(1, 11)))
mock_fetch_and_parse.assert_called_once_with(
'http://commondatastorage.googleapis.com/chromium-browser-snapshots/'
'?delimiter=/&prefix=Linux_x64/')
class ASANBuildTest(BisectTestCase):
CommonDataXMLContent = '''<?xml version='1.0' encoding='UTF-8'?>
<ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
<Name>chromium-browser-asan</Name>
<Prefix>mac-release/asan-mac-release</Prefix>
<Marker></Marker>
<NextMarker></NextMarker>
<Delimiter>.zip</Delimiter>
<IsTruncated>true</IsTruncated>
<CommonPrefixes>
<Prefix>mac-release/asan-mac-release-1313186.zip</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>mac-release/asan-mac-release-1313195.zip</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>mac-release/asan-mac-release-1313210.zip</Prefix>
</CommonPrefixes>
</ListBucketResult>
'''
@maybe_patch('urllib.request.urlopen',
return_value=io.StringIO(CommonDataXMLContent))
def test_get_rev_list(self, mock_urlopen):
options, args = bisect_builds.ParseCommandLine(
['--asan', '-a', 'mac', '-g', '1313161', '-b', '1313210'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.ASANBuild)
rev_list = build.get_rev_list()
# print(mock_urlopen.call_args_list)
mock_urlopen.assert_any_call(
'http://commondatastorage.googleapis.com/chromium-browser-asan/'
'?delimiter=.zip&prefix=mac-release/asan-mac-release'
'&marker=mac-release/asan-mac-release-1313161.zip')
self.assertEqual(mock_urlopen.call_count, 1)
self.assertEqual(rev_list, [1313186, 1313195, 1313210])
class AndroidBuildTest(BisectTestCase):
def setUp(self):
# patch for devil_imports
self.patchers = []
flag_changer_patcher = maybe_patch('bisect-builds.flag_changer',
create=True)
self.patchers.append(flag_changer_patcher)
self.mock_flag_changer = flag_changer_patcher.start()
chrome_patcher = maybe_patch('bisect-builds.chrome', create=True)
self.patchers.append(chrome_patcher)
self.mock_chrome = chrome_patcher.start()
version_codes_patcher = maybe_patch('bisect-builds.version_codes',
create=True)
self.patchers.append(version_codes_patcher)
self.mock_version_codes = version_codes_patcher.start()
self.mock_version_codes.LOLLIPOP = 21
self.mock_version_codes.NOUGAT = 24
self.mock_version_codes.PIE = 28
self.mock_version_codes.Q = 29
initial_android_device_patcher = patch(
'bisect-builds.InitializeAndroidDevice')
self.patchers.append(initial_android_device_patcher)
self.mock_initial_android_device = initial_android_device_patcher.start()
self.device = self.mock_initial_android_device.return_value
self.set_sdk_level(bisect_builds.version_codes.Q)
def set_sdk_level(self, level):
self.device.build_version_sdk = level
def tearDown(self):
for patcher in self.patchers:
patcher.stop()
class AndroidReleaseBuildTest(AndroidBuildTest):
def setUp(self):
super().setUp()
self.set_sdk_level(bisect_builds.version_codes.PIE)
@maybe_patch(
'bisect-builds.GsutilList',
return_value=[
'gs://chrome-signed/android-B0urB0N/%s/arm_64/MonochromeStable.apk' %
x for x in ['127.0.6533.76', '127.0.6533.78', '127.0.6533.79']
])
def test_get_android_rev_list(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64', '--apk', 'chrome_stable', '-g',
'127.0.6533.76', '-b', '127.0.6533.79', '--signed'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidReleaseBuild)
self.assertEqual(build.get_rev_list(),
['127.0.6533.76', '127.0.6533.78', '127.0.6533.79'])
mock_GsutilList.assert_any_call('gs://chrome-signed/android-B0urB0N')
mock_GsutilList.assert_any_call(*[
'gs://chrome-signed/android-B0urB0N/%s/arm_64/MonochromeStable.apk' % x
for x in ['127.0.6533.76', '127.0.6533.78', '127.0.6533.79']
],
ignore_fail=True)
self.assertEqual(mock_GsutilList.call_count, 2)
@patch('bisect-builds.InstallOnAndroid')
def test_install_revision(self, mock_InstallOnAndroid):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64', '-g', '1313161', '-b', '1313210', '--apk',
'chrome'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidReleaseBuild)
build._install_revision('chrome.apk', 'temp-dir')
mock_InstallOnAndroid.assert_called_once_with(self.device, 'chrome.apk')
@patch('bisect-builds.LaunchOnAndroid')
def test_launch_revision(self, mock_LaunchOnAndroid):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64', '-g', '1313161', '-b', '1313210', '--apk',
'chrome'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidReleaseBuild)
build._launch_revision('temp-dir', None)
mock_LaunchOnAndroid.assert_called_once_with(self.device, 'chrome')
class AndroidSnapshotBuildTest(AndroidBuildTest):
def setUp(self):
super().setUp()
self.set_sdk_level(bisect_builds.version_codes.PIE)
@patch('bisect-builds.InstallOnAndroid')
@patch('bisect-builds.ArchiveBuild._install_revision',
return_value='chrome.apk')
def test_install_revision(self, mock_install_revision, mock_InstallOnAndroid):
options, args = bisect_builds.ParseCommandLine([
'-a', 'android-arm64', '-g', '1313161', '-b', '1313210', '--apk',
'chrome'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidSnapshotBuild)
build._install_revision('chrome.zip', 'temp-dir')
mock_install_revision.assert_called_once_with('chrome.zip', 'temp-dir')
mock_InstallOnAndroid.assert_called_once_with(self.device, 'chrome.apk')
class AndroidTrichromeReleaseBuildTest(AndroidBuildTest):
def setUp(self):
super().setUp()
self.set_sdk_level(bisect_builds.version_codes.Q)
@maybe_patch(
'bisect-builds.GsutilList',
side_effect=[[
'gs://chrome-unsigned/android-B0urB0N/%s/' % x for x in [
'129.0.6626.0', '129.0.6626.1', '129.0.6627.0', '129.0.6627.1',
'129.0.6628.0', '129.0.6628.1'
]
],
[('gs://chrome-unsigned/android-B0urB0N/%s/'
'high-arm_64/TrichromeChromeGoogle6432Stable.apks') % x
for x in ['129.0.6626.0', '129.0.6627.0', '129.0.6628.0']]])
def test_get_rev_list(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64-high', '--apk', 'chrome_stable', '-g',
'129.0.6626.0', '-b', '129.0.6628.0'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeReleaseBuild)
self.assertEqual(build.get_rev_list(),
['129.0.6626.0', '129.0.6627.0', '129.0.6628.0'])
print(mock_GsutilList.call_args_list)
mock_GsutilList.assert_any_call('gs://chrome-unsigned/android-B0urB0N')
mock_GsutilList.assert_any_call(*[
('gs://chrome-unsigned/android-B0urB0N/%s/'
'high-arm_64/TrichromeChromeGoogle6432Stable.apks') % x for x in [
'129.0.6626.0', '129.0.6626.1', '129.0.6627.0', '129.0.6627.1',
'129.0.6628.0'
]
],
ignore_fail=True)
self.assertEqual(mock_GsutilList.call_count, 2)
def test_should_raise_exception_for_PIE(self):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64-high', '--apk', 'chrome_stable', '-g',
'129.0.6626.0', '-b', '129.0.6667.0'
])
self.set_sdk_level(bisect_builds.version_codes.PIE)
with self.assertRaises(bisect_builds.BisectException):
bisect_builds.create_archive_build(options)
def test_get_download_url(self):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64-high', '--apk', 'chrome_stable', '-g',
'129.0.6626.0', '-b', '129.0.6628.0'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeReleaseBuild)
download_urls = build.get_download_url('129.0.6626.0')
self.maxDiff = 1000
self.assertDictEqual(
download_urls, {
'trichrome':
('gs://chrome-unsigned/android-B0urB0N/129.0.6626.0/high-arm_64/'
'TrichromeChromeGoogle6432Stable.apks'),
'trichrome_library':
('gs://chrome-unsigned/android-B0urB0N/129.0.6626.0/high-arm_64/'
'TrichromeLibraryGoogle6432Stable.apk'),
})
@patch('bisect-builds.InstallOnAndroid')
def test_install_revision(self, mock_InstallOnAndroid):
downloads = {
'trichrome': 'some-file.apks',
'trichrome_library': 'file2.apk',
}
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'android-arm64-high', '--apk', 'chrome_stable', '-g',
'129.0.6626.0', '-b', '129.0.6628.0'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeReleaseBuild)
build._install_revision(downloads, 'tmp-dir')
mock_InstallOnAndroid.assert_any_call(self.device, 'some-file.apks')
mock_InstallOnAndroid.assert_any_call(self.device, 'file2.apk')
class AndroidTrichromeOfficialBuildTest(AndroidBuildTest):
@maybe_patch('bisect-builds.GsutilList',
return_value=[
'full-build-linux_%d.zip' % x
for x in [1334339, 1334342, 1334344, 1334345, 1334356]
])
def test_get_rev_list(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine([
'-o', '-a', 'android-arm64-high', '--apk', 'chrome', '-g', '1334338',
'-b', '1334380'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeOfficialBuild)
self.assertEqual(build.get_rev_list(),
[1334339, 1334342, 1334344, 1334345, 1334356])
mock_GsutilList.assert_called_once_with(
'gs://chrome-test-builds/official-by-commit/'
'android_arm64_high_end-builder-perf/')
def test_get_download_url(self):
options, args = bisect_builds.ParseCommandLine([
'-o', '-a', 'android-arm64-high', '--apk', 'chrome', '-g', '1334338',
'-b', '1334380'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeOfficialBuild)
self.assertEqual(
build.get_download_url(1334338),
'gs://chrome-test-builds/official-by-commit'
'/android_arm64_high_end-builder-perf/full-build-linux_1334338.zip')
@patch('glob.glob',
side_effect=[[
'temp-dir/full-build-linux/apks/TrichromeChromeGoogle6432.apks'
], ['temp-dir/full-build-linux/apks/TrichromeLibraryGoogle6432.apk']])
@patch('bisect-builds.UnzipFilenameToDir')
@patch('bisect-builds.InstallOnAndroid')
def test_install_revision(self, mock_InstallOnAndroid,
mock_UnzipFilenameToDir, mock_glob):
options, args = bisect_builds.ParseCommandLine([
'-o', '-a', 'android-arm64-high', '--apk', 'chrome', '-g', '1334338',
'-b', '1334380'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeOfficialBuild)
build._install_revision('download.zip', 'tmp-dir')
mock_UnzipFilenameToDir.assert_called_once_with('download.zip', 'tmp-dir')
mock_InstallOnAndroid.assert_any_call(
self.device,
'temp-dir/full-build-linux/apks/TrichromeLibraryGoogle6432.apk')
mock_InstallOnAndroid.assert_any_call(
self.device,
'temp-dir/full-build-linux/apks/TrichromeChromeGoogle6432.apks')
@unittest.skipUnless('NO_MOCK_SERVER' in os.environ,
'The test only valid when NO_MOCK_SERVER')
@patch('bisect-builds.InstallOnAndroid')
@patch('bisect-builds.LaunchOnAndroid')
def test_run_revision_with_real_zipfile(self, mock_LaunchOnAndroid,
mock_InstallOnAndroid):
options, args = bisect_builds.ParseCommandLine([
'-o', '-a', 'android-arm64-high', '--apk', 'chrome', '-g', '1334338',
'-b', '1334380'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.AndroidTrichromeOfficialBuild)
download_job = build.get_download_job(1334339)
zip_file = download_job.start().wait_for()
build.run_revision(zip_file, [])
print(mock_InstallOnAndroid.call_args_list)
self.assertRegex(mock_InstallOnAndroid.mock_calls[0].args[1],
'full-build-linux/apks/TrichromeLibraryGoogle6432.apk$')
self.assertRegex(
mock_InstallOnAndroid.mock_calls[1].args[1],
'full-build-linux/apks/TrichromeChromeGoogle6432.minimal.apks$')
mock_LaunchOnAndroid.assert_called_once_with(self.device, 'chrome')
class LinuxReleaseBuildTest(BisectTestCase):
@patch('subprocess.Popen', spec=subprocess.Popen)
def test_launch_revision_should_has_no_sandbox(self, mock_Popen):
mock_Popen.return_value.communicate.return_value = ('', '')
mock_Popen.return_value.returncode = 0
options, args = bisect_builds.ParseCommandLine(
['-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88'])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.LinuxReleaseBuild)
build._launch_revision('temp-dir', 'temp-dir/linux64/chrome', [])
mock_Popen.assert_called_once_with(
['temp-dir/linux64/chrome', '--user-data-dir=profile', '--no-sandbox'],
cwd='temp-dir',
bufsize=-1,
stdout=ANY,
stderr=ANY)
class IOSReleaseBuildTest(BisectTestCase):
@maybe_patch(
'bisect-builds.GsutilList',
side_effect=[[
'gs://chrome-unsigned/ios-G1N/127.0.6533.76/',
'gs://chrome-unsigned/ios-G1N/127.0.6533.77/',
'gs://chrome-unsigned/ios-G1N/127.0.6533.78/'
],
[
'gs://chrome-unsigned/ios-G1N'
'/127.0.6533.76/iphoneos17.5/ios/10863/canary.ipa',
'gs://chrome-unsigned/ios-G1N'
'/127.0.6533.77/iphoneos17.5/ios/10866/canary.ipa',
'gs://chrome-unsigned/ios-G1N'
'/127.0.6533.78/iphoneos17.5/ios/10868/canary.ipa'
]])
def test_list_rev(self, mock_GsutilList):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g',
'127.0.6533.74', '-b', '127.0.6533.78'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.IOSReleaseBuild)
self.assertEqual(build.get_rev_list(),
['127.0.6533.76', '127.0.6533.77', '127.0.6533.78'])
mock_GsutilList.assert_any_call('gs://chrome-unsigned/ios-G1N')
mock_GsutilList.assert_any_call(*[
'gs://chrome-unsigned/ios-G1N/%s/*/ios/*/canary.ipa' % x
for x in ['127.0.6533.76', '127.0.6533.77', '127.0.6533.78']
],
ignore_fail=True)
@patch('bisect-builds.UnzipFilenameToDir')
@patch('glob.glob', return_value=['Payload/canary.app/Info.plist'])
@patch('subprocess.Popen', spec=subprocess.Popen)
def test_install_revision(self, mock_Popen, mock_glob,
mock_UnzipFilenameToDir):
mock_Popen.return_value.communicate.return_value = ('', '')
mock_Popen.return_value.returncode = 0
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g',
'127.0.6533.74', '-b', '127.0.6533.78'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.IOSReleaseBuild)
build._install_revision('canary.ipa', 'tempdir')
mock_glob.assert_called_once_with('tempdir/Payload/*/Info.plist')
mock_Popen.assert_has_calls([
call([
'xcrun', 'devicectl', 'device', 'install', 'app', '--device', '321',
'canary.ipa'
],
cwd=None,
bufsize=-1,
stdout=-1,
stderr=-1),
call([
'plutil', '-extract', 'CFBundleIdentifier', 'raw',
'Payload/canary.app/Info.plist'
],
cwd=None,
bufsize=-1,
stdout=-1,
stderr=-1)
],
any_order=True)
@patch('subprocess.Popen', spec=subprocess.Popen)
def test_launch_revision(self, mock_Popen):
mock_Popen.return_value.communicate.return_value = ('', '')
mock_Popen.return_value.returncode = 0
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g',
'127.0.6533.74', '-b', '127.0.6533.78'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.IOSReleaseBuild)
build._launch_revision('tempdir', 'com.google.chrome.ios',
['args1', 'args2'])
mock_Popen.assert_any_call([
'xcrun', 'devicectl', 'device', 'process', 'launch', '--device', '321',
'com.google.chrome.ios', 'args1', 'args2'
],
cwd=None,
bufsize=-1,
stdout=-1,
stderr=-1)
@unittest.skipUnless('NO_MOCK_SERVER' in os.environ,
'The test only valid when NO_MOCK_SERVER')
@patch('bisect-builds.IOSReleaseBuild._run', return_value=(0, 'stdout', ''))
def test_run_revision(self, mock_run):
options, args = bisect_builds.ParseCommandLine([
'-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g',
'127.0.6533.74', '-b', '127.0.6533.78'
])
build = bisect_builds.create_archive_build(options)
self.assertIsInstance(build, bisect_builds.IOSReleaseBuild)
job = build.get_download_job('127.0.6533.76')
ipa = job.start().wait_for()
build.run_revision(ipa, args)
mock_run.assert_has_calls([
call([
'xcrun', 'devicectl', 'device', 'install', 'app', '--device', '321',
ANY
]),
call(['plutil', '-extract', 'CFBundleIdentifier', 'raw', ANY]),
call([
'xcrun', 'devicectl', 'device', 'process', 'launch', '--device',
'321', 'stdout'
])
])
if __name__ == '__main__':
unittest.main()