1*9c5db199SXin Li#!/usr/bin/python3 2*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Lifrom __future__ import absolute_import 7*9c5db199SXin Lifrom __future__ import division 8*9c5db199SXin Lifrom __future__ import print_function 9*9c5db199SXin Li 10*9c5db199SXin Liimport six.moves.builtins 11*9c5db199SXin Liimport six.moves.queue 12*9c5db199SXin Liimport json 13*9c5db199SXin Liimport logging 14*9c5db199SXin Liimport os 15*9c5db199SXin Liimport shutil 16*9c5db199SXin Liimport signal 17*9c5db199SXin Liimport stat 18*9c5db199SXin Liimport subprocess 19*9c5db199SXin Liimport sys 20*9c5db199SXin Liimport tarfile 21*9c5db199SXin Liimport tempfile 22*9c5db199SXin Liimport time 23*9c5db199SXin Liimport unittest 24*9c5db199SXin Lifrom unittest import mock 25*9c5db199SXin Li 26*9c5db199SXin Liimport common 27*9c5db199SXin Li 28*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 29*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 30*9c5db199SXin Lifrom autotest_lib.client.common_lib.test_utils.comparators import IsA 31*9c5db199SXin Li 32*9c5db199SXin Li#For unittest without cloud_client.proto compiled. 33*9c5db199SXin Litry: 34*9c5db199SXin Li from autotest_lib.site_utils import cloud_console_client 35*9c5db199SXin Liexcept ImportError: 36*9c5db199SXin Li cloud_console_client = None 37*9c5db199SXin Lifrom autotest_lib.site_utils import gs_offloader 38*9c5db199SXin Lifrom autotest_lib.site_utils import job_directories 39*9c5db199SXin Lifrom autotest_lib.site_utils import job_directories_unittest as jd_test 40*9c5db199SXin Lifrom autotest_lib.tko import models 41*9c5db199SXin Lifrom autotest_lib.utils import gslib 42*9c5db199SXin Lifrom autotest_lib.site_utils import pubsub_utils 43*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import timeout_util 44*9c5db199SXin Lifrom six.moves import range 45*9c5db199SXin Li 46*9c5db199SXin Li# Test value to use for `days_old`, if nothing else is required. 47*9c5db199SXin Li_TEST_EXPIRATION_AGE = 7 48*9c5db199SXin Li 49*9c5db199SXin Li 50*9c5db199SXin Lidef _get_options(argv): 51*9c5db199SXin Li """Helper function to exercise command line parsing. 52*9c5db199SXin Li 53*9c5db199SXin Li @param argv Value of sys.argv to be parsed. 54*9c5db199SXin Li 55*9c5db199SXin Li """ 56*9c5db199SXin Li sys.argv = ['bogus.py'] + argv 57*9c5db199SXin Li return gs_offloader.parse_options() 58*9c5db199SXin Li 59*9c5db199SXin Li 60*9c5db199SXin Lidef is_fifo(path): 61*9c5db199SXin Li """Determines whether a path is a fifo. 62*9c5db199SXin Li 63*9c5db199SXin Li @param path: fifo path string. 64*9c5db199SXin Li """ 65*9c5db199SXin Li return stat.S_ISFIFO(os.lstat(path).st_mode) 66*9c5db199SXin Li 67*9c5db199SXin Li 68*9c5db199SXin Lidef _get_fake_process(): 69*9c5db199SXin Li return FakeProcess() 70*9c5db199SXin Li 71*9c5db199SXin Li 72*9c5db199SXin Liclass FakeProcess(object): 73*9c5db199SXin Li """Fake process object.""" 74*9c5db199SXin Li 75*9c5db199SXin Li def __init__(self): 76*9c5db199SXin Li self.returncode = 0 77*9c5db199SXin Li 78*9c5db199SXin Li 79*9c5db199SXin Li def wait(self): 80*9c5db199SXin Li return True 81*9c5db199SXin Li 82*9c5db199SXin Li 83*9c5db199SXin Liclass OffloaderOptionsTests(unittest.TestCase): 84*9c5db199SXin Li """Tests for the `Offloader` constructor. 85*9c5db199SXin Li 86*9c5db199SXin Li Tests that offloader instance fields are set as expected 87*9c5db199SXin Li for given command line options. 88*9c5db199SXin Li 89*9c5db199SXin Li """ 90*9c5db199SXin Li 91*9c5db199SXin Li _REGULAR_ONLY = {job_directories.SwarmingJobDirectory, 92*9c5db199SXin Li job_directories.RegularJobDirectory} 93*9c5db199SXin Li _SPECIAL_ONLY = {job_directories.SwarmingJobDirectory, 94*9c5db199SXin Li job_directories.SpecialJobDirectory} 95*9c5db199SXin Li _BOTH = _REGULAR_ONLY | _SPECIAL_ONLY 96*9c5db199SXin Li 97*9c5db199SXin Li 98*9c5db199SXin Li def setUp(self): 99*9c5db199SXin Li super(OffloaderOptionsTests, self).setUp() 100*9c5db199SXin Li patcher = mock.patch.object(utils, 'get_offload_gsuri') 101*9c5db199SXin Li self.gsuri_patch = patcher.start() 102*9c5db199SXin Li self.addCleanup(patcher.stop) 103*9c5db199SXin Li 104*9c5db199SXin Li gs_offloader.GS_OFFLOADING_ENABLED = True 105*9c5db199SXin Li gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False 106*9c5db199SXin Li 107*9c5db199SXin Li 108*9c5db199SXin Li def _mock_get_sub_offloader(self, is_moblab, multiprocessing=False, 109*9c5db199SXin Li console_client=None, delete_age=0): 110*9c5db199SXin Li """Mock the process of getting the offload_dir function.""" 111*9c5db199SXin Li if is_moblab: 112*9c5db199SXin Li self.expected_gsuri = '%sresults/%s/%s/' % ( 113*9c5db199SXin Li global_config.global_config.get_config_value( 114*9c5db199SXin Li 'CROS', 'image_storage_server'), 115*9c5db199SXin Li 'Fa:ke:ma:c0:12:34', 'rand0m-uu1d') 116*9c5db199SXin Li else: 117*9c5db199SXin Li self.expected_gsuri = utils.DEFAULT_OFFLOAD_GSURI 118*9c5db199SXin Li utils.get_offload_gsuri.return_value = self.expected_gsuri 119*9c5db199SXin Li sub_offloader = gs_offloader.GSOffloader(self.expected_gsuri, 120*9c5db199SXin Li multiprocessing, delete_age, 121*9c5db199SXin Li console_client) 122*9c5db199SXin Li 123*9c5db199SXin Li GsOffloader_patcher = mock.patch.object(gs_offloader, 'GSOffloader') 124*9c5db199SXin Li self.GsOffloader_patch = GsOffloader_patcher.start() 125*9c5db199SXin Li self.addCleanup(GsOffloader_patcher.stop) 126*9c5db199SXin Li 127*9c5db199SXin Li if cloud_console_client: 128*9c5db199SXin Li console_patcher = mock.patch.object( 129*9c5db199SXin Li cloud_console_client, 'is_cloud_notification_enabled') 130*9c5db199SXin Li self.ccc_notification_patch = console_patcher.start() 131*9c5db199SXin Li self.addCleanup(console_patcher.stop) 132*9c5db199SXin Li 133*9c5db199SXin Li if console_client: 134*9c5db199SXin Li cloud_console_client.is_cloud_notification_enabled.return_value = True 135*9c5db199SXin Li gs_offloader.GSOffloader.return_value = sub_offloader 136*9c5db199SXin Li else: 137*9c5db199SXin Li if cloud_console_client: 138*9c5db199SXin Li cloud_console_client.is_cloud_notification_enabled.return_value = False 139*9c5db199SXin Li gs_offloader.GSOffloader.return_value = sub_offloader 140*9c5db199SXin Li 141*9c5db199SXin Li return sub_offloader 142*9c5db199SXin Li 143*9c5db199SXin Li def _verify_sub_offloader(self, 144*9c5db199SXin Li is_moblab, 145*9c5db199SXin Li multiprocessing=False, 146*9c5db199SXin Li console_client=None, 147*9c5db199SXin Li delete_age=0): 148*9c5db199SXin Li if console_client: 149*9c5db199SXin Li self.GsOffloader_patch.assert_called_with( 150*9c5db199SXin Li self.expected_gsuri, multiprocessing, delete_age, 151*9c5db199SXin Li IsA(cloud_console_client.PubSubBasedClient)) 152*9c5db199SXin Li 153*9c5db199SXin Li else: 154*9c5db199SXin Li self.GsOffloader_patch.assert_called_with(self.expected_gsuri, 155*9c5db199SXin Li multiprocessing, 156*9c5db199SXin Li delete_age, None) 157*9c5db199SXin Li 158*9c5db199SXin Li def test_process_no_options(self): 159*9c5db199SXin Li """Test default offloader options.""" 160*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(False) 161*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options([])) 162*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 163*9c5db199SXin Li self._REGULAR_ONLY) 164*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 165*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 166*9c5db199SXin Li sub_offloader) 167*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 168*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 169*9c5db199SXin Li self._verify_sub_offloader(False) 170*9c5db199SXin Li 171*9c5db199SXin Li def test_process_all_option(self): 172*9c5db199SXin Li """Test offloader handling for the --all option.""" 173*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(False) 174*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options(['--all'])) 175*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), self._BOTH) 176*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 177*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 178*9c5db199SXin Li sub_offloader) 179*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 180*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 181*9c5db199SXin Li self._verify_sub_offloader(False) 182*9c5db199SXin Li 183*9c5db199SXin Li 184*9c5db199SXin Li def test_process_hosts_option(self): 185*9c5db199SXin Li """Test offloader handling for the --hosts option.""" 186*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(False) 187*9c5db199SXin Li offloader = gs_offloader.Offloader( 188*9c5db199SXin Li _get_options(['--hosts'])) 189*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 190*9c5db199SXin Li self._SPECIAL_ONLY) 191*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 192*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 193*9c5db199SXin Li sub_offloader) 194*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 195*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 196*9c5db199SXin Li self._verify_sub_offloader(False) 197*9c5db199SXin Li 198*9c5db199SXin Li 199*9c5db199SXin Li def test_parallelism_option(self): 200*9c5db199SXin Li """Test offloader handling for the --parallelism option.""" 201*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(False) 202*9c5db199SXin Li offloader = gs_offloader.Offloader( 203*9c5db199SXin Li _get_options(['--parallelism', '2'])) 204*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 205*9c5db199SXin Li self._REGULAR_ONLY) 206*9c5db199SXin Li self.assertEqual(offloader._processes, 2) 207*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 208*9c5db199SXin Li sub_offloader) 209*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 210*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 211*9c5db199SXin Li self._verify_sub_offloader(False) 212*9c5db199SXin Li 213*9c5db199SXin Li 214*9c5db199SXin Li def test_delete_only_option(self): 215*9c5db199SXin Li """Test offloader handling for the --delete_only option.""" 216*9c5db199SXin Li offloader = gs_offloader.Offloader( 217*9c5db199SXin Li _get_options(['--delete_only'])) 218*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 219*9c5db199SXin Li self._REGULAR_ONLY) 220*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 221*9c5db199SXin Li self.assertIsInstance(offloader._gs_offloader, 222*9c5db199SXin Li gs_offloader.FakeGSOffloader) 223*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 224*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 225*9c5db199SXin Li 226*9c5db199SXin Li 227*9c5db199SXin Li def test_days_old_option(self): 228*9c5db199SXin Li """Test offloader handling for the --days_old option.""" 229*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(False, delete_age=7) 230*9c5db199SXin Li offloader = gs_offloader.Offloader( 231*9c5db199SXin Li _get_options(['--days_old', '7'])) 232*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 233*9c5db199SXin Li self._REGULAR_ONLY) 234*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 235*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 236*9c5db199SXin Li sub_offloader) 237*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 7) 238*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 7) 239*9c5db199SXin Li self._verify_sub_offloader(False, delete_age=7) 240*9c5db199SXin Li 241*9c5db199SXin Li 242*9c5db199SXin Li def test_moblab_gsuri_generation(self): 243*9c5db199SXin Li """Test offloader construction for Moblab.""" 244*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(True) 245*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options([])) 246*9c5db199SXin Li self.assertEqual(set(offloader._jobdir_classes), 247*9c5db199SXin Li self._REGULAR_ONLY) 248*9c5db199SXin Li self.assertEqual(offloader._processes, 1) 249*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 250*9c5db199SXin Li sub_offloader) 251*9c5db199SXin Li self.assertEqual(offloader._upload_age_limit, 0) 252*9c5db199SXin Li self.assertEqual(offloader._delete_age_limit, 0) 253*9c5db199SXin Li self._verify_sub_offloader(True) 254*9c5db199SXin Li 255*9c5db199SXin Li 256*9c5db199SXin Li def test_globalconfig_offloading_flag(self): 257*9c5db199SXin Li """Test enabling of --delete_only via global_config.""" 258*9c5db199SXin Li gs_offloader.GS_OFFLOADING_ENABLED = False 259*9c5db199SXin Li offloader = gs_offloader.Offloader( 260*9c5db199SXin Li _get_options([])) 261*9c5db199SXin Li self.assertIsInstance(offloader._gs_offloader, 262*9c5db199SXin Li gs_offloader.FakeGSOffloader) 263*9c5db199SXin Li 264*9c5db199SXin Li def test_offloader_multiprocessing_flag_set(self): 265*9c5db199SXin Li """Test multiprocessing is set.""" 266*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(True, True) 267*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options(['-m'])) 268*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 269*9c5db199SXin Li sub_offloader) 270*9c5db199SXin Li self._verify_sub_offloader(True, True) 271*9c5db199SXin Li 272*9c5db199SXin Li def test_offloader_multiprocessing_flag_not_set_default_false(self): 273*9c5db199SXin Li """Test multiprocessing is set.""" 274*9c5db199SXin Li gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False 275*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(True, False) 276*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options([])) 277*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 278*9c5db199SXin Li sub_offloader) 279*9c5db199SXin Li self._verify_sub_offloader(True, False) 280*9c5db199SXin Li 281*9c5db199SXin Li def test_offloader_multiprocessing_flag_not_set_default_true(self): 282*9c5db199SXin Li """Test multiprocessing is set.""" 283*9c5db199SXin Li gs_offloader.GS_OFFLOADER_MULTIPROCESSING = True 284*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader(True, True) 285*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options([])) 286*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, 287*9c5db199SXin Li sub_offloader) 288*9c5db199SXin Li self._verify_sub_offloader(True, True) 289*9c5db199SXin Li 290*9c5db199SXin Li 291*9c5db199SXin Li def test_offloader_pubsub_enabled(self): 292*9c5db199SXin Li """Test multiprocessing is set.""" 293*9c5db199SXin Li if not cloud_console_client: 294*9c5db199SXin Li return 295*9c5db199SXin Li with mock.patch.object(pubsub_utils, "PubSubClient"): 296*9c5db199SXin Li sub_offloader = self._mock_get_sub_offloader( 297*9c5db199SXin Li True, False, cloud_console_client.PubSubBasedClient()) 298*9c5db199SXin Li offloader = gs_offloader.Offloader(_get_options([])) 299*9c5db199SXin Li self.assertEqual(offloader._gs_offloader, sub_offloader) 300*9c5db199SXin Li self._verify_sub_offloader( 301*9c5db199SXin Li True, False, cloud_console_client.PubSubBasedClient()) 302*9c5db199SXin Li 303*9c5db199SXin Li 304*9c5db199SXin Liclass _MockJobDirectory(job_directories._JobDirectory): 305*9c5db199SXin Li """Subclass of `_JobDirectory` used as a helper for tests.""" 306*9c5db199SXin Li 307*9c5db199SXin Li GLOB_PATTERN = '[0-9]*-*' 308*9c5db199SXin Li 309*9c5db199SXin Li 310*9c5db199SXin Li def __init__(self, resultsdir): 311*9c5db199SXin Li """Create new job in initial state.""" 312*9c5db199SXin Li super(_MockJobDirectory, self).__init__(resultsdir) 313*9c5db199SXin Li self._timestamp = None 314*9c5db199SXin Li self.queue_args = [resultsdir, os.path.dirname(resultsdir), self._timestamp] 315*9c5db199SXin Li 316*9c5db199SXin Li 317*9c5db199SXin Li def get_timestamp_if_finished(self): 318*9c5db199SXin Li return self._timestamp 319*9c5db199SXin Li 320*9c5db199SXin Li 321*9c5db199SXin Li def set_finished(self, days_old): 322*9c5db199SXin Li """Make this job appear to be finished. 323*9c5db199SXin Li 324*9c5db199SXin Li After calling this function, calls to `enqueue_offload()` 325*9c5db199SXin Li will find this job as finished, but not expired and ready 326*9c5db199SXin Li for offload. Note that when `days_old` is 0, 327*9c5db199SXin Li `enqueue_offload()` will treat a finished job as eligible 328*9c5db199SXin Li for offload. 329*9c5db199SXin Li 330*9c5db199SXin Li @param days_old The value of the `days_old` parameter that 331*9c5db199SXin Li will be passed to `enqueue_offload()` for 332*9c5db199SXin Li testing. 333*9c5db199SXin Li 334*9c5db199SXin Li """ 335*9c5db199SXin Li self._timestamp = jd_test.make_timestamp(days_old, False) 336*9c5db199SXin Li self.queue_args[2] = self._timestamp 337*9c5db199SXin Li 338*9c5db199SXin Li 339*9c5db199SXin Li def set_expired(self, days_old): 340*9c5db199SXin Li """Make this job eligible to be offloaded. 341*9c5db199SXin Li 342*9c5db199SXin Li After calling this function, calls to `offload` will attempt 343*9c5db199SXin Li to offload this job. 344*9c5db199SXin Li 345*9c5db199SXin Li @param days_old The value of the `days_old` parameter that 346*9c5db199SXin Li will be passed to `enqueue_offload()` for 347*9c5db199SXin Li testing. 348*9c5db199SXin Li 349*9c5db199SXin Li """ 350*9c5db199SXin Li self._timestamp = jd_test.make_timestamp(days_old, True) 351*9c5db199SXin Li self.queue_args[2] = self._timestamp 352*9c5db199SXin Li 353*9c5db199SXin Li 354*9c5db199SXin Li def set_incomplete(self): 355*9c5db199SXin Li """Make this job appear to have failed offload just once.""" 356*9c5db199SXin Li self.offload_count += 1 357*9c5db199SXin Li self.first_offload_start = time.time() 358*9c5db199SXin Li if not os.path.isdir(self.dirname): 359*9c5db199SXin Li os.mkdir(self.dirname) 360*9c5db199SXin Li 361*9c5db199SXin Li 362*9c5db199SXin Li def set_reportable(self): 363*9c5db199SXin Li """Make this job be reportable.""" 364*9c5db199SXin Li self.set_incomplete() 365*9c5db199SXin Li self.offload_count += 1 366*9c5db199SXin Li 367*9c5db199SXin Li 368*9c5db199SXin Li def set_complete(self): 369*9c5db199SXin Li """Make this job be completed.""" 370*9c5db199SXin Li self.offload_count += 1 371*9c5db199SXin Li if os.path.isdir(self.dirname): 372*9c5db199SXin Li os.rmdir(self.dirname) 373*9c5db199SXin Li 374*9c5db199SXin Li 375*9c5db199SXin Li def process_gs_instructions(self): 376*9c5db199SXin Li """Always still offload the job directory.""" 377*9c5db199SXin Li return True 378*9c5db199SXin Li 379*9c5db199SXin Li 380*9c5db199SXin Liclass CommandListTests(unittest.TestCase): 381*9c5db199SXin Li """Tests for `_get_cmd_list()`.""" 382*9c5db199SXin Li 383*9c5db199SXin Li def _command_list_assertions(self, job, use_rsync=True, multi=False): 384*9c5db199SXin Li """Call `_get_cmd_list()` and check the return value. 385*9c5db199SXin Li 386*9c5db199SXin Li Check the following assertions: 387*9c5db199SXin Li * The command name (argv[0]) is 'gsutil'. 388*9c5db199SXin Li * '-m' option (argv[1]) is on when the argument, multi, is True. 389*9c5db199SXin Li * The arguments contain the 'cp' subcommand. 390*9c5db199SXin Li * The next-to-last argument (the source directory) is the 391*9c5db199SXin Li job's `queue_args[0]`. 392*9c5db199SXin Li * The last argument (the destination URL) is the job's 393*9c5db199SXin Li 'queue_args[1]'. 394*9c5db199SXin Li 395*9c5db199SXin Li @param job A job with properly calculated arguments to 396*9c5db199SXin Li `_get_cmd_list()` 397*9c5db199SXin Li @param use_rsync True when using 'rsync'. False when using 'cp'. 398*9c5db199SXin Li @param multi True when using '-m' option for gsutil. 399*9c5db199SXin Li 400*9c5db199SXin Li """ 401*9c5db199SXin Li test_bucket_uri = 'gs://a-test-bucket' 402*9c5db199SXin Li 403*9c5db199SXin Li gs_offloader.USE_RSYNC_ENABLED = use_rsync 404*9c5db199SXin Li 405*9c5db199SXin Li gs_path = os.path.join(test_bucket_uri, job.queue_args[1]) 406*9c5db199SXin Li 407*9c5db199SXin Li command = gs_offloader._get_cmd_list( 408*9c5db199SXin Li multi, job.queue_args[0], gs_path) 409*9c5db199SXin Li 410*9c5db199SXin Li self.assertEqual(command[0], 'gsutil') 411*9c5db199SXin Li if multi: 412*9c5db199SXin Li self.assertEqual(command[1], '-m') 413*9c5db199SXin Li self.assertEqual(command[-2], job.queue_args[0]) 414*9c5db199SXin Li 415*9c5db199SXin Li if use_rsync: 416*9c5db199SXin Li self.assertTrue('rsync' in command) 417*9c5db199SXin Li self.assertEqual(command[-1], 418*9c5db199SXin Li os.path.join(test_bucket_uri, job.queue_args[0])) 419*9c5db199SXin Li else: 420*9c5db199SXin Li self.assertTrue('cp' in command) 421*9c5db199SXin Li self.assertEqual(command[-1], 422*9c5db199SXin Li os.path.join(test_bucket_uri, job.queue_args[1])) 423*9c5db199SXin Li 424*9c5db199SXin Li finish_command = gs_offloader._get_finish_cmd_list(gs_path) 425*9c5db199SXin Li self.assertEqual(finish_command[0], 'gsutil') 426*9c5db199SXin Li self.assertEqual(finish_command[1], 'cp') 427*9c5db199SXin Li self.assertEqual(finish_command[2], '/dev/null') 428*9c5db199SXin Li self.assertEqual(finish_command[3], 429*9c5db199SXin Li os.path.join(gs_path, '.finished_offload')) 430*9c5db199SXin Li 431*9c5db199SXin Li 432*9c5db199SXin Li def test__get_cmd_list_regular(self): 433*9c5db199SXin Li """Test `_get_cmd_list()` as for a regular job.""" 434*9c5db199SXin Li job = _MockJobDirectory('118-debug') 435*9c5db199SXin Li self._command_list_assertions(job) 436*9c5db199SXin Li 437*9c5db199SXin Li 438*9c5db199SXin Li def test__get_cmd_list_special(self): 439*9c5db199SXin Li """Test `_get_cmd_list()` as for a special job.""" 440*9c5db199SXin Li job = _MockJobDirectory('hosts/host1/118-reset') 441*9c5db199SXin Li self._command_list_assertions(job) 442*9c5db199SXin Li 443*9c5db199SXin Li 444*9c5db199SXin Li def test_get_cmd_list_regular_no_rsync(self): 445*9c5db199SXin Li """Test `_get_cmd_list()` as for a regular job.""" 446*9c5db199SXin Li job = _MockJobDirectory('118-debug') 447*9c5db199SXin Li self._command_list_assertions(job, use_rsync=False) 448*9c5db199SXin Li 449*9c5db199SXin Li 450*9c5db199SXin Li def test_get_cmd_list_special_no_rsync(self): 451*9c5db199SXin Li """Test `_get_cmd_list()` as for a special job.""" 452*9c5db199SXin Li job = _MockJobDirectory('hosts/host1/118-reset') 453*9c5db199SXin Li self._command_list_assertions(job, use_rsync=False) 454*9c5db199SXin Li 455*9c5db199SXin Li 456*9c5db199SXin Li def test_get_cmd_list_regular_multi(self): 457*9c5db199SXin Li """Test `_get_cmd_list()` as for a regular job with True multi.""" 458*9c5db199SXin Li job = _MockJobDirectory('118-debug') 459*9c5db199SXin Li self._command_list_assertions(job, multi=True) 460*9c5db199SXin Li 461*9c5db199SXin Li 462*9c5db199SXin Li def test__get_cmd_list_special_multi(self): 463*9c5db199SXin Li """Test `_get_cmd_list()` as for a special job with True multi.""" 464*9c5db199SXin Li job = _MockJobDirectory('hosts/host1/118-reset') 465*9c5db199SXin Li self._command_list_assertions(job, multi=True) 466*9c5db199SXin Li 467*9c5db199SXin Li 468*9c5db199SXin Liclass _TempResultsDirTestCase(unittest.TestCase): 469*9c5db199SXin Li """Mixin class for tests using a temporary results directory.""" 470*9c5db199SXin Li 471*9c5db199SXin Li REGULAR_JOBLIST = [ 472*9c5db199SXin Li '111-fubar', '112-fubar', '113-fubar', '114-snafu'] 473*9c5db199SXin Li HOST_LIST = ['host1', 'host2', 'host3'] 474*9c5db199SXin Li SPECIAL_JOBLIST = [ 475*9c5db199SXin Li 'hosts/host1/333-reset', 'hosts/host1/334-reset', 476*9c5db199SXin Li 'hosts/host2/444-reset', 'hosts/host3/555-reset'] 477*9c5db199SXin Li 478*9c5db199SXin Li 479*9c5db199SXin Li def setUp(self): 480*9c5db199SXin Li super(_TempResultsDirTestCase, self).setUp() 481*9c5db199SXin Li self._resultsroot = tempfile.mkdtemp() 482*9c5db199SXin Li self._cwd = os.getcwd() 483*9c5db199SXin Li os.chdir(self._resultsroot) 484*9c5db199SXin Li 485*9c5db199SXin Li 486*9c5db199SXin Li def tearDown(self): 487*9c5db199SXin Li os.chdir(self._cwd) 488*9c5db199SXin Li shutil.rmtree(self._resultsroot) 489*9c5db199SXin Li super(_TempResultsDirTestCase, self).tearDown() 490*9c5db199SXin Li 491*9c5db199SXin Li 492*9c5db199SXin Li def make_job(self, jobdir): 493*9c5db199SXin Li """Create a job with results in `self._resultsroot`. 494*9c5db199SXin Li 495*9c5db199SXin Li @param jobdir Name of the subdirectory to be created in 496*9c5db199SXin Li `self._resultsroot`. 497*9c5db199SXin Li 498*9c5db199SXin Li """ 499*9c5db199SXin Li os.makedirs(jobdir) 500*9c5db199SXin Li return _MockJobDirectory(jobdir) 501*9c5db199SXin Li 502*9c5db199SXin Li 503*9c5db199SXin Li def make_job_hierarchy(self): 504*9c5db199SXin Li """Create a sample hierarchy of job directories. 505*9c5db199SXin Li 506*9c5db199SXin Li `self.REGULAR_JOBLIST` is a list of directories for regular 507*9c5db199SXin Li jobs to be created; `self.SPECIAL_JOBLIST` is a list of 508*9c5db199SXin Li directories for special jobs to be created. 509*9c5db199SXin Li 510*9c5db199SXin Li """ 511*9c5db199SXin Li for d in self.REGULAR_JOBLIST: 512*9c5db199SXin Li os.mkdir(d) 513*9c5db199SXin Li hostsdir = 'hosts' 514*9c5db199SXin Li os.mkdir(hostsdir) 515*9c5db199SXin Li for host in self.HOST_LIST: 516*9c5db199SXin Li os.mkdir(os.path.join(hostsdir, host)) 517*9c5db199SXin Li for d in self.SPECIAL_JOBLIST: 518*9c5db199SXin Li os.mkdir(d) 519*9c5db199SXin Li 520*9c5db199SXin Li 521*9c5db199SXin Liclass _TempResultsDirTestBase(_TempResultsDirTestCase, unittest.TestCase): 522*9c5db199SXin Li """Base test class for tests using a temporary results directory.""" 523*9c5db199SXin Li 524*9c5db199SXin Li 525*9c5db199SXin Liclass FailedOffloadsLogTest(_TempResultsDirTestBase): 526*9c5db199SXin Li """Test the formatting of failed offloads log file.""" 527*9c5db199SXin Li # Below is partial sample of a failed offload log file. This text is 528*9c5db199SXin Li # deliberately hard-coded and then parsed to create the test data; the idea 529*9c5db199SXin Li # is to make sure the actual text format will be reviewed by a human being. 530*9c5db199SXin Li # 531*9c5db199SXin Li # first offload count directory 532*9c5db199SXin Li # --+----1----+---- ----+ ----+----1----+----2----+----3 533*9c5db199SXin Li _SAMPLE_DIRECTORIES_REPORT = '''\ 534*9c5db199SXin Li =================== ====== ============================== 535*9c5db199SXin Li 2014-03-14 15:09:26 1 118-fubar 536*9c5db199SXin Li 2014-03-14 15:19:23 2 117-fubar 537*9c5db199SXin Li 2014-03-14 15:29:20 6 116-fubar 538*9c5db199SXin Li 2014-03-14 15:39:17 24 115-fubar 539*9c5db199SXin Li 2014-03-14 15:49:14 120 114-fubar 540*9c5db199SXin Li 2014-03-14 15:59:11 720 113-fubar 541*9c5db199SXin Li 2014-03-14 16:09:08 5040 112-fubar 542*9c5db199SXin Li 2014-03-14 16:19:05 40320 111-fubar 543*9c5db199SXin Li ''' 544*9c5db199SXin Li 545*9c5db199SXin Li def setUp(self): 546*9c5db199SXin Li super(FailedOffloadsLogTest, self).setUp() 547*9c5db199SXin Li self._offloader = gs_offloader.Offloader(_get_options([])) 548*9c5db199SXin Li self._joblist = [] 549*9c5db199SXin Li for line in self._SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]: 550*9c5db199SXin Li date_, time_, count, dir_ = line.split() 551*9c5db199SXin Li job = _MockJobDirectory(dir_) 552*9c5db199SXin Li job.offload_count = int(count) 553*9c5db199SXin Li timestruct = time.strptime("%s %s" % (date_, time_), 554*9c5db199SXin Li gs_offloader.FAILED_OFFLOADS_TIME_FORMAT) 555*9c5db199SXin Li job.first_offload_start = time.mktime(timestruct) 556*9c5db199SXin Li # enter the jobs in reverse order, to make sure we 557*9c5db199SXin Li # test that the output will be sorted. 558*9c5db199SXin Li self._joblist.insert(0, job) 559*9c5db199SXin Li 560*9c5db199SXin Li 561*9c5db199SXin Li def assert_report_well_formatted(self, report_file): 562*9c5db199SXin Li """Assert that report file is well formatted. 563*9c5db199SXin Li 564*9c5db199SXin Li @param report_file: Path to report file 565*9c5db199SXin Li """ 566*9c5db199SXin Li with open(report_file, 'r') as f: 567*9c5db199SXin Li report_lines = f.read().split() 568*9c5db199SXin Li 569*9c5db199SXin Li for end_of_header_index in range(len(report_lines)): 570*9c5db199SXin Li if report_lines[end_of_header_index].startswith('=='): 571*9c5db199SXin Li break 572*9c5db199SXin Li self.assertLess(end_of_header_index, len(report_lines), 573*9c5db199SXin Li 'Failed to find end-of-header marker in the report') 574*9c5db199SXin Li 575*9c5db199SXin Li relevant_lines = report_lines[end_of_header_index:] 576*9c5db199SXin Li expected_lines = self._SAMPLE_DIRECTORIES_REPORT.split() 577*9c5db199SXin Li self.assertListEqual(relevant_lines, expected_lines) 578*9c5db199SXin Li 579*9c5db199SXin Li 580*9c5db199SXin Li def test_failed_offload_log_format(self): 581*9c5db199SXin Li """Trigger an e-mail report and check its contents.""" 582*9c5db199SXin Li log_file = os.path.join(self._resultsroot, 'failed_log') 583*9c5db199SXin Li report = self._offloader._log_failed_jobs_locally(self._joblist, 584*9c5db199SXin Li log_file=log_file) 585*9c5db199SXin Li self.assert_report_well_formatted(log_file) 586*9c5db199SXin Li 587*9c5db199SXin Li 588*9c5db199SXin Li def test_failed_offload_file_overwrite(self): 589*9c5db199SXin Li """Verify that we can saefly overwrite the log file.""" 590*9c5db199SXin Li log_file = os.path.join(self._resultsroot, 'failed_log') 591*9c5db199SXin Li with open(log_file, 'w') as f: 592*9c5db199SXin Li f.write('boohoohoo') 593*9c5db199SXin Li report = self._offloader._log_failed_jobs_locally(self._joblist, 594*9c5db199SXin Li log_file=log_file) 595*9c5db199SXin Li self.assert_report_well_formatted(log_file) 596*9c5db199SXin Li 597*9c5db199SXin Li 598*9c5db199SXin Liclass OffloadDirectoryTests(_TempResultsDirTestBase): 599*9c5db199SXin Li """Tests for `offload_dir()`.""" 600*9c5db199SXin Li 601*9c5db199SXin Li def setUp(self): 602*9c5db199SXin Li super(OffloadDirectoryTests, self).setUp() 603*9c5db199SXin Li # offload_dir() logs messages; silence them. 604*9c5db199SXin Li self._saved_loglevel = logging.getLogger().getEffectiveLevel() 605*9c5db199SXin Li logging.getLogger().setLevel(logging.CRITICAL+1) 606*9c5db199SXin Li self._job = self.make_job(self.REGULAR_JOBLIST[0]) 607*9c5db199SXin Li 608*9c5db199SXin Li cmd_list_patcher = mock.patch.object(gs_offloader, '_get_cmd_list') 609*9c5db199SXin Li cmd_list_patch = cmd_list_patcher.start() 610*9c5db199SXin Li self.addCleanup(cmd_list_patcher.stop) 611*9c5db199SXin Li 612*9c5db199SXin Li alarm = mock.patch('signal.alarm', return_value=0) 613*9c5db199SXin Li alarm.start() 614*9c5db199SXin Li self.addCleanup(alarm.stop) 615*9c5db199SXin Li 616*9c5db199SXin Li cmd_list_patcher = mock.patch.object(models.test, 'parse_job_keyval') 617*9c5db199SXin Li cmd_list_patch = cmd_list_patcher.start() 618*9c5db199SXin Li self.addCleanup(cmd_list_patcher.stop) 619*9c5db199SXin Li 620*9c5db199SXin Li self.should_remove_sarming_req_dir = False 621*9c5db199SXin Li 622*9c5db199SXin Li 623*9c5db199SXin Li def tearDown(self): 624*9c5db199SXin Li logging.getLogger().setLevel(self._saved_loglevel) 625*9c5db199SXin Li super(OffloadDirectoryTests, self).tearDown() 626*9c5db199SXin Li 627*9c5db199SXin Li def _mock_create_marker_file(self): 628*9c5db199SXin Li open_patcher = mock.patch.object(six.moves.builtins, 'open') 629*9c5db199SXin Li open_patch = open_patcher.start() 630*9c5db199SXin Li self.addCleanup(open_patcher.stop) 631*9c5db199SXin Li 632*9c5db199SXin Li open.return_value = mock.MagicMock() 633*9c5db199SXin Li 634*9c5db199SXin Li 635*9c5db199SXin Li def _mock_offload_dir_calls(self, command, queue_args, 636*9c5db199SXin Li marker_initially_exists=False): 637*9c5db199SXin Li """Mock out the calls needed by `offload_dir()`. 638*9c5db199SXin Li 639*9c5db199SXin Li This covers only the calls made when there is no timeout. 640*9c5db199SXin Li 641*9c5db199SXin Li @param command Command list to be returned by the mocked 642*9c5db199SXin Li call to `_get_cmd_list()`. 643*9c5db199SXin Li 644*9c5db199SXin Li """ 645*9c5db199SXin Li isfile_patcher = mock.patch.object(os.path, 'isfile') 646*9c5db199SXin Li isfile_patcher.start() 647*9c5db199SXin Li self.addCleanup(isfile_patcher.stop) 648*9c5db199SXin Li 649*9c5db199SXin Li os.path.isfile.return_value = marker_initially_exists 650*9c5db199SXin Li command.append(queue_args[0]) 651*9c5db199SXin Li gs_offloader._get_cmd_list( 652*9c5db199SXin Li False, queue_args[0], 653*9c5db199SXin Li '%s%s' % (utils.DEFAULT_OFFLOAD_GSURI, 654*9c5db199SXin Li queue_args[1])).AndReturn(command) 655*9c5db199SXin Li 656*9c5db199SXin Li 657*9c5db199SXin Li def _run_offload_dir(self, should_succeed, delete_age): 658*9c5db199SXin Li """Make one call to `offload_dir()`. 659*9c5db199SXin Li 660*9c5db199SXin Li The caller ensures all mocks are set up already. 661*9c5db199SXin Li 662*9c5db199SXin Li @param should_succeed True iff the call to `offload_dir()` 663*9c5db199SXin Li is expected to succeed and remove the 664*9c5db199SXin Li offloaded job directory. 665*9c5db199SXin Li 666*9c5db199SXin Li """ 667*9c5db199SXin Li gs_offloader.GSOffloader( 668*9c5db199SXin Li utils.DEFAULT_OFFLOAD_GSURI, False, delete_age).offload( 669*9c5db199SXin Li self._job.queue_args[0], 670*9c5db199SXin Li self._job.queue_args[1], 671*9c5db199SXin Li self._job.queue_args[2]) 672*9c5db199SXin Li self.assertEqual(not should_succeed, 673*9c5db199SXin Li os.path.isdir(self._job.queue_args[0])) 674*9c5db199SXin Li swarming_req_dir = gs_offloader._get_swarming_req_dir( 675*9c5db199SXin Li self._job.queue_args[0]) 676*9c5db199SXin Li if swarming_req_dir: 677*9c5db199SXin Li self.assertEqual(not self.should_remove_sarming_req_dir, 678*9c5db199SXin Li os.path.exists(swarming_req_dir)) 679*9c5db199SXin Li 680*9c5db199SXin Li 681*9c5db199SXin Li def test_offload_success(self): 682*9c5db199SXin Li """Test that `offload_dir()` can succeed correctly.""" 683*9c5db199SXin Li self._mock_offload_dir_calls(['test', '-d'], 684*9c5db199SXin Li self._job.queue_args) 685*9c5db199SXin Li os.path.isfile.return_value = True 686*9c5db199SXin Li self._mock_create_marker_file() 687*9c5db199SXin Li self._run_offload_dir(True, 0) 688*9c5db199SXin Li 689*9c5db199SXin Li 690*9c5db199SXin Li def test_offload_failure(self): 691*9c5db199SXin Li """Test that `offload_dir()` can fail correctly.""" 692*9c5db199SXin Li self._mock_offload_dir_calls(['test', '!', '-d'], 693*9c5db199SXin Li self._job.queue_args) 694*9c5db199SXin Li self._run_offload_dir(False, 0) 695*9c5db199SXin Li 696*9c5db199SXin Li 697*9c5db199SXin Li def test_offload_swarming_req_dir_remove(self): 698*9c5db199SXin Li """Test that `offload_dir()` can prune the empty swarming task dir.""" 699*9c5db199SXin Li should_remove = os.path.join('results', 'swarming-123abc0') 700*9c5db199SXin Li self._job = self.make_job(os.path.join(should_remove, '1')) 701*9c5db199SXin Li self._mock_offload_dir_calls(['test', '-d'], 702*9c5db199SXin Li self._job.queue_args) 703*9c5db199SXin Li 704*9c5db199SXin Li os.path.isfile.return_value = True 705*9c5db199SXin Li self.should_remove_sarming_req_dir = True 706*9c5db199SXin Li self._mock_create_marker_file() 707*9c5db199SXin Li self._run_offload_dir(True, 0) 708*9c5db199SXin Li 709*9c5db199SXin Li 710*9c5db199SXin Li def test_offload_swarming_req_dir_exist(self): 711*9c5db199SXin Li """Test that `offload_dir()` keeps the non-empty swarming task dir.""" 712*9c5db199SXin Li should_not_remove = os.path.join('results', 'swarming-456edf0') 713*9c5db199SXin Li self._job = self.make_job(os.path.join(should_not_remove, '1')) 714*9c5db199SXin Li self.make_job(os.path.join(should_not_remove, '2')) 715*9c5db199SXin Li self._mock_offload_dir_calls(['test', '-d'], 716*9c5db199SXin Li self._job.queue_args) 717*9c5db199SXin Li 718*9c5db199SXin Li os.path.isfile.return_value = True 719*9c5db199SXin Li self.should_remove_sarming_req_dir = False 720*9c5db199SXin Li self._mock_create_marker_file() 721*9c5db199SXin Li self._run_offload_dir(True, 0) 722*9c5db199SXin Li 723*9c5db199SXin Li 724*9c5db199SXin Li def test_sanitize_dir(self): 725*9c5db199SXin Li """Test that folder/file name with invalid character can be corrected. 726*9c5db199SXin Li """ 727*9c5db199SXin Li results_folder = tempfile.mkdtemp() 728*9c5db199SXin Li invalid_chars = '_'.join(['[', ']', '*', '?', '#']) 729*9c5db199SXin Li invalid_files = [] 730*9c5db199SXin Li invalid_folder_name = 'invalid_name_folder_%s' % invalid_chars 731*9c5db199SXin Li invalid_folder = os.path.join( 732*9c5db199SXin Li results_folder, 733*9c5db199SXin Li invalid_folder_name) 734*9c5db199SXin Li invalid_files.append(os.path.join( 735*9c5db199SXin Li invalid_folder, 736*9c5db199SXin Li 'invalid_name_file_%s' % invalid_chars)) 737*9c5db199SXin Li good_folder = os.path.join(results_folder, 'valid_name_folder') 738*9c5db199SXin Li good_file = os.path.join(good_folder, 'valid_name_file') 739*9c5db199SXin Li for folder in [invalid_folder, good_folder]: 740*9c5db199SXin Li os.makedirs(folder) 741*9c5db199SXin Li for f in invalid_files + [good_file]: 742*9c5db199SXin Li with open(f, 'w'): 743*9c5db199SXin Li pass 744*9c5db199SXin Li # check that broken symlinks don't break sanitization 745*9c5db199SXin Li symlink = os.path.join(invalid_folder, 'broken-link') 746*9c5db199SXin Li os.symlink(os.path.join(results_folder, 'no-such-file'), 747*9c5db199SXin Li symlink) 748*9c5db199SXin Li fifo1 = os.path.join(results_folder, 'test_fifo1') 749*9c5db199SXin Li fifo2 = os.path.join(good_folder, 'test_fifo2') 750*9c5db199SXin Li fifo3 = os.path.join(invalid_folder, 'test_fifo3') 751*9c5db199SXin Li invalid_fifo4_name = 'test_fifo4_%s' % invalid_chars 752*9c5db199SXin Li fifo4 = os.path.join(invalid_folder, invalid_fifo4_name) 753*9c5db199SXin Li os.mkfifo(fifo1) 754*9c5db199SXin Li os.mkfifo(fifo2) 755*9c5db199SXin Li os.mkfifo(fifo3) 756*9c5db199SXin Li os.mkfifo(fifo4) 757*9c5db199SXin Li gs_offloader.sanitize_dir(results_folder) 758*9c5db199SXin Li for _, dirs, files in os.walk(results_folder): 759*9c5db199SXin Li for name in dirs + files: 760*9c5db199SXin Li self.assertEqual(name, gslib.escape(name)) 761*9c5db199SXin Li for c in name: 762*9c5db199SXin Li self.assertFalse(c in ['[', ']', '*', '?', '#']) 763*9c5db199SXin Li self.assertTrue(os.path.exists(good_file)) 764*9c5db199SXin Li 765*9c5db199SXin Li self.assertTrue(os.path.exists(fifo1)) 766*9c5db199SXin Li self.assertFalse(is_fifo(fifo1)) 767*9c5db199SXin Li self.assertTrue(os.path.exists(fifo2)) 768*9c5db199SXin Li self.assertFalse(is_fifo(fifo2)) 769*9c5db199SXin Li corrected_folder = os.path.join( 770*9c5db199SXin Li results_folder, gslib.escape(invalid_folder_name)) 771*9c5db199SXin Li corrected_fifo3 = os.path.join( 772*9c5db199SXin Li corrected_folder, 773*9c5db199SXin Li 'test_fifo3') 774*9c5db199SXin Li self.assertFalse(os.path.exists(fifo3)) 775*9c5db199SXin Li self.assertTrue(os.path.exists(corrected_fifo3)) 776*9c5db199SXin Li self.assertFalse(is_fifo(corrected_fifo3)) 777*9c5db199SXin Li corrected_fifo4 = os.path.join( 778*9c5db199SXin Li corrected_folder, gslib.escape(invalid_fifo4_name)) 779*9c5db199SXin Li self.assertFalse(os.path.exists(fifo4)) 780*9c5db199SXin Li self.assertTrue(os.path.exists(corrected_fifo4)) 781*9c5db199SXin Li self.assertFalse(is_fifo(corrected_fifo4)) 782*9c5db199SXin Li 783*9c5db199SXin Li corrected_symlink = os.path.join( 784*9c5db199SXin Li corrected_folder, 785*9c5db199SXin Li 'broken-link') 786*9c5db199SXin Li self.assertFalse(os.path.lexists(symlink)) 787*9c5db199SXin Li self.assertTrue(os.path.exists(corrected_symlink)) 788*9c5db199SXin Li self.assertFalse(os.path.islink(corrected_symlink)) 789*9c5db199SXin Li shutil.rmtree(results_folder) 790*9c5db199SXin Li 791*9c5db199SXin Li 792*9c5db199SXin Li def check_limit_file_count(self, is_test_job=True): 793*9c5db199SXin Li """Test that folder with too many files can be compressed. 794*9c5db199SXin Li 795*9c5db199SXin Li @param is_test_job: True to check the method with test job result 796*9c5db199SXin Li folder. Set to False for special task folder. 797*9c5db199SXin Li """ 798*9c5db199SXin Li results_folder = tempfile.mkdtemp() 799*9c5db199SXin Li host_folder = os.path.join( 800*9c5db199SXin Li results_folder, 801*9c5db199SXin Li 'lab1-host1' if is_test_job else 'hosts/lab1-host1/1-repair') 802*9c5db199SXin Li debug_folder = os.path.join(host_folder, 'debug') 803*9c5db199SXin Li sysinfo_folder = os.path.join(host_folder, 'sysinfo') 804*9c5db199SXin Li for folder in [debug_folder, sysinfo_folder]: 805*9c5db199SXin Li os.makedirs(folder) 806*9c5db199SXin Li for i in range(10): 807*9c5db199SXin Li with open(os.path.join(folder, str(i)), 'w') as f: 808*9c5db199SXin Li f.write('test') 809*9c5db199SXin Li 810*9c5db199SXin Li gs_offloader._MAX_FILE_COUNT = 100 811*9c5db199SXin Li gs_offloader.limit_file_count( 812*9c5db199SXin Li results_folder if is_test_job else host_folder) 813*9c5db199SXin Li self.assertTrue(os.path.exists(sysinfo_folder)) 814*9c5db199SXin Li 815*9c5db199SXin Li gs_offloader._MAX_FILE_COUNT = 10 816*9c5db199SXin Li gs_offloader.limit_file_count( 817*9c5db199SXin Li results_folder if is_test_job else host_folder) 818*9c5db199SXin Li self.assertFalse(os.path.exists(sysinfo_folder)) 819*9c5db199SXin Li self.assertTrue(os.path.exists(sysinfo_folder + '.tgz')) 820*9c5db199SXin Li self.assertTrue(os.path.exists(debug_folder)) 821*9c5db199SXin Li 822*9c5db199SXin Li shutil.rmtree(results_folder) 823*9c5db199SXin Li 824*9c5db199SXin Li 825*9c5db199SXin Li def test_limit_file_count(self): 826*9c5db199SXin Li """Test that folder with too many files can be compressed. 827*9c5db199SXin Li """ 828*9c5db199SXin Li self.check_limit_file_count(is_test_job=True) 829*9c5db199SXin Li self.check_limit_file_count(is_test_job=False) 830*9c5db199SXin Li 831*9c5db199SXin Li 832*9c5db199SXin Li def test_get_metrics_fields(self): 833*9c5db199SXin Li """Test method _get_metrics_fields.""" 834*9c5db199SXin Li results_folder, host_folder = self._create_results_folder() 835*9c5db199SXin Li models.test.parse_job_keyval.return_value = ({ 836*9c5db199SXin Li 'build': 'veyron_minnie-cheets-release/R52-8248.0.0', 837*9c5db199SXin Li 'parent_job_id': 'p_id', 838*9c5db199SXin Li 'suite': 'arc-cts' 839*9c5db199SXin Li }) 840*9c5db199SXin Li try: 841*9c5db199SXin Li self.assertEqual({'board': 'veyron_minnie-cheets', 842*9c5db199SXin Li 'milestone': 'R52'}, 843*9c5db199SXin Li gs_offloader._get_metrics_fields(host_folder)) 844*9c5db199SXin Li finally: 845*9c5db199SXin Li shutil.rmtree(results_folder) 846*9c5db199SXin Li 847*9c5db199SXin Li 848*9c5db199SXin Li def _create_results_folder(self): 849*9c5db199SXin Li results_folder = tempfile.mkdtemp() 850*9c5db199SXin Li host_folder = os.path.join(results_folder, 'chromeos4-row9-rack11-host22') 851*9c5db199SXin Li 852*9c5db199SXin Li # Build host keyvals set to parse model info. 853*9c5db199SXin Li host_info_path = os.path.join(host_folder, 'host_keyvals') 854*9c5db199SXin Li dir_to_create = '/' 855*9c5db199SXin Li for tdir in host_info_path.split('/'): 856*9c5db199SXin Li dir_to_create = os.path.join(dir_to_create, tdir) 857*9c5db199SXin Li if not os.path.exists(dir_to_create): 858*9c5db199SXin Li os.mkdir(dir_to_create) 859*9c5db199SXin Li with open(os.path.join(host_info_path, 'chromeos4-row9-rack11-host22'), 'w') as store_file: 860*9c5db199SXin Li store_file.write('labels=board%3Acoral,hw_video_acc_vp9,cros,'+ 861*9c5db199SXin Li 'hw_jpeg_acc_dec,bluetooth,model%3Arobo360,'+ 862*9c5db199SXin Li 'accel%3Acros-ec,'+ 863*9c5db199SXin Li 'sku%3Arobo360_IntelR_CeleronR_CPU_N3450_1_10GHz_4Gb') 864*9c5db199SXin Li 865*9c5db199SXin Li # .autoserv_execute file is needed for the test results package to look 866*9c5db199SXin Li # legit. 867*9c5db199SXin Li autoserve_path = os.path.join(host_folder, '.autoserv_execute') 868*9c5db199SXin Li with open(autoserve_path, 'w') as temp_file: 869*9c5db199SXin Li temp_file.write(' ') 870*9c5db199SXin Li 871*9c5db199SXin Li return (results_folder, host_folder) 872*9c5db199SXin Li 873*9c5db199SXin Li 874*9c5db199SXin Liclass OffladerConfigTests(_TempResultsDirTestBase): 875*9c5db199SXin Li """Tests for the `Offloader` to follow side_effect config.""" 876*9c5db199SXin Li 877*9c5db199SXin Li def setUp(self): 878*9c5db199SXin Li super(OffladerConfigTests, self).setUp() 879*9c5db199SXin Li gs_offloader.GS_OFFLOADING_ENABLED = True 880*9c5db199SXin Li gs_offloader.GS_OFFLOADER_MULTIPROCESSING = True 881*9c5db199SXin Li self.dest_path = '/results' 882*9c5db199SXin Li 883*9c5db199SXin Li metrics_fields_patcher = mock.patch.object(gs_offloader, 884*9c5db199SXin Li '_get_metrics_fields') 885*9c5db199SXin Li metrics_fields_patcher.start() 886*9c5db199SXin Li self.addCleanup(metrics_fields_patcher.stop) 887*9c5db199SXin Li 888*9c5db199SXin Li offloadError_patcher = mock.patch.object(gs_offloader, '_OffloadError') 889*9c5db199SXin Li offloadError_patcher.start() 890*9c5db199SXin Li self.addCleanup(offloadError_patcher.stop) 891*9c5db199SXin Li 892*9c5db199SXin Li offload_metrics_patcher = mock.patch.object(gs_offloader, 893*9c5db199SXin Li '_emit_offload_metrics') 894*9c5db199SXin Li offload_metrics_patcher.start() 895*9c5db199SXin Li self.addCleanup(offload_metrics_patcher.stop) 896*9c5db199SXin Li 897*9c5db199SXin Li cmd_list_patcher = mock.patch.object(gs_offloader, '_get_cmd_list') 898*9c5db199SXin Li cmd_list_patcher.start() 899*9c5db199SXin Li self.addCleanup(cmd_list_patcher.stop) 900*9c5db199SXin Li 901*9c5db199SXin Li Popen_patcher = mock.patch.object(subprocess, 'Popen') 902*9c5db199SXin Li Popen_patcher.start() 903*9c5db199SXin Li self.addCleanup(Popen_patcher.stop) 904*9c5db199SXin Li 905*9c5db199SXin Li returncode_metric_patcher = mock.patch.object( 906*9c5db199SXin Li gs_offloader, '_emit_gs_returncode_metric') 907*9c5db199SXin Li returncode_metric_patcher.start() 908*9c5db199SXin Li self.addCleanup(returncode_metric_patcher.stop) 909*9c5db199SXin Li 910*9c5db199SXin Li def _run(self, results_dir, gs_bucket, expect_dest): 911*9c5db199SXin Li stdout = os.path.join(results_dir, 'std.log') 912*9c5db199SXin Li stderr = os.path.join(results_dir, 'std.err') 913*9c5db199SXin Li config = { 914*9c5db199SXin Li 'tko': { 915*9c5db199SXin Li 'proxy_socket': '/file-system/foo-socket', 916*9c5db199SXin Li 'mysql_user': 'foo-user', 917*9c5db199SXin Li 'mysql_password_file': '/file-system/foo-password-file' 918*9c5db199SXin Li }, 919*9c5db199SXin Li 'google_storage': { 920*9c5db199SXin Li 'bucket': gs_bucket, 921*9c5db199SXin Li 'credentials_file': '/foo-creds' 922*9c5db199SXin Li }, 923*9c5db199SXin Li 'this_field_is_ignored': True 924*9c5db199SXin Li } 925*9c5db199SXin Li path = os.path.join(results_dir, 'side_effects_config.json') 926*9c5db199SXin Li with open(path, 'w') as f: 927*9c5db199SXin Li f.write(json.dumps(config)) 928*9c5db199SXin Li gs_offloader._get_metrics_fields(results_dir) 929*9c5db199SXin Li gs_offloader._get_cmd_list.return_value = ['test', '-d', expect_dest] 930*9c5db199SXin Li subprocess.Popen.side_effect = [ 931*9c5db199SXin Li _get_fake_process(), _get_fake_process() 932*9c5db199SXin Li ] 933*9c5db199SXin Li gs_offloader._OffloadError(mock.ANY) 934*9c5db199SXin Li gs_offloader._emit_gs_returncode_metric.return_value = True 935*9c5db199SXin Li gs_offloader._emit_offload_metrics.return_value = True 936*9c5db199SXin Li sub_offloader = gs_offloader.GSOffloader(results_dir, True, 0, None) 937*9c5db199SXin Li sub_offloader._try_offload(results_dir, self.dest_path, stdout, stderr) 938*9c5db199SXin Li shutil.rmtree(results_dir) 939*9c5db199SXin Li 940*9c5db199SXin Li def _verify(self, results_dir, gs_bucket, expect_dest): 941*9c5db199SXin Li gs_offloader._get_cmd_list.assert_called_with(True, mock.ANY, 942*9c5db199SXin Li expect_dest) 943*9c5db199SXin Li 944*9c5db199SXin Li stdout = os.path.join(results_dir, 'std.log') 945*9c5db199SXin Li stderr = os.path.join(results_dir, 'std.err') 946*9c5db199SXin Li 947*9c5db199SXin Li # x2 948*9c5db199SXin Li subprocess_call = mock.call(mock.ANY, stdout=stdout, stderr=stderr) 949*9c5db199SXin Li subprocess.Popen.assert_has_calls([subprocess_call, subprocess_call]) 950*9c5db199SXin Li 951*9c5db199SXin Li def test_skip_gs_prefix(self): 952*9c5db199SXin Li """Test skip the 'gs://' prefix if already presented.""" 953*9c5db199SXin Li res = tempfile.mkdtemp() 954*9c5db199SXin Li gs_bucket = 'gs://prod-bucket' 955*9c5db199SXin Li expect_dest = gs_bucket + self.dest_path 956*9c5db199SXin Li self._run(res, gs_bucket, expect_dest) 957*9c5db199SXin Li self._verify(res, gs_bucket, expect_dest) 958*9c5db199SXin Li 959*9c5db199SXin Li 960*9c5db199SXin Liclass JobDirectoryOffloadTests(_TempResultsDirTestBase): 961*9c5db199SXin Li """Tests for `_JobDirectory.enqueue_offload()`. 962*9c5db199SXin Li 963*9c5db199SXin Li When testing with a `days_old` parameter of 0, we use 964*9c5db199SXin Li `set_finished()` instead of `set_expired()`. This causes the 965*9c5db199SXin Li job's timestamp to be set in the future. This is done so as 966*9c5db199SXin Li to test that when `days_old` is 0, the job is always treated 967*9c5db199SXin Li as eligible for offload, regardless of the timestamp's value. 968*9c5db199SXin Li 969*9c5db199SXin Li Testing covers the following assertions: 970*9c5db199SXin Li A. Each time `enqueue_offload()` is called, a message that 971*9c5db199SXin Li includes the job's directory name will be logged using 972*9c5db199SXin Li `logging.debug()`, regardless of whether the job was 973*9c5db199SXin Li enqueued. Nothing else is allowed to be logged. 974*9c5db199SXin Li B. If the job is not eligible to be offloaded, 975*9c5db199SXin Li `first_offload_start` and `offload_count` are 0. 976*9c5db199SXin Li C. If the job is not eligible for offload, nothing is 977*9c5db199SXin Li enqueued in `queue`. 978*9c5db199SXin Li D. When the job is offloaded, `offload_count` increments 979*9c5db199SXin Li each time. 980*9c5db199SXin Li E. When the job is offloaded, the appropriate parameters are 981*9c5db199SXin Li enqueued exactly once. 982*9c5db199SXin Li F. The first time a job is offloaded, `first_offload_start` is 983*9c5db199SXin Li set to the current time. 984*9c5db199SXin Li G. `first_offload_start` only changes the first time that the 985*9c5db199SXin Li job is offloaded. 986*9c5db199SXin Li 987*9c5db199SXin Li The test cases below are designed to exercise all of the 988*9c5db199SXin Li meaningful state transitions at least once. 989*9c5db199SXin Li 990*9c5db199SXin Li """ 991*9c5db199SXin Li 992*9c5db199SXin Li def setUp(self): 993*9c5db199SXin Li super(JobDirectoryOffloadTests, self).setUp() 994*9c5db199SXin Li self._job = self.make_job(self.REGULAR_JOBLIST[0]) 995*9c5db199SXin Li self._queue = six.moves.queue.Queue() 996*9c5db199SXin Li 997*9c5db199SXin Li 998*9c5db199SXin Li def _offload_unexpired_job(self, days_old): 999*9c5db199SXin Li """Make calls to `enqueue_offload()` for an unexpired job. 1000*9c5db199SXin Li 1001*9c5db199SXin Li This method tests assertions B and C that calling 1002*9c5db199SXin Li `enqueue_offload()` has no effect. 1003*9c5db199SXin Li 1004*9c5db199SXin Li """ 1005*9c5db199SXin Li self.assertEqual(self._job.offload_count, 0) 1006*9c5db199SXin Li self.assertEqual(self._job.first_offload_start, 0) 1007*9c5db199SXin Li gs_offloader._enqueue_offload(self._job, self._queue, days_old) 1008*9c5db199SXin Li gs_offloader._enqueue_offload(self._job, self._queue, days_old) 1009*9c5db199SXin Li self.assertTrue(self._queue.empty()) 1010*9c5db199SXin Li self.assertEqual(self._job.offload_count, 0) 1011*9c5db199SXin Li self.assertEqual(self._job.first_offload_start, 0) 1012*9c5db199SXin Li 1013*9c5db199SXin Li 1014*9c5db199SXin Li def _offload_expired_once(self, days_old, count): 1015*9c5db199SXin Li """Make one call to `enqueue_offload()` for an expired job. 1016*9c5db199SXin Li 1017*9c5db199SXin Li This method tests assertions D and E regarding side-effects 1018*9c5db199SXin Li expected when a job is offloaded. 1019*9c5db199SXin Li 1020*9c5db199SXin Li """ 1021*9c5db199SXin Li gs_offloader._enqueue_offload(self._job, self._queue, days_old) 1022*9c5db199SXin Li self.assertEqual(self._job.offload_count, count) 1023*9c5db199SXin Li self.assertFalse(self._queue.empty()) 1024*9c5db199SXin Li v = self._queue.get_nowait() 1025*9c5db199SXin Li self.assertTrue(self._queue.empty()) 1026*9c5db199SXin Li self.assertEqual(v, self._job.queue_args) 1027*9c5db199SXin Li 1028*9c5db199SXin Li 1029*9c5db199SXin Li def _offload_expired_job(self, days_old): 1030*9c5db199SXin Li """Make calls to `enqueue_offload()` for a just-expired job. 1031*9c5db199SXin Li 1032*9c5db199SXin Li This method directly tests assertions F and G regarding 1033*9c5db199SXin Li side-effects on `first_offload_start`. 1034*9c5db199SXin Li 1035*9c5db199SXin Li """ 1036*9c5db199SXin Li t0 = time.time() 1037*9c5db199SXin Li self._offload_expired_once(days_old, 1) 1038*9c5db199SXin Li t1 = self._job.first_offload_start 1039*9c5db199SXin Li self.assertLessEqual(t1, time.time()) 1040*9c5db199SXin Li self.assertGreaterEqual(t1, t0) 1041*9c5db199SXin Li self._offload_expired_once(days_old, 2) 1042*9c5db199SXin Li self.assertEqual(self._job.first_offload_start, t1) 1043*9c5db199SXin Li self._offload_expired_once(days_old, 3) 1044*9c5db199SXin Li self.assertEqual(self._job.first_offload_start, t1) 1045*9c5db199SXin Li 1046*9c5db199SXin Li 1047*9c5db199SXin Li def test_case_1_no_expiration(self): 1048*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` of 0. 1049*9c5db199SXin Li 1050*9c5db199SXin Li This tests that offload works as expected if calls are 1051*9c5db199SXin Li made both before and after the job becomes expired. 1052*9c5db199SXin Li 1053*9c5db199SXin Li """ 1054*9c5db199SXin Li self._offload_unexpired_job(0) 1055*9c5db199SXin Li self._job.set_finished(0) 1056*9c5db199SXin Li self._offload_expired_job(0) 1057*9c5db199SXin Li 1058*9c5db199SXin Li 1059*9c5db199SXin Li def test_case_2_no_expiration(self): 1060*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` of 0. 1061*9c5db199SXin Li 1062*9c5db199SXin Li This tests that offload works as expected if calls are made 1063*9c5db199SXin Li only after the job becomes expired. 1064*9c5db199SXin Li 1065*9c5db199SXin Li """ 1066*9c5db199SXin Li self._job.set_finished(0) 1067*9c5db199SXin Li self._offload_expired_job(0) 1068*9c5db199SXin Li 1069*9c5db199SXin Li 1070*9c5db199SXin Li def test_case_1_with_expiration(self): 1071*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1072*9c5db199SXin Li 1073*9c5db199SXin Li This tests that offload works as expected if calls are made 1074*9c5db199SXin Li before the job finishes, before the job expires, and after 1075*9c5db199SXin Li the job expires. 1076*9c5db199SXin Li 1077*9c5db199SXin Li """ 1078*9c5db199SXin Li self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1079*9c5db199SXin Li self._job.set_finished(_TEST_EXPIRATION_AGE) 1080*9c5db199SXin Li self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1081*9c5db199SXin Li self._job.set_expired(_TEST_EXPIRATION_AGE) 1082*9c5db199SXin Li self._offload_expired_job(_TEST_EXPIRATION_AGE) 1083*9c5db199SXin Li 1084*9c5db199SXin Li 1085*9c5db199SXin Li def test_case_2_with_expiration(self): 1086*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1087*9c5db199SXin Li 1088*9c5db199SXin Li This tests that offload works as expected if calls are made 1089*9c5db199SXin Li between finishing and expiration, and after the job expires. 1090*9c5db199SXin Li 1091*9c5db199SXin Li """ 1092*9c5db199SXin Li self._job.set_finished(_TEST_EXPIRATION_AGE) 1093*9c5db199SXin Li self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1094*9c5db199SXin Li self._job.set_expired(_TEST_EXPIRATION_AGE) 1095*9c5db199SXin Li self._offload_expired_job(_TEST_EXPIRATION_AGE) 1096*9c5db199SXin Li 1097*9c5db199SXin Li 1098*9c5db199SXin Li def test_case_3_with_expiration(self): 1099*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1100*9c5db199SXin Li 1101*9c5db199SXin Li This tests that offload works as expected if calls are made 1102*9c5db199SXin Li only before finishing and after expiration. 1103*9c5db199SXin Li 1104*9c5db199SXin Li """ 1105*9c5db199SXin Li self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1106*9c5db199SXin Li self._job.set_expired(_TEST_EXPIRATION_AGE) 1107*9c5db199SXin Li self._offload_expired_job(_TEST_EXPIRATION_AGE) 1108*9c5db199SXin Li 1109*9c5db199SXin Li 1110*9c5db199SXin Li def test_case_4_with_expiration(self): 1111*9c5db199SXin Li """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1112*9c5db199SXin Li 1113*9c5db199SXin Li This tests that offload works as expected if calls are made 1114*9c5db199SXin Li only after expiration. 1115*9c5db199SXin Li 1116*9c5db199SXin Li """ 1117*9c5db199SXin Li self._job.set_expired(_TEST_EXPIRATION_AGE) 1118*9c5db199SXin Li self._offload_expired_job(_TEST_EXPIRATION_AGE) 1119*9c5db199SXin Li 1120*9c5db199SXin Li 1121*9c5db199SXin Liclass GetJobDirectoriesTests(_TempResultsDirTestBase): 1122*9c5db199SXin Li """Tests for `_JobDirectory.get_job_directories()`.""" 1123*9c5db199SXin Li 1124*9c5db199SXin Li def setUp(self): 1125*9c5db199SXin Li super(GetJobDirectoriesTests, self).setUp() 1126*9c5db199SXin Li self.make_job_hierarchy() 1127*9c5db199SXin Li os.mkdir('not-a-job') 1128*9c5db199SXin Li open('not-a-dir', 'w').close() 1129*9c5db199SXin Li 1130*9c5db199SXin Li 1131*9c5db199SXin Li def _run_get_directories(self, cls, expected_list): 1132*9c5db199SXin Li """Test `get_job_directories()` for the given class. 1133*9c5db199SXin Li 1134*9c5db199SXin Li Calls the method, and asserts that the returned list of 1135*9c5db199SXin Li directories matches the expected return value. 1136*9c5db199SXin Li 1137*9c5db199SXin Li @param expected_list Expected return value from the call. 1138*9c5db199SXin Li """ 1139*9c5db199SXin Li dirlist = cls.get_job_directories() 1140*9c5db199SXin Li self.assertEqual(set(dirlist), set(expected_list)) 1141*9c5db199SXin Li 1142*9c5db199SXin Li 1143*9c5db199SXin Li def test_get_regular_jobs(self): 1144*9c5db199SXin Li """Test `RegularJobDirectory.get_job_directories()`.""" 1145*9c5db199SXin Li self._run_get_directories(job_directories.RegularJobDirectory, 1146*9c5db199SXin Li self.REGULAR_JOBLIST) 1147*9c5db199SXin Li 1148*9c5db199SXin Li 1149*9c5db199SXin Li def test_get_special_jobs(self): 1150*9c5db199SXin Li """Test `SpecialJobDirectory.get_job_directories()`.""" 1151*9c5db199SXin Li self._run_get_directories(job_directories.SpecialJobDirectory, 1152*9c5db199SXin Li self.SPECIAL_JOBLIST) 1153*9c5db199SXin Li 1154*9c5db199SXin Li 1155*9c5db199SXin Liclass AddJobsTests(_TempResultsDirTestBase): 1156*9c5db199SXin Li """Tests for `Offloader._add_new_jobs()`.""" 1157*9c5db199SXin Li 1158*9c5db199SXin Li MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu'] 1159*9c5db199SXin Li 1160*9c5db199SXin Li def setUp(self): 1161*9c5db199SXin Li super(AddJobsTests, self).setUp() 1162*9c5db199SXin Li self._initial_job_names = ( 1163*9c5db199SXin Li set(self.REGULAR_JOBLIST) | set(self.SPECIAL_JOBLIST)) 1164*9c5db199SXin Li self.make_job_hierarchy() 1165*9c5db199SXin Li self._offloader = gs_offloader.Offloader(_get_options(['-a'])) 1166*9c5db199SXin Li 1167*9c5db199SXin Li logging_patcher = mock.patch.object(logging, 'debug') 1168*9c5db199SXin Li self.logging_patch = logging_patcher.start() 1169*9c5db199SXin Li self.addCleanup(logging_patcher.stop) 1170*9c5db199SXin Li 1171*9c5db199SXin Li def _run_add_new_jobs(self, expected_key_set): 1172*9c5db199SXin Li """Basic test assertions for `_add_new_jobs()`. 1173*9c5db199SXin Li 1174*9c5db199SXin Li Asserts the following: 1175*9c5db199SXin Li * The keys in the offloader's `_open_jobs` dictionary 1176*9c5db199SXin Li matches the expected set of keys. 1177*9c5db199SXin Li * For every job in `_open_jobs`, the job has the expected 1178*9c5db199SXin Li directory name. 1179*9c5db199SXin Li 1180*9c5db199SXin Li """ 1181*9c5db199SXin Li count = len(expected_key_set) - len(self._offloader._open_jobs) 1182*9c5db199SXin Li self._offloader._add_new_jobs() 1183*9c5db199SXin Li self.assertEqual(expected_key_set, 1184*9c5db199SXin Li set(self._offloader._open_jobs.keys())) 1185*9c5db199SXin Li for jobkey, job in self._offloader._open_jobs.items(): 1186*9c5db199SXin Li self.assertEqual(jobkey, job.dirname) 1187*9c5db199SXin Li 1188*9c5db199SXin Li self.logging_patch.assert_called_with(mock.ANY, count) 1189*9c5db199SXin Li 1190*9c5db199SXin Li def test_add_jobs_empty(self): 1191*9c5db199SXin Li """Test adding jobs to an empty dictionary. 1192*9c5db199SXin Li 1193*9c5db199SXin Li Calls the offloader's `_add_new_jobs()`, then perform 1194*9c5db199SXin Li the assertions of `self._check_open_jobs()`. 1195*9c5db199SXin Li 1196*9c5db199SXin Li """ 1197*9c5db199SXin Li self._run_add_new_jobs(self._initial_job_names) 1198*9c5db199SXin Li 1199*9c5db199SXin Li 1200*9c5db199SXin Li def test_add_jobs_non_empty(self): 1201*9c5db199SXin Li """Test adding jobs to a non-empty dictionary. 1202*9c5db199SXin Li 1203*9c5db199SXin Li Calls the offloader's `_add_new_jobs()` twice; once from 1204*9c5db199SXin Li initial conditions, and then again after adding more 1205*9c5db199SXin Li directories. After the second call, perform the assertions 1206*9c5db199SXin Li of `self._check_open_jobs()`. Additionally, assert that 1207*9c5db199SXin Li keys added by the first call still map to their original 1208*9c5db199SXin Li job object after the second call. 1209*9c5db199SXin Li 1210*9c5db199SXin Li """ 1211*9c5db199SXin Li self._run_add_new_jobs(self._initial_job_names) 1212*9c5db199SXin Li jobs_copy = self._offloader._open_jobs.copy() 1213*9c5db199SXin Li for d in self.MOREJOBS: 1214*9c5db199SXin Li os.mkdir(d) 1215*9c5db199SXin Li self._run_add_new_jobs(self._initial_job_names | 1216*9c5db199SXin Li set(self.MOREJOBS)) 1217*9c5db199SXin Li for key in jobs_copy.keys(): 1218*9c5db199SXin Li self.assertIs(jobs_copy[key], 1219*9c5db199SXin Li self._offloader._open_jobs[key]) 1220*9c5db199SXin Li 1221*9c5db199SXin Li 1222*9c5db199SXin Liclass ReportingTests(_TempResultsDirTestBase): 1223*9c5db199SXin Li """Tests for `Offloader._report_failed_jobs()`.""" 1224*9c5db199SXin Li 1225*9c5db199SXin Li def setUp(self): 1226*9c5db199SXin Li super(ReportingTests, self).setUp() 1227*9c5db199SXin Li self._offloader = gs_offloader.Offloader(_get_options([])) 1228*9c5db199SXin Li 1229*9c5db199SXin Li failed_jobs_patcher = mock.patch.object(self._offloader, 1230*9c5db199SXin Li '_log_failed_jobs_locally') 1231*9c5db199SXin Li self.failed_jobs_patch = failed_jobs_patcher.start() 1232*9c5db199SXin Li self.addCleanup(failed_jobs_patcher.stop) 1233*9c5db199SXin Li 1234*9c5db199SXin Li logging_patcher = mock.patch.object(logging, 'debug') 1235*9c5db199SXin Li self.logging_patch = logging_patcher.start() 1236*9c5db199SXin Li self.addCleanup(logging_patcher.stop) 1237*9c5db199SXin Li 1238*9c5db199SXin Li def _add_job(self, jobdir): 1239*9c5db199SXin Li """Add a job to the dictionary of unfinished jobs.""" 1240*9c5db199SXin Li j = self.make_job(jobdir) 1241*9c5db199SXin Li self._offloader._open_jobs[j.dirname] = j 1242*9c5db199SXin Li return j 1243*9c5db199SXin Li 1244*9c5db199SXin Li 1245*9c5db199SXin Li def _expect_log_message(self, new_open_jobs, with_failures, count=None): 1246*9c5db199SXin Li """Mock expected logging calls. 1247*9c5db199SXin Li 1248*9c5db199SXin Li `_report_failed_jobs()` logs one message with the number 1249*9c5db199SXin Li of jobs removed from the open job set and the number of jobs 1250*9c5db199SXin Li still remaining. Additionally, if there are reportable 1251*9c5db199SXin Li jobs, then it logs the number of jobs that haven't yet 1252*9c5db199SXin Li offloaded. 1253*9c5db199SXin Li 1254*9c5db199SXin Li This sets up the logging calls using `new_open_jobs` to 1255*9c5db199SXin Li figure the job counts. If `with_failures` is true, then 1256*9c5db199SXin Li the log message is set up assuming that all jobs in 1257*9c5db199SXin Li `new_open_jobs` have offload failures. 1258*9c5db199SXin Li 1259*9c5db199SXin Li @param new_open_jobs New job set for calculating counts 1260*9c5db199SXin Li in the messages. 1261*9c5db199SXin Li @param with_failures Whether the log message with a 1262*9c5db199SXin Li failure count is expected. 1263*9c5db199SXin Li 1264*9c5db199SXin Li """ 1265*9c5db199SXin Li if not count: 1266*9c5db199SXin Li count = len(self._offloader._open_jobs) - len(new_open_jobs) 1267*9c5db199SXin Li self.logging_patch.assert_called_with(mock.ANY, count, 1268*9c5db199SXin Li len(new_open_jobs)) 1269*9c5db199SXin Li if with_failures: 1270*9c5db199SXin Li self.logging_patch.assert_called_with(mock.ANY, len(new_open_jobs)) 1271*9c5db199SXin Li 1272*9c5db199SXin Li def _run_update(self, new_open_jobs): 1273*9c5db199SXin Li """Call `_report_failed_jobs()`. 1274*9c5db199SXin Li 1275*9c5db199SXin Li Initial conditions are set up by the caller. This calls 1276*9c5db199SXin Li `_report_failed_jobs()` once, and then checks these 1277*9c5db199SXin Li assertions: 1278*9c5db199SXin Li * The offloader's new `_open_jobs` field contains only 1279*9c5db199SXin Li the entries in `new_open_jobs`. 1280*9c5db199SXin Li 1281*9c5db199SXin Li @param new_open_jobs A dictionary representing the expected 1282*9c5db199SXin Li new value of the offloader's 1283*9c5db199SXin Li `_open_jobs` field. 1284*9c5db199SXin Li """ 1285*9c5db199SXin Li self._offloader._report_failed_jobs() 1286*9c5db199SXin Li self._offloader._remove_offloaded_jobs() 1287*9c5db199SXin Li self.assertEqual(self._offloader._open_jobs, new_open_jobs) 1288*9c5db199SXin Li 1289*9c5db199SXin Li 1290*9c5db199SXin Li def _expect_failed_jobs(self, failed_jobs): 1291*9c5db199SXin Li """Mock expected call to log the failed jobs on local disk. 1292*9c5db199SXin Li 1293*9c5db199SXin Li TODO(crbug.com/686904): The fact that we have to mock an internal 1294*9c5db199SXin Li function for this test is evidence that we need to pull out the local 1295*9c5db199SXin Li file formatter in its own object in a future CL. 1296*9c5db199SXin Li 1297*9c5db199SXin Li @param failed_jobs: The list of jobs being logged as failed. 1298*9c5db199SXin Li """ 1299*9c5db199SXin Li self._offloader._log_failed_jobs_locally(failed_jobs) 1300*9c5db199SXin Li 1301*9c5db199SXin Li 1302*9c5db199SXin Li def test_no_jobs(self): 1303*9c5db199SXin Li """Test `_report_failed_jobs()` with no open jobs. 1304*9c5db199SXin Li 1305*9c5db199SXin Li Initial conditions are an empty `_open_jobs` list. 1306*9c5db199SXin Li Expected result is an empty `_open_jobs` list. 1307*9c5db199SXin Li 1308*9c5db199SXin Li """ 1309*9c5db199SXin Li self._expect_failed_jobs([]) 1310*9c5db199SXin Li self._run_update({}) 1311*9c5db199SXin Li self._expect_log_message({}, False) 1312*9c5db199SXin Li 1313*9c5db199SXin Li 1314*9c5db199SXin Li def test_all_completed(self): 1315*9c5db199SXin Li """Test `_report_failed_jobs()` with only complete jobs. 1316*9c5db199SXin Li 1317*9c5db199SXin Li Initial conditions are an `_open_jobs` list consisting of only completed 1318*9c5db199SXin Li jobs. 1319*9c5db199SXin Li Expected result is an empty `_open_jobs` list. 1320*9c5db199SXin Li 1321*9c5db199SXin Li """ 1322*9c5db199SXin Li 1323*9c5db199SXin Li for d in self.REGULAR_JOBLIST: 1324*9c5db199SXin Li self._add_job(d).set_complete() 1325*9c5db199SXin Li count = len(self._offloader._open_jobs) 1326*9c5db199SXin Li self._expect_failed_jobs([]) 1327*9c5db199SXin Li self._run_update({}) 1328*9c5db199SXin Li self._expect_log_message({}, False, count) 1329*9c5db199SXin Li 1330*9c5db199SXin Li 1331*9c5db199SXin Li def test_none_finished(self): 1332*9c5db199SXin Li """Test `_report_failed_jobs()` with only unfinished jobs. 1333*9c5db199SXin Li 1334*9c5db199SXin Li Initial conditions are an `_open_jobs` list consisting of only 1335*9c5db199SXin Li unfinished jobs. 1336*9c5db199SXin Li Expected result is no change to the `_open_jobs` list. 1337*9c5db199SXin Li 1338*9c5db199SXin Li """ 1339*9c5db199SXin Li for d in self.REGULAR_JOBLIST: 1340*9c5db199SXin Li self._add_job(d) 1341*9c5db199SXin Li new_jobs = self._offloader._open_jobs.copy() 1342*9c5db199SXin Li self._expect_failed_jobs([]) 1343*9c5db199SXin Li self._run_update(new_jobs) 1344*9c5db199SXin Li self._expect_log_message(new_jobs, False) 1345*9c5db199SXin Li 1346*9c5db199SXin Li 1347*9c5db199SXin Liclass GsOffloaderMockTests(_TempResultsDirTestCase): 1348*9c5db199SXin Li """Tests using mock instead of mox.""" 1349*9c5db199SXin Li 1350*9c5db199SXin Li def setUp(self): 1351*9c5db199SXin Li super(GsOffloaderMockTests, self).setUp() 1352*9c5db199SXin Li alarm = mock.patch('signal.alarm', return_value=0) 1353*9c5db199SXin Li alarm.start() 1354*9c5db199SXin Li self.addCleanup(alarm.stop) 1355*9c5db199SXin Li 1356*9c5db199SXin Li self._saved_loglevel = logging.getLogger().getEffectiveLevel() 1357*9c5db199SXin Li logging.getLogger().setLevel(logging.CRITICAL + 1) 1358*9c5db199SXin Li 1359*9c5db199SXin Li self._job = self.make_job(self.REGULAR_JOBLIST[0]) 1360*9c5db199SXin Li 1361*9c5db199SXin Li 1362*9c5db199SXin Li def test_offload_timeout_early(self): 1363*9c5db199SXin Li """Test that `offload_dir()` times out correctly. 1364*9c5db199SXin Li 1365*9c5db199SXin Li This test triggers timeout at the earliest possible moment, 1366*9c5db199SXin Li at the first call to set the timeout alarm. 1367*9c5db199SXin Li 1368*9c5db199SXin Li """ 1369*9c5db199SXin Li signal.alarm.side_effect = [0, timeout_util.TimeoutError('fubar')] 1370*9c5db199SXin Li gs_offloader.GSOffloader( 1371*9c5db199SXin Li utils.DEFAULT_OFFLOAD_GSURI, False, 0).offload( 1372*9c5db199SXin Li self._job.queue_args[0], 1373*9c5db199SXin Li self._job.queue_args[1], 1374*9c5db199SXin Li self._job.queue_args[2]) 1375*9c5db199SXin Li self.assertTrue(os.path.isdir(self._job.queue_args[0])) 1376*9c5db199SXin Li 1377*9c5db199SXin Li 1378*9c5db199SXin Li # TODO(ayatane): This tests passes when run locally, but it fails 1379*9c5db199SXin Li # when run on trybot. I have no idea why, but the assert isdir 1380*9c5db199SXin Li # fails. 1381*9c5db199SXin Li # 1382*9c5db199SXin Li # This test is also kind of redundant since we are using the timeout 1383*9c5db199SXin Li # from chromite which has its own tests. 1384*9c5db199SXin Li @unittest.skip('This fails on trybot') 1385*9c5db199SXin Li def test_offload_timeout_late(self): 1386*9c5db199SXin Li """Test that `offload_dir()` times out correctly. 1387*9c5db199SXin Li 1388*9c5db199SXin Li This test triggers timeout at the latest possible moment, at 1389*9c5db199SXin Li the call to clear the timeout alarm. 1390*9c5db199SXin Li 1391*9c5db199SXin Li """ 1392*9c5db199SXin Li signal.alarm.side_effect = [0, 0, timeout_util.TimeoutError('fubar')] 1393*9c5db199SXin Li with mock.patch.object(gs_offloader, '_get_cmd_list', 1394*9c5db199SXin Li autospec=True) as get_cmd_list: 1395*9c5db199SXin Li get_cmd_list.return_value = ['test', '-d', self._job.queue_args[0]] 1396*9c5db199SXin Li gs_offloader.GSOffloader( 1397*9c5db199SXin Li utils.DEFAULT_OFFLOAD_GSURI, False, 0).offload( 1398*9c5db199SXin Li self._job.queue_args[0], 1399*9c5db199SXin Li self._job.queue_args[1], 1400*9c5db199SXin Li self._job.queue_args[2]) 1401*9c5db199SXin Li self.assertTrue(os.path.isdir(self._job.queue_args[0])) 1402*9c5db199SXin Li 1403*9c5db199SXin Li 1404*9c5db199SXin Li 1405*9c5db199SXin Liif __name__ == '__main__': 1406*9c5db199SXin Li unittest.main() 1407