| #!/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 trigger_multiple_dimensions.py.""" | 
 |  | 
 | import unittest | 
 |  | 
 | import trigger_multiple_dimensions | 
 |  | 
 | class Args(object): | 
 |   def __init__(self): | 
 |     self.shards = 1 | 
 |     self.dump_json = '' | 
 |     self.multiple_trigger_configs = [] | 
 |     self.multiple_dimension_script_verbose = False | 
 |  | 
 |  | 
 | class FakeTriggerer(trigger_multiple_dimensions.MultiDimensionTestTriggerer): | 
 |   def __init__(self, bot_configs, bot_statuses, | 
 |                use_superclass_random_number_generator, first_random_number): | 
 |     super(FakeTriggerer, self).__init__() | 
 |     self._bot_configs = bot_configs | 
 |     self._bot_statuses = bot_statuses | 
 |     self._swarming_runs = [] | 
 |     self._files = {} | 
 |     self._temp_file_id = 0 | 
 |     self._use_superclass_rng = use_superclass_random_number_generator | 
 |     self._last_random_number = first_random_number | 
 |  | 
 |   def set_files(self, files): | 
 |     self._files = files | 
 |  | 
 |   def choose_random_int(self, max_num): | 
 |     if self._use_superclass_rng: | 
 |       return super(FakeTriggerer, self).choose_random_int(max_num) | 
 |     if self._last_random_number > max_num: | 
 |       self._last_random_number = 1 | 
 |     result = self._last_random_number | 
 |     self._last_random_number += 1 | 
 |     return result | 
 |  | 
 |   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 write_json_to_file(self, merged_json, output_file): | 
 |     self._files[output_file] = merged_json | 
 |  | 
 |   def parse_bot_configs(self, args): | 
 |     pass | 
 |  | 
 |   def query_swarming_for_bot_configs(self, verbose): | 
 |     # Sum up the total count of all bots. | 
 |     self._total_bots = sum(x['total'] for x in self._bot_statuses) | 
 |  | 
 |   def run_swarming(self, args, verbose): | 
 |     self._swarming_runs.append(args) | 
 |  | 
 |  | 
 | WIN_NVIDIA_QUADRO_P400_STABLE_DRIVER = '10de:1cb3-23.21.13.8792' | 
 | WIN7 = 'Windows-2008ServerR2-SP1' | 
 | WIN10 = 'Windows-10' | 
 |  | 
 | WIN7_NVIDIA = { | 
 |   'gpu': WIN_NVIDIA_QUADRO_P400_STABLE_DRIVER, | 
 |   'os': WIN7, | 
 |   'pool': 'Chrome-GPU', | 
 | } | 
 |  | 
 | WIN10_NVIDIA = { | 
 |   'gpu': WIN_NVIDIA_QUADRO_P400_STABLE_DRIVER, | 
 |   'os': WIN10, | 
 |   'pool': 'Chrome-GPU', | 
 | } | 
 |  | 
 | class UnitTest(unittest.TestCase): | 
 |   def basic_win7_win10_setup(self, bot_statuses, | 
 |                              use_superclass_random_number_generator=False, | 
 |                              first_random_number=1): | 
 |     triggerer = FakeTriggerer( | 
 |       [ | 
 |         WIN7_NVIDIA, | 
 |         WIN10_NVIDIA | 
 |       ], | 
 |       bot_statuses, | 
 |       use_superclass_random_number_generator, | 
 |       first_random_number | 
 |     ) | 
 |     # Note: the contents of these JSON files don't accurately reflect | 
 |     # that produced by "swarming.py trigger". The unit tests only | 
 |     # verify that shard 0's JSON is preserved. | 
 |     triggerer.set_files({ | 
 |       'base_trigger_dimensions0.json': { | 
 |         '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': 'f001', | 
 |           }, | 
 |         }, | 
 |       }, | 
 |       'base_trigger_dimensions1.json': { | 
 |         'tasks': { | 
 |           'webgl_conformance_tests on NVIDIA GPU on Windows': { | 
 |             'task_id': 'f002', | 
 |           }, | 
 |         }, | 
 |       }, | 
 |     }) | 
 |     args = Args() | 
 |     args.shards = 2 | 
 |     args.dump_json = 'output.json' | 
 |     args.multiple_dimension_script_verbose = False | 
 |     triggerer.trigger_tasks( | 
 |       args, | 
 |       [ | 
 |         'trigger', | 
 |         '--dimension', | 
 |         'gpu', | 
 |         WIN_NVIDIA_QUADRO_P400_STABLE_DRIVER, | 
 |         '--dimension', | 
 |         'os', | 
 |         WIN7, | 
 |         '--dimension', | 
 |         'pool', | 
 |         'Chrome-GPU', | 
 |         '--', | 
 |         'webgl_conformance', | 
 |       ]) | 
 |     return triggerer | 
 |  | 
 |   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 shard_runs_on_os(self, triggerer, shard_index, os): | 
 |     return self.list_contains_sublist(triggerer._swarming_runs[shard_index], | 
 |                                       ['--dimension', 'os', os]) | 
 |  | 
 |   def test_parse_bot_configs(self): | 
 |     triggerer = trigger_multiple_dimensions.MultiDimensionTestTriggerer() | 
 |     args = Args() | 
 |     args.multiple_trigger_configs = "{ foo }" | 
 |     self.assertRaisesRegexp(ValueError, "Error while parsing JSON.*", | 
 |                             triggerer.parse_bot_configs, args) | 
 |     args.multiple_trigger_configs = "{ \"foo\": \"bar\" }" | 
 |     self.assertRaisesRegexp(ValueError, "Bot configurations must be a list.*", | 
 |                             triggerer.parse_bot_configs, args) | 
 |     args.multiple_trigger_configs = "[]" | 
 |     self.assertRaisesRegexp(ValueError, | 
 |                             "Bot configuration list must have at least.*", | 
 |                             triggerer.parse_bot_configs, args) | 
 |     args.multiple_trigger_configs = "[{}, \"\"]" | 
 |     self.assertRaisesRegexp(ValueError, | 
 |                             "Bot configurations must all be.*", | 
 |                             triggerer.parse_bot_configs, args) | 
 |     args.multiple_trigger_configs = "[{}]" | 
 |     triggerer.parse_bot_configs(args) | 
 |     self.assertEqual(triggerer._bot_configs, [{}]) | 
 |  | 
 |   def test_split_with_available_machines(self): | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 1, | 
 |           'available': 1, | 
 |         }, | 
 |         { | 
 |           'total': 1, | 
 |           'available': 1, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     # First shard should run on Win7. | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN7)) | 
 |     # Second shard should run on Win10. | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 1, WIN10)) | 
 |     # And not vice versa. | 
 |     self.assertFalse(self.shard_runs_on_os(triggerer, 0, WIN10)) | 
 |     self.assertFalse(self.shard_runs_on_os(triggerer, 1, WIN7)) | 
 |  | 
 |   def test_shard_env_vars(self): | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 2, | 
 |           'available': 2, | 
 |         }, | 
 |         { | 
 |           'total': 2, | 
 |           'available': 0, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     self.assertTrue(self.list_contains_sublist( | 
 |       triggerer._swarming_runs[0], ['--env', 'GTEST_SHARD_INDEX', '0'])) | 
 |     self.assertTrue(self.list_contains_sublist( | 
 |       triggerer._swarming_runs[1], ['--env', 'GTEST_SHARD_INDEX', '1'])) | 
 |     self.assertTrue(self.list_contains_sublist( | 
 |       triggerer._swarming_runs[0], ['--env', 'GTEST_TOTAL_SHARDS', '2'])) | 
 |     self.assertTrue(self.list_contains_sublist( | 
 |       triggerer._swarming_runs[1], ['--env', 'GTEST_TOTAL_SHARDS', '2'])) | 
 |  | 
 |   def test_json_merging(self): | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 1, | 
 |           'available': 1, | 
 |         }, | 
 |         { | 
 |           'total': 1, | 
 |           'available': 1, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     self.assertTrue('output.json' in triggerer._files) | 
 |     output_json = triggerer._files['output.json'] | 
 |     self.assertTrue('base_task_name' in output_json) | 
 |     self.assertTrue('request' in output_json) | 
 |     self.assertEqual(output_json['request']['expiration_secs'], 3600) | 
 |     self.assertEqual( | 
 |       output_json['request']['properties']['execution_timeout_secs'], 3600) | 
 |  | 
 |   def test_split_with_only_one_config_available(self): | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 2, | 
 |           'available': 2, | 
 |         }, | 
 |         { | 
 |           'total': 2, | 
 |           'available': 0, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     # Both shards should run on Win7. | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN7)) | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 1, WIN7)) | 
 |     # Redo with only Win10 bots available. | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 2, | 
 |           'available': 0, | 
 |         }, | 
 |         { | 
 |           'total': 2, | 
 |           'available': 2, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN10)) | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 1, WIN10)) | 
 |  | 
 |   def test_split_with_no_bots_available(self): | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 1, | 
 |           'available': 0, | 
 |         }, | 
 |         { | 
 |           'total': 1, | 
 |           'available': 0, | 
 |         }, | 
 |       ], | 
 |     ) | 
 |     # Given the fake random number generator above, first shard should | 
 |     # run on Win7. | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN7)) | 
 |     # Second shard should run on Win10. | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 1, WIN10)) | 
 |     # Try again with different bot distribution and random numbers. | 
 |     triggerer = self.basic_win7_win10_setup( | 
 |       [ | 
 |         { | 
 |           'total': 2, | 
 |           'available': 0, | 
 |         }, | 
 |         { | 
 |           'total': 2, | 
 |           'available': 0, | 
 |         }, | 
 |       ], | 
 |       first_random_number=3, | 
 |     ) | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN10)) | 
 |     self.assertTrue(self.shard_runs_on_os(triggerer, 1, WIN10)) | 
 |  | 
 |   def test_superclass_random_number_generator_works(self): | 
 |     # Probe randomly a certain number of times. | 
 |     num_runs = 0 | 
 |     for _ in xrange(100): | 
 |       triggerer = self.basic_win7_win10_setup( | 
 |         [ | 
 |           { | 
 |             'total': 2, | 
 |             'available': 0, | 
 |           }, | 
 |           { | 
 |             'total': 2, | 
 |             'available': 0, | 
 |           }, | 
 |         ], | 
 |         use_superclass_random_number_generator=True | 
 |       ) | 
 |       for _ in xrange(2): | 
 |         self.assertTrue(self.shard_runs_on_os(triggerer, 0, WIN7) or | 
 |                         self.shard_runs_on_os(triggerer, 0, WIN10)) | 
 |         num_runs += 1 | 
 |     self.assertEqual(num_runs, 200) | 
 |  | 
 | if __name__ == '__main__': | 
 |   unittest.main() |