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