# 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 base64 import functools import io import json import os import re import subprocess import sys import tempfile import unittest import urllib.request from unittest.mock import (ANY, Mock, MagicMock, mock_open, patch, call, PropertyMock) 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', '-g', '1']) 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): # Patch the name pattern for pkgutil to accept "bisect-builds" as module # name. if sys.version_info[:2] > (3, 8): dotted_words = r'(?!\d)([\w-]+)(\.(?!\d)(\w+))*' name_pattern = re.compile( f'^(?P{dotted_words})' f'(?P:(?P{dotted_words})?)?$', re.UNICODE) cls.name_pattern_patcher = patch('pkgutil._NAME_PATTERN', name_pattern) cls.name_pattern_patcher.start() # patch cache filename to prevent pollute working dir. fd, cls.tmp_cache_file = tempfile.mkstemp(suffix='.json') os.close(fd) cls.cache_filename_patcher = patch( 'bisect-builds.ArchiveBuild._rev_list_cache_filename', new=PropertyMock(return_value=cls.tmp_cache_file)) cls.cache_filename_patcher.start() @classmethod def tearDownClass(cls): if sys.version_info[:2] > (3, 8): cls.name_pattern_patcher.stop() cls.cache_filename_patcher.stop() os.unlink(cls.tmp_cache_file) class BisectTest(BisectTestCase): max_rev = 10000 def setUp(self): self.patchers = [] self.patchers.append(patch('bisect-builds.DownloadJob._fetch')) self.patchers.append( patch('bisect-builds.ArchiveBuild.run_revision', return_value=(0, '', ''))) self.patchers.append( patch('bisect-builds.SnapshotBuild._get_rev_list', return_value=range(self.max_rev))) for each in self.patchers: each.start() def tearDown(self): for each in self.patchers: each.stop() def bisect(self, good_rev, bad_rev, evaluate, num_runs=1): options = bisect_builds.ParseCommandLine([ '-a', 'linux64', '-g', str(good_rev), '-b', str(bad_rev), '--times', str(num_runs), '--no-local-cache' ]) archive_build = bisect_builds.create_archive_build(options) (minrev, maxrev) = bisect_builds.Bisect(archive_build=archive_build, evaluate=evaluate, try_args=options.args) return (minrev, maxrev) @patch('builtins.print') def testBisectConsistentAnswer(self, mock_print): def get_steps(): steps = [] for call in mock_print.call_args_list: if call.args and call.args[0].startswith('You have'): steps.append(int(re.search(r'(\d+) steps', call.args[0])[1])) return steps self.assertEqual(self.bisect(1000, 100, lambda *args: 'g'), (100, 101)) self.assertSequenceEqual(get_steps(), range(10, 1, -1)) mock_print.reset_mock() self.assertEqual(self.bisect(100, 1000, lambda *args: 'b'), (100, 101)) self.assertSequenceEqual(get_steps(), range(10, 0, -1)) mock_print.reset_mock() self.assertEqual(self.bisect(2000, 200, lambda *args: 'b'), (1999, 2000)) self.assertSequenceEqual(get_steps(), range(11, 0, -1)) mock_print.reset_mock() self.assertEqual(self.bisect(200, 2000, lambda *args: 'g'), (1999, 2000)) self.assertSequenceEqual(get_steps(), range(11, 1, -1)) @patch('bisect-builds.ArchiveBuild.run_revision', return_value=(0, '', '')) def test_bisect_should_retry(self, mock_run_revision): evaluator = Mock(side_effect='rgrgrbr') self.assertEqual(self.bisect(9, 1, evaluator), (2, 3)) tested_revisions = [c.args[0] for c in evaluator.call_args_list] self.assertEqual(tested_revisions, [5, 5, 3, 3, 2, 2]) self.assertEqual(mock_run_revision.call_count, 6) evaluator = Mock(side_effect='rgrrrgrbr') self.assertEqual(self.bisect(1, 10, evaluator), (8, 9)) tested_revisions = [c.args[0] for c in evaluator.call_args_list] self.assertEqual(tested_revisions, [6, 6, 8, 8, 8, 8, 9, 9]) def test_bisect_should_unknown(self): evaluator = Mock(side_effect='uuuggggg') self.assertEqual(self.bisect(9, 1, evaluator), (1, 2)) tested_revisions = [c.args[0] for c in evaluator.call_args_list] self.assertEqual(tested_revisions, [5, 3, 6, 7, 2]) evaluator = Mock(side_effect='uuugggggg') self.assertEqual(self.bisect(1, 9, evaluator), (8, 9)) tested_revisions = [c.args[0] for c in evaluator.call_args_list] self.assertEqual(tested_revisions, [5, 7, 4, 3, 8]) def test_bisect_should_quit(self): evaluator = Mock(side_effect=SystemExit()) with self.assertRaises(SystemExit): self.assertEqual(self.bisect(9, 1, evaluator), (None, None)) def test_edge_cases(self): with self.assertRaises(bisect_builds.BisectException): self.assertEqual(self.bisect(1, 1, Mock()), (1, 1)) self.assertEqual(self.bisect(2, 1, Mock()), (1, 2)) self.assertEqual(self.bisect(1, 2, Mock()), (1, 2)) 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) mock_unlink.assert_has_calls([call('some-file.apks'), call('file2.apk')]) self.assertEqual(mock_gsutil.call_count, 2) @patch( "urllib.request.urlopen", side_effect=urllib.request.HTTPError('url', 404, 'Not Found', None, None), ) @patch('subprocess.Popen', spec=subprocess.Popen) @patch('bisect-builds.GSUTILS_PATH', new='/some/path') def test_download_failure_should_raised(self, mock_Popen, mock_urlopen): fetch = bisect_builds.DownloadJob('http://some-file.zip', 123) with self.assertRaises(urllib.request.HTTPError): fetch.start().wait_for() mock_Popen.return_value.communicate.return_value = (b'', b'status=403') mock_Popen.return_value.returncode = 1 fetch = bisect_builds.DownloadJob('gs://some-file.zip', 123) with self.assertRaises(bisect_builds.BisectException): fetch.start().wait_for() 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): args = ['-a', 'linux64', '-g', '0', '-b', '9', *args] options = bisect_builds.ParseCommandLine(args) return bisect_builds.ArchiveBuild(options) def test_cache_should_not_work_if_not_enabled(self): build = self.create_build('--no-local-cache') 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() 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('--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('--no-local-cache') # 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('--no-local-cache') 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('--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'), {'chrome': '/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('bisect-builds.UnzipFilenameToDir') @patch('glob.glob', side_effect=[['temp-dir/chrome-linux64/chrome'], ['temp-dir/chromedriver_linux64/chromedriver']]) @patch('os.path.abspath', side_effect=[ '/tmp/temp-dir/chrome-linux64/chrome', '/tmp/temp-dir/chromedriver_linux64/chromedriver' ]) def test_install_chromedriver(self, mock_abspath, mock_glob, mock_UnzipFilenameToDir): build = self.create_build('--chromedriver') self.assertEqual( build._install_revision( { 'chrome': 'some-file.zip', 'chromedriver': 'some-other-file.zip', }, 'temp-dir'), { 'chrome': '/tmp/temp-dir/chrome-linux64/chrome', 'chromedriver': '/tmp/temp-dir/chromedriver_linux64/chromedriver', }) mock_UnzipFilenameToDir.assert_has_calls([ call('some-file.zip', 'temp-dir'), call('some-other-file.zip', 'temp-dir'), ]) mock_glob.assert_has_calls([ call('temp-dir/*/chrome'), call('temp-dir/*/chromedriver'), ]) mock_abspath.assert_has_calls([ call('temp-dir/chrome-linux64/chrome'), call('temp-dir/chromedriver_linux64/chromedriver') ]) @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', {'chrome': 'temp-dir/linux64/chrome'}, []) mock_Popen.assert_called_once_with( 'temp-dir/linux64/chrome --user-data-dir=temp-dir/profile', cwd=None, shell=True, bufsize=-1, stdout=ANY, stderr=ANY) @unittest.skipIf(sys.platform.startswith('win'), 'This test is not for win') @patch('subprocess.Popen', spec=subprocess.Popen) def test_launch_revision_should_run_command_for_mac(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', { 'chrome': 'temp-dir/full-build-mac/' 'Google Chrome.app/Contents/MacOS/Google Chrome' }, []) mock_Popen.assert_called_once_with( "'temp-dir/full-build-mac/" "Google Chrome.app/Contents/MacOS/Google Chrome'" ' --user-data-dir=temp-dir/profile', cwd=None, shell=True, bufsize=-1, stdout=ANY, stderr=ANY) @unittest.skipUnless(sys.platform.startswith('win'), 'This test is for win only') @patch('subprocess.Popen', spec=subprocess.Popen) def test_launch_revision_should_run_command_for_win(self, mock_Popen): mock_Popen.return_value.communicate.return_value = ('', '') mock_Popen.return_value.returncode = 0 build = self.create_build() build._launch_revision( 'C:\\temp-dir', { 'chrome': 'C:\\temp-dir\\full-build-win\\chrome.exe' }, []) mock_Popen.assert_called_once_with( "C:\\temp-dir\\full-build-win\\chrome.exe " '--user-data-dir=C:\\temp-dir/profile', cwd=None, shell=True, bufsize=-1, stdout=ANY, stderr=ANY) @patch('subprocess.Popen', spec=subprocess.Popen) def test_command_replacement(self, mock_Popen): mock_Popen.return_value.communicate.return_value = ('', '') mock_Popen.return_value.returncode = 0 build = self.create_build( '--chromedriver', '-c', 'CHROMEDRIVER=%d BROWSER_EXECUTABLE_PATH=%p pytest %a') build._launch_revision('/tmp', { 'chrome': '/tmp/chrome', 'chromedriver': '/tmp/chromedriver' }, ['--args', '--args2="word 1"', 'word 2']) if sys.platform.startswith('win'): mock_Popen.assert_called_once_with( 'CHROMEDRIVER=/tmp/chromedriver BROWSER_EXECUTABLE_PATH=/tmp/chrome ' 'pytest --user-data-dir=/tmp/profile --args "--args2=\\"word 1\\"" ' '"word 2"', cwd=None, shell=True, bufsize=-1, stdout=ANY, stderr=ANY) else: mock_Popen.assert_called_once_with( 'CHROMEDRIVER=/tmp/chromedriver BROWSER_EXECUTABLE_PATH=/tmp/chrome ' 'pytest --user-data-dir=/tmp/profile --args \'--args2="word 1"\' ' '\'word 2\'', cwd=None, shell=True, bufsize=-1, stdout=ANY, stderr=ANY) class ReleaseBuildTest(BisectTestCase): def test_should_look_up_path_context(self): options = 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.76', '--no-local-cache' ]) 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 = 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) def test_get_download_url(self): options = bisect_builds.ParseCommandLine( ['-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.77']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ReleaseBuild) download_urls = build.get_download_url('127.0.6533.74') self.assertEqual( download_urls, 'gs://chrome-unsigned/desktop-5c0tCh' '/127.0.6533.74/linux64/chrome-linux64.zip') options = bisect_builds.ParseCommandLine([ '-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.77', '--chromedriver' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ReleaseBuild) download_urls = build.get_download_url('127.0.6533.74') self.assertEqual( download_urls, { 'chrome': 'gs://chrome-unsigned/desktop-5c0tCh' '/127.0.6533.74/linux64/chrome-linux64.zip', 'chromedriver': 'gs://chrome-unsigned/desktop-5c0tCh' '/127.0.6533.74/linux64/chromedriver_linux64.zip', }) @unittest.skipUnless('NO_MOCK_SERVER' in os.environ, 'The test only valid when NO_MOCK_SERVER') @patch('bisect-builds.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision_with_real_zipfile(self, mock_run): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.77', '--chromedriver', '-c', 'driver=%d prog=%p' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ReleaseBuild) download_job = build.get_download_job('127.0.6533.74') zip_file = download_job.start().wait_for() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(zip_file, tempdir, []) self.assertRegex(mock_run.call_args.args[0], r'driver=.+/chromedriver prog=.+/chrome') 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 = 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 = 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 = bisect_builds.ParseCommandLine([ '-o', '-a', 'linux64', '-g', '1313161', '-b', '1313163', '--no-local-cache' ]) 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/') @unittest.skipUnless('NO_MOCK_SERVER' in os.environ, 'The test only valid when NO_MOCK_SERVER') @patch('bisect-builds.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision_with_real_zipfile(self, mock_run): options = bisect_builds.ParseCommandLine([ '-o', '-a', 'linux64', '-g', '1313161', '-b', '1313163', '--chromedriver', '-c', 'driver=%d prog=%p' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.OfficialBuild) download_job = build.get_download_job(1334339) zip_file = download_job.start().wait_for() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(zip_file, tempdir, []) self.assertRegex(mock_run.call_args.args[0], r'driver=.+/chromedriver prog=.+/chrome') class SnapshotBuildTest(BisectTestCase): def test_should_lookup_path_context(self): options = 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 = ''' chromium-browser-snapshots Linux_x64/ / true Linux_x64/1313161/ Linux_x64/1313163/ Linux_x64/1313185/ ''' @maybe_patch('urllib.request.urlopen', return_value=io.BytesIO(CommonDataXMLContent.encode('utf8'))) @patch('bisect-builds.GetChromiumRevision', return_value=1313185) def test_get_rev_list(self, mock_GetChromiumRevision, mock_urlopen): options = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '1313161', '-b', '1313185', '--no-local-cache']) 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 = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '0', '-b', '9', '--no-local-cache']) 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 = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '3', '-b', '11', '--no-local-cache']) 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/') def test_get_download_url(self): options = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '3', '-b', '11']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.SnapshotBuild) download_urls = build.get_download_url(123) self.assertEqual( download_urls, 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' '/Linux_x64/123/chrome-linux.zip', ) options = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '3', '-b', '11', '--chromedriver']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.SnapshotBuild) download_urls = build.get_download_url(123) self.assertDictEqual( download_urls, { 'chrome': 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' '/Linux_x64/123/chrome-linux.zip', 'chromedriver': 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' '/Linux_x64/123/chromedriver_linux64.zip' }) @unittest.skipUnless('NO_MOCK_SERVER' in os.environ, 'The test only valid when NO_MOCK_SERVER') @patch('bisect-builds.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision_with_real_zipfile(self, mock_run): options = bisect_builds.ParseCommandLine([ '-a', 'linux64', '-g', '1313161', '-b', '1313185', '--chromedriver', '-c', 'driver=%d prog=%p' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.SnapshotBuild) download_job = build.get_download_job(1313161) zip_file = download_job.start().wait_for() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(zip_file, tempdir, []) self.assertRegex(mock_run.call_args.args[0], r'driver=.+/chromedriver prog=.+/chrome') @patch('bisect-builds.GetChromiumRevision', return_value=1313185) def test_get_bad_revision(self, mock_GetChromiumRevision): options = bisect_builds.ParseCommandLine(['-a', 'linux64', '-g', '1313161']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.SnapshotBuild) mock_GetChromiumRevision.assert_called_once_with( 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' '/Linux_x64/LAST_CHANGE') self.assertEqual(build.bad_revision, 1313185) class ChromeForTestingBuild(BisectTestCase): def test_should_lookup_path_context(self): options = bisect_builds.ParseCommandLine( ['-cft', '-a', 'linux64', '-g', '0', '-b', '10']) self.assertEqual(options.archive, 'linux64') build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) self.assertEqual(build.binary_name, 'chrome') self.assertEqual(build.listing_platform_dir, 'linux64/') self.assertEqual(build.archive_name, 'chrome-linux64.zip') self.assertEqual(build.chromedriver_binary_name, 'chromedriver') self.assertEqual(build.chromedriver_archive_name, 'chromedriver-linux64.zip') CommonDataXMLContent = ''' chrome-for-testing-per-commit-public linux64/ / false linux64/LAST_CHANGE 1733959087133532 1 2024-12-11T23:18:07.235Z "dd60cb93e225ab33d7254beca56b507a" 7 linux64/r1390729/ linux64/r1390746/ linux64/r1390757/ ''' @maybe_patch('urllib.request.urlopen', return_value=io.BytesIO(CommonDataXMLContent.encode('utf8'))) @patch('bisect-builds.GetChromiumRevision', return_value=1390757) def test_get_rev_list(self, mock_GetChromiumRevision, mock_urlopen): options = bisect_builds.ParseCommandLine([ '-cft', '-a', 'linux64', '-g', '1390729', '-b', '1390757', '--no-local-cache' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) rev_list = build.get_rev_list() mock_urlopen.assert_any_call( 'https://storage.googleapis.com/chrome-for-testing-per-commit-public/' '?delimiter=/&prefix=linux64/&marker=linux64/r1390729') self.assertEqual(mock_urlopen.call_count, 1) self.assertEqual(rev_list, [1390729, 1390746, 1390757]) def test_get_marker(self): options = bisect_builds.ParseCommandLine( ['-cft', '-a', 'linux64', '-g', '1', '-b', '3']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) self.assertEqual('linux64/r1390729', build._get_marker_for_revision(1390729)) def test_get_download_url(self): options = bisect_builds.ParseCommandLine( ['-cft', '-a', 'linux64', '-g', '3', '-b', '11']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) download_urls = build.get_download_url(123) self.assertEqual( download_urls, 'https://storage.googleapis.com/chrome-for-testing-per-commit-public' '/linux64/r123/chrome-linux64.zip', ) options = bisect_builds.ParseCommandLine( ['-cft', '-a', 'linux64', '-g', '3', '-b', '11', '--chromedriver']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) download_urls = build.get_download_url(123) download_urls = build.get_download_url(123) self.assertEqual( download_urls, { 'chrome': 'https://storage.googleapis.com/chrome-for-testing-per-commit-public' '/linux64/r123/chrome-linux64.zip', 'chromedriver': 'https://storage.googleapis.com/chrome-for-testing-per-commit-public' '/linux64/r123/chromedriver-linux64.zip', }) @patch('bisect-builds.GetChromiumRevision', return_value=1390757) def test_get_bad_revision(self, mock_GetChromiumRevision): options = bisect_builds.ParseCommandLine( ['-cft', '-a', 'linux64', '-g', '3']) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.ChromeForTestingBuild) mock_GetChromiumRevision.assert_called_once_with( 'https://storage.googleapis.com/chrome-for-testing-per-commit-public' '/linux64/LAST_CHANGE') self.assertEqual(build.bad_revision, 1390757) @unittest.skipUnless('NO_MOCK_SERVER' in os.environ, 'The test only valid when NO_MOCK_SERVER') @patch('bisect-builds.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision_with_real_zipfile(self, mock_run): options = bisect_builds.ParseCommandLine([ '--cft', '-a', 'linux64', '-g', '1', '--chromedriver', '-c', 'driver=%d prog=%p' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.SnapshotBuild) download_job = build.get_download_job(build.bad_revision) zip_file = download_job.start().wait_for() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(zip_file, tempdir, []) self.assertRegex(mock_run.call_args.args[0], r'driver=.+/chromedriver prog=.+/chrome') class ASANBuildTest(BisectTestCase): CommonDataXMLContent = ''' chromium-browser-asan mac-release/asan-mac-release .zip true mac-release/asan-mac-release-1313186.zip mac-release/asan-mac-release-1313195.zip mac-release/asan-mac-release-1313210.zip ''' @maybe_patch('urllib.request.urlopen', return_value=io.BytesIO(CommonDataXMLContent.encode('utf8'))) def test_get_rev_list(self, mock_urlopen): options = bisect_builds.ParseCommandLine([ '--asan', '-a', 'mac', '-g', '1313161', '-b', '1313210', '--no-local-cache' ]) 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'android-arm64', '--apk', 'chrome_stable', '-g', '127.0.6533.76', '-b', '127.0.6533.79', '--signed', '--no-local-cache' ]) 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'android-arm64', '-g', '127.0.6533.76', '-b', '127.0.6533.79', '--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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'android-arm64', '-g', '127.0.6533.76', '-b', '127.0.6533.79', '--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') @patch('bisect-builds.LaunchOnAndroid') def test_webview_launch_revision(self, mock_LaunchOnAndroid): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'android-arm64', '-g', '127.0.6533.76', '-b', '127.0.6533.79', '--apk', 'system_webview' ]) 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, 'system_webview') with self.assertRaises(bisect_builds.BisectException): build._launch_revision('temp-dir', None, ['args']) class AndroidSnapshotBuildTest(AndroidBuildTest): def setUp(self): super().setUp() self.set_sdk_level(bisect_builds.version_codes.PIE) @patch('bisect-builds.UnzipFilenameToDir') @patch('bisect-builds.InstallOnAndroid') @patch('glob.glob', return_value=['Monochrome.apk']) def test_install_revision(self, mock_glob, mock_InstallOnAndroid, mock_unzip): options = 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_glob.assert_called_once_with('temp-dir/*/apks/Monochrome.apk') mock_InstallOnAndroid.assert_called_once_with(self.device, 'Monochrome.apk') @patch('bisect-builds.UnzipFilenameToDir') @patch('sys.stdout', new_callable=io.StringIO) @patch('glob.glob', side_effect=[[], [ "temp-dir/full-build-linux/apks/MonochromeBeta.apk", "temp-dir/full-build-linux/apks/ChromePublic.apk" ]]) def test_install_revision_with_show_available_apks(self, mock_glob, mock_stdout, mock_unzip): options = 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) with self.assertRaises(bisect_builds.BisectException): build._install_revision('chrome.zip', 'temp-dir') self.assertIn("The list of available --apk:", mock_stdout.getvalue()) self.assertIn("chrome_beta", mock_stdout.getvalue()) self.assertIn("chromium", mock_stdout.getvalue()) @patch('bisect-builds.UnzipFilenameToDir') @patch('sys.stdout', new_callable=io.StringIO) @patch('glob.glob', side_effect=[[], ["temp-dir/full-build-linux/apks/unknown.apks"]]) def test_install_revision_with_show_unknown_apks(self, mock_glob, mock_stdout, mock_unzip): options = 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) with self.assertRaises(bisect_builds.BisectException): build._install_revision('chrome.zip', 'temp-dir') self.assertIn("No supported apk found. But found following", mock_stdout.getvalue()) self.assertIn("unknown.apks", mock_stdout.getvalue()) 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'android-arm64-high', '--apk', 'chrome_stable', '-g', '129.0.6626.0', '-b', '129.0.6628.0', '--no-local-cache' ]) 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 = 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 = 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 = 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 = bisect_builds.ParseCommandLine([ '-o', '-a', 'android-arm64-high', '--apk', 'chrome', '-g', '1334338', '-b', '1334380', '--no-local-cache' ]) 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 = 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 = 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') @patch('sys.stdout', new_callable=io.StringIO) @patch('glob.glob', side_effect=[[], ['temp-dir/TrichromeChromeGoogle6432Canary.minimal.apks'] ]) @patch('bisect-builds.UnzipFilenameToDir') def test_install_revision_with_show_available_apks(self, mock_UnzipFilenameToDir, mock_glob, mock_stdout): options = 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) with self.assertRaises(bisect_builds.BisectException): build._install_revision('download.zip', 'tmp-dir') self.assertIn("The list of available --apk:", mock_stdout.getvalue()) self.assertIn("chrome_canary", mock_stdout.getvalue()) @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 = 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() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(zip_file, tempdir, []) 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 = 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', {'chrome': 'temp-dir/linux64/chrome'}, []) mock_Popen.assert_called_once_with( 'temp-dir/linux64/chrome --user-data-dir=temp-dir/profile --no-sandbox', cwd=None, shell=True, 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g', '127.0.6533.74', '-b', '127.0.6533.78', '--no-local-cache' ]) 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 = 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, shell=False, bufsize=-1, stdout=-1, stderr=-1), call([ 'plutil', '-extract', 'CFBundleIdentifier', 'raw', 'Payload/canary.app/Info.plist' ], cwd=None, shell=False, 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 = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g', '127.0.6533.74', '-b', '127.0.6533.78', '--', 'args1', 'args2' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.IOSReleaseBuild) build._launch_revision('tempdir', 'com.google.chrome.ios', options.args) mock_Popen.assert_any_call([ 'xcrun', 'devicectl', 'device', 'process', 'launch', '--device', '321', 'com.google.chrome.ios', 'args1', 'args2' ], cwd=None, shell=False, 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.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision(self, mock_run): options = 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() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(ipa, tempdir, options.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' ]) ]) class IOSSimulatorReleaseBuildTest(BisectTestCase): @maybe_patch( 'bisect-builds.GsutilList', side_effect=[ [ 'gs://bling-archive/128.0.6534.0/', 'gs://bling-archive/128.0.6534.1/', 'gs://bling-archive/128.0.6535.0/', 'gs://bling-archive/128.0.6535.1/', 'gs://bling-archive/128.0.6536.0/', ], [ 'gs://bling-archive/128.0.6534.0/20240612011643/Chromium.tar.gz', 'gs://bling-archive/128.0.6536.0/20240613011356/Chromium.tar.gz', ] ]) @patch('bisect-builds.ArchiveBuild._run', return_value=(0, '', '')) def test_list_rev(self, mock_run, mock_GsutilList): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios-simulator', '--device-id', '321', '-g', '128.0.6534.0', '-b', '128.0.6536.0', '--no-local-cache' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.IOSSimulatorReleaseBuild) self.assertEqual(build.get_rev_list(), ['128.0.6534.0', '128.0.6536.0']) mock_run.assert_called_once_with(['xcrun', 'simctl', 'boot', '321']) mock_GsutilList.assert_any_call('gs://bling-archive') mock_GsutilList.assert_any_call(*[ 'gs://bling-archive/%s/*/Chromium.tar.gz' % x for x in [ '128.0.6534.0', '128.0.6534.1', '128.0.6535.0', '128.0.6535.1', '128.0.6536.0' ] ], ignore_fail=True) @patch('bisect-builds.UnzipFilenameToDir') @patch('glob.glob', return_value=['Info.plist']) @patch('bisect-builds.ArchiveBuild._run', return_value=(0, '', '')) def test_install_revision(self, mock_run, mock_glob, mock_UnzipFilenameToDir): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios-simulator', '--device-id', '321', '-g', '128.0.6534.0', '-b', '128.0.6539.0' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.IOSSimulatorReleaseBuild) build._install_revision('Chromium.tar.gz', 'tempdir') mock_UnzipFilenameToDir.assert_called_once_with('Chromium.tar.gz', 'tempdir') self.assertEqual(mock_glob.call_count, 2) mock_run.assert_has_calls([ call(['xcrun', 'simctl', 'boot', '321']), call(['xcrun', 'simctl', 'install', '321', ANY]), call(['plutil', '-extract', 'CFBundleIdentifier', 'raw', 'Info.plist']), ]) @patch('bisect-builds.ArchiveBuild._run', return_value=(0, '', '')) def test_launch_revision(self, mock_run): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios-simulator', '--device-id', '321', '-g', '128.0.6534.0', '-b', '128.0.6539.0' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.IOSSimulatorReleaseBuild) build._launch_revision('tempdir', 'com.google.chrome.ios.dev', ['args1', 'args2']) mock_run.assert_any_call([ 'xcrun', 'simctl', 'launch', '321', 'com.google.chrome.ios.dev', 'args1', 'args2' ]) @unittest.skipUnless('NO_MOCK_SERVER' in os.environ, 'The test only valid when NO_MOCK_SERVER') @patch('bisect-builds.ArchiveBuild._run', return_value=(0, 'stdout', '')) def test_run_revision(self, mock_run): options = bisect_builds.ParseCommandLine([ '-r', '-a', 'ios-simulator', '--device-id', '321', '-g', '128.0.6534.0', '-b', '128.0.6539.0' ]) build = bisect_builds.create_archive_build(options) self.assertIsInstance(build, bisect_builds.IOSSimulatorReleaseBuild) job = build.get_download_job('128.0.6534.0') download = job.start().wait_for() with tempfile.TemporaryDirectory(prefix='bisect_tmp') as tempdir: build.run_revision(download, tempdir, options.args) mock_run.assert_has_calls([ call(['xcrun', 'simctl', 'boot', '321']), call(['xcrun', 'simctl', 'install', '321', ANY]), call(['plutil', '-extract', 'CFBundleIdentifier', 'raw', ANY]), call(['xcrun', 'simctl', 'launch', '321', 'stdout']) ]) class MaybeSwitchBuildTypeTest(BisectTestCase): def test_generate_new_command_without_cache(self): command_line = [ '-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88', '--no-local-cache' ] options = bisect_builds.ParseCommandLine(command_line) with patch('sys.argv', ['bisect-builds.py', *command_line]): new_cmd = bisect_builds.MaybeSwitchBuildType( options, bisect_builds.ChromiumVersion('127.0.6533.74'), bisect_builds.ChromiumVersion('127.0.6533.88')) self.assertEqual(new_cmd[1:], [ '-o', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88', '--verify-range', '--no-local-cache' ]) def test_android_signed_with_args(self): command_line = [ '-r', '--archive=android-arm64-high', '--good=127.0.6533.74', '-b', '127.0.6533.88', '--apk=chrome', '--signed', '--no-local-cache', '--', 'args1', '--args2' ] options = bisect_builds.ParseCommandLine(command_line) with patch('sys.argv', ['bisect-builds.py', *command_line]): new_cmd = bisect_builds.MaybeSwitchBuildType(options, '127.0.6533.74', '127.0.6533.88') self.assertEqual(new_cmd[1:], [ '-o', '-a', 'android-arm64-high', '-g', '127.0.6533.74', '-b', '127.0.6533.88', '--verify-range', '--apk=chrome', '--no-local-cache', '--', 'args1', '--args2' ]) def test_no_official_build(self): command_line = [ '-r', '-a', 'ios', '--ipa=canary.ipa', '--device-id', '321', '-g', '127.0.6533.74', '-b', '127.0.6533.78', '--no-local-cache' ] options = bisect_builds.ParseCommandLine(command_line) with patch('sys.argv', ['bisect-builds.py', *command_line]): new_cmd = bisect_builds.MaybeSwitchBuildType(options, '127.0.6533.74', '127.0.6533.88') self.assertEqual(new_cmd, None) @patch('bisect-builds.ArchiveBuild.get_rev_list', return_value=list(range(3))) def test_generate_suggestion_with_cache(self, mock_get_rev_list): command_line = [ '-r', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88', '--use-local-cache' ] options = bisect_builds.ParseCommandLine(command_line) with patch('sys.argv', ['bisect-builds.py', *command_line]): new_cmd = bisect_builds.MaybeSwitchBuildType(options, '127.0.6533.74', '127.0.6533.88') self.assertEqual(new_cmd[1:], [ '-o', '-a', 'linux64', '-g', '127.0.6533.74', '-b', '127.0.6533.88', '--verify-range', '--use-local-cache' ]) mock_get_rev_list.assert_called() class MethodTest(BisectTestCase): @patch('sys.stderr', new_callable=io.StringIO) def test_ParseCommandLine(self, mock_stderr): opts = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '1', 'args1', 'args2 3', '-b', '2']) self.assertEqual(opts.build_type, 'snapshot') self.assertEqual(opts.args, ['args1', 'args2 3']) opts = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '1', 'args1', 'args2 3']) self.assertEqual(opts.args, ['args1', 'args2 3']) opts = bisect_builds.ParseCommandLine( ['-a', 'linux64', '-g', '1', '--', 'args1', 'args2 3', '-b', '2']) self.assertEqual(opts.args, ['args1', 'args2 3', '-b', '2']) with self.assertRaises(SystemExit): bisect_builds.ParseCommandLine(['-a', 'mac64', '-o', '-g', '1']) self.assertRegexpMatches( mock_stderr.getvalue(), r'To bisect for mac64, please choose from ' r'release(-r), snapshot(-s)') @patch('bisect-builds._DetectArchive', return_value='linux64') def test_ParseCommandLine_DetectArchive(self, mock_detect_archive): opts = bisect_builds.ParseCommandLine(['-o', '-g', '1']) self.assertEqual(opts.archive, 'linux64') def test_ParseCommandLine_default_apk(self): opts = bisect_builds.ParseCommandLine( ['-o', '-a', 'android-arm', '-g', '1']) self.assertEqual(opts.apk, 'chrome') opts = bisect_builds.ParseCommandLine(['-a', 'android-arm64', '-g', '1']) self.assertEqual(opts.apk, 'chromium') def test_ParseCommandLine_default_ipa(self): opts = bisect_builds.ParseCommandLine( ['-r', '-a', 'ios', '-g', '127.0.6533.74', '-b', '127.0.6533.88']) self.assertEqual(opts.ipa, 'canary') def test_ParseCommandLine_DetectArchive_with_apk(self): opts = bisect_builds.ParseCommandLine(['-o', '--apk', 'chrome', '-g', '1']) self.assertEqual(opts.archive, 'android-arm64') def test_ParseCommandLine_DetectArchive_with_ipa(self): opts = bisect_builds.ParseCommandLine( ['-r', '--ipa', 'stable', '-g', '127.0.6533.74', '-b', '127.0.6533.88']) self.assertEqual(opts.archive, 'ios-simulator') @patch('sys.stderr', new_callable=io.StringIO) def test_ParseCommandLine_apk_error(self, mock_stderr): with self.assertRaises(SystemExit): bisect_builds.ParseCommandLine( ['-a', 'linux64', '--apk', 'chrome', '-g', '1']) self.assertIn('--apk is only supported', mock_stderr.getvalue()) @patch('sys.stderr', new_callable=io.StringIO) def test_ParseCommandLine_ipa_error(self, mock_stderr): with self.assertRaises(SystemExit): bisect_builds.ParseCommandLine( ['-a', 'linux64', '--ipa', 'stable', '-g', '1']) self.assertIn('--ipa is only supported', mock_stderr.getvalue()) @patch('sys.stderr', new_callable=io.StringIO) def test_ParseCommandLine_signed_error(self, mock_stderr): with self.assertRaises(SystemExit): bisect_builds.ParseCommandLine(['-a', 'linux64', '--signed', '-g', '1']) self.assertIn('--signed is only supported', mock_stderr.getvalue()) @patch('urllib.request.urlopen') @patch('builtins.open') @patch('sys.stdout', new_callable=io.StringIO) def test_update_script(self, mock_stdout, mock_open, mock_urlopen): mock_urlopen.return_value = io.BytesIO( base64.b64encode('content'.encode('utf-8'))) with self.assertRaises(SystemExit): bisect_builds.ParseCommandLine(['--update-script']) mock_urlopen.assert_called_once_with( 'https://chromium.googlesource.com/chromium/src/+/HEAD/' 'tools/bisect-builds.py?format=TEXT') mock_open.assert_called_once() mock_open.return_value.__enter__().write.assert_called_once_with('content') self.assertEqual(mock_stdout.getvalue(), 'Update successful!\n') @patch("urllib.request.urlopen", side_effect=[ urllib.request.HTTPError('url', 404, 'Not Found', None, None), urllib.request.HTTPError('url', 404, 'Not Found', None, None), io.BytesIO(b"NOT_A_JSON"), io.BytesIO(b'{"chromium_main_branch_position": 123}'), ]) def test_GetRevisionFromVersion(self, mock_urlopen): self.assertEqual(123, bisect_builds.GetRevisionFromVersion('127.0.6533.134')) mock_urlopen.assert_has_calls([ call('https://chromiumdash.appspot.com/fetch_version' '?version=127.0.6533.134'), call('https://chromiumdash.appspot.com/fetch_version' '?version=127.0.6533.0'), ]) @maybe_patch("urllib.request.urlopen", side_effect=[ io.BytesIO(b'{"chromium_main_branch_position": null}'), io.BytesIO(b'{"message": "DEP\\n"}'), io.BytesIO(b')]}\'\n{"message": "Cr-Branched-From: ' b'3d60439cfb36485e76a1c5bb7f513d3721b20da1-' b'refs/heads/master@{#870763}\\n"}'), ]) def test_GetRevisionFromSourceTag(self, mock_urlopen): self.assertEqual(870763, bisect_builds.GetRevisionFromVersion('91.0.4472.38')) mock_urlopen.assert_has_calls([ call('https://chromiumdash.appspot.com/fetch_version' '?version=91.0.4472.38'), call('https://chromium.googlesource.com/chromium/src/' '+/refs/tags/91.0.4472.38?format=JSON'), call('https://chromium.googlesource.com/chromium/src/' '+/refs/tags/91.0.4472.38^?format=JSON'), ]) def test_join_args(self): test_data = ['a', 'b c', 'C:\\a b\\c', '/a b/c', '"a"', "'a'"] quoted_command = bisect_builds.join_args( [sys.executable, '-c', 'import sys, json; print(json.dumps(sys.argv))'] + test_data) subproc = subprocess.Popen( quoted_command, shell=True, stdout=subprocess.PIPE) stdout, _ = subproc.communicate() dumped_argv = json.loads(stdout.decode('utf-8')) self.assertListEqual(dumped_argv, ['-c'] + test_data) class ChromiumVersionTest(BisectTestCase): def test_cmpare_version_numbers(self): v127_0_6533_74 = bisect_builds.ChromiumVersion('127.0.6533.74') v127_0_6533_75 = bisect_builds.ChromiumVersion('127.0.6533.75') v127_0_6533_75_with_space = bisect_builds.ChromiumVersion('127.0.6533.75 ') v127 = bisect_builds.ChromiumVersion('127') self.assertLess(v127_0_6533_74, v127_0_6533_75) self.assertLessEqual(v127_0_6533_74, v127_0_6533_75) self.assertGreater(v127_0_6533_75, v127_0_6533_74) self.assertGreaterEqual(v127_0_6533_75, v127_0_6533_74) self.assertEqual(v127_0_6533_75, v127_0_6533_75_with_space) self.assertLess(v127, v127_0_6533_74) if __name__ == '__main__': unittest.main()