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