1 # Copyright 2016 The Chromium OS Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import __builtin__ 6 import Queue 7 import base64 8 import datetime 9 import logging 10 import os 11 import shutil 12 import signal 13 import stat 14 import sys 15 import tempfile 16 import time 17 import unittest 18 19 import mox 20 21 import common 22 from autotest_lib.client.common_lib import global_config, site_utils 23 from autotest_lib.client.common_lib import time_utils 24 from autotest_lib.client.common_lib import utils 25 from autotest_lib.site_utils import gs_offloader 26 from autotest_lib.site_utils import job_directories 27 from autotest_lib.tko import models 28 29 # Test value to use for `days_old`, if nothing else is required. 30 _TEST_EXPIRATION_AGE = 7 31 32 # When constructing sample time values for testing expiration, 33 # allow this many seconds between the expiration time and the 34 # current time. 35 _MARGIN_SECS = 10.0 36 37 38 def _get_options(argv): 39 """Helper function to exercise command line parsing. 40 41 @param argv Value of sys.argv to be parsed. 42 43 """ 44 sys.argv = ['bogus.py'] + argv 45 return gs_offloader.parse_options() 46 47 48 def is_fifo(path): 49 """Determines whether a path is a fifo.""" 50 return stat.S_ISFIFO(os.lstat(path).st_mode) 51 52 53 class OffloaderOptionsTests(mox.MoxTestBase): 54 """Tests for the `Offloader` constructor. 55 56 Tests that offloader instance fields are set as expected 57 for given command line options. 58 59 """ 60 61 _REGULAR_ONLY = set([job_directories.RegularJobDirectory]) 62 _SPECIAL_ONLY = set([job_directories.SpecialJobDirectory]) 63 _BOTH = _REGULAR_ONLY | _SPECIAL_ONLY 64 65 66 def setUp(self): 67 super(OffloaderOptionsTests, self).setUp() 68 self.mox.StubOutWithMock(utils, 'get_offload_gsuri') 69 gs_offloader.GS_OFFLOADING_ENABLED = True 70 gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False 71 72 73 def _mock_get_offload_func(self, is_moblab, multiprocessing=False, 74 pubsub_topic=None, delete_age=0): 75 """Mock the process of getting the offload_dir function.""" 76 if is_moblab: 77 expected_gsuri = '%sresults/%s/%s/' % ( 78 global_config.global_config.get_config_value( 79 'CROS', 'image_storage_server'), 80 'Fa:ke:ma:c0:12:34', 'rand0m-uu1d') 81 else: 82 expected_gsuri = utils.DEFAULT_OFFLOAD_GSURI 83 utils.get_offload_gsuri().AndReturn(expected_gsuri) 84 offload_func = gs_offloader.get_offload_dir_func(expected_gsuri, 85 multiprocessing, delete_age, pubsub_topic) 86 self.mox.StubOutWithMock(gs_offloader, 'get_offload_dir_func') 87 gs_offloader.get_offload_dir_func(expected_gsuri, multiprocessing, 88 delete_age, pubsub_topic).AndReturn(offload_func) 89 self.mox.ReplayAll() 90 return offload_func 91 92 93 def test_process_no_options(self): 94 """Test default offloader options.""" 95 offload_func = self._mock_get_offload_func(False) 96 offloader = gs_offloader.Offloader(_get_options([])) 97 self.assertEqual(set(offloader._jobdir_classes), 98 self._REGULAR_ONLY) 99 self.assertEqual(offloader._processes, 1) 100 self.assertEqual(offloader._offload_func, 101 offload_func) 102 self.assertEqual(offloader._upload_age_limit, 0) 103 self.assertEqual(offloader._delete_age_limit, 0) 104 105 106 def test_process_all_option(self): 107 """Test offloader handling for the --all option.""" 108 offload_func = self._mock_get_offload_func(False) 109 offloader = gs_offloader.Offloader(_get_options(['--all'])) 110 self.assertEqual(set(offloader._jobdir_classes), self._BOTH) 111 self.assertEqual(offloader._processes, 1) 112 self.assertEqual(offloader._offload_func, 113 offload_func) 114 self.assertEqual(offloader._upload_age_limit, 0) 115 self.assertEqual(offloader._delete_age_limit, 0) 116 117 118 def test_process_hosts_option(self): 119 """Test offloader handling for the --hosts option.""" 120 offload_func = self._mock_get_offload_func(False) 121 offloader = gs_offloader.Offloader( 122 _get_options(['--hosts'])) 123 self.assertEqual(set(offloader._jobdir_classes), 124 self._SPECIAL_ONLY) 125 self.assertEqual(offloader._processes, 1) 126 self.assertEqual(offloader._offload_func, 127 offload_func) 128 self.assertEqual(offloader._upload_age_limit, 0) 129 self.assertEqual(offloader._delete_age_limit, 0) 130 131 132 def test_parallelism_option(self): 133 """Test offloader handling for the --parallelism option.""" 134 offload_func = self._mock_get_offload_func(False) 135 offloader = gs_offloader.Offloader( 136 _get_options(['--parallelism', '2'])) 137 self.assertEqual(set(offloader._jobdir_classes), 138 self._REGULAR_ONLY) 139 self.assertEqual(offloader._processes, 2) 140 self.assertEqual(offloader._offload_func, 141 offload_func) 142 self.assertEqual(offloader._upload_age_limit, 0) 143 self.assertEqual(offloader._delete_age_limit, 0) 144 145 146 def test_delete_only_option(self): 147 """Test offloader handling for the --delete_only option.""" 148 offloader = gs_offloader.Offloader( 149 _get_options(['--delete_only'])) 150 self.assertEqual(set(offloader._jobdir_classes), 151 self._REGULAR_ONLY) 152 self.assertEqual(offloader._processes, 1) 153 self.assertEqual(offloader._offload_func, 154 gs_offloader.delete_files) 155 self.assertEqual(offloader._upload_age_limit, 0) 156 self.assertEqual(offloader._delete_age_limit, 0) 157 self.assertIsNone(offloader._pubsub_topic) 158 159 160 def test_days_old_option(self): 161 """Test offloader handling for the --days_old option.""" 162 offload_func = self._mock_get_offload_func(False, delete_age=7) 163 offloader = gs_offloader.Offloader( 164 _get_options(['--days_old', '7'])) 165 self.assertEqual(set(offloader._jobdir_classes), 166 self._REGULAR_ONLY) 167 self.assertEqual(offloader._processes, 1) 168 self.assertEqual(offloader._offload_func, 169 offload_func) 170 self.assertEqual(offloader._upload_age_limit, 7) 171 self.assertEqual(offloader._delete_age_limit, 7) 172 173 174 def test_moblab_gsuri_generation(self): 175 """Test offloader construction for Moblab.""" 176 offload_func = self._mock_get_offload_func(True) 177 offloader = gs_offloader.Offloader(_get_options([])) 178 self.assertEqual(set(offloader._jobdir_classes), 179 self._REGULAR_ONLY) 180 self.assertEqual(offloader._processes, 1) 181 self.assertEqual(offloader._offload_func, 182 offload_func) 183 self.assertEqual(offloader._upload_age_limit, 0) 184 self.assertEqual(offloader._delete_age_limit, 0) 185 186 187 def test_globalconfig_offloading_flag(self): 188 """Test enabling of --delete_only via global_config.""" 189 gs_offloader.GS_OFFLOADING_ENABLED = False 190 offloader = gs_offloader.Offloader( 191 _get_options([])) 192 self.assertEqual(offloader._offload_func, 193 gs_offloader.delete_files) 194 195 def test_offloader_multiprocessing_flag_set(self): 196 """Test multiprocessing is set.""" 197 offload_func = self._mock_get_offload_func(True, True) 198 offloader = gs_offloader.Offloader(_get_options(['-m'])) 199 self.assertEqual(offloader._offload_func, 200 offload_func) 201 self.mox.VerifyAll() 202 203 def test_offloader_multiprocessing_flag_not_set_default_false(self): 204 """Test multiprocessing is set.""" 205 gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False 206 offload_func = self._mock_get_offload_func(True, False) 207 offloader = gs_offloader.Offloader(_get_options([])) 208 self.assertEqual(offloader._offload_func, 209 offload_func) 210 self.mox.VerifyAll() 211 212 def test_offloader_multiprocessing_flag_not_set_default_true(self): 213 """Test multiprocessing is set.""" 214 gs_offloader.GS_OFFLOADER_MULTIPROCESSING = True 215 offload_func = self._mock_get_offload_func(True, True) 216 offloader = gs_offloader.Offloader(_get_options([])) 217 self.assertEqual(offloader._offload_func, 218 offload_func) 219 self.mox.VerifyAll() 220 221 def test_offloader_pubsub_topic_not_set(self): 222 """Test multiprocessing is set.""" 223 offload_func = self._mock_get_offload_func(True, False) 224 offloader = gs_offloader.Offloader(_get_options([])) 225 self.assertEqual(offloader._offload_func, 226 offload_func) 227 self.mox.VerifyAll() 228 229 def test_offloader_pubsub_topic_set(self): 230 """Test multiprocessing is set.""" 231 offload_func = self._mock_get_offload_func(True, False, 'test-topic') 232 offloader = gs_offloader.Offloader(_get_options(['-t', 'test-topic'])) 233 self.assertEqual(offloader._offload_func, 234 offload_func) 235 self.mox.VerifyAll() 236 237 238 def _make_timestamp(age_limit, is_expired): 239 """Create a timestamp for use by `job_directories.is_job_expired()`. 240 241 The timestamp will meet the syntactic requirements for 242 timestamps used as input to `is_job_expired()`. If 243 `is_expired` is true, the timestamp will be older than 244 `age_limit` days before the current time; otherwise, the 245 date will be younger. 246 247 @param age_limit The number of days before expiration of the 248 target timestamp. 249 @param is_expired Whether the timestamp should be expired 250 relative to `age_limit`. 251 252 """ 253 seconds = -_MARGIN_SECS 254 if is_expired: 255 seconds = -seconds 256 delta = datetime.timedelta(days=age_limit, seconds=seconds) 257 reference_time = datetime.datetime.now() - delta 258 return reference_time.strftime(time_utils.TIME_FMT) 259 260 261 class JobExpirationTests(unittest.TestCase): 262 """Tests to exercise `job_directories.is_job_expired()`.""" 263 264 def test_expired(self): 265 """Test detection of an expired job.""" 266 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, True) 267 self.assertTrue( 268 job_directories.is_job_expired( 269 _TEST_EXPIRATION_AGE, timestamp)) 270 271 272 def test_alive(self): 273 """Test detection of a job that's not expired.""" 274 # N.B. This test may fail if its run time exceeds more than 275 # about _MARGIN_SECS seconds. 276 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, False) 277 self.assertFalse( 278 job_directories.is_job_expired( 279 _TEST_EXPIRATION_AGE, timestamp)) 280 281 282 class _MockJobDirectory(job_directories._JobDirectory): 283 """Subclass of `_JobDirectory` used as a helper for tests.""" 284 285 GLOB_PATTERN = '[0-9]*-*' 286 287 288 def __init__(self, resultsdir): 289 """Create new job in initial state.""" 290 super(_MockJobDirectory, self).__init__(resultsdir) 291 self._timestamp = None 292 self.queue_args = [resultsdir, os.path.dirname(resultsdir), self._timestamp] 293 294 295 def get_timestamp_if_finished(self): 296 return self._timestamp 297 298 299 def set_finished(self, days_old): 300 """Make this job appear to be finished. 301 302 After calling this function, calls to `enqueue_offload()` 303 will find this job as finished, but not expired and ready 304 for offload. Note that when `days_old` is 0, 305 `enqueue_offload()` will treat a finished job as eligible 306 for offload. 307 308 @param days_old The value of the `days_old` parameter that 309 will be passed to `enqueue_offload()` for 310 testing. 311 312 """ 313 self._timestamp = _make_timestamp(days_old, False) 314 self.queue_args[2] = self._timestamp 315 316 317 def set_expired(self, days_old): 318 """Make this job eligible to be offloaded. 319 320 After calling this function, calls to `offload` will attempt 321 to offload this job. 322 323 @param days_old The value of the `days_old` parameter that 324 will be passed to `enqueue_offload()` for 325 testing. 326 327 """ 328 self._timestamp = _make_timestamp(days_old, True) 329 self.queue_args[2] = self._timestamp 330 331 332 def set_incomplete(self): 333 """Make this job appear to have failed offload just once.""" 334 self._offload_count += 1 335 self._first_offload_start = time.time() 336 if not os.path.isdir(self._dirname): 337 os.mkdir(self._dirname) 338 339 340 def set_reportable(self): 341 """Make this job be reportable.""" 342 self.set_incomplete() 343 self._offload_count += 1 344 345 346 def set_complete(self): 347 """Make this job be completed.""" 348 self._offload_count += 1 349 if os.path.isdir(self._dirname): 350 os.rmdir(self._dirname) 351 352 353 def process_gs_instructions(self): 354 """Always still offload the job directory.""" 355 return True 356 357 358 class CommandListTests(unittest.TestCase): 359 """Tests for `get_cmd_list()`.""" 360 361 def _command_list_assertions(self, job, use_rsync=True, multi=False): 362 """Call `get_cmd_list()` and check the return value. 363 364 Check the following assertions: 365 * The command name (argv[0]) is 'gsutil'. 366 * '-m' option (argv[1]) is on when the argument, multi, is True. 367 * The arguments contain the 'cp' subcommand. 368 * The next-to-last argument (the source directory) is the 369 job's `queue_args[0]`. 370 * The last argument (the destination URL) is the job's 371 'queue_args[1]'. 372 373 @param job A job with properly calculated arguments to 374 `get_cmd_list()` 375 @param use_rsync True when using 'rsync'. False when using 'cp'. 376 @param multi True when using '-m' option for gsutil. 377 378 """ 379 test_bucket_uri = 'gs://a-test-bucket' 380 381 gs_offloader.USE_RSYNC_ENABLED = use_rsync 382 383 command = gs_offloader.get_cmd_list( 384 multi, job.queue_args[0], 385 os.path.join(test_bucket_uri, job.queue_args[1])) 386 387 self.assertEqual(command[0], 'gsutil') 388 if multi: 389 self.assertEqual(command[1], '-m') 390 self.assertEqual(command[-2], job.queue_args[0]) 391 392 if use_rsync: 393 self.assertTrue('rsync' in command) 394 self.assertEqual(command[-1], 395 os.path.join(test_bucket_uri, job.queue_args[0])) 396 else: 397 self.assertTrue('cp' in command) 398 self.assertEqual(command[-1], 399 os.path.join(test_bucket_uri, job.queue_args[1])) 400 401 402 def test_get_cmd_list_regular(self): 403 """Test `get_cmd_list()` as for a regular job.""" 404 job = _MockJobDirectory('118-debug') 405 self._command_list_assertions(job) 406 407 408 def test_get_cmd_list_special(self): 409 """Test `get_cmd_list()` as for a special job.""" 410 job = _MockJobDirectory('hosts/host1/118-reset') 411 self._command_list_assertions(job) 412 413 414 def test_get_cmd_list_regular_no_rsync(self): 415 """Test `get_cmd_list()` as for a regular job.""" 416 job = _MockJobDirectory('118-debug') 417 self._command_list_assertions(job, use_rsync=False) 418 419 420 def test_get_cmd_list_special_no_rsync(self): 421 """Test `get_cmd_list()` as for a special job.""" 422 job = _MockJobDirectory('hosts/host1/118-reset') 423 self._command_list_assertions(job, use_rsync=False) 424 425 426 def test_get_cmd_list_regular_multi(self): 427 """Test `get_cmd_list()` as for a regular job with True multi.""" 428 job = _MockJobDirectory('118-debug') 429 self._command_list_assertions(job, multi=True) 430 431 432 def test_get_cmd_list_special_multi(self): 433 """Test `get_cmd_list()` as for a special job with True multi.""" 434 job = _MockJobDirectory('hosts/host1/118-reset') 435 self._command_list_assertions(job, multi=True) 436 437 438 class PubSubTest(mox.MoxTestBase): 439 """Test the test result notifcation data structure.""" 440 441 def test_create_test_result_notification(self): 442 """Tests the test result notification message.""" 443 self.mox.StubOutWithMock(site_utils, 'get_moblab_id') 444 self.mox.StubOutWithMock(site_utils, 445 'get_default_interface_mac_address') 446 site_utils.get_default_interface_mac_address().AndReturn( 447 '1c:dc:d1:11:01:e1') 448 site_utils.get_moblab_id().AndReturn( 449 'c8386d92-9ad1-11e6-80f5-111111111111') 450 self.mox.ReplayAll() 451 msg = gs_offloader._create_test_result_notification( 452 'gs://test_bucket', '123-moblab') 453 self.assertEquals(base64.b64encode( 454 gs_offloader.NEW_TEST_RESULT_MESSAGE), msg['data']) 455 self.assertEquals( 456 gs_offloader.NOTIFICATION_VERSION, 457 msg['attributes'][gs_offloader.NOTIFICATION_ATTR_VERSION]) 458 self.assertEquals( 459 '1c:dc:d1:11:01:e1', 460 msg['attributes'][gs_offloader.NOTIFICATION_ATTR_MOBLAB_MAC]) 461 self.assertEquals( 462 'c8386d92-9ad1-11e6-80f5-111111111111', 463 msg['attributes'][gs_offloader.NOTIFICATION_ATTR_MOBLAB_ID]) 464 self.assertEquals( 465 'gs://test_bucket/123-moblab', 466 msg['attributes'][gs_offloader.NOTIFICATION_ATTR_GCS_URI]) 467 self.mox.VerifyAll() 468 469 470 class _MockJob(object): 471 """Class to mock the return value of `AFE.get_jobs()`.""" 472 def __init__(self, created): 473 self.created_on = created 474 475 476 class _MockHostQueueEntry(object): 477 """Class to mock the return value of `AFE.get_host_queue_entries()`.""" 478 def __init__(self, finished): 479 self.finished_on = finished 480 481 482 class _MockSpecialTask(object): 483 """Class to mock the return value of `AFE.get_special_tasks()`.""" 484 def __init__(self, finished): 485 self.time_finished = finished 486 487 488 class JobDirectorySubclassTests(mox.MoxTestBase): 489 """Test specific to RegularJobDirectory and SpecialJobDirectory. 490 491 This provides coverage for the implementation in both 492 RegularJobDirectory and SpecialJobDirectory. 493 494 """ 495 496 def setUp(self): 497 super(JobDirectorySubclassTests, self).setUp() 498 self.mox.StubOutWithMock(job_directories._AFE, 'get_jobs') 499 self.mox.StubOutWithMock(job_directories._AFE, 500 'get_host_queue_entries') 501 self.mox.StubOutWithMock(job_directories._AFE, 502 'get_special_tasks') 503 504 505 def test_regular_job_fields(self): 506 """Test the constructor for `RegularJobDirectory`. 507 508 Construct a regular job, and assert that the `_dirname` 509 and `_id` attributes are set as expected. 510 511 """ 512 resultsdir = '118-fubar' 513 job = job_directories.RegularJobDirectory(resultsdir) 514 self.assertEqual(job._dirname, resultsdir) 515 self.assertEqual(job._id, 118) 516 517 518 def test_special_job_fields(self): 519 """Test the constructor for `SpecialJobDirectory`. 520 521 Construct a special job, and assert that the `_dirname` 522 and `_id` attributes are set as expected. 523 524 """ 525 destdir = 'hosts/host1' 526 resultsdir = destdir + '/118-reset' 527 job = job_directories.SpecialJobDirectory(resultsdir) 528 self.assertEqual(job._dirname, resultsdir) 529 self.assertEqual(job._id, 118) 530 531 532 def _check_finished_job(self, jobtime, hqetimes, expected): 533 """Mock and test behavior of a finished job. 534 535 Initialize the mocks for a call to 536 `get_timestamp_if_finished()`, then simulate one call. 537 Assert that the returned timestamp matches the passed 538 in expected value. 539 540 @param jobtime Time used to construct a _MockJob object. 541 @param hqetimes List of times used to construct 542 _MockHostQueueEntry objects. 543 @param expected Expected time to be returned by 544 get_timestamp_if_finished 545 546 """ 547 job = job_directories.RegularJobDirectory('118-fubar') 548 job_directories._AFE.get_jobs( 549 id=job._id, finished=True).AndReturn( 550 [_MockJob(jobtime)]) 551 job_directories._AFE.get_host_queue_entries( 552 finished_on__isnull=False, 553 job_id=job._id).AndReturn( 554 [_MockHostQueueEntry(t) for t in hqetimes]) 555 self.mox.ReplayAll() 556 self.assertEqual(expected, job.get_timestamp_if_finished()) 557 self.mox.VerifyAll() 558 559 560 def test_finished_regular_job(self): 561 """Test getting the timestamp for a finished regular job. 562 563 Tests the return value for 564 `RegularJobDirectory.get_timestamp_if_finished()` when 565 the AFE indicates the job is finished. 566 567 """ 568 created_timestamp = _make_timestamp(1, True) 569 hqe_timestamp = _make_timestamp(0, True) 570 self._check_finished_job(created_timestamp, 571 [hqe_timestamp], 572 hqe_timestamp) 573 574 575 def test_finished_regular_job_multiple_hqes(self): 576 """Test getting the timestamp for a regular job with multiple hqes. 577 578 Tests the return value for 579 `RegularJobDirectory.get_timestamp_if_finished()` when 580 the AFE indicates the job is finished and the job has multiple host 581 queue entries. 582 583 Tests that the returned timestamp is the latest timestamp in 584 the list of HQEs, regardless of the returned order. 585 586 """ 587 created_timestamp = _make_timestamp(2, True) 588 older_hqe_timestamp = _make_timestamp(1, True) 589 newer_hqe_timestamp = _make_timestamp(0, True) 590 hqe_list = [older_hqe_timestamp, 591 newer_hqe_timestamp] 592 self._check_finished_job(created_timestamp, 593 hqe_list, 594 newer_hqe_timestamp) 595 self.mox.ResetAll() 596 hqe_list.reverse() 597 self._check_finished_job(created_timestamp, 598 hqe_list, 599 newer_hqe_timestamp) 600 601 602 def test_finished_regular_job_null_finished_times(self): 603 """Test getting the timestamp for an aborted regular job. 604 605 Tests the return value for 606 `RegularJobDirectory.get_timestamp_if_finished()` when 607 the AFE indicates the job is finished and the job has aborted host 608 queue entries. 609 610 """ 611 timestamp = _make_timestamp(0, True) 612 self._check_finished_job(timestamp, [], timestamp) 613 614 615 def test_unfinished_regular_job(self): 616 """Test getting the timestamp for an unfinished regular job. 617 618 Tests the return value for 619 `RegularJobDirectory.get_timestamp_if_finished()` when 620 the AFE indicates the job is not finished. 621 622 """ 623 job = job_directories.RegularJobDirectory('118-fubar') 624 job_directories._AFE.get_jobs( 625 id=job._id, finished=True).AndReturn([]) 626 self.mox.ReplayAll() 627 self.assertIsNone(job.get_timestamp_if_finished()) 628 self.mox.VerifyAll() 629 630 631 def test_finished_special_job(self): 632 """Test getting the timestamp for a finished special job. 633 634 Tests the return value for 635 `SpecialJobDirectory.get_timestamp_if_finished()` when 636 the AFE indicates the job is finished. 637 638 """ 639 job = job_directories.SpecialJobDirectory( 640 'hosts/host1/118-reset') 641 timestamp = _make_timestamp(0, True) 642 job_directories._AFE.get_special_tasks( 643 id=job._id, is_complete=True).AndReturn( 644 [_MockSpecialTask(timestamp)]) 645 self.mox.ReplayAll() 646 self.assertEqual(timestamp, 647 job.get_timestamp_if_finished()) 648 self.mox.VerifyAll() 649 650 651 def test_unfinished_special_job(self): 652 """Test getting the timestamp for an unfinished special job. 653 654 Tests the return value for 655 `SpecialJobDirectory.get_timestamp_if_finished()` when 656 the AFE indicates the job is not finished. 657 658 """ 659 job = job_directories.SpecialJobDirectory( 660 'hosts/host1/118-reset') 661 job_directories._AFE.get_special_tasks( 662 id=job._id, is_complete=True).AndReturn([]) 663 self.mox.ReplayAll() 664 self.assertIsNone(job.get_timestamp_if_finished()) 665 self.mox.VerifyAll() 666 667 668 class _TempResultsDirTestBase(mox.MoxTestBase): 669 """Base class for tests using a temporary results directory.""" 670 671 REGULAR_JOBLIST = [ 672 '111-fubar', '112-fubar', '113-fubar', '114-snafu'] 673 HOST_LIST = ['host1', 'host2', 'host3'] 674 SPECIAL_JOBLIST = [ 675 'hosts/host1/333-reset', 'hosts/host1/334-reset', 676 'hosts/host2/444-reset', 'hosts/host3/555-reset'] 677 678 679 def setUp(self): 680 super(_TempResultsDirTestBase, self).setUp() 681 self._resultsroot = tempfile.mkdtemp() 682 self._cwd = os.getcwd() 683 os.chdir(self._resultsroot) 684 685 686 def tearDown(self): 687 os.chdir(self._cwd) 688 shutil.rmtree(self._resultsroot) 689 super(_TempResultsDirTestBase, self).tearDown() 690 691 692 def make_job(self, jobdir): 693 """Create a job with results in `self._resultsroot`. 694 695 @param jobdir Name of the subdirectory to be created in 696 `self._resultsroot`. 697 698 """ 699 os.mkdir(jobdir) 700 return _MockJobDirectory(jobdir) 701 702 703 def make_job_hierarchy(self): 704 """Create a sample hierarchy of job directories. 705 706 `self.REGULAR_JOBLIST` is a list of directories for regular 707 jobs to be created; `self.SPECIAL_JOBLIST` is a list of 708 directories for special jobs to be created. 709 710 """ 711 for d in self.REGULAR_JOBLIST: 712 os.mkdir(d) 713 hostsdir = 'hosts' 714 os.mkdir(hostsdir) 715 for host in self.HOST_LIST: 716 os.mkdir(os.path.join(hostsdir, host)) 717 for d in self.SPECIAL_JOBLIST: 718 os.mkdir(d) 719 720 721 class FailedOffloadsLogTest(_TempResultsDirTestBase): 722 """Test the formatting of failed offloads log file.""" 723 # Below is partial sample of a failed offload log file. This text is 724 # deliberately hard-coded and then parsed to create the test data; the idea 725 # is to make sure the actual text format will be reviewed by a human being. 726 # 727 # first offload count directory 728 # --+----1----+---- ----+ ----+----1----+----2----+----3 729 _SAMPLE_DIRECTORIES_REPORT = '''\ 730 =================== ====== ============================== 731 2014-03-14 15:09:26 1 118-fubar 732 2014-03-14 15:19:23 2 117-fubar 733 2014-03-14 15:29:20 6 116-fubar 734 2014-03-14 15:39:17 24 115-fubar 735 2014-03-14 15:49:14 120 114-fubar 736 2014-03-14 15:59:11 720 113-fubar 737 2014-03-14 16:09:08 5040 112-fubar 738 2014-03-14 16:19:05 40320 111-fubar 739 ''' 740 741 def setUp(self): 742 super(FailedOffloadsLogTest, self).setUp() 743 self._offloader = gs_offloader.Offloader(_get_options([])) 744 self._joblist = [] 745 for line in self._SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]: 746 date_, time_, count, dir_ = line.split() 747 job = _MockJobDirectory(dir_) 748 job._offload_count = int(count) 749 timestruct = time.strptime("%s %s" % (date_, time_), 750 gs_offloader.FAILED_OFFLOADS_TIME_FORMAT) 751 job._first_offload_start = time.mktime(timestruct) 752 # enter the jobs in reverse order, to make sure we 753 # test that the output will be sorted. 754 self._joblist.insert(0, job) 755 756 757 def assert_report_well_formatted(self, report_file): 758 with open(report_file, 'r') as f: 759 report_lines = f.read().split() 760 761 for end_of_header_index in range(len(report_lines)): 762 if report_lines[end_of_header_index].startswith('=='): 763 break 764 self.assertLess(end_of_header_index, len(report_lines), 765 'Failed to find end-of-header marker in the report') 766 767 relevant_lines = report_lines[end_of_header_index:] 768 expected_lines = self._SAMPLE_DIRECTORIES_REPORT.split() 769 self.assertListEqual(relevant_lines, expected_lines) 770 771 772 def test_failed_offload_log_format(self): 773 """Trigger an e-mail report and check its contents.""" 774 log_file = os.path.join(self._resultsroot, 'failed_log') 775 report = self._offloader._log_failed_jobs_locally(self._joblist, 776 log_file=log_file) 777 self.assert_report_well_formatted(log_file) 778 779 780 def test_failed_offload_file_overwrite(self): 781 """Verify that we can saefly overwrite the log file.""" 782 log_file = os.path.join(self._resultsroot, 'failed_log') 783 with open(log_file, 'w') as f: 784 f.write('boohoohoo') 785 report = self._offloader._log_failed_jobs_locally(self._joblist, 786 log_file=log_file) 787 self.assert_report_well_formatted(log_file) 788 789 790 class OffloadDirectoryTests(_TempResultsDirTestBase): 791 """Tests for `offload_dir()`.""" 792 793 def setUp(self): 794 super(OffloadDirectoryTests, self).setUp() 795 # offload_dir() logs messages; silence them. 796 self._saved_loglevel = logging.getLogger().getEffectiveLevel() 797 logging.getLogger().setLevel(logging.CRITICAL+1) 798 self._job = self.make_job(self.REGULAR_JOBLIST[0]) 799 self.mox.StubOutWithMock(gs_offloader, 'get_cmd_list') 800 self.mox.StubOutWithMock(signal, 'alarm') 801 self.mox.StubOutWithMock(models.test, 'parse_job_keyval') 802 803 804 def tearDown(self): 805 logging.getLogger().setLevel(self._saved_loglevel) 806 super(OffloadDirectoryTests, self).tearDown() 807 808 def _mock_upload_testresult_files(self): 809 self.mox.StubOutWithMock(gs_offloader, 'upload_testresult_files') 810 gs_offloader.upload_testresult_files( 811 mox.IgnoreArg(),mox.IgnoreArg()).AndReturn(None) 812 813 def _mock_create_marker_file(self): 814 self.mox.StubOutWithMock(__builtin__, 'open') 815 mock_marker_file = self.mox.CreateMock(file) 816 open(mox.IgnoreArg(), 'a').AndReturn(mock_marker_file) 817 mock_marker_file.close() 818 819 820 def _mock_offload_dir_calls(self, command, queue_args, 821 marker_initially_exists=False, 822 marker_eventually_exists=True): 823 """Mock out the calls needed by `offload_dir()`. 824 825 This covers only the calls made when there is no timeout. 826 827 @param command Command list to be returned by the mocked 828 call to `get_cmd_list()`. 829 830 """ 831 self.mox.StubOutWithMock(os.path, 'isfile') 832 os.path.isfile(mox.IgnoreArg()).AndReturn(marker_initially_exists) 833 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS) 834 command.append(queue_args[0]) 835 gs_offloader.get_cmd_list( 836 False, queue_args[0], 837 '%s%s' % (utils.DEFAULT_OFFLOAD_GSURI, 838 queue_args[1])).AndReturn(command) 839 self._mock_upload_testresult_files() 840 signal.alarm(0) 841 signal.alarm(0) 842 os.path.isfile(mox.IgnoreArg()).AndReturn(marker_eventually_exists) 843 844 845 def _run_offload_dir(self, should_succeed, delete_age): 846 """Make one call to `offload_dir()`. 847 848 The caller ensures all mocks are set up already. 849 850 @param should_succeed True iff the call to `offload_dir()` 851 is expected to succeed and remove the 852 offloaded job directory. 853 854 """ 855 self.mox.ReplayAll() 856 gs_offloader.get_offload_dir_func( 857 utils.DEFAULT_OFFLOAD_GSURI, False, delete_age)( 858 self._job.queue_args[0], 859 self._job.queue_args[1], 860 self._job.queue_args[2]) 861 self.mox.VerifyAll() 862 self.assertEqual(not should_succeed, 863 os.path.isdir(self._job.queue_args[0])) 864 865 866 def test_offload_success(self): 867 """Test that `offload_dir()` can succeed correctly.""" 868 self._mock_offload_dir_calls(['test', '-d'], 869 self._job.queue_args) 870 self._mock_create_marker_file() 871 self._run_offload_dir(True, 0) 872 873 874 def test_offload_failure(self): 875 """Test that `offload_dir()` can fail correctly.""" 876 self._mock_offload_dir_calls(['test', '!', '-d'], 877 self._job.queue_args, 878 marker_eventually_exists=False) 879 self._run_offload_dir(False, 0) 880 881 882 def test_offload_timeout_early(self): 883 """Test that `offload_dir()` times out correctly. 884 885 This test triggers timeout at the earliest possible moment, 886 at the first call to set the timeout alarm. 887 888 """ 889 self._mock_upload_testresult_files() 890 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS).AndRaise( 891 gs_offloader.TimeoutException('fubar')) 892 signal.alarm(0) 893 self._run_offload_dir(False, 0) 894 895 896 def test_offload_timeout_late(self): 897 """Test that `offload_dir()` times out correctly. 898 899 This test triggers timeout at the latest possible moment, at 900 the call to clear the timeout alarm. 901 902 """ 903 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS) 904 gs_offloader.get_cmd_list( 905 False, mox.IgnoreArg(), mox.IgnoreArg()).AndReturn( 906 ['test', '-d', self._job.queue_args[0]]) 907 self._mock_upload_testresult_files() 908 signal.alarm(0).AndRaise( 909 gs_offloader.TimeoutException('fubar')) 910 signal.alarm(0) 911 self._run_offload_dir(False, 0) 912 913 914 def test_sanitize_dir(self): 915 """Test that folder/file name with invalid character can be corrected. 916 """ 917 results_folder = tempfile.mkdtemp() 918 invalid_chars = '_'.join(gs_offloader.INVALID_GS_CHARS) 919 invalid_files = [] 920 invalid_folder_name = 'invalid_name_folder_%s' % invalid_chars 921 invalid_folder = os.path.join( 922 results_folder, 923 invalid_folder_name) 924 invalid_files.append(os.path.join( 925 invalid_folder, 926 'invalid_name_file_%s' % invalid_chars)) 927 for r in gs_offloader.INVALID_GS_CHAR_RANGE: 928 for c in range(r[0], r[1]+1): 929 # NULL cannot be in file name. 930 if c != 0: 931 invalid_files.append(os.path.join( 932 invalid_folder, 933 'invalid_name_file_%s' % chr(c))) 934 good_folder = os.path.join(results_folder, 'valid_name_folder') 935 good_file = os.path.join(good_folder, 'valid_name_file') 936 for folder in [invalid_folder, good_folder]: 937 os.makedirs(folder) 938 for f in invalid_files + [good_file]: 939 with open(f, 'w'): 940 pass 941 # check that broken symlinks don't break sanitization 942 symlink = os.path.join(invalid_folder, 'broken-link') 943 os.symlink(os.path.join(results_folder, 'no-such-file'), 944 symlink) 945 fifo1 = os.path.join(results_folder, 'test_fifo1') 946 fifo2 = os.path.join(good_folder, 'test_fifo2') 947 fifo3 = os.path.join(invalid_folder, 'test_fifo3') 948 invalid_fifo4_name = 'test_fifo4_%s' % invalid_chars 949 fifo4 = os.path.join(invalid_folder, invalid_fifo4_name) 950 os.mkfifo(fifo1) 951 os.mkfifo(fifo2) 952 os.mkfifo(fifo3) 953 os.mkfifo(fifo4) 954 gs_offloader.sanitize_dir(results_folder) 955 for _, dirs, files in os.walk(results_folder): 956 for name in dirs + files: 957 self.assertEqual(name, gs_offloader.get_sanitized_name(name)) 958 for c in name: 959 self.assertFalse(c in gs_offloader.INVALID_GS_CHARS) 960 for r in gs_offloader.INVALID_GS_CHAR_RANGE: 961 self.assertFalse(ord(c) >= r[0] and ord(c) <= r[1]) 962 self.assertTrue(os.path.exists(good_file)) 963 964 self.assertTrue(os.path.exists(fifo1)) 965 self.assertFalse(is_fifo(fifo1)) 966 self.assertTrue(os.path.exists(fifo2)) 967 self.assertFalse(is_fifo(fifo2)) 968 corrected_folder = os.path.join( 969 results_folder, 970 gs_offloader.get_sanitized_name(invalid_folder_name)) 971 corrected_fifo3 = os.path.join( 972 corrected_folder, 973 'test_fifo3') 974 self.assertFalse(os.path.exists(fifo3)) 975 self.assertTrue(os.path.exists(corrected_fifo3)) 976 self.assertFalse(is_fifo(corrected_fifo3)) 977 corrected_fifo4 = os.path.join( 978 corrected_folder, 979 gs_offloader.get_sanitized_name(invalid_fifo4_name)) 980 self.assertFalse(os.path.exists(fifo4)) 981 self.assertTrue(os.path.exists(corrected_fifo4)) 982 self.assertFalse(is_fifo(corrected_fifo4)) 983 984 corrected_symlink = os.path.join( 985 corrected_folder, 986 'broken-link') 987 self.assertFalse(os.path.lexists(symlink)) 988 self.assertTrue(os.path.exists(corrected_symlink)) 989 self.assertFalse(os.path.islink(corrected_symlink)) 990 shutil.rmtree(results_folder) 991 992 993 def check_limit_file_count(self, is_test_job=True): 994 """Test that folder with too many files can be compressed. 995 996 @param is_test_job: True to check the method with test job result 997 folder. Set to False for special task folder. 998 """ 999 results_folder = tempfile.mkdtemp() 1000 host_folder = os.path.join( 1001 results_folder, 1002 'lab1-host1' if is_test_job else 'hosts/lab1-host1/1-repair') 1003 debug_folder = os.path.join(host_folder, 'debug') 1004 sysinfo_folder = os.path.join(host_folder, 'sysinfo') 1005 for folder in [debug_folder, sysinfo_folder]: 1006 os.makedirs(folder) 1007 for i in range(10): 1008 with open(os.path.join(folder, str(i)), 'w') as f: 1009 f.write('test') 1010 1011 gs_offloader.MAX_FILE_COUNT = 100 1012 gs_offloader.limit_file_count( 1013 results_folder if is_test_job else host_folder) 1014 self.assertTrue(os.path.exists(sysinfo_folder)) 1015 1016 gs_offloader.MAX_FILE_COUNT = 10 1017 gs_offloader.limit_file_count( 1018 results_folder if is_test_job else host_folder) 1019 self.assertFalse(os.path.exists(sysinfo_folder)) 1020 self.assertTrue(os.path.exists(sysinfo_folder + '.tgz')) 1021 self.assertTrue(os.path.exists(debug_folder)) 1022 1023 shutil.rmtree(results_folder) 1024 1025 1026 def test_limit_file_count(self): 1027 """Test that folder with too many files can be compressed. 1028 """ 1029 self.check_limit_file_count(is_test_job=True) 1030 self.check_limit_file_count(is_test_job=False) 1031 1032 1033 def test_is_valid_result(self): 1034 """Test _is_valid_result.""" 1035 release_build = 'veyron_minnie-cheets-release/R52-8248.0.0' 1036 pfq_build = 'cyan-cheets-android-pfq/R54-8623.0.0-rc1' 1037 trybot_build = 'trybot-samus-release/R54-8640.0.0-b5092' 1038 trybot_2_build = 'trybot-samus-pfq/R54-8640.0.0-b5092' 1039 release_2_build = 'test-trybot-release/R54-8640.0.0-b5092' 1040 self.assertTrue(gs_offloader._is_valid_result( 1041 release_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1042 self.assertTrue(gs_offloader._is_valid_result( 1043 release_build, gs_offloader.CTS_RESULT_PATTERN, 'test_that_wrapper')) 1044 self.assertFalse(gs_offloader._is_valid_result( 1045 release_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-bvt-cq')) 1046 self.assertTrue(gs_offloader._is_valid_result( 1047 release_build, gs_offloader.CTS_V2_RESULT_PATTERN, 'arc-gts')) 1048 self.assertFalse(gs_offloader._is_valid_result( 1049 None, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1050 self.assertFalse(gs_offloader._is_valid_result( 1051 release_build, gs_offloader.CTS_RESULT_PATTERN, None)) 1052 self.assertFalse(gs_offloader._is_valid_result( 1053 pfq_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1054 self.assertFalse(gs_offloader._is_valid_result( 1055 trybot_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1056 self.assertFalse(gs_offloader._is_valid_result( 1057 trybot_2_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1058 self.assertTrue(gs_offloader._is_valid_result( 1059 release_2_build, gs_offloader.CTS_RESULT_PATTERN, 'arc-cts')) 1060 1061 1062 def create_results_folder(self): 1063 """Create CTS/GTS results folders.""" 1064 results_folder = tempfile.mkdtemp() 1065 host_folder = os.path.join(results_folder, 'chromeos4-row9-rack11-host22') 1066 debug_folder = os.path.join(host_folder, 'debug') 1067 sysinfo_folder = os.path.join(host_folder, 'sysinfo') 1068 cts_result_folder = os.path.join( 1069 host_folder, 'cheets_CTS.android.dpi', 'results', 'cts-results') 1070 cts_v2_result_folder = os.path.join(host_folder, 1071 'cheets_CTS_N.CtsGraphicsTestCases', 'results', 'android-cts') 1072 gts_result_folder = os.path.join( 1073 host_folder, 'cheets_GTS.google.admin', 'results', 'android-gts') 1074 timestamp_str = '2016.04.28_01.41.44' 1075 timestamp_cts_folder = os.path.join(cts_result_folder, timestamp_str) 1076 timestamp_cts_v2_folder = os.path.join(cts_v2_result_folder, timestamp_str) 1077 timestamp_gts_folder = os.path.join(gts_result_folder, timestamp_str) 1078 1079 # Test results in cts_result_folder with a different time-stamp. 1080 timestamp_str_2 = '2016.04.28_10.41.44' 1081 timestamp_cts_folder_2 = os.path.join(cts_result_folder, timestamp_str_2) 1082 1083 for folder in [debug_folder, sysinfo_folder, cts_result_folder, 1084 timestamp_cts_folder, timestamp_cts_folder_2, 1085 timestamp_cts_v2_folder, timestamp_gts_folder]: 1086 os.makedirs(folder) 1087 1088 path_pattern_pair = [(timestamp_cts_folder, gs_offloader.CTS_RESULT_PATTERN), 1089 (timestamp_cts_folder_2, gs_offloader.CTS_RESULT_PATTERN), 1090 (timestamp_cts_v2_folder, gs_offloader.CTS_V2_RESULT_PATTERN), 1091 (timestamp_gts_folder, gs_offloader.CTS_V2_RESULT_PATTERN)] 1092 1093 # Create timestamp.zip file_path. 1094 cts_zip_file = os.path.join(cts_result_folder, timestamp_str + '.zip') 1095 cts_zip_file_2 = os.path.join(cts_result_folder, timestamp_str_2 + '.zip') 1096 cts_v2_zip_file = os.path.join(cts_v2_result_folder, timestamp_str + '.zip') 1097 gts_zip_file = os.path.join(gts_result_folder, timestamp_str + '.zip') 1098 1099 # Create xml file_path. 1100 cts_result_file = os.path.join(timestamp_cts_folder, 'testResult.xml') 1101 cts_result_file_2 = os.path.join(timestamp_cts_folder_2, 1102 'testResult.xml') 1103 gts_result_file = os.path.join(timestamp_gts_folder, 'test_result.xml') 1104 cts_v2_result_file = os.path.join(timestamp_cts_v2_folder, 1105 'test_result.xml') 1106 1107 for file_path in [cts_zip_file, cts_zip_file_2, cts_v2_zip_file, 1108 gts_zip_file, cts_result_file, cts_result_file_2, 1109 gts_result_file, cts_v2_result_file]: 1110 with open(file_path, 'w') as f: 1111 f.write('test') 1112 1113 return (results_folder, host_folder, path_pattern_pair) 1114 1115 1116 def test_upload_testresult_files(self): 1117 """Test upload_testresult_files.""" 1118 results_folder, host_folder, path_pattern_pair = self.create_results_folder() 1119 1120 self.mox.StubOutWithMock(gs_offloader, '_upload_files') 1121 gs_offloader._upload_files( 1122 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg(), False).AndReturn( 1123 ['test', '-d', host_folder]) 1124 gs_offloader._upload_files( 1125 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg(), False).AndReturn( 1126 ['test', '-d', host_folder]) 1127 gs_offloader._upload_files( 1128 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg(), False).AndReturn( 1129 ['test', '-d', host_folder]) 1130 1131 self.mox.ReplayAll() 1132 gs_offloader.upload_testresult_files(results_folder, False) 1133 self.mox.VerifyAll() 1134 shutil.rmtree(results_folder) 1135 1136 1137 def test_upload_files(self): 1138 """Test upload_files""" 1139 results_folder, host_folder, path_pattern_pair = self.create_results_folder() 1140 1141 for path, pattern in path_pattern_pair: 1142 models.test.parse_job_keyval(mox.IgnoreArg()).AndReturn({ 1143 'build': 'veyron_minnie-cheets-release/R52-8248.0.0', 1144 'parent_job_id': 'p_id', 1145 'suite': 'arc-cts' 1146 }) 1147 1148 gs_offloader.get_cmd_list( 1149 False, mox.IgnoreArg(), mox.IgnoreArg()).AndReturn( 1150 ['test', '-d', path]) 1151 gs_offloader.get_cmd_list( 1152 False, mox.IgnoreArg(), mox.IgnoreArg()).AndReturn( 1153 ['test', '-d', path]) 1154 1155 self.mox.ReplayAll() 1156 gs_offloader._upload_files(host_folder, path, pattern, False) 1157 self.mox.VerifyAll() 1158 self.mox.ResetAll() 1159 1160 shutil.rmtree(results_folder) 1161 1162 1163 class JobDirectoryOffloadTests(_TempResultsDirTestBase): 1164 """Tests for `_JobDirectory.enqueue_offload()`. 1165 1166 When testing with a `days_old` parameter of 0, we use 1167 `set_finished()` instead of `set_expired()`. This causes the 1168 job's timestamp to be set in the future. This is done so as 1169 to test that when `days_old` is 0, the job is always treated 1170 as eligible for offload, regardless of the timestamp's value. 1171 1172 Testing covers the following assertions: 1173 A. Each time `enqueue_offload()` is called, a message that 1174 includes the job's directory name will be logged using 1175 `logging.debug()`, regardless of whether the job was 1176 enqueued. Nothing else is allowed to be logged. 1177 B. If the job is not eligible to be offloaded, 1178 `get_failure_time()` and `get_failure_count()` are 0. 1179 C. If the job is not eligible for offload, nothing is 1180 enqueued in `queue`. 1181 D. When the job is offloaded, `get_failure_count()` increments 1182 each time. 1183 E. When the job is offloaded, the appropriate parameters are 1184 enqueued exactly once. 1185 F. The first time a job is offloaded, `get_failure_time()` is 1186 set to the current time. 1187 G. `get_failure_time()` only changes the first time that the 1188 job is offloaded. 1189 1190 The test cases below are designed to exercise all of the 1191 meaningful state transitions at least once. 1192 1193 """ 1194 1195 def setUp(self): 1196 super(JobDirectoryOffloadTests, self).setUp() 1197 self._job = self.make_job(self.REGULAR_JOBLIST[0]) 1198 self._queue = Queue.Queue() 1199 1200 1201 def _offload_unexpired_job(self, days_old): 1202 """Make calls to `enqueue_offload()` for an unexpired job. 1203 1204 This method tests assertions B and C that calling 1205 `enqueue_offload()` has no effect. 1206 1207 """ 1208 self.assertEqual(self._job.get_failure_count(), 0) 1209 self.assertEqual(self._job.get_failure_time(), 0) 1210 self._job.enqueue_offload(self._queue, days_old) 1211 self._job.enqueue_offload(self._queue, days_old) 1212 self.assertTrue(self._queue.empty()) 1213 self.assertEqual(self._job.get_failure_count(), 0) 1214 self.assertEqual(self._job.get_failure_time(), 0) 1215 1216 1217 def _offload_expired_once(self, days_old, count): 1218 """Make one call to `enqueue_offload()` for an expired job. 1219 1220 This method tests assertions D and E regarding side-effects 1221 expected when a job is offloaded. 1222 1223 """ 1224 self._job.enqueue_offload(self._queue, days_old) 1225 self.assertEqual(self._job.get_failure_count(), count) 1226 self.assertFalse(self._queue.empty()) 1227 v = self._queue.get_nowait() 1228 self.assertTrue(self._queue.empty()) 1229 self.assertEqual(v, self._job.queue_args) 1230 1231 1232 def _offload_expired_job(self, days_old): 1233 """Make calls to `enqueue_offload()` for a just-expired job. 1234 1235 This method directly tests assertions F and G regarding 1236 side-effects on `get_failure_time()`. 1237 1238 """ 1239 t0 = time.time() 1240 self._offload_expired_once(days_old, 1) 1241 t1 = self._job.get_failure_time() 1242 self.assertLessEqual(t1, time.time()) 1243 self.assertGreaterEqual(t1, t0) 1244 self._offload_expired_once(days_old, 2) 1245 self.assertEqual(self._job.get_failure_time(), t1) 1246 self._offload_expired_once(days_old, 3) 1247 self.assertEqual(self._job.get_failure_time(), t1) 1248 1249 1250 def test_case_1_no_expiration(self): 1251 """Test a series of `enqueue_offload()` calls with `days_old` of 0. 1252 1253 This tests that offload works as expected if calls are 1254 made both before and after the job becomes expired. 1255 1256 """ 1257 self._offload_unexpired_job(0) 1258 self._job.set_finished(0) 1259 self._offload_expired_job(0) 1260 1261 1262 def test_case_2_no_expiration(self): 1263 """Test a series of `enqueue_offload()` calls with `days_old` of 0. 1264 1265 This tests that offload works as expected if calls are made 1266 only after the job becomes expired. 1267 1268 """ 1269 self._job.set_finished(0) 1270 self._offload_expired_job(0) 1271 1272 1273 def test_case_1_with_expiration(self): 1274 """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1275 1276 This tests that offload works as expected if calls are made 1277 before the job finishes, before the job expires, and after 1278 the job expires. 1279 1280 """ 1281 self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1282 self._job.set_finished(_TEST_EXPIRATION_AGE) 1283 self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1284 self._job.set_expired(_TEST_EXPIRATION_AGE) 1285 self._offload_expired_job(_TEST_EXPIRATION_AGE) 1286 1287 1288 def test_case_2_with_expiration(self): 1289 """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1290 1291 This tests that offload works as expected if calls are made 1292 between finishing and expiration, and after the job expires. 1293 1294 """ 1295 self._job.set_finished(_TEST_EXPIRATION_AGE) 1296 self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1297 self._job.set_expired(_TEST_EXPIRATION_AGE) 1298 self._offload_expired_job(_TEST_EXPIRATION_AGE) 1299 1300 1301 def test_case_3_with_expiration(self): 1302 """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1303 1304 This tests that offload works as expected if calls are made 1305 only before finishing and after expiration. 1306 1307 """ 1308 self._offload_unexpired_job(_TEST_EXPIRATION_AGE) 1309 self._job.set_expired(_TEST_EXPIRATION_AGE) 1310 self._offload_expired_job(_TEST_EXPIRATION_AGE) 1311 1312 1313 def test_case_4_with_expiration(self): 1314 """Test a series of `enqueue_offload()` calls with `days_old` non-zero. 1315 1316 This tests that offload works as expected if calls are made 1317 only after expiration. 1318 1319 """ 1320 self._job.set_expired(_TEST_EXPIRATION_AGE) 1321 self._offload_expired_job(_TEST_EXPIRATION_AGE) 1322 1323 1324 class GetJobDirectoriesTests(_TempResultsDirTestBase): 1325 """Tests for `_JobDirectory.get_job_directories()`.""" 1326 1327 def setUp(self): 1328 super(GetJobDirectoriesTests, self).setUp() 1329 self.make_job_hierarchy() 1330 os.mkdir('not-a-job') 1331 open('not-a-dir', 'w').close() 1332 1333 1334 def _run_get_directories(self, cls, expected_list): 1335 """Test `get_job_directories()` for the given class. 1336 1337 Calls the method, and asserts that the returned list of 1338 directories matches the expected return value. 1339 1340 @param expected_list Expected return value from the call. 1341 """ 1342 dirlist = cls.get_job_directories() 1343 self.assertEqual(set(dirlist), set(expected_list)) 1344 1345 1346 def test_get_regular_jobs(self): 1347 """Test `RegularJobDirectory.get_job_directories()`.""" 1348 self._run_get_directories(job_directories.RegularJobDirectory, 1349 self.REGULAR_JOBLIST) 1350 1351 1352 def test_get_special_jobs(self): 1353 """Test `SpecialJobDirectory.get_job_directories()`.""" 1354 self._run_get_directories(job_directories.SpecialJobDirectory, 1355 self.SPECIAL_JOBLIST) 1356 1357 1358 class AddJobsTests(_TempResultsDirTestBase): 1359 """Tests for `Offloader._add_new_jobs()`.""" 1360 1361 MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu'] 1362 1363 def setUp(self): 1364 super(AddJobsTests, self).setUp() 1365 self._initial_job_names = ( 1366 set(self.REGULAR_JOBLIST) | set(self.SPECIAL_JOBLIST)) 1367 self.make_job_hierarchy() 1368 self._offloader = gs_offloader.Offloader(_get_options(['-a'])) 1369 self.mox.StubOutWithMock(logging, 'debug') 1370 1371 1372 def _run_add_new_jobs(self, expected_key_set): 1373 """Basic test assertions for `_add_new_jobs()`. 1374 1375 Asserts the following: 1376 * The keys in the offloader's `_open_jobs` dictionary 1377 matches the expected set of keys. 1378 * For every job in `_open_jobs`, the job has the expected 1379 directory name. 1380 1381 """ 1382 count = len(expected_key_set) - len(self._offloader._open_jobs) 1383 logging.debug(mox.IgnoreArg(), count) 1384 self.mox.ReplayAll() 1385 self._offloader._add_new_jobs() 1386 self.assertEqual(expected_key_set, 1387 set(self._offloader._open_jobs.keys())) 1388 for jobkey, job in self._offloader._open_jobs.items(): 1389 self.assertEqual(jobkey, job._dirname) 1390 self.mox.VerifyAll() 1391 self.mox.ResetAll() 1392 1393 1394 def test_add_jobs_empty(self): 1395 """Test adding jobs to an empty dictionary. 1396 1397 Calls the offloader's `_add_new_jobs()`, then perform 1398 the assertions of `self._check_open_jobs()`. 1399 1400 """ 1401 self._run_add_new_jobs(self._initial_job_names) 1402 1403 1404 def test_add_jobs_non_empty(self): 1405 """Test adding jobs to a non-empty dictionary. 1406 1407 Calls the offloader's `_add_new_jobs()` twice; once from 1408 initial conditions, and then again after adding more 1409 directories. After the second call, perform the assertions 1410 of `self._check_open_jobs()`. Additionally, assert that 1411 keys added by the first call still map to their original 1412 job object after the second call. 1413 1414 """ 1415 self._run_add_new_jobs(self._initial_job_names) 1416 jobs_copy = self._offloader._open_jobs.copy() 1417 for d in self.MOREJOBS: 1418 os.mkdir(d) 1419 self._run_add_new_jobs(self._initial_job_names | 1420 set(self.MOREJOBS)) 1421 for key in jobs_copy.keys(): 1422 self.assertIs(jobs_copy[key], 1423 self._offloader._open_jobs[key]) 1424 1425 1426 class JobStateTests(_TempResultsDirTestBase): 1427 """Tests for job state predicates. 1428 1429 This tests for the expected results from the 1430 `is_offloaded()` predicate method. 1431 1432 """ 1433 1434 def test_unfinished_job(self): 1435 """Test that an unfinished job reports the correct state. 1436 1437 A job is "unfinished" if it isn't marked complete in the 1438 database. A job in this state is neither "complete" nor 1439 "reportable". 1440 1441 """ 1442 job = self.make_job(self.REGULAR_JOBLIST[0]) 1443 self.assertFalse(job.is_offloaded()) 1444 1445 1446 def test_incomplete_job(self): 1447 """Test that an incomplete job reports the correct state. 1448 1449 A job is "incomplete" if exactly one attempt has been made 1450 to offload the job, but its results directory still exists. 1451 A job in this state is neither "complete" nor "reportable". 1452 1453 """ 1454 job = self.make_job(self.REGULAR_JOBLIST[0]) 1455 job.set_incomplete() 1456 self.assertFalse(job.is_offloaded()) 1457 1458 1459 def test_reportable_job(self): 1460 """Test that a reportable job reports the correct state. 1461 1462 A job is "reportable" if more than one attempt has been made 1463 to offload the job, and its results directory still exists. 1464 A job in this state is "reportable", but not "complete". 1465 1466 """ 1467 job = self.make_job(self.REGULAR_JOBLIST[0]) 1468 job.set_reportable() 1469 self.assertFalse(job.is_offloaded()) 1470 1471 1472 def test_completed_job(self): 1473 """Test that a completed job reports the correct state. 1474 1475 A job is "completed" if at least one attempt has been made 1476 to offload the job, and its results directory still exists. 1477 A job in this state is "complete", and not "reportable". 1478 1479 """ 1480 job = self.make_job(self.REGULAR_JOBLIST[0]) 1481 job.set_complete() 1482 self.assertTrue(job.is_offloaded()) 1483 1484 1485 class ReportingTests(_TempResultsDirTestBase): 1486 """Tests for `Offloader._update_offload_results()`.""" 1487 1488 def setUp(self): 1489 super(ReportingTests, self).setUp() 1490 self._offloader = gs_offloader.Offloader(_get_options([])) 1491 self.mox.StubOutWithMock(self._offloader, '_log_failed_jobs_locally') 1492 self.mox.StubOutWithMock(logging, 'debug') 1493 1494 1495 def _add_job(self, jobdir): 1496 """Add a job to the dictionary of unfinished jobs.""" 1497 j = self.make_job(jobdir) 1498 self._offloader._open_jobs[j._dirname] = j 1499 return j 1500 1501 1502 def _expect_log_message(self, new_open_jobs, with_failures): 1503 """Mock expected logging calls. 1504 1505 `_update_offload_results()` logs one message with the number 1506 of jobs removed from the open job set and the number of jobs 1507 still remaining. Additionally, if there are reportable 1508 jobs, then it logs the number of jobs that haven't yet 1509 offloaded. 1510 1511 This sets up the logging calls using `new_open_jobs` to 1512 figure the job counts. If `with_failures` is true, then 1513 the log message is set up assuming that all jobs in 1514 `new_open_jobs` have offload failures. 1515 1516 @param new_open_jobs New job set for calculating counts 1517 in the messages. 1518 @param with_failures Whether the log message with a 1519 failure count is expected. 1520 1521 """ 1522 count = len(self._offloader._open_jobs) - len(new_open_jobs) 1523 logging.debug(mox.IgnoreArg(), count, len(new_open_jobs)) 1524 if with_failures: 1525 logging.debug(mox.IgnoreArg(), len(new_open_jobs)) 1526 1527 1528 def _run_update(self, new_open_jobs): 1529 """Call `_update_offload_results()`. 1530 1531 Initial conditions are set up by the caller. This calls 1532 `_update_offload_results()` once, and then checks these 1533 assertions: 1534 * The offloader's new `_open_jobs` field contains only 1535 the entries in `new_open_jobs`. 1536 1537 @param new_open_jobs A dictionary representing the expected 1538 new value of the offloader's 1539 `_open_jobs` field. 1540 """ 1541 self.mox.ReplayAll() 1542 self._offloader._update_offload_results() 1543 self.assertEqual(self._offloader._open_jobs, new_open_jobs) 1544 self.mox.VerifyAll() 1545 self.mox.ResetAll() 1546 1547 1548 def _expect_failed_jobs(self, failed_jobs): 1549 """Mock expected call to log the failed jobs on local disk. 1550 1551 TODO(crbug.com/686904): The fact that we have to mock an internal 1552 function for this test is evidence that we need to pull out the local 1553 file formatter in its own object in a future CL. 1554 1555 @param failed_jobs: The list of jobs being logged as failed. 1556 """ 1557 self._offloader._log_failed_jobs_locally(failed_jobs) 1558 1559 1560 def test_no_jobs(self): 1561 """Test `_update_offload_results()` with no open jobs. 1562 1563 Initial conditions are an empty `_open_jobs` list. 1564 Expected result is an empty `_open_jobs` list. 1565 1566 """ 1567 self._expect_log_message({}, False) 1568 self._expect_failed_jobs([]) 1569 self._run_update({}) 1570 1571 1572 def test_all_completed(self): 1573 """Test `_update_offload_results()` with only complete jobs. 1574 1575 Initial conditions are an `_open_jobs` list consisting of only completed 1576 jobs. 1577 Expected result is an empty `_open_jobs` list. 1578 1579 """ 1580 for d in self.REGULAR_JOBLIST: 1581 self._add_job(d).set_complete() 1582 self._expect_log_message({}, False) 1583 self._expect_failed_jobs([]) 1584 self._run_update({}) 1585 1586 1587 def test_none_finished(self): 1588 """Test `_update_offload_results()` with only unfinished jobs. 1589 1590 Initial conditions are an `_open_jobs` list consisting of only 1591 unfinished jobs. 1592 Expected result is no change to the `_open_jobs` list. 1593 1594 """ 1595 for d in self.REGULAR_JOBLIST: 1596 self._add_job(d) 1597 new_jobs = self._offloader._open_jobs.copy() 1598 self._expect_log_message(new_jobs, False) 1599 self._expect_failed_jobs([]) 1600 self._run_update(new_jobs) 1601 1602 1603 if __name__ == '__main__': 1604 unittest.main() 1605