Home | History | Annotate | Download | only in site_utils
      1 #!/usr/bin/python
      2 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import collections
      7 import datetime as datetime_base
      8 from datetime import datetime
      9 import mock
     10 import time
     11 import unittest
     12 
     13 import common
     14 
     15 from autotest_lib.server.cros.dynamic_suite import constants
     16 from autotest_lib.site_utils import run_suite
     17 from autotest_lib.site_utils import diagnosis_utils
     18 
     19 
     20 class ReturnResultUnittest(unittest.TestCase):
     21     """_ReturnResult tests."""
     22 
     23     def setUp(self):
     24         super(ReturnResultUnittest, self).setUp()
     25         patcher = mock.patch.object(run_suite, '_RETURN_RESULTS',
     26                                     collections.OrderedDict())
     27         self.results = results = patcher.start()
     28         self.addCleanup(patcher.stop)
     29         results['small'] = run_suite._ReturnResult(0, 'small')
     30         results['big'] = run_suite._ReturnResult(1, 'big')
     31 
     32         patcher = mock.patch.object(run_suite, '_RETURN_RESULTS_LIST',
     33                                     list(results.values()))
     34         patcher.start()
     35         self.addCleanup(patcher.stop)
     36 
     37     def test_equal(self):
     38         """Test _ReturnResult equal."""
     39         self.assertEqual(self.results['small'], self.results['small'])
     40 
     41     def test_unequal(self):
     42         """Test _ReturnResult unequal."""
     43         self.assertNotEqual(self.results['big'], self.results['small'])
     44 
     45     def test_greater_than(self):
     46         """Test _ReturnResult greater than."""
     47         self.assertGreater(self.results['big'], self.results['small'])
     48 
     49     def test_bitwise_or(self):
     50         """Test _ReturnResult bitwise or."""
     51         self.assertEqual(self.results['big'],
     52                          self.results['big'] | self.results['small'])
     53 
     54 
     55 class ResultCollectorUnittest(unittest.TestCase):
     56     """Runsuite unittest"""
     57 
     58     JOB_MAX_RUNTIME_MINS = 10
     59 
     60     def setUp(self):
     61         """Set up test."""
     62         self.afe = mock.MagicMock()
     63         self.tko = mock.MagicMock()
     64 
     65 
     66     def _build_view(self, test_idx, test_name, subdir, status, afe_job_id,
     67                     job_name='fake_job_name', reason='fake reason',
     68                     job_keyvals=None, test_started_time=None,
     69                     test_finished_time=None, invalidates_test_idx=None,
     70                     job_started_time=None, job_finished_time=None):
     71         """Build a test view using the given fields.
     72 
     73         @param test_idx: An integer representing test_idx.
     74         @param test_name: A string, e.g. 'dummy_Pass'
     75         @param subdir: A string representing the subdir field of the test view.
     76                        e.g. 'dummy_Pass'.
     77         @param status: A string representing the test status.
     78                        e.g. 'FAIL', 'PASS'
     79         @param afe_job_id: An integer representing the afe job id.
     80         @param job_name: A string representing the job name.
     81         @param reason: A string representing the reason field of the test view.
     82         @param job_keyvals: A dictionary stroing the job keyvals.
     83         @param test_started_time: A string, e.g. '2014-04-12 12:35:33'
     84         @param test_finished_time: A string, e.g. '2014-04-12 12:35:33'
     85         @param invalidates_test_idx: An integer, representing the idx of the
     86                                      test that has been retried.
     87         @param job_started_time: A string, e.g. '2014-04-12 12:35:33'
     88         @param job_finished_time: A string, e.g. '2014-04-12 12:35:33'
     89 
     90         @reutrn: A dictionary representing a test view.
     91 
     92         """
     93         if job_keyvals is None:
     94             job_keyvals = {}
     95         return {'test_idx': test_idx, 'test_name': test_name, 'subdir':subdir,
     96                 'status': status, 'afe_job_id': afe_job_id,
     97                 'job_name': job_name, 'reason': reason,
     98                 'job_keyvals': job_keyvals,
     99                 'test_started_time': test_started_time,
    100                 'test_finished_time': test_finished_time,
    101                 'invalidates_test_idx': invalidates_test_idx,
    102                 'job_started_time': job_started_time,
    103                 'job_finished_time': job_finished_time}
    104 
    105 
    106     def _mock_tko_get_detailed_test_views(self, test_views,
    107                                           missing_results=[]):
    108         """Mock tko method get_detailed_test_views call.
    109 
    110         @param test_views: A list of test views that will be returned
    111                            by get_detailed_test_views.
    112         """
    113         return_values = {}
    114         for v in test_views:
    115             views_of_job = return_values.setdefault(
    116                     ('get_detailed_test_views', v['afe_job_id']), [])
    117             views_of_job.append(v)
    118         for job_id in missing_results:
    119             views_of_job = return_values.setdefault(
    120                     ('get_detailed_test_views', job_id), [])
    121 
    122         def side_effect(*args, **kwargs):
    123             """Maps args and kwargs to the mocked return values."""
    124             key = (kwargs['call'], kwargs['afe_job_id'])
    125             return return_values[key]
    126 
    127         self.tko.run = mock.MagicMock(side_effect=side_effect)
    128 
    129 
    130     def _mock_afe_get_jobs(self, suite_job_id, child_job_ids):
    131         """Mock afe get_jobs call.
    132 
    133         @param suite_job_id: The afe job id of the suite job.
    134         @param child_job_ids: A list of job ids of the child jobs.
    135 
    136         """
    137         suite_job = mock.MagicMock()
    138         suite_job.id = suite_job_id
    139         suite_job.max_runtime_mins = 10
    140         suite_job.parent_job = None
    141 
    142         return_values = {suite_job_id: []}
    143         for job_id in child_job_ids:
    144             new_job = mock.MagicMock()
    145             new_job.id = job_id
    146             new_job.name = 'test.%d' % job_id
    147             new_job.max_runtime_mins = self.JOB_MAX_RUNTIME_MINS
    148             new_job.parent_job = suite_job
    149             return_values[suite_job_id].append(new_job)
    150 
    151         def side_effect(*args, **kwargs):
    152             """Maps args and kwargs to the mocked return values."""
    153             if kwargs.get('id') == suite_job_id:
    154                 return [suite_job]
    155             return return_values[kwargs['parent_job_id']]
    156 
    157         self.afe.get_jobs = mock.MagicMock(side_effect=side_effect)
    158 
    159 
    160     def testFetchSuiteTestView(self):
    161         """Test that it fetches the correct suite test views."""
    162         suite_job_id = 100
    163         suite_name = 'dummy'
    164         build = 'R23-1.1.1.1'
    165         server_job_view = self._build_view(
    166                 10, 'SERVER_JOB', '----', 'GOOD', suite_job_id)
    167         test_to_ignore = self._build_view(
    168                 11, 'dummy_Pass', '101-user/host/dummy_Pass',
    169                 'GOOD', suite_job_id)
    170         test_to_include = self._build_view(
    171                 12, 'dummy_Pass.bluetooth', None, 'TEST_NA', suite_job_id)
    172         test_missing = self._build_view(
    173                 13, 'dummy_Missing', None, 'ABORT', suite_job_id)
    174         self._mock_afe_get_jobs(suite_job_id, [])
    175         self._mock_tko_get_detailed_test_views(
    176                 [server_job_view, test_to_ignore, test_to_include,
    177                  test_missing])
    178         collector = run_suite.ResultCollector(
    179                 'fake_server', self.afe, self.tko,
    180                 build=build, board='fake', suite_name=suite_name,
    181                 suite_job_id=suite_job_id,
    182                 return_code_function=run_suite._ReturnCodeComputer())
    183         collector._missing_results = {
    184                 test_missing['test_name']: [14, 15],
    185         }
    186         suite_views = collector._fetch_relevant_test_views_of_suite()
    187         suite_views = sorted(suite_views, key=lambda view: view['test_idx'])
    188         # Verify that SERVER_JOB is renamed to 'Suite job'
    189         self.assertEqual(suite_views[0].get_testname(),
    190                          run_suite.TestView.SUITE_JOB)
    191         # Verify that the test with a subidr is not included.
    192         self.assertEqual(suite_views[0]['test_idx'], 10)
    193         self.assertEqual(suite_views[1]['test_idx'], 12)
    194         self.assertEqual(suite_views[1]['afe_job_id'], suite_job_id)
    195         # Verify that the test with missing results had it's AFE job id
    196         # replaced.
    197         self.assertEqual(suite_views[2]['test_idx'], 13)
    198         self.assertEqual(suite_views[2]['afe_job_id'], 14)
    199 
    200 
    201     def testFetchTestViewOfChildJobs(self):
    202         """Test that it fetches the correct child test views."""
    203         build = 'lumpy-release/R36-5788.0.0'
    204         board = 'lumpy'
    205         suite_name = 'my_suite'
    206         suite_job_id = 100
    207         invalid_job_id = 101
    208         invalid_job_name = '%s/%s/test_Pass' % (build, suite_name)
    209         good_job_id = 102
    210         good_job_name = '%s/%s/test_Pass' % (build, suite_name)
    211         bad_job_id = 103
    212         bad_job_name = '%s/%s/test_ServerJobFail' % (build, suite_name)
    213         missing_job_id = 104
    214 
    215         invalid_test = self._build_view(
    216                 19, 'test_Pass_Old', 'fake/subdir',
    217                 'FAIL', invalid_job_id, invalid_job_name)
    218         good_job_server_job = self._build_view(
    219                 20, 'SERVER_JOB', '----', 'GOOD', good_job_id, good_job_name)
    220         good_job_test = self._build_view(
    221                 21, 'test_Pass', 'fake/subdir', 'GOOD',
    222                 good_job_id, good_job_name,
    223                 job_keyvals={'retry_original_job_id': invalid_job_id})
    224         bad_job_server_job = self._build_view(
    225                 22, 'SERVER_JOB', '----', 'FAIL', bad_job_id, bad_job_name)
    226         bad_job_test = self._build_view(
    227                 23, 'test_ServerJobFail', 'fake/subdir', 'GOOD',
    228                 bad_job_id, bad_job_name)
    229         self._mock_tko_get_detailed_test_views(
    230                 [good_job_server_job, good_job_test,
    231                  bad_job_server_job, bad_job_test, invalid_test],
    232                 [missing_job_id])
    233         self._mock_afe_get_jobs(suite_job_id,
    234                                 [good_job_id, bad_job_id, missing_job_id])
    235         collector = run_suite.ResultCollector(
    236                 'fake_server', self.afe, self.tko,
    237                 build, board, suite_name, suite_job_id,
    238                 return_code_function=run_suite._ReturnCodeComputer())
    239         child_views, retry_counts, missing_results = (
    240                 collector._fetch_test_views_of_child_jobs())
    241         # child_views should contain tests 21, 22, 23
    242         child_views = sorted(child_views, key=lambda view: view['test_idx'])
    243         # Verify that the SERVER_JOB has been renamed properly
    244         self.assertEqual(child_views[1].get_testname(),
    245                          'test_ServerJobFail_SERVER_JOB')
    246         self.assertEqual(missing_results, {'test.104': [104]})
    247         # Verify that failed SERVER_JOB and actual invalid tests are included,
    248         expected = [good_job_test['test_idx'], bad_job_server_job['test_idx'],
    249                     bad_job_test['test_idx']]
    250         child_view_ids = [v['test_idx'] for v in child_views]
    251         self.assertEqual(child_view_ids, expected)
    252         self.afe.get_jobs.assert_called_once_with(
    253                 parent_job_id=suite_job_id)
    254         # Verify the retry_counts is calculated correctly
    255         self.assertEqual(len(retry_counts), 1)
    256         self.assertEqual(retry_counts[21], 1)
    257 
    258 
    259     def testGenerateLinks(self):
    260         """Test that it generates correct web and buildbot links."""
    261         suite_job_id = 100
    262         suite_name = 'my_suite'
    263         build = 'lumpy-release/R36-5788.0.0'
    264         board = 'lumpy'
    265         fake_job = mock.MagicMock()
    266         fake_job.parent = suite_job_id
    267         test_sponge_url = 'http://test_url'
    268         job_keyvals = {'sponge_url': test_sponge_url}
    269         suite_job_view = run_suite.TestView(
    270                 self._build_view(
    271                     20, 'Suite job', '----', 'GOOD', suite_job_id,
    272                     job_keyvals=job_keyvals),
    273                 fake_job, suite_name, build, 'chromeos-test')
    274         good_test = run_suite.TestView(
    275                 self._build_view(
    276                     21, 'test_Pass', 'fake/subdir', 'GOOD', 101,
    277                     job_keyvals=job_keyvals),
    278                 fake_job, suite_name, build, 'chromeos-test')
    279         bad_test = run_suite.TestView(
    280                 self._build_view(
    281                     23, 'test_Fail', 'fake/subdir', 'FAIL', 102,
    282                     job_keyvals=job_keyvals),
    283                 fake_job, suite_name, build, 'chromeos-test')
    284 
    285         collector = run_suite.ResultCollector(
    286                 'fake_server', self.afe, self.tko,
    287                 build, board, suite_name, suite_job_id, user='chromeos-test',
    288                 return_code_function=run_suite._ReturnCodeComputer())
    289         collector._suite_views = [suite_job_view]
    290         collector._test_views = [suite_job_view, good_test, bad_test]
    291         collector._max_testname_width = max(
    292                 [len(v.get_testname()) for v in collector._test_views]) + 3
    293         collector._generate_web_and_buildbot_links()
    294         URL_PATTERN = run_suite._URL_PATTERN
    295         # expected_web_links is list of (anchor, url) tuples we
    296         # are expecting.
    297         expected_web_links = [
    298                  (v.get_testname(),
    299                   URL_PATTERN % ('http://fake_server',
    300                                  '%s-%s' % (v['afe_job_id'], 'chromeos-test')),
    301                   test_sponge_url)
    302                  for v in collector._test_views]
    303         # Verify web links are generated correctly.
    304         for i in range(len(collector._web_links)):
    305             expect = expected_web_links[i]
    306             self.assertEqual(collector._web_links[i].anchor, expect[0])
    307             self.assertEqual(collector._web_links[i].url, expect[1])
    308             self.assertEqual(collector._web_links[i].sponge_url, expect[2])
    309 
    310         expected_buildbot_links = [
    311                  (v.get_testname(),
    312                   URL_PATTERN % ('http://fake_server',
    313                                  '%s-%s' % (v['afe_job_id'], 'chromeos-test')))
    314                  for v in collector._test_views if v['status'] != 'GOOD']
    315         # Verify buildbot links are generated correctly.
    316         for i in range(len(collector.buildbot_links)):
    317             expect = expected_buildbot_links[i]
    318             self.assertEqual(collector.buildbot_links[i].anchor, expect[0])
    319             self.assertEqual(collector.buildbot_links[i].url, expect[1])
    320             self.assertEqual(collector.buildbot_links[i].retry_count, 0)
    321             # Assert that a retry dashboard link is created.
    322             self.assertNotEqual(
    323                     collector.buildbot_links[i].GenerateRetryLink(), '')
    324             self.assertNotEqual(
    325                     collector.buildbot_links[i].GenerateHistoryLink(), '')
    326 
    327 
    328     def _end_to_end_test_helper(
    329             self, include_bad_test=False, include_warn_test=False,
    330             include_timeout_test=False,
    331             include_self_aborted_test=False,
    332             include_aborted_by_suite_test=False,
    333             include_good_retry=False, include_bad_retry=False,
    334             include_good_test=True,
    335             suite_job_timed_out=False, suite_job_status='GOOD'):
    336         """A helper method for testing ResultCollector end-to-end.
    337 
    338         This method mocks the retrieving of required test views,
    339         and call ResultCollector.run() to collect the results.
    340 
    341         @param include_bad_test:
    342                 If True, include a view of a test which has status 'FAIL'.
    343         @param include_warn_test:
    344                 If True, include a view of a test which has status 'WARN'
    345         @param include_timeout_test:
    346                 If True, include a view of a test which was aborted before
    347                 started.
    348         @param include_self_aborted_test:
    349                 If True, include a view of test which was aborted after
    350                 started and hit hits own timeout.
    351         @param include_self_aborted_by_suite_test:
    352                 If True, include a view of test which was aborted after
    353                 started but has not hit its own timeout.
    354         @param include_good_retry:
    355                 If True, include a test that passed after retry.
    356         @param include_bad_retry:
    357                 If True, include a test that failed after retry.
    358         @param include_good_test:
    359                 If True, include a test that passed. If False, pretend no tests
    360                 (including the parent suite job) came back with any test
    361                 results.
    362         @param suite_job_status: One of 'GOOD' 'FAIL' 'ABORT' 'RUNNING'
    363 
    364         @returns: A ResultCollector instance.
    365         """
    366         suite_job_id = 100
    367         good_job_id = 101
    368         bad_job_id = 102
    369         warn_job_id = 102
    370         timeout_job_id = 100
    371         self_aborted_job_id = 104
    372         aborted_by_suite_job_id = 105
    373         good_retry_job_id = 106
    374         bad_retry_job_id = 107
    375         invalid_job_id_1 = 90
    376         invalid_job_id_2 = 91
    377         suite_name = 'dummy'
    378         build = 'lumpy-release/R27-3888.0.0'
    379         suite_job_keyvals = {
    380                 constants.DOWNLOAD_STARTED_TIME: '2014-04-29 13:14:20',
    381                 constants.PAYLOAD_FINISHED_TIME: '2014-04-29 13:14:25',
    382                 constants.ARTIFACT_FINISHED_TIME: '2014-04-29 13:14:30'}
    383 
    384         suite_job_started_time = '2014-04-29 13:14:37'
    385         if suite_job_timed_out:
    386             suite_job_keyvals['aborted_by'] = 'test_user'
    387             suite_job_finished_time = '2014-04-29 13:25:37'
    388             suite_job_status = 'ABORT'
    389         else:
    390             suite_job_finished_time = '2014-04-29 13:23:37'
    391 
    392         server_job_view = self._build_view(
    393                 10, 'SERVER_JOB', '----', suite_job_status, suite_job_id,
    394                 'lumpy-release/R27-3888.0.0-test_suites/control.dummy',
    395                 '', suite_job_keyvals, '2014-04-29 13:14:37',
    396                 '2014-04-29 13:20:27', job_started_time=suite_job_started_time,
    397                 job_finished_time=suite_job_finished_time)
    398         good_test = self._build_view(
    399                 11, 'dummy_Pass', '101-user/host/dummy_Pass', 'GOOD',
    400                 good_job_id, 'lumpy-release/R27-3888.0.0/dummy/dummy_Pass',
    401                 '', {}, '2014-04-29 13:15:35', '2014-04-29 13:15:36')
    402         bad_test = self._build_view(
    403                 12, 'dummy_Fail.Fail', '102-user/host/dummy_Fail.Fail', 'FAIL',
    404                 bad_job_id, 'lumpy-release/R27-3888.0.0/dummy/dummy_Fail.Fail',
    405                 'always fail', {}, '2014-04-29 13:16:00',
    406                 '2014-04-29 13:16:02')
    407         warn_test = self._build_view(
    408                 13, 'dummy_Fail.Warn', '102-user/host/dummy_Fail.Warn', 'WARN',
    409                 warn_job_id, 'lumpy-release/R27-3888.0.0/dummy/dummy_Fail.Warn',
    410                 'always warn', {}, '2014-04-29 13:16:00',
    411                 '2014-04-29 13:16:02')
    412         timeout_test = self._build_view(
    413                 15, 'dummy_Timeout', '', 'ABORT',
    414                 timeout_job_id,
    415                 'lumpy-release/R27-3888.0.0/dummy/dummy_Timeout',
    416                 'child job did not run', {}, '2014-04-29 13:15:37',
    417                 '2014-04-29 13:15:38')
    418         self_aborted_test = self._build_view(
    419                 16, 'dummy_Abort', '104-user/host/dummy_Abort', 'ABORT',
    420                 self_aborted_job_id,
    421                 'lumpy-release/R27-3888.0.0/dummy/dummy_Abort',
    422                 'child job aborted', {'aborted_by': 'test_user'},
    423                 '2014-04-29 13:15:39', '2014-04-29 13:15:40',
    424                 job_started_time='2014-04-29 13:15:39',
    425                 job_finished_time='2014-04-29 13:25:40')
    426         aborted_by_suite = self._build_view(
    427                 17, 'dummy_AbortBySuite', '105-user/host/dummy_AbortBySuite',
    428                 'RUNNING', aborted_by_suite_job_id,
    429                 'lumpy-release/R27-3888.0.0/dummy/dummy_Abort',
    430                 'aborted by suite', {'aborted_by': 'test_user'},
    431                 '2014-04-29 13:15:39', '2014-04-29 13:15:40',
    432                 job_started_time='2014-04-29 13:15:39',
    433                 job_finished_time='2014-04-29 13:15:40')
    434         good_retry = self._build_view(
    435                 18, 'dummy_RetryPass', '106-user/host/dummy_RetryPass', 'GOOD',
    436                 good_retry_job_id,
    437                 'lumpy-release/R27-3888.0.0/dummy/dummy_RetryPass',
    438                 '', {'retry_original_job_id': invalid_job_id_1},
    439                 '2014-04-29 13:15:37',
    440                 '2014-04-29 13:15:38', invalidates_test_idx=1)
    441         bad_retry = self._build_view(
    442                 19, 'dummy_RetryFail', '107-user/host/dummy_RetryFail', 'FAIL',
    443                 bad_retry_job_id,
    444                 'lumpy-release/R27-3888.0.0/dummy/dummy_RetryFail',
    445                 'retry failed', {'retry_original_job_id': invalid_job_id_2},
    446                 '2014-04-29 13:15:39', '2014-04-29 13:15:40',
    447                 invalidates_test_idx=2)
    448         invalid_test_1 = self._build_view(
    449                 1, 'dummy_RetryPass', '90-user/host/dummy_RetryPass', 'GOOD',
    450                 invalid_job_id_1,
    451                 'lumpy-release/R27-3888.0.0/dummy/dummy_RetryPass',
    452                 'original test failed', {}, '2014-04-29 13:10:00',
    453                 '2014-04-29 13:10:01')
    454         invalid_test_2 = self._build_view(
    455                 2, 'dummy_RetryFail', '91-user/host/dummy_RetryFail', 'FAIL',
    456                 invalid_job_id_2,
    457                 'lumpy-release/R27-3888.0.0/dummy/dummy_RetryFail',
    458                 'original test failed', {},
    459                 '2014-04-29 13:10:03', '2014-04-29 13:10:04')
    460 
    461         test_views = []
    462         child_jobs = set()
    463         missing_results = []
    464         if include_good_test:
    465             test_views.append(server_job_view)
    466             test_views.append(good_test)
    467             child_jobs.add(good_job_id)
    468         # Emulate missing even the parent/suite job.
    469         else:
    470             missing_results.append(suite_job_id)
    471         if include_bad_test:
    472             test_views.append(bad_test)
    473             child_jobs.add(bad_job_id)
    474         if include_warn_test:
    475             test_views.append(warn_test)
    476             child_jobs.add(warn_job_id)
    477         if include_timeout_test:
    478             test_views.append(timeout_test)
    479         if include_self_aborted_test:
    480             test_views.append(self_aborted_test)
    481             child_jobs.add(self_aborted_job_id)
    482         if include_good_retry:
    483             test_views.extend([good_retry, invalid_test_1])
    484             child_jobs.add(good_retry_job_id)
    485         if include_bad_retry:
    486             test_views.extend([bad_retry, invalid_test_2])
    487             child_jobs.add(bad_retry_job_id)
    488         if include_aborted_by_suite_test:
    489             test_views.append(aborted_by_suite)
    490             child_jobs.add(aborted_by_suite_job_id)
    491         self._mock_tko_get_detailed_test_views(test_views,
    492                missing_results=missing_results)
    493         self._mock_afe_get_jobs(suite_job_id, child_jobs)
    494         collector = run_suite.ResultCollector(
    495                'fake_server', self.afe, self.tko,
    496                'lumpy-release/R36-5788.0.0', 'lumpy', 'dummy', suite_job_id,
    497                return_code_function=run_suite._ReturnCodeComputer())
    498         collector.run()
    499         collector.output_results()
    500         return collector
    501 
    502 
    503     def testEndToEndSuiteEmpty(self):
    504         """Test it returns code INFRA_FAILURE when no tests report back."""
    505         collector = self._end_to_end_test_helper(include_good_test=False)
    506         self.assertEqual(collector.return_result.return_code,
    507                          run_suite.RETURN_CODES.INFRA_FAILURE)
    508 
    509 
    510     def testEndToEndSuitePass(self):
    511         """Test it returns code OK when all test pass."""
    512         collector = self._end_to_end_test_helper()
    513         self.assertEqual(collector.return_result.return_code,
    514                          run_suite.RETURN_CODES.OK)
    515 
    516 
    517     def testEndToEndSuiteWarn(self):
    518         """Test it returns code WARNING when there is a test that warns."""
    519         collector = self._end_to_end_test_helper(include_warn_test=True)
    520         self.assertEqual(collector.return_result.return_code,
    521                          run_suite.RETURN_CODES.WARNING)
    522 
    523 
    524     def testEndToEndSuiteFail(self):
    525         """Test it returns code ERROR when there is a test that fails."""
    526         collector = self._end_to_end_test_helper(include_bad_test=True)
    527         self.assertEqual(collector.return_result.return_code,
    528                          run_suite.RETURN_CODES.ERROR)
    529 
    530 
    531     def testEndToEndSuiteJobFail(self):
    532         """Test it returns code SUITE_FAILURE when only the suite job failed."""
    533         collector = self._end_to_end_test_helper(suite_job_status='ABORT')
    534         self.assertEqual(
    535                 collector.return_result.return_code,
    536                 run_suite.RETURN_CODES.INFRA_FAILURE)
    537 
    538         collector = self._end_to_end_test_helper(suite_job_status='ERROR')
    539         self.assertEqual(
    540                 collector.return_result.return_code,
    541                 run_suite.RETURN_CODES.INFRA_FAILURE)
    542 
    543 
    544     def testEndToEndRetry(self):
    545         """Test it returns correct code when a test was retried."""
    546         collector = self._end_to_end_test_helper(include_good_retry=True)
    547         self.assertEqual(
    548                 collector.return_result.return_code,
    549                 run_suite.RETURN_CODES.WARNING)
    550 
    551         collector = self._end_to_end_test_helper(include_good_retry=True,
    552                 include_self_aborted_test=True)
    553         self.assertEqual(
    554                 collector.return_result.return_code,
    555                 run_suite.RETURN_CODES.ERROR)
    556 
    557         collector = self._end_to_end_test_helper(include_good_retry=True,
    558                 include_bad_test=True)
    559         self.assertEqual(
    560                 collector.return_result.return_code,
    561                 run_suite.RETURN_CODES.ERROR)
    562 
    563         collector = self._end_to_end_test_helper(include_bad_retry=True)
    564         self.assertEqual(
    565                 collector.return_result.return_code,
    566                 run_suite.RETURN_CODES.ERROR)
    567 
    568 
    569     def testEndToEndSuiteTimeout(self):
    570         """Test it returns correct code when a child job timed out."""
    571         # a child job timed out before started, none failed.
    572         collector = self._end_to_end_test_helper(include_timeout_test=True)
    573         self.assertEqual(
    574                 collector.return_result.return_code,
    575                 run_suite.RETURN_CODES.SUITE_TIMEOUT)
    576 
    577         # a child job timed out before started, and one test failed.
    578         collector = self._end_to_end_test_helper(
    579                 include_bad_test=True, include_timeout_test=True)
    580         self.assertEqual(collector.return_result.return_code,
    581                          run_suite.RETURN_CODES.ERROR)
    582 
    583         # a child job timed out before started, and one test warned.
    584         collector = self._end_to_end_test_helper(
    585                 include_warn_test=True, include_timeout_test=True)
    586         self.assertEqual(collector.return_result.return_code,
    587                          run_suite.RETURN_CODES.SUITE_TIMEOUT)
    588 
    589         # a child job timed out before started, and one test was retried.
    590         collector = self._end_to_end_test_helper(include_good_retry=True,
    591                 include_timeout_test=True)
    592         self.assertEqual(
    593                 collector.return_result.return_code,
    594                 run_suite.RETURN_CODES.SUITE_TIMEOUT)
    595 
    596         # a child jot was aborted because suite timed out.
    597         collector = self._end_to_end_test_helper(
    598                 include_aborted_by_suite_test=True)
    599         self.assertEqual(
    600                 collector.return_result.return_code,
    601                 run_suite.RETURN_CODES.SUITE_TIMEOUT)
    602 
    603         # suite job timed out.
    604         collector = self._end_to_end_test_helper(suite_job_timed_out=True)
    605         self.assertEqual(
    606                 collector.return_result.return_code,
    607                 run_suite.RETURN_CODES.SUITE_TIMEOUT)
    608 
    609 
    610 class LogLinkUnittests(unittest.TestCase):
    611     """Test the LogLink"""
    612 
    613     def testGenerateBuildbotLinks(self):
    614         """Test LogLink GenerateBuildbotLinks"""
    615         log_link_a = run_suite.LogLink('mock_anchor', 'mock_server',
    616                                       'mock_job_string',
    617                                       bug_info=('mock_bug_id', 1),
    618                                       reason='mock_reason',
    619                                       retry_count=1,
    620                                       testname='mock_testname')
    621         # Generate a bug link and a log link when bug_info is present
    622         self.assertTrue(len(list(log_link_a.GenerateBuildbotLinks())) == 2)
    623 
    624         log_link_b = run_suite.LogLink('mock_anchor', 'mock_server',
    625                                       'mock_job_string_b',
    626                                       reason='mock_reason',
    627                                       retry_count=1,
    628                                       testname='mock_testname')
    629         # Generate a log link when there is no bug_info
    630         self.assertTrue(len(list(log_link_b.GenerateBuildbotLinks())) == 1)
    631 
    632 
    633 class SimpleTimerUnittests(unittest.TestCase):
    634     """Test the simple timer."""
    635 
    636     def testPoll(self):
    637         """Test polling the timer."""
    638         interval_hours = 0.0001
    639         t = diagnosis_utils.SimpleTimer(interval_hours=interval_hours)
    640         deadline = t.deadline
    641         self.assertTrue(deadline is not None and
    642                         t.interval_hours == interval_hours)
    643         min_deadline = (datetime.now() +
    644                         datetime_base.timedelta(hours=interval_hours))
    645         time.sleep(interval_hours * 3600)
    646         self.assertTrue(t.poll())
    647         self.assertTrue(t.deadline >= min_deadline)
    648 
    649 
    650     def testBadInterval(self):
    651         """Test a bad interval."""
    652         t = diagnosis_utils.SimpleTimer(interval_hours=-1)
    653         self.assertTrue(t.deadline is None and t.poll() == False)
    654         t._reset()
    655         self.assertTrue(t.deadline is None and t.poll() == False)
    656 
    657 
    658 if __name__ == '__main__':
    659     unittest.main()
    660