def setUp(self): super(V1ApiTest, self).setUp() gae_ts_mon.reset_for_unittest(disable=True) auth.disable_process_cache() user.clear_request_cache() self.patch( 'components.utils.utcnow', return_value=datetime.datetime(2017, 1, 1) ) self.future_date = utils.utcnow() + datetime.timedelta(days=1) # future_ts is str because INT64 values are formatted as strings. self.future_ts = str(utils.datetime_to_timestamp(self.future_date)) config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg( ''' name: "luci.chromium.try" acls { role: SCHEDULER identity: "anonymous:anonymous" } ''' ), ) self.build_infra = test_util.build_bundle(id=1).infra self.build_infra.put()
def test_get_bucket(self, get_buildbucket_cfg_url): get_buildbucket_cfg_url.return_value = 'https://example.com/buildbucket.cfg' bucket_cfg = test_util.parse_bucket_cfg( ''' name: "master.tryserver.chromium.linux" acls { role: READER identity: "anonymous:anonymous" } ''' ) config.put_bucket('chromium', 'deadbeef', bucket_cfg) req = { 'bucket': 'master.tryserver.chromium.linux', } res = self.call_api('get_bucket', req).json_body self.assertEqual( res, { 'name': 'master.tryserver.chromium.linux', 'project_id': 'chromium', 'config_file_content': text_format.MessageToString(bucket_cfg), 'config_file_url': 'https://example.com/buildbucket.cfg', 'config_file_rev': 'deadbeef', } )
def test_pause_bucket(self): config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "master.foo"'), ) config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "master.bar"'), ) self.put_many_builds(5, builder=dict(project='chromium', bucket='master.foo')) self.put_many_builds(5, builder=dict(project='chromium', bucket='master.bar')) service.pause('chromium/master.foo', True) builds, _ = service.peek( ['chromium/master.foo', 'chromium/master.bar']) self.assertEqual(len(builds), 5) self.assertTrue( all(b.bucket_id == 'chromium/master.bar' for b in builds))
def test_get_builders_with_bucket_filtering(self): # Add a second bucket with a different name. other_bucket = ''' name: "luci.other.try" acls { role: SCHEDULER group: "all" } swarming { hostname: "swarming.example.com" builders { name: "a" swarming_host: "swarming.example.com" } } ''' config.put_bucket('other', 'deadbeef', test_util.parse_bucket_cfg(other_bucket)) req = { 'bucket': ['luci.chromium.try'], } resp = self.call_api('get_builders', req).json_body self.assertEqual( test_util.ununicode(resp), { 'buckets': [{ 'name': 'luci.chromium.try', 'swarming_hostname': 'swarming.example.com', 'builders': [ { 'name': 'linux', 'category': 'Chromium', 'properties_json': json.dumps({ 'foo': 'bar', 'baz': 1 }), 'swarming_hostname': 'swarming.example.com', 'swarming_dimensions': [ 'baz:baz', 'builder:linux', 'foo:bar', ], }, { 'name': 'windows', 'category': 'Chromium', 'properties_json': json.dumps({}), 'swarming_hostname': 'swarming.example.com', }, ], }], }, )
def test_get_bucket(self): config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) rev, cfg = config.get_bucket('chromium/try') self.assertEqual(rev, 'deadbeef') self.assertEqual(cfg, short_bucket_cfg(LUCI_CHROMIUM_TRY)) self.assertIsNone(config.get_bucket('chromium/nonexistent')[0])
def test_get_buckets_async_with_bucket_ids(self): config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_WIN) bid = 'chromium/try' actual = config.get_buckets_async([bid]).get_result() expected = {'chromium/try': short_bucket_cfg(LUCI_CHROMIUM_TRY)} self.assertEqual(actual, expected)
def test_put_batch_auth_error(self): # Not a SCHEDULER role. bucket_cfg = test_util.parse_bucket_cfg( ''' name: "ci" acls { role: READER identity: "anonymous:anonymous" } ''' ) config.put_bucket('chromium', 'deadbeef', bucket_cfg) req = { 'builds': [ { 'bucket': 'luci.chromium.try', 'tags': ['a:b'], 'client_operation_id': '0', }, { 'bucket': 'luci.chromium.ci', 'client_operation_id': '1', }, ], } self.call_api('put_batch', req, status=403)
def setUp(self): super(ToBucketIDTest, self).setUp() config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "luci.chromium.try"'), )
def test_resolve_bucket_name_async_cache_key(self): config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_LINUX) self.assertEqual(self.resolve_bucket('try'), 'chromium/try') self.assertEqual( self.resolve_bucket('master.tryserver.chromium.linux'), 'chromium/master.tryserver.chromium.linux')
def test_convert_bucket_access_check(self): config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "secret"'), ) with self.assertRaises(auth.AuthorizationError): api.convert_bucket('secret')
def test_pause_all_requested_buckets(self): config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "master.foo"'), ) self.put_many_builds(5, builder=dict(project='chromium', bucket='master.foo')) service.pause('chromium/master.foo', True) builds, _ = service.peek(['chromium/master.foo']) self.assertEqual(len(builds), 0)
def test_good_request(self): config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "try" acls { role: SCHEDULER identity: "anonymous:anonymous" } '''), ) config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "ci" acls { role: READER identity: "anonymous:anonymous" } '''), ) request = access_pb2.PermittedActionsRequest( resource_kind='bucket', resource_ids=['luci.chromium.try', 'luci.chromium.ci'], ) result = self.servicer.PermittedActions(request, None) self.assertEqual(len(result.permitted), 2) self.assertEqual( set(result.permitted.keys()), {'luci.chromium.try', 'luci.chromium.ci'}, ) try_perms = result.permitted['luci.chromium.try'] self.assertEqual(len(try_perms.actions), 5) # Sanity check. self.assertEqual( set(try_perms.actions), {action.name for action in user.ROLE_TO_ACTIONS[Acl.SCHEDULER]}, ) ci_perms = result.permitted['luci.chromium.ci'] self.assertEqual( set(ci_perms.actions), {action.name for action in user.ROLE_TO_ACTIONS[Acl.READER]}, )
def setUp(self): super(ConvertBucketTest, self).setUp() user.clear_request_cache() config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "luci.chromium.try" acls { role: READER identity: "anonymous:anonymous" } '''), )
def test_cron_update_buckets_with_broken_configs(self, get_project_configs): config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_LINUX) get_project_configs.return_value = { 'chromium': ( 'new!', None, config_component.ConfigFormatError('broken!') ), } config.cron_update_buckets() # We must not delete buckets defined in a project that currently have a # broken config. bucket_id = 'chromium/' + MASTER_TRYSERVER_CHROMIUM_LINUX.name _, actual = config.get_bucket(bucket_id) self.assertEqual(actual, MASTER_TRYSERVER_CHROMIUM_LINUX)
def test_retry_forbidden(self): config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "readonly" acls { role: READER identity: "anonymous:anonymous" } '''), ) test_util.build(id=1, builder=dict(project='chromium', bucket='readonly')).put() self.call_api('retry', {'id': '1'}, status=403)
def test_pause_then_unpause(self): build = self.classic_build( builder=dict(project='chromium', bucket='try')) build.put() config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg('name: "ci"'), ) service.pause(build.bucket_id, True) service.pause(build.bucket_id, True) # Again, to cover equality case. builds, _ = service.peek([build.bucket_id]) self.assertEqual(len(builds), 0) service.pause(build.bucket_id, False) builds, _ = service.peek([build.bucket_id]) self.assertEqual(len(builds), 1)
def test_update_global_metrics(self, set_build_count_metric_async, set_build_latency): set_build_count_metric_async.return_value = future(None) set_build_latency.return_value = future(None) model.Builder(id='chromium:luci.chromium.try:release').put() model.Builder(id='chromium:luci.chromium.try:debug').put() model.Builder(id='chromium:try:debug').put() config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "luci.chromium.try" swarming { builders {} } '''), ) metrics.update_global_metrics() set_build_latency.assert_any_call('chromium/try', 'luci.chromium.try', 'release', True) set_build_latency.assert_any_call('chromium/try', 'luci.chromium.try', 'release', False) set_build_latency.assert_any_call('chromium/try', 'luci.chromium.try', 'debug', True) set_build_latency.assert_any_call('chromium/try', 'luci.chromium.try', 'debug', False) set_build_count_metric_async.assert_any_call( 'chromium/try', 'luci.chromium.try', 'release', model.BuildStatus.SCHEDULED, False) set_build_count_metric_async.assert_any_call( 'chromium/try', 'luci.chromium.try', 'release', model.BuildStatus.SCHEDULED, True) set_build_count_metric_async.assert_any_call( 'chromium/try', 'luci.chromium.try', 'debug', model.BuildStatus.SCHEDULED, False) set_build_count_metric_async.assert_any_call( 'chromium/try', 'luci.chromium.try', 'debug', model.BuildStatus.SCHEDULED, True)
def test_get_builders(self): secret_cfg = 'name: "secret"' config.put_bucket('secret', 'deadbeef', test_util.parse_bucket_cfg(secret_cfg)) resp = self.call_api('get_builders').json_body self.assertEqual( test_util.ununicode(resp), { 'buckets': [{ 'name': 'luci.chromium.try', 'swarming_hostname': 'swarming.example.com', 'builders': [ { 'name': 'linux', 'category': 'Chromium', 'properties_json': json.dumps({ 'foo': 'bar', 'baz': 1 }), 'swarming_hostname': 'swarming.example.com', 'swarming_dimensions': ['baz:baz', 'builder:linux', 'foo:bar'], }, { 'name': 'windows', 'category': 'Chromium', 'properties_json': json.dumps({}), 'swarming_hostname': 'swarming.example.com', }, ], }], }, )
def test_get_buckets_async(self): config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_LINUX) config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) config.put_bucket('dart', 'deadbeef', LUCI_DART_TRY) actual = config.get_buckets_async().get_result() expected = { 'chromium/master.tryserver.chromium.linux': MASTER_TRYSERVER_CHROMIUM_LINUX, 'chromium/try': short_bucket_cfg(LUCI_CHROMIUM_TRY), 'dart/try': short_bucket_cfg(LUCI_DART_TRY), } self.assertEqual(actual, expected)
def setUp(self): super(BuildBucketServiceTest, self).setUp() user.clear_request_cache() self.current_identity = auth.Identity('service', 'unittest') self.patch('components.auth.get_current_identity', side_effect=lambda: self.current_identity) self.patch('user.can_async', return_value=future(True)) self.now = datetime.datetime(2015, 1, 1) self.patch('components.utils.utcnow', side_effect=lambda: self.now) config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "try" acls { role: READER identity: "anonymous:anonymous" } '''), ) config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg(''' name: "luci" acls { role: READER identity: "anonymous:anonymous" } swarming { builders { name: "linux" swarming_host: "chromium-swarm.appspot.com" build_numbers: YES recipe { cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" name: "recipe" } } } '''), ) self.patch('swarming.cancel_task_async', return_value=future(None)) self.patch( 'google.appengine.api.app_identity.get_default_version_hostname', autospec=True, return_value='buildbucket.example.com') self.patch('tq.enqueue_async', autospec=True, return_value=future(None)) self.patch('config.get_settings_async', autospec=True, return_value=future(service_config_pb2.SettingsCfg())) self.patch('swarming.cancel_task_transactionally_async', autospec=True, return_value=future(None)) self.patch('search.TagIndex.random_shard_index', return_value=0)
def test_cron_update_buckets_with_existing(self, get_project_configs): chromium_buildbucket_cfg = parse_cfg(''' buckets { name: "master.tryserver.chromium.linux" acls { role: READER group: "all" } acls { role: SCHEDULER group: "tryjob-access" } } buckets { name: "master.tryserver.chromium.mac" acls { role: READER group: "all" } acls { role: SCHEDULER group: "tryjob-access" } } ''') v8_buildbucket_cfg = parse_cfg(''' buckets { name: "master.tryserver.v8" acls { role: WRITER group: "v8-team" } } ''') get_project_configs.return_value = { 'chromium': ('new!', chromium_buildbucket_cfg, None), 'v8': ('deadbeef', v8_buildbucket_cfg, None), } config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_LINUX) # Will not be updated. config.put_bucket('v8', 'deadbeef', MASTER_TRYSERVER_V8) # Will be deleted. config.put_bucket('chromium', 'deadbeef', MASTER_TRYSERVER_CHROMIUM_WIN) config.cron_update_buckets() actual = config.Bucket.query().fetch() actual = sorted(actual, key=lambda b: b.key) expected = [ config.Bucket( parent=ndb.Key(config.Project, 'chromium'), id='master.tryserver.chromium.linux', entity_schema_version=config.CURRENT_BUCKET_SCHEMA_VERSION, revision='new!', config=MASTER_TRYSERVER_CHROMIUM_LINUX, ), config.Bucket( parent=ndb.Key(config.Project, 'chromium'), id='master.tryserver.chromium.mac', entity_schema_version=config.CURRENT_BUCKET_SCHEMA_VERSION, revision='new!', config=MASTER_TRYSERVER_CHROMIUM_MAC, ), config.Bucket( parent=ndb.Key(config.Project, 'v8'), id='master.tryserver.v8', entity_schema_version=config.CURRENT_BUCKET_SCHEMA_VERSION, revision='deadbeef', config=MASTER_TRYSERVER_V8, ), ] self.assertEqual(actual, expected)
def test_resolve_bucket_name_async_ambiguous(self): config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) config.put_bucket('dart', 'deadbeef', LUCI_DART_TRY) with self.assertRaisesRegexp(errors.InvalidInputError, r'ambiguous'): self.resolve_bucket('try')
def test_resolve_bucket_name_async_unique(self): config.put_bucket('chromium', 'deadbeef', LUCI_CHROMIUM_TRY) self.assertEqual(self.resolve_bucket('try'), 'chromium/try')
def setUp(self): super(SwarmbucketApiTest, self).setUp() self.patch('components.utils.utcnow', autospec=True, return_value=datetime.datetime(2015, 11, 30)) self.patch( 'google.appengine.api.app_identity.get_default_version_hostname', return_value='cr-buildbucket.appspot.com') self.patch('creation._should_be_canary', side_effect=lambda p: p > 50) auth_testing.reset_local_state() auth.bootstrap_group('all', [auth.Anonymous]) user.clear_request_cache() chromium_cfg = test_util.parse_bucket_cfg(''' name: "luci.chromium.try" acls { role: SCHEDULER group: "all" } swarming { hostname: "swarming.example.com" builders { name: "linux" swarming_host: "swarming.example.com" category: "Chromium" build_numbers: YES recipe { cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" name: "presubmit" properties: "foo:bar" properties_j: "baz:1" } dimensions: "foo:bar" dimensions: "baz:baz" auto_builder_dimension: YES # Override builder cache without timeout to make tests # simpler. caches { path: "builder" name: "builder_cache_name" } } builders { name: "windows" category: "Chromium" swarming_host: "swarming.example.com" recipe { cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" name: "presubmit" } # Override builder cache without timeout to make tests # simpler. caches { path: "builder" name: "builder_cache_name" } } } ''') config.put_bucket('chromium', 'deadbeef', chromium_cfg) v8_cfg = test_util.parse_bucket_cfg(''' name: "luci.v8.try" acls { role: READER group: "all" } ''') config.put_bucket('v8', 'deadbeef', v8_cfg) self.settings = service_config_pb2.SettingsCfg( swarming=dict( milo_hostname='milo.example.com', bbagent_package=dict( package_name='infra/tools/bbagent', version='luci-runner-version', ), kitchen_package=dict( package_name='infra/tools/kitchen', version='kitchen-version', ), user_packages=[ dict( package_name='infra/tools/git', version='git-version', ), ], ), logdog=dict(hostname='logdog.example.com'), ) self.patch( 'config.get_settings_async', autospec=True, return_value=future(self.settings), )
def setUp(self): super(SwarmbucketApiTest, self).setUp() self.patch('components.utils.utcnow', autospec=True, return_value=datetime.datetime(2015, 11, 30)) self.patch( 'google.appengine.api.app_identity.get_default_version_hostname', return_value='cr-buildbucket.appspot.com') self.patch('creation._should_be_canary', side_effect=lambda p: p > 50) auth_testing.reset_local_state() auth.bootstrap_group('all', [auth.Anonymous]) user.clear_request_cache() chromium_cfg = test_util.parse_bucket_cfg(''' name: "luci.chromium.try" acls { role: SCHEDULER group: "all" } swarming { hostname: "swarming.example.com" builders { name: "linux" swarming_host: "swarming.example.com" category: "Chromium" build_numbers: YES recipe { cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" name: "presubmit" properties: "foo:bar" properties_j: "baz:1" } dimensions: "foo:bar" dimensions: "baz:baz" auto_builder_dimension: YES # Override builder cache without timeout to make tests # simpler. caches { path: "builder" name: "builder_cache_name" } } builders { name: "windows" category: "Chromium" swarming_host: "swarming.example.com" recipe { cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" name: "presubmit" } # Override builder cache without timeout to make tests # simpler. caches { path: "builder" name: "builder_cache_name" } } } ''') config.put_bucket('chromium', 'deadbeef', chromium_cfg) v8_cfg = test_util.parse_bucket_cfg(''' name: "luci.v8.try" acls { role: READER group: "all" } ''') config.put_bucket('v8', 'deadbeef', v8_cfg) props_def = { 'extra_args': [ 'cook', '-recipe', '${recipe}', '-properties', '${properties_json}', '-logdog-project', '${project}', ], 'cipd_input': { 'packages': [ { 'package_name': 'infra/test/bar/${os_ver}', 'path': '.', 'version': 'latest', }, { 'package_name': 'infra/test/foo/${platform}', 'path': 'third_party', 'version': 'stable', }, ], }, } self.task_template = { 'name': 'bb-${build_id}-${project}-${builder}', 'task_slices': [{ 'properties': props_def, 'wait_for_capacity': False, }], } self.patch( 'swarming._get_task_template', autospec=True, return_value=('rev', self.task_template), ) self.settings = service_config_pb2.SettingsCfg( swarming=dict( milo_hostname='milo.example.com', luci_runner_package=dict( package_name='infra/tools/luci_runner', version='luci-runner-version', ), kitchen_package=dict( package_name='infra/tools/kitchen', version='kitchen-version', ), user_packages=[ dict( package_name='infra/tools/git', version='git-version', ), ], ), logdog=dict(hostname='logdog.example.com'), ) self.patch( 'config.get_settings_async', autospec=True, return_value=future(self.settings), )
def mutate_builder_cfg(self): yield self.chromium_try.swarming.builders[0] config.put_bucket('chromium', 'a' * 40, self.chromium_try)
def setUp(self): super(CreationTest, self).setUp() user.clear_request_cache() self.current_identity = auth.Identity('service', 'unittest') self.patch('components.auth.get_current_identity', side_effect=lambda: self.current_identity) self.patch('user.can_async', return_value=future(True)) self.now = datetime.datetime(2015, 1, 1) self.patch('components.utils.utcnow', side_effect=lambda: self.now) self.chromium_try = test_util.parse_bucket_cfg(''' name: "luci.chromium.try" swarming { builders { name: "linux" build_numbers: YES swarming_host: "chromium-swarm.appspot.com" recipe { name: "recipe" cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" } } builders { name: "mac" swarming_host: "chromium-swarm.appspot.com" recipe { name: "recipe" cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" } } builders { name: "win" swarming_host: "chromium-swarm.appspot.com" recipe { name: "recipe" cipd_package: "infra/recipe_bundle" cipd_version: "refs/heads/master" } } } ''') config.put_bucket('chromium', 'a' * 40, self.chromium_try) self.create_sync_task = self.patch( 'swarming.create_sync_task', autospec=True, return_value={'is_payload': True}, ) self.patch('swarming.cancel_task_async', return_value=future(None)) self.patch( 'google.appengine.api.app_identity.get_default_version_hostname', autospec=True, return_value='buildbucket.example.com') self.patch('tq.enqueue_async', autospec=True, return_value=future(None)) self.settings = service_config_pb2.SettingsCfg( swarming=dict(global_caches=[dict(path='git')]), logdog=dict(hostname='logs.example.com'), ) self.patch('config.get_settings_async', autospec=True, return_value=future(self.settings)) self.patch('creation._should_update_builder', side_effect=lambda p: p > 0.5) self.patch('creation._should_be_canary', side_effect=lambda p: p > 50) self.patch('search.TagIndex.random_shard_index', return_value=0)
def test_put_batch(self, add_many_async): bundle1 = test_util.build_bundle(id=1, tags=[dict(key='a', value='b')]) bundle2 = test_util.build_bundle(id=2) bundle1.infra.put() bundle2.infra.put() config.put_bucket( 'chromium', 'a' * 40, test_util.parse_bucket_cfg( ''' name: "luci.chromium.try" acls { role: SCHEDULER identity: "anonymous:anonymous" } ''' ), ) add_many_async.return_value = future([ (bundle1.build, None), (bundle2.build, None), (None, errors.InvalidInputError('bad')), ]) req = { 'builds': [ { 'bucket': 'luci.chromium.try', 'tags': ['a:b'], 'client_operation_id': '0', }, { 'bucket': 'luci.chromium.try', 'client_operation_id': '1', }, { 'bucket': 'luci.chromium.try', 'tags': ['bad tag'], 'client_operation_id': '2', }, { 'bucket': 'luci.chromium.try', 'client_operation_id': '3', }, ], } resp = self.call_api('put_batch', req).json_body add_many_async.assert_called_once_with([ creation.BuildRequest( schedule_build_request=rpc_pb2.ScheduleBuildRequest( builder=dict(project='chromium', bucket='try'), tags=[dict(key='a', value='b')], request_id='0', properties=dict(), ), parameters={}, ), creation.BuildRequest( schedule_build_request=rpc_pb2.ScheduleBuildRequest( builder=dict(project='chromium', bucket='try'), request_id='1', properties=dict(), ), parameters={}, ), creation.BuildRequest( schedule_build_request=rpc_pb2.ScheduleBuildRequest( builder=dict(project='chromium', bucket='try'), request_id='3', properties=dict(), ), parameters={}, ), ]) res0 = resp['results'][0] self.assertEqual(res0['client_operation_id'], '0') self.assertEqual(res0['build']['id'], '1') self.assertEqual(res0['build']['bucket'], 'luci.chromium.try') res1 = resp['results'][1] self.assertEqual(res1['client_operation_id'], '1') self.assertEqual(res1['build']['id'], '2') self.assertEqual(res1['build']['bucket'], 'luci.chromium.try') res2 = resp['results'][2] self.assertEqual( res2, { 'client_operation_id': '2', 'error': { 'reason': 'INVALID_INPUT', 'message': u'Invalid tag "bad tag": does not contain ":"', }, } ) res3 = resp['results'][3] self.assertEqual( res3, { 'client_operation_id': '3', 'error': { 'reason': 'INVALID_INPUT', 'message': 'bad', }, } )