|  | #!/usr/bin/python | 
|  | # Copyright 2018 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Tests for perf_device_trigger_unittest.py.""" | 
|  |  | 
|  | import unittest | 
|  |  | 
|  | import perf_device_trigger | 
|  |  | 
|  | class Args(object): | 
|  | def __init__(self): | 
|  | self.shards = 1 | 
|  | self.dump_json = '' | 
|  | self.multiple_trigger_configs = None | 
|  | self.multiple_dimension_script_verbose = False | 
|  |  | 
|  |  | 
|  | class FakeTriggerer(perf_device_trigger.PerfDeviceTriggerer): | 
|  | def __init__(self, args, swarming_args, files): | 
|  | self._bot_statuses = [] | 
|  | self._swarming_runs = [] | 
|  | self._files = files | 
|  | self._temp_file_id = 0 | 
|  | super(FakeTriggerer, self).__init__(args, swarming_args) | 
|  |  | 
|  |  | 
|  | def set_files(self, files): | 
|  | self._files = files | 
|  |  | 
|  | def make_temp_file(self, prefix=None, suffix=None): | 
|  | result = prefix + str(self._temp_file_id) + suffix | 
|  | self._temp_file_id += 1 | 
|  | return result | 
|  |  | 
|  | def delete_temp_file(self, temp_file): | 
|  | pass | 
|  |  | 
|  | def read_json_from_temp_file(self, temp_file): | 
|  | return self._files[temp_file] | 
|  |  | 
|  | def read_encoded_json_from_temp_file(self, temp_file): | 
|  | return self._files[temp_file] | 
|  |  | 
|  | def write_json_to_file(self, merged_json, output_file): | 
|  | self._files[output_file] = merged_json | 
|  |  | 
|  | def run_swarming(self, args, verbose): | 
|  | del verbose #unused | 
|  | self._swarming_runs.append(args) | 
|  |  | 
|  |  | 
|  | class UnitTest(unittest.TestCase): | 
|  | def setup_and_trigger( | 
|  | self, previous_task_assignment_map, alive_bots, dead_bots): | 
|  | args = Args() | 
|  | args.shards = len(previous_task_assignment_map) | 
|  | args.dump_json = 'output.json' | 
|  | swarming_args = [ | 
|  | 'trigger', | 
|  | '--swarming', | 
|  | 'http://foo_server', | 
|  | '--auth-service-account-json', | 
|  | '/creds/test_service_account', | 
|  | '--dimension', | 
|  | 'pool', | 
|  | 'chrome-perf-fyi', | 
|  | '--dimension', | 
|  | 'os', | 
|  | 'windows', | 
|  | '--', | 
|  | 'benchmark1', | 
|  | ] | 
|  |  | 
|  | triggerer = FakeTriggerer(args, swarming_args, | 
|  | self.get_files(args.shards, previous_task_assignment_map, | 
|  | alive_bots, dead_bots)) | 
|  | triggerer.trigger_tasks( | 
|  | args, | 
|  | swarming_args) | 
|  | return triggerer | 
|  |  | 
|  | def get_files(self, num_shards, previous_task_assignment_map, | 
|  | alive_bots, dead_bots): | 
|  | files = {} | 
|  | file_index = 0 | 
|  | files['base_trigger_dimensions%d.json' % file_index] = ( | 
|  | self.generate_list_of_eligible_bots_query_response( | 
|  | alive_bots, dead_bots)) | 
|  | file_index = file_index + 1 | 
|  | # Perf device trigger will call swarming n times: | 
|  | #   1. Once for all eligible bots | 
|  | #   2. once per shard to determine last bot run | 
|  | # Shard builders is a list of build ids that represents | 
|  | # the last build that ran the shard that corresponds to that | 
|  | # index.  If that shard hasn't been run before the entry | 
|  | # should be an empty string. | 
|  | for i in xrange(num_shards): | 
|  | bot_id = previous_task_assignment_map.get(i) | 
|  | files['base_trigger_dimensions%d.json' % file_index] = ( | 
|  | self.generate_last_task_to_shard_query_response(i, bot_id)) | 
|  | file_index = file_index + 1 | 
|  | for i in xrange(num_shards): | 
|  | task = { | 
|  | 'base_task_name': 'webgl_conformance_tests', | 
|  | 'request': { | 
|  | 'expiration_secs': 3600, | 
|  | 'properties': { | 
|  | 'execution_timeout_secs': 3600, | 
|  | }, | 
|  | }, | 
|  | 'tasks': { | 
|  | 'webgl_conformance_tests on NVIDIA GPU on Windows': { | 
|  | 'task_id': 'f%d' % i, | 
|  | }, | 
|  | }, | 
|  | } | 
|  | files['base_trigger_dimensions%d.json' % file_index] = task | 
|  | file_index = file_index + 1 | 
|  | return files | 
|  |  | 
|  | def generate_last_task_to_shard_query_response(self, shard, bot_id): | 
|  | if len(bot_id): | 
|  | # Test both cases where bot_id is present and you have to parse | 
|  | # out of the tags. | 
|  | if shard % 2: | 
|  | return {'items': [{'bot_id': bot_id}]} | 
|  | else: | 
|  | return {'items': [{'tags': [('id:%s' % bot_id)]}]} | 
|  | return {} | 
|  |  | 
|  | def generate_list_of_eligible_bots_query_response( | 
|  | self, alive_bots, dead_bots): | 
|  | items = {'items': []} | 
|  | for bot_id in alive_bots: | 
|  | items['items'].append( | 
|  | { 'bot_id': ('%s' % bot_id), 'is_dead': False, 'quarantined': False }) | 
|  | is_dead = True | 
|  | for bot_id in dead_bots: | 
|  | is_quarantined = (not is_dead) | 
|  | items['items'].append({ | 
|  | 'bot_id': ('%s' % bot_id), | 
|  | 'is_dead': is_dead, | 
|  | 'quarantined': is_quarantined | 
|  | }) | 
|  | is_dead = (not is_dead) | 
|  | return items | 
|  |  | 
|  |  | 
|  | def list_contains_sublist(self, main_list, sub_list): | 
|  | return any(sub_list == main_list[offset:offset + len(sub_list)] | 
|  | for offset in xrange(len(main_list) - (len(sub_list) - 1))) | 
|  |  | 
|  | def assert_query_swarming_args(self, triggerer, num_shards): | 
|  | # Assert the calls to query swarming send the right args | 
|  | # First call is to get eligible bots and then one query | 
|  | # per shard | 
|  | for i in range(num_shards + 1): | 
|  | self.assertTrue('query' in triggerer._swarming_runs[i]) | 
|  | self.assertTrue(self.list_contains_sublist( | 
|  | triggerer._swarming_runs[i], ['-S', 'foo_server'])) | 
|  | self.assertTrue(self.list_contains_sublist( | 
|  | triggerer._swarming_runs[i], ['--auth-service-account-json', | 
|  | '/creds/test_service_account'])) | 
|  |  | 
|  | def get_triggered_shard_to_bot(self, triggerer, num_shards): | 
|  | self.assert_query_swarming_args(triggerer, num_shards) | 
|  | triggered_map = {} | 
|  | for run in triggerer._swarming_runs: | 
|  | if not 'trigger' in run: | 
|  | continue | 
|  | bot_id = run[(run.index('id') + 1)] | 
|  | shard = int(run[(run.index('GTEST_SHARD_INDEX') + 1)]) | 
|  | triggered_map[shard] = bot_id | 
|  | return triggered_map | 
|  |  | 
|  |  | 
|  | def test_all_healthy_shards(self): | 
|  | triggerer = self.setup_and_trigger( | 
|  | previous_task_assignment_map={0: 'build3', 1: 'build4', 2: 'build5'}, | 
|  | alive_bots=['build3', 'build4', 'build5'], | 
|  | dead_bots=['build1', 'build2']) | 
|  | expected_task_assignment = self.get_triggered_shard_to_bot( | 
|  | triggerer, num_shards=3) | 
|  | self.assertEquals(len(set(expected_task_assignment.values())), 3) | 
|  |  | 
|  | # All three bots were healthy so we should expect the task assignment to | 
|  | # stay the same | 
|  | self.assertEquals(expected_task_assignment.get(0), 'build3') | 
|  | self.assertEquals(expected_task_assignment.get(1), 'build4') | 
|  | self.assertEquals(expected_task_assignment.get(2), 'build5') | 
|  |  | 
|  | def test_previously_healthy_now_dead(self): | 
|  | # Test that it swaps out build1 and build2 that are dead | 
|  | # for two healthy bots | 
|  | triggerer = self.setup_and_trigger( | 
|  | previous_task_assignment_map={0: 'build1', 1: 'build2', 2: 'build3'}, | 
|  | alive_bots=['build3', 'build4', 'build5'], | 
|  | dead_bots=['build1', 'build2']) | 
|  | expected_task_assignment = self.get_triggered_shard_to_bot( | 
|  | triggerer, num_shards=3) | 
|  | self.assertEquals(len(set(expected_task_assignment.values())), 3) | 
|  |  | 
|  | # The first two should be assigned to one of the unassigned healthy bots | 
|  | new_healthy_bots = ['build4', 'build5'] | 
|  | self.assertIn(expected_task_assignment.get(0), new_healthy_bots) | 
|  | self.assertIn(expected_task_assignment.get(1), new_healthy_bots) | 
|  | self.assertEquals(expected_task_assignment.get(2), 'build3') | 
|  |  | 
|  | def test_not_enough_healthy_bots(self): | 
|  | triggerer = self.setup_and_trigger( | 
|  | previous_task_assignment_map= {0: 'build1', 1: 'build2', | 
|  | 2: 'build3', 3: 'build4', 4: 'build5'}, | 
|  | alive_bots=['build3', 'build4', 'build5'], | 
|  | dead_bots=['build1', 'build2']) | 
|  | expected_task_assignment = self.get_triggered_shard_to_bot( | 
|  | triggerer, num_shards=5) | 
|  | self.assertEquals(len(set(expected_task_assignment.values())), 5) | 
|  |  | 
|  | # We have 5 shards and 5 bots that ran them, but two | 
|  | # are now dead and there aren't any other healthy bots | 
|  | # to swap out to.  Make sure they still assign to the | 
|  | # same shards. | 
|  | self.assertEquals(expected_task_assignment.get(0), 'build1') | 
|  | self.assertEquals(expected_task_assignment.get(1), 'build2') | 
|  | self.assertEquals(expected_task_assignment.get(2), 'build3') | 
|  | self.assertEquals(expected_task_assignment.get(3), 'build4') | 
|  | self.assertEquals(expected_task_assignment.get(4), 'build5') | 
|  |  | 
|  | def test_not_enough_healthy_bots_shard_not_seen(self): | 
|  | triggerer = self.setup_and_trigger( | 
|  | previous_task_assignment_map= {0: 'build1', 1: '', | 
|  | 2: 'build3', 3: 'build4', 4: 'build5'}, | 
|  | alive_bots=['build3', 'build4', 'build5'], | 
|  | dead_bots=['build1', 'build2']) | 
|  | expected_task_assignment = self.get_triggered_shard_to_bot( | 
|  | triggerer, num_shards=5) | 
|  | self.assertEquals(len(set(expected_task_assignment.values())), 5) | 
|  |  | 
|  | # Not enough healthy bots so make sure shard 0 is still assigned to its | 
|  | # same dead bot. | 
|  | self.assertEquals(expected_task_assignment.get(0), 'build1') | 
|  | # Shard 1 had not been triggered yet, but there weren't enough | 
|  | # healthy bots.  Make sure it got assigned to the other dead bot. | 
|  | self.assertEquals(expected_task_assignment.get(1), 'build2') | 
|  | # The rest of the assignments should stay the same. | 
|  | self.assertEquals(expected_task_assignment.get(2), 'build3') | 
|  | self.assertEquals(expected_task_assignment.get(3), 'build4') | 
|  | self.assertEquals(expected_task_assignment.get(4), 'build5') | 
|  |  | 
|  | def test_shards_not_triggered_yet(self): | 
|  | # First time this configuration has been seen.  Choose three | 
|  | # healthy shards to trigger jobs on | 
|  | triggerer = self.setup_and_trigger( | 
|  | previous_task_assignment_map= {0: '', 1: '', 2: ''}, | 
|  | alive_bots=['build3', 'build4', 'build5'], | 
|  | dead_bots=['build1', 'build2']) | 
|  | expected_task_assignment = self.get_triggered_shard_to_bot( | 
|  | triggerer, num_shards=3) | 
|  | self.assertEquals(len(set(expected_task_assignment.values())), 3) | 
|  | new_healthy_bots = ['build3', 'build4', 'build5'] | 
|  | self.assertIn(expected_task_assignment.get(0), new_healthy_bots) | 
|  | self.assertIn(expected_task_assignment.get(1), new_healthy_bots) | 
|  | self.assertIn(expected_task_assignment.get(2), new_healthy_bots) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | unittest.main() |