1 #!/usr/bin/python 2 # 3 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 8 """Unit tests for server/cros/dynamic_suite/dynamic_suite.py.""" 9 10 import collections 11 from collections import OrderedDict 12 import os 13 import shutil 14 import tempfile 15 import unittest 16 17 import mock 18 import mox 19 20 import common 21 22 from autotest_lib.client.common_lib import base_job 23 from autotest_lib.client.common_lib import control_data 24 from autotest_lib.client.common_lib import error 25 from autotest_lib.client.common_lib import priorities 26 from autotest_lib.client.common_lib import utils 27 from autotest_lib.client.common_lib.cros import dev_server 28 from autotest_lib.server import frontend 29 from autotest_lib.server.cros import provision 30 from autotest_lib.server.cros.dynamic_suite import control_file_getter 31 from autotest_lib.server.cros.dynamic_suite import constants 32 from autotest_lib.server.cros.dynamic_suite import job_status 33 from autotest_lib.server.cros.dynamic_suite import suite as SuiteBase 34 from autotest_lib.server.cros.dynamic_suite import suite_common 35 from autotest_lib.server.cros.dynamic_suite.comparators import StatusContains 36 from autotest_lib.server.cros.dynamic_suite.fakes import FakeControlData 37 from autotest_lib.server.cros.dynamic_suite.fakes import FakeJob 38 from autotest_lib.server.cros.dynamic_suite.fakes import FakeMultiprocessingPool 39 from autotest_lib.server.cros.dynamic_suite.suite import RetryHandler 40 from autotest_lib.server.cros.dynamic_suite.suite import Suite 41 42 class SuiteTest(mox.MoxTestBase): 43 """Unit tests for dynamic_suite Suite class. 44 45 @var _BUILDS: fake build 46 @var _TAG: fake suite tag 47 """ 48 49 _BOARD = 'board:board' 50 _BUILDS = {provision.CROS_VERSION_PREFIX:'build_1', 51 provision.FW_RW_VERSION_PREFIX:'fwrw_build_1'} 52 _TAG = 'au' 53 _ATTR = {'attr:attr'} 54 _DEVSERVER_HOST = 'http://dontcare:8080' 55 _FAKE_JOB_ID = 10 56 57 58 def setUp(self): 59 """Setup.""" 60 super(SuiteTest, self).setUp() 61 self.maxDiff = None 62 self.use_batch = suite_common.ENABLE_CONTROLS_IN_BATCH 63 suite_common.ENABLE_CONTROLS_IN_BATCH = False 64 self.afe = self.mox.CreateMock(frontend.AFE) 65 self.tko = self.mox.CreateMock(frontend.TKO) 66 67 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__) 68 69 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter) 70 self.devserver = dev_server.ImageServer(self._DEVSERVER_HOST) 71 72 self.files = OrderedDict( 73 [('one', FakeControlData(self._TAG, self._ATTR, 'data_one', 74 'FAST', job_retries=None)), 75 ('two', FakeControlData(self._TAG, self._ATTR, 'data_two', 76 'SHORT', dependencies=['feta'])), 77 ('three', FakeControlData(self._TAG, self._ATTR, 'data_three', 78 'MEDIUM')), 79 ('four', FakeControlData('other', self._ATTR, 'data_four', 80 'LONG', dependencies=['arugula'])), 81 ('five', FakeControlData(self._TAG, {'other'}, 'data_five', 82 'LONG', dependencies=['arugula', 83 'caligula'])), 84 ('six', FakeControlData(self._TAG, self._ATTR, 'data_six', 85 'LENGTHY')), 86 ('seven', FakeControlData(self._TAG, self._ATTR, 'data_seven', 87 'FAST', job_retries=1))]) 88 89 self.files_to_filter = { 90 'with/deps/...': FakeControlData(self._TAG, self._ATTR, 91 'gets filtered'), 92 'with/profilers/...': FakeControlData(self._TAG, self._ATTR, 93 'gets filtered')} 94 95 96 def tearDown(self): 97 """Teardown.""" 98 suite_common.ENABLE_CONTROLS_IN_BATCH = self.use_batch 99 super(SuiteTest, self).tearDown() 100 shutil.rmtree(self.tmpdir, ignore_errors=True) 101 102 103 def expect_control_file_parsing(self, suite_name=_TAG): 104 """Expect an attempt to parse the 'control files' in |self.files|. 105 106 @param suite_name: The suite name to parse control files for. 107 """ 108 all_files = self.files.keys() + self.files_to_filter.keys() 109 self._set_control_file_parsing_expectations(False, all_files, 110 self.files, suite_name) 111 112 113 def _set_control_file_parsing_expectations(self, already_stubbed, 114 file_list, files_to_parse, 115 suite_name): 116 """Expect an attempt to parse the 'control files' in |files|. 117 118 @param already_stubbed: parse_control_string already stubbed out. 119 @param file_list: the files the dev server returns 120 @param files_to_parse: the {'name': FakeControlData} dict of files we 121 expect to get parsed. 122 """ 123 if not already_stubbed: 124 self.mox.StubOutWithMock(control_data, 'parse_control_string') 125 126 self.mox.StubOutWithMock(suite_common.multiprocessing, 'Pool') 127 suite_common.multiprocessing.Pool( 128 processes=suite_common.get_process_limit()).AndReturn( 129 FakeMultiprocessingPool()) 130 131 self.getter.get_control_file_list( 132 suite_name=suite_name).AndReturn(file_list) 133 for file, data in files_to_parse.iteritems(): 134 self.getter.get_control_file_contents( 135 file).InAnyOrder().AndReturn(data.string) 136 control_data.parse_control_string( 137 data.string, 138 raise_warnings=True, 139 path=file).InAnyOrder().AndReturn(data) 140 141 142 def expect_control_file_parsing_in_batch(self, suite_name=_TAG): 143 """Expect an attempt to parse the contents of all control files in 144 |self.files| and |self.files_to_filter|, form them to a dict. 145 146 @param suite_name: The suite name to parse control files for. 147 """ 148 self.getter = self.mox.CreateMock(control_file_getter.DevServerGetter) 149 self.mox.StubOutWithMock(control_data, 'parse_control_string') 150 151 self.mox.StubOutWithMock(suite_common.multiprocessing, 'Pool') 152 suite_common.multiprocessing.Pool( 153 processes=suite_common.get_process_limit()).AndReturn( 154 FakeMultiprocessingPool()) 155 156 suite_info = {} 157 for k, v in self.files.iteritems(): 158 suite_info[k] = v.string 159 control_data.parse_control_string( 160 v.string, 161 raise_warnings=True, 162 path=k).InAnyOrder().AndReturn(v) 163 for k, v in self.files_to_filter.iteritems(): 164 suite_info[k] = v.string 165 self.getter._dev_server = self._DEVSERVER_HOST 166 self.getter.get_suite_info( 167 suite_name=suite_name).AndReturn(suite_info) 168 169 170 def testFindAllTestInBatch(self): 171 """Test switch on enable_getting_controls_in_batch for function 172 find_all_test.""" 173 self.use_batch = suite_common.ENABLE_CONTROLS_IN_BATCH 174 self.expect_control_file_parsing_in_batch() 175 suite_common.ENABLE_CONTROLS_IN_BATCH = True 176 177 self.mox.ReplayAll() 178 179 predicate = lambda d: d.suite == self._TAG 180 tests = SuiteBase.find_and_parse_tests(self.getter, 181 predicate, 182 self._TAG) 183 self.assertEquals(len(tests), 6) 184 self.assertTrue(self.files['one'] in tests) 185 self.assertTrue(self.files['two'] in tests) 186 self.assertTrue(self.files['three'] in tests) 187 self.assertTrue(self.files['five'] in tests) 188 self.assertTrue(self.files['six'] in tests) 189 self.assertTrue(self.files['seven'] in tests) 190 suite_common.ENABLE_CONTROLS_IN_BATCH = self.use_batch 191 192 193 def testFindAndParseStableTests(self): 194 """Should find only tests that match a predicate.""" 195 self.expect_control_file_parsing() 196 self.mox.ReplayAll() 197 198 predicate = lambda d: d.text == self.files['two'].string 199 tests = SuiteBase.find_and_parse_tests(self.getter, 200 predicate, 201 self._TAG) 202 self.assertEquals(len(tests), 1) 203 self.assertEquals(tests[0], self.files['two']) 204 205 206 def testFindSuiteSyntaxErrors(self): 207 """Check all control files for syntax errors. 208 209 This test actually parses all control files in the autotest directory 210 for syntax errors, by using the un-forgiving parser and pretending to 211 look for all control files with the suite attribute. 212 """ 213 autodir = os.path.abspath( 214 os.path.join(os.path.dirname(__file__), '..', '..', '..')) 215 fs_getter = SuiteBase.create_fs_getter(autodir) 216 predicate = lambda t: hasattr(t, 'suite') 217 SuiteBase.find_and_parse_tests(fs_getter, predicate, 218 forgiving_parser=False) 219 220 221 def testFindAndParseTestsSuite(self): 222 """Should find all tests that match a predicate.""" 223 self.expect_control_file_parsing() 224 self.mox.ReplayAll() 225 226 predicate = lambda d: d.suite == self._TAG 227 tests = SuiteBase.find_and_parse_tests(self.getter, 228 predicate, 229 self._TAG) 230 self.assertEquals(len(tests), 6) 231 self.assertTrue(self.files['one'] in tests) 232 self.assertTrue(self.files['two'] in tests) 233 self.assertTrue(self.files['three'] in tests) 234 self.assertTrue(self.files['five'] in tests) 235 self.assertTrue(self.files['six'] in tests) 236 self.assertTrue(self.files['seven'] in tests) 237 238 239 def testFindAndParseTestsAttr(self): 240 """Should find all tests that match a predicate.""" 241 self.expect_control_file_parsing() 242 self.mox.ReplayAll() 243 244 predicate = SuiteBase.matches_attribute_expression_predicate('attr:attr') 245 tests = SuiteBase.find_and_parse_tests(self.getter, 246 predicate, 247 self._TAG) 248 self.assertEquals(len(tests), 6) 249 self.assertTrue(self.files['one'] in tests) 250 self.assertTrue(self.files['two'] in tests) 251 self.assertTrue(self.files['three'] in tests) 252 self.assertTrue(self.files['four'] in tests) 253 self.assertTrue(self.files['six'] in tests) 254 self.assertTrue(self.files['seven'] in tests) 255 256 257 def testAdHocSuiteCreation(self): 258 """Should be able to schedule an ad-hoc suite by specifying 259 a single test name.""" 260 self.expect_control_file_parsing(suite_name='ad_hoc_suite') 261 self.mox.ReplayAll() 262 predicate = SuiteBase.test_name_equals_predicate('name-data_five') 263 suite = Suite.create_from_predicates([predicate], self._BUILDS, 264 self._BOARD, devserver=None, 265 cf_getter=self.getter, 266 afe=self.afe, tko=self.tko) 267 268 self.assertFalse(self.files['one'] in suite.tests) 269 self.assertFalse(self.files['two'] in suite.tests) 270 self.assertFalse(self.files['four'] in suite.tests) 271 self.assertTrue(self.files['five'] in suite.tests) 272 273 274 def mock_control_file_parsing(self): 275 """Fake out find_and_parse_tests(), returning content from |self.files|. 276 """ 277 for test in self.files.values(): 278 test.text = test.string # mimic parsing. 279 self.mox.StubOutWithMock(SuiteBase, 'find_and_parse_tests') 280 SuiteBase.find_and_parse_tests( 281 mox.IgnoreArg(), 282 mox.IgnoreArg(), 283 mox.IgnoreArg(), 284 forgiving_parser=True, 285 run_prod_code=False, 286 test_args=None).AndReturn(self.files.values()) 287 288 289 def expect_job_scheduling(self, recorder, 290 tests_to_skip=[], ignore_deps=False, 291 raises=False, suite_deps=[], suite=None, 292 extra_keyvals={}): 293 """Expect jobs to be scheduled for 'tests' in |self.files|. 294 295 @param recorder: object with a record_entry to be used to record test 296 results. 297 @param tests_to_skip: [list, of, test, names] that we expect to skip. 298 @param ignore_deps: If true, ignore tests' dependencies. 299 @param raises: If True, expect exceptions. 300 @param suite_deps: If True, add suite level dependencies. 301 @param extra_keyvals: Extra keyvals set to tests. 302 """ 303 record_job_id = suite and suite._results_dir 304 if record_job_id: 305 self.mox.StubOutWithMock(suite, '_remember_job_keyval') 306 recorder.record_entry( 307 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG), 308 log_in_subdir=False) 309 tests = self.files.values() 310 n = 1 311 for test in tests: 312 if test.name in tests_to_skip: 313 continue 314 dependencies = [] 315 if not ignore_deps: 316 dependencies.extend(test.dependencies) 317 if suite_deps: 318 dependencies.extend(suite_deps) 319 dependencies.append(self._BOARD) 320 build = self._BUILDS[provision.CROS_VERSION_PREFIX] 321 keyvals = { 322 'build': build, 323 'suite': self._TAG, 324 'builds': SuiteTest._BUILDS, 325 'experimental':test.experimental, 326 } 327 keyvals.update(extra_keyvals) 328 job_mock = self.afe.create_job( 329 control_file=test.text, 330 name=mox.And(mox.StrContains(build), 331 mox.StrContains(test.name)), 332 control_type=mox.IgnoreArg(), 333 meta_hosts=[self._BOARD], 334 dependencies=dependencies, 335 keyvals=keyvals, 336 max_runtime_mins=24*60, 337 timeout_mins=1440, 338 parent_job_id=None, 339 reboot_before=mox.IgnoreArg(), 340 run_reset=mox.IgnoreArg(), 341 priority=priorities.Priority.DEFAULT, 342 synch_count=test.sync_count, 343 require_ssp=test.require_ssp 344 ) 345 if raises: 346 job_mock.AndRaise(error.NoEligibleHostException()) 347 recorder.record_entry( 348 StatusContains.CreateFromStrings('START', test.name), 349 log_in_subdir=False) 350 recorder.record_entry( 351 StatusContains.CreateFromStrings('TEST_NA', test.name), 352 log_in_subdir=False) 353 recorder.record_entry( 354 StatusContains.CreateFromStrings('END', test.name), 355 log_in_subdir=False) 356 else: 357 fake_job = FakeJob(id=n) 358 job_mock.AndReturn(fake_job) 359 if record_job_id: 360 suite._remember_job_keyval(fake_job) 361 n += 1 362 363 364 def testScheduleTestsAndRecord(self): 365 """Should schedule stable and experimental tests with the AFE.""" 366 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 367 'name-data_four', 'name-data_five', 'name-data_six', 368 'name-data_seven'] 369 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: 7, 370 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 371 372 self.mock_control_file_parsing() 373 self.mox.ReplayAll() 374 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 375 self.devserver, 376 afe=self.afe, tko=self.tko, 377 results_dir=self.tmpdir) 378 self.mox.ResetAll() 379 recorder = self.mox.CreateMock(base_job.base_job) 380 self.expect_job_scheduling(recorder, suite=suite) 381 382 self.mox.StubOutWithMock(utils, 'write_keyval') 383 utils.write_keyval(self.tmpdir, keyval_dict) 384 self.mox.ReplayAll() 385 suite.schedule(recorder.record_entry) 386 for job in suite._jobs: 387 self.assertTrue(hasattr(job, 'test_name')) 388 389 390 def testScheduleTests(self): 391 """Should schedule tests with the AFE.""" 392 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 393 'name-data_four', 'name-data_five', 'name-data_six', 394 'name-data_seven'] 395 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: len(name_list), 396 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 397 398 self.mock_control_file_parsing() 399 recorder = self.mox.CreateMock(base_job.base_job) 400 self.expect_job_scheduling(recorder) 401 self.mox.StubOutWithMock(utils, 'write_keyval') 402 utils.write_keyval(None, keyval_dict) 403 404 self.mox.ReplayAll() 405 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 406 self.devserver, 407 afe=self.afe, tko=self.tko) 408 suite.schedule(recorder.record_entry) 409 410 411 def testScheduleTestsIgnoreDeps(self): 412 """Test scheduling tests ignoring deps.""" 413 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 414 'name-data_four', 'name-data_five', 'name-data_six', 415 'name-data_seven'] 416 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: len(name_list), 417 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 418 419 self.mock_control_file_parsing() 420 recorder = self.mox.CreateMock(base_job.base_job) 421 self.expect_job_scheduling(recorder, ignore_deps=True) 422 self.mox.StubOutWithMock(utils, 'write_keyval') 423 utils.write_keyval(None, keyval_dict) 424 425 self.mox.ReplayAll() 426 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 427 self.devserver, 428 afe=self.afe, tko=self.tko, 429 ignore_deps=True) 430 suite.schedule(recorder.record_entry) 431 432 433 def testScheduleUnrunnableTestsTESTNA(self): 434 """Tests which fail to schedule should be TEST_NA.""" 435 # Since all tests will be fail to schedule, the num of scheduled tests 436 # will be zero. 437 name_list = [] 438 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: 0, 439 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 440 441 self.mock_control_file_parsing() 442 recorder = self.mox.CreateMock(base_job.base_job) 443 self.expect_job_scheduling(recorder, raises=True) 444 self.mox.StubOutWithMock(utils, 'write_keyval') 445 utils.write_keyval(None, keyval_dict) 446 self.mox.ReplayAll() 447 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 448 self.devserver, 449 afe=self.afe, tko=self.tko) 450 suite.schedule(recorder.record_entry) 451 452 453 def testRetryMapAfterScheduling(self): 454 """Test job-test and test-job mapping are correctly updated.""" 455 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 456 'name-data_four', 'name-data_five', 'name-data_six', 457 'name-data_seven'] 458 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: 7, 459 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 460 461 self.mock_control_file_parsing() 462 recorder = self.mox.CreateMock(base_job.base_job) 463 self.expect_job_scheduling(recorder) 464 self.mox.StubOutWithMock(utils, 'write_keyval') 465 utils.write_keyval(None, keyval_dict) 466 467 all_files = self.files.items() 468 # Sort tests in self.files so that they are in the same 469 # order as they are scheduled. 470 expected_retry_map = {} 471 for n in range(len(all_files)): 472 test = all_files[n][1] 473 job_id = n + 1 474 job_retries = 1 if test.job_retries is None else test.job_retries 475 if job_retries > 0: 476 expected_retry_map[job_id] = { 477 'state': RetryHandler.States.NOT_ATTEMPTED, 478 'retry_max': job_retries} 479 480 self.mox.ReplayAll() 481 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 482 self.devserver, 483 afe=self.afe, tko=self.tko, 484 job_retry=True) 485 suite.schedule(recorder.record_entry) 486 487 self.assertEqual(expected_retry_map, suite._retry_handler._retry_map) 488 489 490 def testSuiteMaxRetries(self): 491 """Test suite max retries.""" 492 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 493 'name-data_four', 'name-data_five', 494 'name-data_six', 'name-data_seven'] 495 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: 7, 496 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 497 498 self.mock_control_file_parsing() 499 recorder = self.mox.CreateMock(base_job.base_job) 500 self.expect_job_scheduling(recorder) 501 self.mox.StubOutWithMock(utils, 'write_keyval') 502 utils.write_keyval(None, keyval_dict) 503 self.mox.ReplayAll() 504 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 505 self.devserver, 506 afe=self.afe, tko=self.tko, 507 job_retry=True, max_retries=1) 508 suite.schedule(recorder.record_entry) 509 self.assertEqual(suite._retry_handler._max_retries, 1) 510 # Find the job_id of the test that allows retry 511 job_id = suite._retry_handler._retry_map.iterkeys().next() 512 suite._retry_handler.add_retry(old_job_id=job_id, new_job_id=10) 513 self.assertEqual(suite._retry_handler._max_retries, 0) 514 515 516 def testSuiteDependencies(self): 517 """Should add suite dependencies to tests scheduled.""" 518 name_list = ['name-data_one', 'name-data_two', 'name-data_three', 519 'name-data_four', 'name-data_five', 'name-data_six', 520 'name-data_seven'] 521 keyval_dict = {constants.SCHEDULED_TEST_COUNT_KEY: len(name_list), 522 constants.SCHEDULED_TEST_NAMES_KEY: repr(name_list)} 523 524 self.mock_control_file_parsing() 525 recorder = self.mox.CreateMock(base_job.base_job) 526 self.expect_job_scheduling(recorder, suite_deps=['extra']) 527 self.mox.StubOutWithMock(utils, 'write_keyval') 528 utils.write_keyval(None, keyval_dict) 529 530 self.mox.ReplayAll() 531 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 532 self.devserver, extra_deps=['extra'], 533 afe=self.afe, tko=self.tko) 534 suite.schedule(recorder.record_entry) 535 536 537 def testInheritedKeyvals(self): 538 """Tests should inherit some whitelisted job keyvals.""" 539 # Only keyvals in constants.INHERITED_KEYVALS are inherited to tests. 540 job_keyvals = { 541 constants.KEYVAL_CIDB_BUILD_ID: '111', 542 constants.KEYVAL_CIDB_BUILD_STAGE_ID: '222', 543 'your': 'name', 544 } 545 test_keyvals = { 546 constants.KEYVAL_CIDB_BUILD_ID: '111', 547 constants.KEYVAL_CIDB_BUILD_STAGE_ID: '222', 548 } 549 550 self.mock_control_file_parsing() 551 recorder = self.mox.CreateMock(base_job.base_job) 552 self.expect_job_scheduling( 553 recorder, 554 extra_keyvals=test_keyvals) 555 self.mox.StubOutWithMock(utils, 'write_keyval') 556 utils.write_keyval(None, job_keyvals) 557 utils.write_keyval(None, mox.IgnoreArg()) 558 559 self.mox.ReplayAll() 560 suite = Suite.create_from_name(self._TAG, self._BUILDS, self._BOARD, 561 self.devserver, 562 afe=self.afe, tko=self.tko, 563 job_keyvals=job_keyvals) 564 suite.schedule(recorder.record_entry) 565 566 567 def _createSuiteWithMockedTestsAndControlFiles(self, file_bugs=False): 568 """Create a Suite, using mocked tests and control file contents. 569 570 @return Suite object, after mocking out behavior needed to create it. 571 """ 572 self.result_reporter = _MemoryResultReporter() 573 self.expect_control_file_parsing() 574 self.mox.ReplayAll() 575 suite = Suite.create_from_name( 576 self._TAG, 577 self._BUILDS, 578 self._BOARD, 579 self.devserver, 580 self.getter, 581 afe=self.afe, 582 tko=self.tko, 583 file_bugs=file_bugs, 584 job_retry=True, 585 result_reporter=self.result_reporter, 586 ) 587 self.mox.ResetAll() 588 return suite 589 590 591 def _createSuiteMockResults(self, results_dir=None, result_status='FAIL'): 592 """Create a suite, returned a set of mocked results to expect. 593 594 @param results_dir: A mock results directory. 595 @param result_status: A desired result status, e.g. 'FAIL', 'WARN'. 596 597 @return List of mocked results to wait on. 598 """ 599 self.suite = self._createSuiteWithMockedTestsAndControlFiles( 600 file_bugs=True) 601 self.suite._results_dir = results_dir 602 test_report = self._get_bad_test_report(result_status) 603 test_predicates = test_report.predicates 604 test_fallout = test_report.fallout 605 606 self.recorder = self.mox.CreateMock(base_job.base_job) 607 self.recorder.record_entry = self.mox.CreateMock( 608 base_job.base_job.record_entry) 609 self._mock_recorder_with_results([test_predicates], self.recorder) 610 return [test_predicates, test_fallout] 611 612 613 def _mock_recorder_with_results(self, results, recorder): 614 """ 615 Checks that results are recoded in order, eg: 616 START, (status, name, reason) END 617 618 @param results: list of results 619 @param recorder: status recorder 620 """ 621 for result in results: 622 status = result[0] 623 test_name = result[1] 624 recorder.record_entry( 625 StatusContains.CreateFromStrings('START', test_name), 626 log_in_subdir=False) 627 recorder.record_entry( 628 StatusContains.CreateFromStrings(*result), 629 log_in_subdir=False).InAnyOrder('results') 630 recorder.record_entry( 631 StatusContains.CreateFromStrings('END %s' % status, test_name), 632 log_in_subdir=False) 633 634 635 def schedule_and_expect_these_results(self, suite, results, recorder): 636 """Create mox stubs for call to suite.schedule and 637 job_status.wait_for_results 638 639 @param suite: suite object for which to stub out schedule(...) 640 @param results: results object to be returned from 641 job_stats_wait_for_results(...) 642 @param recorder: mocked recorder object to replay status messages 643 """ 644 def result_generator(results): 645 """A simple generator which generates results as Status objects. 646 647 This generator handles 'send' by simply ignoring it. 648 649 @param results: results object to be returned from 650 job_stats_wait_for_results(...) 651 @yield: job_status.Status objects. 652 """ 653 results = map(lambda r: job_status.Status(*r), results) 654 for r in results: 655 new_input = (yield r) 656 if new_input: 657 yield None 658 659 self.mox.StubOutWithMock(suite, 'schedule') 660 suite.schedule(recorder.record_entry) 661 suite._retry_handler = RetryHandler({}) 662 663 waiter_patch = mock.patch.object( 664 job_status.JobResultWaiter, 'wait_for_results', autospec=True) 665 waiter_mock = waiter_patch.start() 666 waiter_mock.return_value = result_generator(results) 667 self.addCleanup(waiter_patch.stop) 668 669 670 def testRunAndWaitSuccess(self): 671 """Should record successful results.""" 672 suite = self._createSuiteWithMockedTestsAndControlFiles() 673 674 recorder = self.mox.CreateMock(base_job.base_job) 675 676 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')] 677 self._mock_recorder_with_results(results, recorder) 678 self.schedule_and_expect_these_results(suite, results, recorder) 679 self.mox.ReplayAll() 680 681 suite.schedule(recorder.record_entry) 682 suite.wait(recorder.record_entry) 683 684 685 def testRunAndWaitFailure(self): 686 """Should record failure to gather results.""" 687 suite = self._createSuiteWithMockedTestsAndControlFiles() 688 689 recorder = self.mox.CreateMock(base_job.base_job) 690 recorder.record_entry( 691 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'), 692 log_in_subdir=False) 693 694 self.mox.StubOutWithMock(suite, 'schedule') 695 suite.schedule(recorder.record_entry) 696 self.mox.ReplayAll() 697 698 with mock.patch.object( 699 job_status.JobResultWaiter, 'wait_for_results', 700 autospec=True) as wait_mock: 701 wait_mock.side_effect = Exception 702 suite.schedule(recorder.record_entry) 703 suite.wait(recorder.record_entry) 704 705 706 def testRunAndWaitScheduleFailure(self): 707 """Should record failure to schedule jobs.""" 708 suite = self._createSuiteWithMockedTestsAndControlFiles() 709 710 recorder = self.mox.CreateMock(base_job.base_job) 711 recorder.record_entry( 712 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG), 713 log_in_subdir=False) 714 715 recorder.record_entry( 716 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'), 717 log_in_subdir=False) 718 719 self.mox.StubOutWithMock(suite._job_creator, 'create_job') 720 suite._job_creator.create_job( 721 mox.IgnoreArg(), retry_for=mox.IgnoreArg()).AndRaise( 722 Exception('Expected during test.')) 723 self.mox.ReplayAll() 724 725 suite.schedule(recorder.record_entry) 726 suite.wait(recorder.record_entry) 727 728 729 def testGetTestsSortedByTime(self): 730 """Should find all tests and sorted by TIME setting.""" 731 self.expect_control_file_parsing() 732 self.mox.ReplayAll() 733 # Get all tests. 734 tests = SuiteBase.find_and_parse_tests(self.getter, 735 lambda d: True, 736 self._TAG) 737 self.assertEquals(len(tests), 7) 738 times = [control_data.ControlData.get_test_time_index(test.time) 739 for test in tests] 740 self.assertTrue(all(x>=y for x, y in zip(times, times[1:])), 741 'Tests are not ordered correctly.') 742 743 744 def _get_bad_test_report(self, result_status='FAIL'): 745 """ 746 Fetch the predicates of a failing test, and the parameters 747 that are a fallout of this test failing. 748 """ 749 predicates = collections.namedtuple('predicates', 750 'status, testname, reason') 751 fallout = collections.namedtuple('fallout', 752 ('time_start, time_end, job_id,' 753 'username, hostname')) 754 test_report = collections.namedtuple('test_report', 755 'predicates, fallout') 756 return test_report(predicates(result_status, 'bad_test', 757 'dreadful_reason'), 758 fallout('2014-01-01 01:01:01', 'None', 759 self._FAKE_JOB_ID, 'user', 'myhost')) 760 761 762 def testJobRetryTestFail(self): 763 """Test retry works.""" 764 test_to_retry = self.files['seven'] 765 fake_new_job_id = self._FAKE_JOB_ID + 1 766 fake_job = FakeJob(id=self._FAKE_JOB_ID) 767 fake_new_job = FakeJob(id=fake_new_job_id) 768 769 test_results = self._createSuiteMockResults() 770 self.schedule_and_expect_these_results( 771 self.suite, 772 [test_results[0] + test_results[1]], 773 self.recorder) 774 self.mox.StubOutWithMock(self.suite._job_creator, 'create_job') 775 self.suite._job_creator.create_job( 776 test_to_retry, 777 retry_for=self._FAKE_JOB_ID).AndReturn(fake_new_job) 778 self.mox.ReplayAll() 779 self.suite.schedule(self.recorder.record_entry) 780 self.suite._retry_handler._retry_map = { 781 self._FAKE_JOB_ID: {'state': RetryHandler.States.NOT_ATTEMPTED, 782 'retry_max': 1} 783 } 784 self.suite._jobs_to_tests[self._FAKE_JOB_ID] = test_to_retry 785 self.suite.wait(self.recorder.record_entry) 786 expected_retry_map = { 787 self._FAKE_JOB_ID: {'state': RetryHandler.States.RETRIED, 788 'retry_max': 1}, 789 fake_new_job_id: {'state': RetryHandler.States.NOT_ATTEMPTED, 790 'retry_max': 0} 791 } 792 # Check retry map is correctly updated 793 self.assertEquals(self.suite._retry_handler._retry_map, 794 expected_retry_map) 795 # Check _jobs_to_tests is correctly updated 796 self.assertEquals(self.suite._jobs_to_tests[fake_new_job_id], 797 test_to_retry) 798 799 800 def testJobRetryTestWarn(self): 801 """Test that no retry is scheduled if test warns.""" 802 test_to_retry = self.files['seven'] 803 fake_job = FakeJob(id=self._FAKE_JOB_ID) 804 test_results = self._createSuiteMockResults(result_status='WARN') 805 self.schedule_and_expect_these_results( 806 self.suite, 807 [test_results[0] + test_results[1]], 808 self.recorder) 809 self.mox.ReplayAll() 810 self.suite.schedule(self.recorder.record_entry) 811 self.suite._retry_handler._retry_map = { 812 self._FAKE_JOB_ID: {'state': RetryHandler.States.NOT_ATTEMPTED, 813 'retry_max': 1} 814 } 815 self.suite._jobs_to_tests[self._FAKE_JOB_ID] = test_to_retry 816 expected_jobs_to_tests = self.suite._jobs_to_tests.copy() 817 expected_retry_map = self.suite._retry_handler._retry_map.copy() 818 self.suite.wait(self.recorder.record_entry) 819 self.assertTrue(self.result_reporter.results) 820 # Check retry map and _jobs_to_tests, ensure no retry was scheduled. 821 self.assertEquals(self.suite._retry_handler._retry_map, 822 expected_retry_map) 823 self.assertEquals(self.suite._jobs_to_tests, expected_jobs_to_tests) 824 825 826 def testFailedJobRetry(self): 827 """Make sure the suite survives even if the retry failed.""" 828 test_to_retry = self.files['seven'] 829 fake_job = FakeJob(id=self._FAKE_JOB_ID) 830 831 test_results = self._createSuiteMockResults() 832 self.schedule_and_expect_these_results( 833 self.suite, 834 [test_results[0] + test_results[1]], 835 self.recorder) 836 self.mox.StubOutWithMock(self.suite._job_creator, 'create_job') 837 self.suite._job_creator.create_job( 838 test_to_retry, retry_for=self._FAKE_JOB_ID).AndRaise( 839 error.RPCException('Expected during test')) 840 # Do not file a bug. 841 self.mox.StubOutWithMock(self.suite, '_should_report') 842 self.suite._should_report(mox.IgnoreArg()).AndReturn(False) 843 844 self.mox.ReplayAll() 845 846 self.suite.schedule(self.recorder.record_entry) 847 self.suite._retry_handler._retry_map = { 848 self._FAKE_JOB_ID: { 849 'state': RetryHandler.States.NOT_ATTEMPTED, 850 'retry_max': 1}} 851 self.suite._jobs_to_tests[self._FAKE_JOB_ID] = test_to_retry 852 self.suite.wait(self.recorder.record_entry) 853 expected_retry_map = { 854 self._FAKE_JOB_ID: { 855 'state': RetryHandler.States.ATTEMPTED, 856 'retry_max': 1}} 857 expected_jobs_to_tests = self.suite._jobs_to_tests.copy() 858 self.assertEquals(self.suite._retry_handler._retry_map, 859 expected_retry_map) 860 self.assertEquals(self.suite._jobs_to_tests, expected_jobs_to_tests) 861 862 863 class _MemoryResultReporter(SuiteBase._ResultReporter): 864 """Reporter that stores results internally for testing.""" 865 def __init__(self): 866 self.results = [] 867 868 def report(self, result): 869 """Reports the result by storing it internally.""" 870 self.results.append(result) 871 872 873 if __name__ == '__main__': 874 unittest.main() 875