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